import { isDefined } from "@devexpress/utils/lib/utils/common";
import { ValidationControllerSettings } from "../../View/Settings/InitSettings/ValidationControllerSettings";
import { ValidationSettings } from "../../View/Settings/ValidationSettings";
import { DateUtils } from "../../View/Utils/DateUtils";
import { DependencyType } from "../Entities/Enums";
import { UpdateTaskHistoryItem } from "../History/HistoryItems/Task/UpdateTaskHistoryItem";
import { IHistory } from "../History/IHistory";
import { ModelManipulator } from "../Manipulators/ModelManipulator";
import { ViewVisualModelItem } from "../VisualModel/ViewVisualModelItem";
import { ViewVisualModel } from "../VisualModel/VisualModel";
import { DateRange } from "../WorkingTime/DateRange";
import { DateTimeUtils } from "../WorkingTime/DateTimeUtils";
import { ValidationError } from "./ValidationError";

export class ValidationController {
    settings: ValidationControllerSettings;

    get viewModel(): ViewVisualModel {
        return this.settings.getViewModel();
    }
    get history(): IHistory {
        return this.settings.getHistory();
    }
    get modelManipulator(): ModelManipulator {
        return this.settings.getModelManipulator();
    }
    get range(): DateRange {
        return this.settings.getRange();
    }
    get validationSettings(): ValidationSettings {
        return this.settings.getValidationSettings();
    }
    get _parentAutoCalc(): boolean {
        return this.viewModel.parentAutoCalc;
    }
    get enablePredecessorGap(): boolean {
        return this.viewModel.enablePredecessorGap;
    }
    get isValidateDependenciesRequired(): boolean {
        return this.settings.getIsValidateDependenciesRequired();
    }
    private lockPredecessorToSuccessor: boolean = true;

    private updateOwnerInAutoParentMode(): void {
        this.settings.updateOwnerInAutoParentMode();
    }

    constructor(settings: ValidationControllerSettings) {
        this.settings = settings;
    }

    checkStartDependencies(taskId: string, date: Date): ValidationError[] {
        const result = [];
        const task = this.viewModel.tasks.getItemById(taskId);
        const dependencies = this.viewModel.dependencies.items.filter(d => d.successorId === taskId);
        dependencies.forEach((dep) => {
            const predecessorTask = this.viewModel.tasks.getItemById(dep.predecessorId);
            if(dep.type === DependencyType.FS && predecessorTask.end > date
                || dep.type === DependencyType.SS && predecessorTask.start > date)
                result.push(new ValidationError(dep.internalId, true));
            if(dep.type === DependencyType.FS && predecessorTask.end.valueOf() === task.start.valueOf() && date > predecessorTask.end ||
               dep.type === DependencyType.SS && predecessorTask.start.valueOf() === task.start.valueOf() && date > predecessorTask.start)
                result.push(new ValidationError(dep.internalId));
        });
        return result;
    }
    checkEndDependencies(taskId: string, date: Date): ValidationError[] {
        const result = [];
        const task = this.viewModel.tasks.getItemById(taskId);
        const dependencies = this.viewModel.dependencies.items.filter(d => d.successorId === taskId);
        dependencies.forEach((dep) => {
            const predecessorTask = this.viewModel.tasks.getItemById(dep.predecessorId);
            if(dep.type === DependencyType.SF && predecessorTask.start > date
                || dep.type === DependencyType.FF && predecessorTask.end > date)
                result.push(new ValidationError(dep.internalId, true));
            if((dep.type === DependencyType.SF && predecessorTask.start.valueOf() === task.end.valueOf() && date > predecessorTask.start) ||
               (dep.type === DependencyType.FF && predecessorTask.end.valueOf() === task.end.valueOf() && date > predecessorTask.end))
                result.push(new ValidationError(dep.internalId));
        });
        return result;
    }
    public moveEndDependTasks(predecessorTaskId: string, predecessorPreviousEndDate: Date, isAfterCorrectParents: boolean = false): void {
        const dependencies = this.viewModel.dependencies.items.filter(d => d.predecessorId === predecessorTaskId && !d.isStartDependency);
        const predecessorTask = this.viewModel.tasks.getItemById(predecessorTaskId);
        dependencies.forEach((dependency) => {
            const successorTask = this.viewModel.tasks.getItemById(dependency.successorId);
            const isMoveNotRequired = !successorTask || (isAfterCorrectParents && predecessorTask.parentId === successorTask.parentId) || (successorTask.parentId == predecessorTask.id);
            if(isMoveNotRequired)
                return;
            const successorRangeBeforeMove = new DateRange(new Date(successorTask.start.getTime()), new Date(successorTask.end.getTime()));
            const validTaskRange = new DateRange(new Date(successorTask.start.getTime()), new Date(successorTask.end.getTime()));
            const delta = predecessorTask.end.getTime() - predecessorPreviousEndDate.getTime();
            const predecessorEnd = this.enablePredecessorGap ? predecessorTask.end : predecessorPreviousEndDate;
            if(dependency.type === DependencyType.FS
                && (successorTask.start < predecessorEnd
                    || (this.lockPredecessorToSuccessor && successorTask.start.getTime() === predecessorPreviousEndDate.getTime()))) {
                validTaskRange.start.setTime(predecessorTask.end.getTime());
                validTaskRange.end.setTime(validTaskRange.start.getTime() + (successorTask.end.getTime() - successorTask.start.getTime()));
                this.correctMoving(successorTask.internalId, validTaskRange);
            }
            else if(dependency.type === DependencyType.FF
                && (successorTask.end < predecessorEnd
                    || (this.lockPredecessorToSuccessor && successorTask.end.getTime() === predecessorPreviousEndDate.getTime()))) {
                validTaskRange.start.setTime(predecessorTask.end.getTime() - (successorTask.end.getTime() - successorTask.start.getTime()));
                validTaskRange.end.setTime(predecessorTask.end.getTime());
                this.correctMoving(successorTask.internalId, validTaskRange);
            }
            else if(!this.enablePredecessorGap) {
                validTaskRange.start.setTime(successorTask.start.getTime() + delta);
                validTaskRange.end.setTime(successorTask.end.getTime() + delta);
            }
            if(!successorRangeBeforeMove.equal(validTaskRange)) {
                const data = { start: validTaskRange.start, end: validTaskRange.end };
                this.history.addAndRedo(new UpdateTaskHistoryItem(this.modelManipulator, dependency.successorId, data));
                this.moveRelatedTasks(dependency, successorRangeBeforeMove, successorTask, validTaskRange);
            }
        });
    }
    public moveStartDependTasks(predecessorTaskId: string, predecessorPreviousStartDate: Date, isAfterCorrectParents: boolean = false): void {
        const dependencies = this.viewModel.dependencies.items.filter(d => d.predecessorId === predecessorTaskId && d.isStartDependency);
        const predecessorTask = this.viewModel.tasks.getItemById(predecessorTaskId);
        dependencies.forEach((dependency) => {
            const successorTask = this.viewModel.tasks.getItemById(dependency.successorId);
            const isMoveNotRequired = !successorTask || (isAfterCorrectParents && predecessorTask.parentId === successorTask.parentId) || (successorTask.parentId == predecessorTask.id);
            if(isMoveNotRequired)
                return;
            const successorRangeBeforeMove = new DateRange(new Date(successorTask.start.getTime()), new Date(successorTask.end.getTime()));
            const validTaskRange = new DateRange(new Date(successorTask.start.getTime()), new Date(successorTask.end.getTime()));
            const delta = predecessorTask.start.getTime() - predecessorPreviousStartDate.getTime();
            const predecessorStart = this.enablePredecessorGap ? predecessorTask.start : predecessorPreviousStartDate;
            if(dependency.type === DependencyType.SF
                && (successorTask.end < predecessorStart
                    || (this.lockPredecessorToSuccessor && successorTask.end.getTime() === predecessorPreviousStartDate.getTime()))) {
                validTaskRange.start.setTime(predecessorTask.start.getTime() - (successorTask.end.getTime() - successorTask.start.getTime()));
                validTaskRange.end.setTime(predecessorTask.start.getTime());
                this.correctMoving(successorTask.internalId, validTaskRange);
            }
            else if(dependency.type === DependencyType.SS
                && (successorTask.start < predecessorStart
                    || (this.lockPredecessorToSuccessor && successorTask.start.getTime() === predecessorPreviousStartDate.getTime()))) {
                validTaskRange.start.setTime(predecessorTask.start.getTime());
                validTaskRange.end.setTime(predecessorTask.start.getTime() + (successorTask.end.getTime() - successorTask.start.getTime()));
                this.correctMoving(successorTask.internalId, validTaskRange);
            }
            else if(!this.enablePredecessorGap) {
                validTaskRange.start.setTime(successorTask.start.getTime() + delta);
                validTaskRange.end.setTime(successorTask.end.getTime() + delta);
            }
            if(!successorRangeBeforeMove.equal(validTaskRange)) {
                const data = { start: validTaskRange.start, end: validTaskRange.end };
                this.history.addAndRedo(new UpdateTaskHistoryItem(this.modelManipulator, dependency.successorId, data));
                this.moveRelatedTasks(dependency, successorRangeBeforeMove, successorTask, validTaskRange);
            }
        });
    }
    private moveRelatedTasks(dependency, successorRangeBeforeMove, successorTask, validTaskRange) {
        const delta = validTaskRange.start.getTime() - successorRangeBeforeMove.start.getTime();
        this.correctParentsOnChildMoving(successorTask.internalId, delta);
        this.moveStartDependTasks(dependency.successorId, successorRangeBeforeMove.start);
        this.moveEndDependTasks(dependency.successorId, successorRangeBeforeMove.end);
    }
    public getCorrectDateRange(taskId: any, startDate: Date, endDate: Date): DateRange {
        const dateRange = new DateRange(new Date(startDate), new Date(endDate));
        const validationErrors = [...this.checkStartDependencies(taskId, dateRange.start), ...this.checkEndDependencies(taskId, dateRange.end)];
        const criticalErrors = validationErrors.filter(e => e.critical);
        criticalErrors.forEach(error => {
            const dependency = this.viewModel.dependencies.getItemById(error.dependencyId);
            const predecessorTask = this.viewModel.tasks.getItemById(dependency.predecessorId);
            if(dependency.type === DependencyType.FS)
                if(dateRange.start < predecessorTask.end)
                    dateRange.start.setTime(predecessorTask.end.getTime());

            if(dependency.type === DependencyType.SS)
                if(dateRange.start < predecessorTask.start)
                    dateRange.start.setTime(predecessorTask.start.getTime());

            if(dependency.type === DependencyType.FF)
                if(dateRange.end < predecessorTask.end)
                    dateRange.end.setTime(predecessorTask.end.getTime());

            if(dependency.type === DependencyType.SF)
                if(dateRange.end < predecessorTask.start)
                    dateRange.end.setTime(predecessorTask.start.getTime());
        });
        return dateRange;
    }
    private correctMoving(taskId, dateRange: DateRange): DateRange {
        const deltaDate = dateRange.end.getTime() - dateRange.start.getTime();
        const validationErrors = [...this.checkStartDependencies(taskId, dateRange.start), ...this.checkEndDependencies(taskId, dateRange.end)];
        const criticalErrors = validationErrors.filter(e => e.critical);
        criticalErrors.forEach(error => {
            const dependency = this.viewModel.dependencies.getItemById(error.dependencyId);
            const predecessorTask = this.viewModel.tasks.getItemById(dependency.predecessorId);
            if(dependency.type === DependencyType.FS)
                if(dateRange.start < predecessorTask.end) {
                    dateRange.start.setTime(predecessorTask.end.getTime());
                    dateRange.end.setTime(dateRange.start.getTime() + deltaDate);
                }
            if(dependency.type === DependencyType.SS)
                if(dateRange.start < predecessorTask.start) {
                    dateRange.start.setTime(predecessorTask.start.getTime());
                    dateRange.end.setTime(dateRange.start.getTime() + deltaDate);
                }
            if(dependency.type === DependencyType.FF)
                if(dateRange.end < predecessorTask.end) {
                    dateRange.end.setTime(predecessorTask.end.getTime());
                    dateRange.start.setTime(dateRange.end.getTime() - deltaDate);
                }
            if(dependency.type === DependencyType.SF)
                if(dateRange.end < predecessorTask.start) {
                    dateRange.end.setTime(predecessorTask.start.getTime());
                    dateRange.start.setTime(dateRange.end.getTime() - deltaDate);
                }
        });
        return dateRange;
    }

    public recalculateParents(child: ViewVisualModelItem, calcStepCallback: (data: any) => any): void {
        let parent = child && child.parent;

        while(parent && parent.task) {
            const children = parent.children;
            let start = this.range.end;
            let end = this.range.start;
            let progress = 0;
            let totalDuration = 0;
            const data = { id: parent.task.internalId };

            for(let i = 0; i < children.length; i++) {
                const childTask = children[i].task;
                if(!childTask.isValid())
                    continue;
                start = DateTimeUtils.getMinDate(start, childTask.start);
                end = DateTimeUtils.getMaxDate(end, childTask.end);

                const duration = childTask.getDuration();
                progress += childTask.progress * duration;
                totalDuration += duration;
            }
            if(!DateTimeUtils.areDatesEqual(parent.task.start, start))
                data["start"] = start;
            if(!DateTimeUtils.areDatesEqual(parent.task.end, end))
                data["end"] = end;
            data["oldStart"] = parent.task.start;
            data["oldEnd"] = parent.task.end;
            progress = totalDuration > 0 ? Math.round(progress / totalDuration) : 0;
            if(progress !== parent.task.progress)
                data["progress"] = progress;

            calcStepCallback(data);
            parent = parent.parent;
        }
    }

    public updateParentsRangeByChild(taskId: string): void {
        this.recalculateParents(this.viewModel.findItem(taskId), (data: any) => {
            if(!isDefined(data.id)) return;
            const history = this.history;
            const manipulator = this.modelManipulator;

            if(isDefined(data.start)) {
                history.addAndRedo(new UpdateTaskHistoryItem(manipulator, data.id, { start: data.start }));
                this.moveStartDependTasks(data.id, data.oldStart);
            }
            if(isDefined(data.end)) {
                history.addAndRedo(new UpdateTaskHistoryItem(manipulator, data.id, { end: data.end }));
                this.moveEndDependTasks(data.id, data.oldEnd);
            }
            if(isDefined(data.progress))
                history.addAndRedo(new UpdateTaskHistoryItem(manipulator, data.id, { progress: data.progress }));
        });
    }

    public updateChildRangeByParent(parentId: string, delta: number, changedTasks: Array<any>): void {
        const item = this.viewModel.findItem(parentId);
        if(!item || item.children.length === 0) return;

        const children = item.children;
        for(let i = 0; i < children.length; i++) {
            const childTask = children[i].task;
            const newStart = new Date(childTask.start.getTime() + delta);
            const taskPeriod = DateUtils.getRangeMSPeriod(childTask.start, childTask.end);
            const newEnd = DateUtils.getDSTCorrectedTaskEnd(newStart, taskPeriod);
            changedTasks.push({ id: childTask.internalId, start: childTask.start, end: childTask.end });
            this.history.addAndRedo(new UpdateTaskHistoryItem(this.modelManipulator, childTask.internalId, { start: newStart, end: newEnd }));
            this.updateChildRangeByParent(childTask.internalId, delta, changedTasks);
        }
    }

    public updateParentsIfRequired(childId: string): void {
        if(this._parentAutoCalc) {
            this.updateParentsRangeByChild(childId);
            this.updateOwnerInAutoParentMode();
        }
    }
    public correctParentsOnChildMoving(taskId: string, delta: number): void {
        if(this._parentAutoCalc && delta !== 0) {
            this.updateParentsRangeByChild(taskId);
            const changedTasks = [];
            this.updateChildRangeByParent(taskId, delta, changedTasks);
            if(this.isValidateDependenciesRequired)
                changedTasks.forEach(i => {
                    this.moveStartDependTasks(i.id, i.start, true);
                    this.moveEndDependTasks(i.id, i.end, true);
                });
            this.updateOwnerInAutoParentMode();
        }
    }

    public canCreateDependency(predecessorId: string, successorId: string): boolean {
        return this.viewModel.canCreateDependency(predecessorId, successorId);
    }
}
