import { BarManager } from "../BarManager/BarManager";
import { Browser } from "@devexpress/utils/lib/browser";
import { CommandManager } from "../Commands/CommandManager";
import { CreateTaskHistoryItem } from "../Model/History/HistoryItems/Task/CreateTaskHistoryItem";
import { DateRange } from "../Model/WorkingTime/DateRange";
import { DateTimeUtils } from "../Model/WorkingTime/DateTimeUtils";
import { DateUtils } from "./Utils/DateUtils";
import { Dependency } from "../Model/Entities/Dependency";
import { DependencyType } from "../Model/Entities/Enums";
import { EditingSettings } from "./Settings/EditingSettings";
import { FullScreenHelperSettings } from "./Settings/InitSettings/FullScreenHelperSettings";
import { FullScreenModeHelper } from "./Helpers/FullScreenModeHelper";
import { GanttClientCommand } from "../Commands/ClientCommand";
import { GanttExportCalculator } from "../Export/Pdf/Calculator";
import { History } from "../Model/History/History";
import { ICommand } from "../Interfaces/ICommand";
import { IGanttOwner } from "../Interfaces/IGanttOwner";
import { IGanttView } from "../Interfaces/IGanttView";
import { IHistory } from "../Model/History/IHistory";
import { isDefined } from "@devexpress/utils/lib/utils/common";
import { ITaskAreaContainer } from "../Interfaces/ITaskAreaContainer";
import { ModelChangesDispatcher } from "../Model/Dispatchers/ModelChangesDispatcher";
import { ModelManipulator } from "../Model/Manipulators/ModelManipulator";
import { PdfGanttExporter } from "../Export/Pdf/Exporter";
import { RenderHelper } from "./Render/RenderHelper";
import { Resource } from "../Model/Entities/Resource";
import { Settings } from "./Settings/Settings";
import { Size } from "@devexpress/utils/lib/geometry/size";
import { StripLineSettings } from "./Settings/StripLineSettings";
import { Task } from "../Model/Entities/Task";
import { TaskEditController } from "./Edit/TaskEditController";
import { TaskEditSettings } from "./Settings/InitSettings/TaskEditSettings";
import { ValidationController } from "../Model/Validation/ValidationController";
import { ValidationControllerSettings } from "./Settings/InitSettings/ValidationControllerSettings";
import { ValidationSettings } from "./Settings/ValidationSettings";
import { ViewType, TaskTitlePosition } from "./Helpers/Enums";
import { ViewVisualModel } from "../Model/VisualModel/VisualModel";
import { ViewVisualModelItem } from "../Model/VisualModel/ViewVisualModelItem";
import { GanttViewApi } from "./Api/GanttViewApi";
import { EvtUtils } from "@devexpress/utils/lib/utils/evt";
import { IHistoryListener } from "../Interfaces/IHistiryListener";
import { DomUtils } from "@devexpress/utils/lib/utils/dom";
import { Point } from "@devexpress/utils/lib/geometry/point";
import { ITaskAreaEventsListener, TaskAreaEventsListener } from "./Events/TaskAreaEventsListener";
import { DialogBase } from "../Dialogs/DialogBase";
import { GanttDataObjectNames } from "../Model/Entities/DataObject";


export class GanttView implements IGanttView {
    static cachedPrefix = "cached_";
    static taskAreaScrollLeftKey = GanttView.cachedPrefix + "taskAreaScrollLeft";
    static taskAreaScrollTopKey = GanttView.cachedPrefix + "taskAreaScrollTop";
    static taskTextHeightKey = GanttView.cachedPrefix + "taskTextHeight";

    private _taskAreaEventsListener: ITaskAreaEventsListener;

    ganttOwner: IGanttOwner;

    barManager: BarManager;
    commandManager: CommandManager;
    dataRange: DateRange;
    dispatcher: ModelChangesDispatcher
    fullScreenModeHelper: FullScreenModeHelper;
    history: IHistory;
    modelManipulator: ModelManipulator;
    range: DateRange;
    renderHelper: RenderHelper;
    settings: Settings;
    taskEditController: TaskEditController;
    validationController: ValidationController;
    viewModel: ViewVisualModel;
    ganttViewApi: GanttViewApi;

    currentSelectedTaskID = "";
    isFocus: boolean = false;
    private _updateWithModelReloadLockedCounter: number = 0;
    private _pendingUpdateInfo: Record<string, any>;
    scaleCount: number = 2;
    tickSize: Size = new Size(0, 0);

    public currentZoom: number = 1;
    private stripLinesUpdaterId: number = null;

    _scrollTimeOut: number;

    constructor(element: HTMLElement, ganttOwner: IGanttOwner, settings: any) {
        this.ganttOwner = ganttOwner;
        this.settings = Settings.parse(settings);


        this.initValidationController();

        this.renderHelper = new RenderHelper(this);
        this.renderHelper.initMarkup(element);

        this.loadOptionsFromGanttOwner();
        this.renderHelper.init(this.tickSize, this.range, this.settings.viewType, this.viewModel, this.settings.firstDayOfWeek);
        this.commandManager = new CommandManager(this);
        this.barManager = new BarManager(this.commandManager, this.ganttOwner.bars);
        this.initTaskEditController();
        this.history = new History(this._getHistoryListener());
        this.initFullScreenModeHelper();

        this.updateView();
        this._scrollTimeOut = setTimeout(() => {
            this.scrollLeftByViewType();
        }, 0);

        this.initializeStripLinesUpdater();
        this.initGanttViewApi();
    }

    initGanttViewApi(): void {
        this.ganttViewApi = new GanttViewApi(this);
    }

    private _getHistoryListener(): IHistoryListener {
        const listener = {
            onTransactionStart: this.onHistoryTransactionStart.bind(this),
            onTransactionEnd: this.onHistoryTransactionEnd.bind(this),
        };
        return listener;
    }
    protected onHistoryTransactionStart(): void { this.lockUpdateWithReload(); }
    protected onHistoryTransactionEnd(): void { this.unlockUpdateWithReload(); }

    public lockUpdateWithReload(): void {
        this._updateWithModelReloadLockedCounter++;
    }
    public unlockUpdateWithReload(): void {
        this._updateWithModelReloadLockedCounter--;
        if(this._updateWithModelReloadLockedCounter === 0 && this._pendingUpdateInfo) {
            this.updateWithDataReload(this._pendingUpdateInfo.keepExpandState);
            this._pendingUpdateInfo = null;
        }
    }
    initValidationController(): void {
        const validationControllerSettings = ValidationControllerSettings.parse({
            getViewModel: () => { return this.viewModel; },
            getHistory: () => { return this.history; },
            getModelManipulator: () => { return this.modelManipulator; },
            getRange: () => { return this.range; },
            getValidationSettings: () => { return this.settings.validation; },
            updateOwnerInAutoParentMode: () => { this.updateOwnerInAutoParentMode(); },
            getIsValidateDependenciesRequired: () => { return this.isValidateDependenciesRequired(); }
        });
        this.validationController = new ValidationController(validationControllerSettings);
    }

    initTaskEditController(): void {
        const taskEditSettings = TaskEditSettings.parse({
            destroyTemplate: (container: HTMLElement) => { this.destroyTemplate(container); },
            formatDate: (date: Date) => { return this.getDateFormat(date); },
            getRenderHelper: () => { return this.renderHelper; },
            getGanttSettings: () => { return this.settings; },
            getViewModel: () => { return this.viewModel; },
            getCommandManager: () => { return this.commandManager; },
            getModelManipulator: () => { return this.modelManipulator; },
            getValidationController: () => { return this.validationController; }
        });
        this.taskEditController = new TaskEditController(taskEditSettings);
    }
    public get taskAreaEventsListener(): ITaskAreaEventsListener {
        this._taskAreaEventsListener ??= new TaskAreaEventsListener(this);
        return this._taskAreaEventsListener;
    }

    initFullScreenModeHelper(): void {
        const fullScreenModeSeettings = FullScreenHelperSettings.parse({
            getMainElement: () => { return this.getOwnerControlMainElement(); },
            adjustControl: () => { this.adjustOwnerControl(); } }
        );
        this.fullScreenModeHelper = new FullScreenModeHelper(fullScreenModeSeettings);
    }

    getDateRange(modelStartDate: Date, modelEndDate: Date): DateRange {
        const visibleAreaTime = this.getVisibleAreaTime();
        let start = this.settings.startDateRange || DateUtils.adjustStartDateByViewType(new Date(modelStartDate.getTime() - visibleAreaTime), this.settings.viewType, this.settings.firstDayOfWeek);
        let end = this.settings.endDateRange || DateUtils.adjustEndDateByViewType(new Date(modelEndDate.getTime() + visibleAreaTime), this.settings.viewType, this.settings.firstDayOfWeek);
        if(this.settings.startDateRange && start > end)
            end = start;
        else if(this.settings.endDateRange && start > end)
            start = end;
        return new DateRange(start, end);
    }

    getVisibleAreaTime(): number {
        const visibleTickCount = Math.ceil(this.renderHelper.getTaskAreaContainerWidth() / this.tickSize.width);
        return visibleTickCount * DateUtils.getTickTimeSpan(this.settings.viewType);
    }

    zoomIn(leftPos: number = this.renderHelper.getTaskAreaContainerWidth() / 2): void {
        this.ganttViewApi.zoomIn(leftPos);
    }
    zoomOut(leftPos: number = this.renderHelper.getTaskAreaContainerWidth() / 2): void {
        this.ganttViewApi.zoomOut(leftPos);
    }
    scrollToDate(date: any): void {
        if(date) {
            const scrollDate = date instanceof Date ? DateUtils.getOrCreateUTCDate(date) : DateUtils.parse(date);
            this.scrollToDateCore(scrollDate, 0);
        }
    }
    public showDialog(name: string, parameters: any, callback: (params: any) => void, afterClosing: () => void) {
        this.ganttOwner.showDialog(name, parameters, callback, afterClosing);
    }
    public showPopupMenu(info: any) {
        this.ganttOwner.showPopupMenu(info);
    }
    public hidePopupMenu(): void {
        if(this.ganttOwner.hidePopupMenu)
            this.ganttOwner.hidePopupMenu();
    }
    public collapseAll(): void {
        this.ganttOwner.collapseAll();
    }
    public expandAll(): void {
        this.ganttOwner.expandAll();
    }
    public onGanttViewContextMenu(evt: any, key: any, type: string): boolean {
        return this.ganttOwner.onGanttViewContextMenu(evt, key, type);
    }
    public changeGanttTaskSelection(id: string, selected: boolean): void {
        this.ganttOwner.changeGanttTaskSelection(id, selected);
    }
    public hideTaskEditControl() : void {
        this.taskEditController.hide();
    }

    public scrollLeftByViewType(): void {
        const adjustedStartDate = DateUtils.roundStartDate(this.dataRange.start, this.settings.viewType);
        this.scrollToDateCore(adjustedStartDate, 1);
    }
    public scrollToDateCore(date: Date, addLeftPos: number): void {
        this.renderHelper.setTaskAreaContainerScrollLeftToDate(date, addLeftPos);
    }

    onVisualModelChanged(): void { 
        this.resetAndUpdate();
    }
    private initializeStripLinesUpdater() {
        if(this.settings.stripLines.showCurrentTime)
            this.stripLinesUpdaterId = setInterval(() => {
                this.renderHelper.recreateStripLines();
            }, Math.max(this.settings.stripLines.currentTimeUpdateInterval, 100));

    }
    private clearStripLinesUpdater() {
        if(this.stripLinesUpdaterId)
            clearInterval(this.stripLinesUpdaterId);
        this.stripLinesUpdaterId = null;
    }

    getGanttViewStartDate(tasks: Task[]): Date {
        if(!tasks) return new Date();
        const dates = tasks.map(t => typeof t.start === "string" ? new Date(t.start) : t.start).filter(d => isDefined(d));
        return dates.length > 0 ? dates.reduce((min, d) => d < min ? d : min, dates[0]) : new Date();
    }
    getGanttViewEndDate(tasks: Task[]): Date {
        if(!tasks) return new Date();
        const dates = tasks.map(t => typeof t.end === "string" ? new Date(t.end) : t.end).filter(d => isDefined(d));
        return dates.length > 0 ? dates.reduce((max, d) => d > max ? d : max, dates[0]) : new Date();
    }
    getTask(index: number): Task {
        const item = this.getViewItem(index);
        return item?.task;
    }
    getViewItem(index: number): ViewVisualModelItem {
        return this.viewModel?.items[index];
    }
    isValidateDependenciesRequired(): boolean {
        return this.settings.validation.validateDependencies && this.settings.showDependencies;
    }

    updateTickSizeWidth(): void {
        this.tickSize.width = this.renderHelper.etalonScaleItemWidths * this.currentZoom;
    }

    updateView(): void {
        this.onBeginUpdateView();
        this.renderHelper.setTimeScaleContainerScrollLeft(this.taskAreaContainerScrollLeft);
        this.processScroll(false);
        this.processScroll(true);
        this.ganttOwner.onGanttScroll(this.taskAreaContainerScrollTop);
        this.onEndUpdateView();
    }
    protected onBeginUpdateView(): void {
        this[GanttView.taskAreaScrollTopKey] = this.renderHelper.taskAreaContainerScrollTop;
        this[GanttView.taskAreaScrollLeftKey] = this.renderHelper.taskAreaContainerScrollLeft;
    }
    protected onEndUpdateView(): void {
        delete this[GanttView.taskAreaScrollTopKey];
        delete this[GanttView.taskAreaScrollLeftKey];
        delete this[GanttView.taskTextHeightKey];
    }

    get taskAreaContainerScrollTop(): number {
        return this[GanttView.taskAreaScrollTopKey] ?? this.renderHelper.taskAreaContainerScrollTop;
    }
    get taskAreaContainerScrollLeft(): number {
        return this[GanttView.taskAreaScrollLeftKey] ?? this.renderHelper.taskAreaContainerScrollLeft;
    }
    processScroll(isVertical: boolean): void {
        this.hideTaskEditControl();
        this.renderHelper.processScroll(isVertical);
    }

    allowTaskAreaBorders(isVerticalScroll: boolean): boolean {
        return isVerticalScroll ? this.settings.areHorizontalBordersEnabled : this.settings.areVerticalBordersEnabled;
    }

    getScaleItemText(index: number, scale: ViewType): string {
        return this.renderHelper.getScaleItemText(index, scale);
    }
    getTaskText(index: number): string {
        return this.renderHelper.getTaskText(index);
    }
    public rowHasChildren(index: number): boolean {
        const item = this.getViewItem(index);
        return item?.children.length > 0;
    }
    public rowHasSelection(index: number): boolean {
        const item = this.getViewItem(index);
        return item?.selected;
    }
    getAllVisibleTaskIndices(start?: number, end?: number): number[] { return this.viewModel.getAllVisibleTaskIndices(start, end); }
    public getVisibleDependencyKeysByTaskRange(indices: number[]): string[] {
        const model = this.viewModel;
        const taskKeys = indices.map(i => model.tasks.items[i].internalId);
        const dependencies = model.dependencies.items;
        return dependencies.filter(d => taskKeys.indexOf(d.successorId) > -1 || taskKeys.indexOf(d.predecessorId) > -1).map(d => d.internalId);
    }
    getTreeListTableStyle(): Record<string, any> {
        return this.ganttOwner.getTreeListTableStyle?.();
    }
    getTreeListColCount(): number {
        return this.ganttOwner.getTreeListColCount?.();
    }
    getTreeListHeaderInfo(colIndex: number): Record<string, any> {
        return this.ganttOwner.getTreeListHeaderInfo?.(colIndex);
    }
    getTreeListCellInfo(rowIndex: number, colIndex: number, key: any): Record<string, any> {
        return this.ganttOwner.getTreeListCellInfo?.(rowIndex, colIndex, key);
    }
    getTreeListEmptyDataCellInfo(): Record<string, any> {
        return this.ganttOwner.getTreeListEmptyDataCellInfo?.();
    }
    public exportToPdf(options: Record<string, any>): any {
        options["docCreateMethod"] ??= this.getDefaultPdfDocCreateMethod();
        const exporter = new PdfGanttExporter(new GanttExportCalculator(this, options));
        return exporter.export();
    }
    private getDefaultPdfDocCreateMethod(): (prop?: Record<string, any>) => any {
        return window["jspdf"]?.["jsPDF"];
    }

    getTaskDependencies(taskInternalId: any): Array<Dependency> {
        return this.viewModel.dependencies.items.filter(d => d.predecessorId == taskInternalId || d.successorId == taskInternalId);
    }

    isHighlightRowElementAllowed(index: number): boolean {
        const viewItem = this.getViewItem(index);
        return index % 2 !== 0 && this.settings.areAlternateRowsEnabled || viewItem?.children.length > 0;
    }

    calculateAutoViewType(startDate, endDate): ViewType {
        const diffInHours = (endDate.getTime() - startDate.getTime()) / (1000 * 3600);
        if(diffInHours > 24 * 365)
            return ViewType.Years;
        if(diffInHours > 24 * 30)
            return ViewType.Months;
        if(diffInHours > 24 * 7)
            return ViewType.Weeks;
        if(diffInHours > 24)
            return ViewType.Days;
        if(diffInHours > 6)
            return ViewType.SixHours;
        if(diffInHours > 1)
            return ViewType.Hours;
        return ViewType.TenMinutes;
    }

    getExternalTaskAreaContainer(parent: HTMLElement): ITaskAreaContainer {
        return this.ganttOwner.getExternalTaskAreaContainer(parent);
    }
    prepareExternalTaskAreaContainer(element: HTMLElement, info: Record<string, any>): void {
        return this.ganttOwner.prepareExternalTaskAreaContainer(element, info);
    }

    getHeaderHeight(): number {
        return this.ganttOwner.getHeaderHeight();
    }

    changeTaskExpanded(publicId: any, expanded: boolean): void {
        const task = this.getTaskByPublicId(publicId);
        if(task)
            this.viewModel.changeTaskExpanded(task.internalId, expanded);
    }
    expandTask(id: string): void { this.viewModel.changeTaskExpanded(id, true); }
    collapseTask(id: string): void { this.viewModel.changeTaskExpanded(id, false); }
    showTask(id: string): void { this.viewModel.changeTaskVisibility(id, true); }
    hideTask(id: string): void { this.viewModel.changeTaskVisibility(id, false); }
    getTaskVisibility(id: string): boolean { return this.viewModel.getTaskVisibility(id); }
    unselectCurrentSelectedTask(): void { this.unselectTask(this.currentSelectedTaskID); }
    getTaskSelected(id: string): boolean { return this.viewModel.getTaskSelected(id); }

    setViewType(viewType: ViewType, autoPositioning: boolean = true): void {
        this.ganttViewApi.setViewType(viewType, autoPositioning);
    }
    setViewTypeRange(min: ViewType, max: ViewType): void {
        this.ganttViewApi.setViewTypeRange(min, max);
    }
    setTaskTitlePosition(taskTitlePosition: TaskTitlePosition): void {
        if(this.settings.taskTitlePosition !== taskTitlePosition) {
            this.settings.taskTitlePosition = taskTitlePosition;
            this.resetAndUpdate();
        }
    }
    setShowResources(showResources: boolean): void {
        if(this.settings.showResources !== showResources) {
            this.settings.showResources = showResources;
            this.resetAndUpdate();
        }
    }
    toggleResources(): void {
        this.setShowResources(!this.settings.showResources);
    }
    setShowDependencies(showDependencies: boolean): void {
        if(this.settings.showDependencies !== showDependencies) {
            this.settings.showDependencies = showDependencies;
            this.resetAndUpdate();
        }
    }
    toggleDependencies(): void {
        this.setShowDependencies(!this.settings.showDependencies);
    }
    setFirstDayOfWeek(firstDayOfWeek: number): void {
        if(this.settings.firstDayOfWeek !== firstDayOfWeek) {
            this.settings.firstDayOfWeek = firstDayOfWeek;
            this.resetAndUpdate();
        }
    }
    setStartDateRange(start: Date): void {
        if(!DateTimeUtils.areDatesEqual(this.settings.startDateRange, start)) {
            this.settings.startDateRange = new Date(start);
            this.resetAndUpdate();
        }
    }
    setEndDateRange(end: Date): void {
        if(!DateTimeUtils.areDatesEqual(this.settings.endDateRange, end)) {
            this.settings.endDateRange = new Date(end);
            this.resetAndUpdate();
        }
    }
    loadOptionsFromGanttOwner(): void {
        this.tickSize.height = this.ganttOwner.getRowHeight();
        const tasksData = this.ganttOwner.getGanttTasksData();
        this.dataRange = new DateRange(this.getGanttViewStartDate(tasksData), this.getGanttViewEndDate(tasksData));
        if(this.settings.viewType == undefined)
            this.settings.viewType = this.calculateAutoViewType(this.dataRange.start, this.dataRange.end);
        this.updateTickSizeWidth();
        this.range = this.getDateRange(this.dataRange.start, this.dataRange.end);
        this.dispatcher = new ModelChangesDispatcher();
        const modelChangesListener = this.ganttOwner.getModelChangesListener();
        if(modelChangesListener)
            this.dispatcher.onModelChanged.add(modelChangesListener);

        this.viewModel = new ViewVisualModel(this,
            tasksData,
            this.ganttOwner.getGanttDependenciesData(),
            this.ganttOwner.getGanttResourcesData(),
            this.ganttOwner.getGanttResourceAssignmentsData(),
            this.range,
            this.ganttOwner.getGanttWorkTimeRules()
        );
        this.modelManipulator = new ModelManipulator(this.viewModel, this.dispatcher);
        this.history?.historyItems.forEach(i => i.setModelManipulator(this.modelManipulator));
    }
    resetAndUpdate(): void {
        this.range = this.getDateRange(this.dataRange.start, this.dataRange.end);
        this.viewModel.updateRange(this.range);
        this.renderHelper.resetAndUpdate(this.tickSize, this.range, this.settings.viewType, this.viewModel, this.settings.firstDayOfWeek);
        if(Browser.IE)
            this.taskEditController.createElements();
        this.updateView();
    }
    cleanMarkup(): void {
        this.renderHelper.taskAreaManagerDetachEvents();
        this.taskEditController.detachEvents();
        this.clearStripLinesUpdater();
        this.renderHelper.reset();
        clearTimeout(this._scrollTimeOut);
    }
    checkAndProcessModelChanges(): boolean {
        const tasks = this.ganttOwner.getGanttTasksData();
        const changed = this.viewModel.refreshTaskDataIfRequires(tasks);
        if(changed)
            this.resetAndUpdate();
        return changed;
    }

    updateHistoryObsoleteInsertedKey(oldKey: string, newKey: string, type: string): void {
        this.history?.updateObsoleteInsertedKey(oldKey, newKey, type);
        if(type === GanttDataObjectNames.dependency)
            this.renderHelper.updateRenderedConnectorLinesId(oldKey, newKey);
    }

    updateRowHeights(height: number): void {
        if(this.tickSize.height !== height) {
            this.tickSize.height = height;
            const leftPosition = this.renderHelper.getTaskAreaContainerScrollLeft();
            this.resetAndUpdate();
            this.renderHelper.setTaskAreaContainerScrollLeft(leftPosition);
        }
    }
    selectTask(id: string): void {
        this.selectDependency(null);
        this.viewModel.changeTaskSelected(id, true);
        this.currentSelectedTaskID = id;
        this.updateBarManager();
    }
    unselectTask(id: string): void {
        this.viewModel.changeTaskSelected(id, false);
        this.updateBarManager();
    }
    selectTaskById(publicId: any): void {
        this.unselectCurrentSelectedTask();
        const task = this.getTaskByPublicId(publicId);
        if(task)
            this.selectTask(task.internalId);
    }
    selectDependency(id: string): void {
        this.taskEditController.selectDependency(id);
        this.renderHelper.createConnectorLines();
    }
    getTaskAreaContainer(): ITaskAreaContainer {
        return this.renderHelper.taskAreaContainer;
    }
    setWidth(value: number): void {
        this.renderHelper.setMainElementWidth(value);
    }
    setHeight(value: number): void {
        this.renderHelper.setMainElementHeight(value);
    }
    setAllowSelection(value: boolean): void {
        this.settings.allowSelectTask = value;
    }
    setEditingSettings(value: EditingSettings): void {
        this.settings.editing = value;
        this.updateBarManager();
    }
    setValidationSettings(value: ValidationSettings): void {
        this.settings.validation = value;
    }
    setRowLinesVisible(value: boolean): void {
        this.settings.areHorizontalBordersEnabled = value;
        this.renderHelper.prepareTaskAreaContainer();
        this.resetAndUpdate();
    }
    setStripLines(stripLines: StripLineSettings): void {
        this.settings.stripLines = StripLineSettings.parse(stripLines);
        this.clearStripLinesUpdater();
        this.initializeStripLinesUpdater();
        this.renderHelper.recreateStripLines();
    }
    deleteTask(key: any): void {
        const task = this.getTaskByPublicId(key.toString());
        if(task)
            this.commandManager.removeTaskCommand.execute(task.internalId, false, true);
    }
    insertTask(data: any): string {
        if(data) {
            const parentId = data.parentId != null ? String(data.parentId) : null;
            const parent = this.getTaskByPublicId(parentId);
            const rootId = this.viewModel.getRootTaskId();
            const start = typeof data.start === "string" ? new Date(data.start) : data.start as Date;
            const end = typeof data.end === "string" ? new Date(data.end) : data.end as Date;
            const taskData = {
                parentId: rootId && parentId === rootId ? parentId : parent?.internalId,
                title: data.title,
                start: start,
                end: end,
                progress: parseInt(data.progress) || 0,
                color: data.color
            };

            if(this.commandManager.createTaskCommand.execute(taskData))
                return this.getLastInsertedTaskId();
        }
        return "";
    }
    updateTask(key: any, data: Record<string, any>): void {
        const task = this.getTaskByPublicId(key.toString());
        const dataToExecute = this._getTaskDataForUpdate(data, task);
        if(dataToExecute)
            this.commandManager.updateTaskCommand.execute(task.internalId, dataToExecute);
    }
    getTaskData(key: any): any {
        const task = this.getTaskByPublicId(key.toString());
        if(task)
            return this.viewModel.getTaskObjectForDataSource(task);
    }
    insertDependency(data: any): void {
        if(!data)
            return;
        const predecessorId = String(data.predecessorId);
        const predecessor = this.getTaskByPublicId(predecessorId);
        const successorId = String(data.successorId);
        const successor = this.getTaskByPublicId(successorId);
        const type = data.type as DependencyType;
        if(predecessor && successor && this.validationController.canCreateDependency(predecessorId, successorId))
            this.commandManager.createDependencyCommand.execute(predecessor.internalId, successor.internalId, type);

    }
    deleteDependency(key: any): void {
        const internalKey = this.viewModel.convertPublicToInternalKey("dependency", key);
        if(isDefined(internalKey))
            this.commandManager.removeDependencyCommand.execute(internalKey);
    }
    getDependencyData(key: any): any {
        return this.viewModel.getDependencyObjectForDataSource(key);
    }
    insertResource(data: any, taskKeys: any[]): void {
        if(data) {
            const callback = (id: any) => {
                if(isDefined(taskKeys))
                    for(let i = 0; i < taskKeys.length; i++)
                        this.assignResourceToTask(id, taskKeys[i]);
            };
            this.commandManager.createResourceCommand.execute(String(data.text), data.color && String(data.color), callback);
        }
    }
    deleteResource(key: any): void {
        const internalKey = this.viewModel.convertPublicToInternalKey("resource", key);
        if(isDefined(internalKey))
            this.commandManager.removeResourceCommand.execute(internalKey);
    }
    assignResourceToTask(resourceKey: any, taskKey: any): void {
        const resourceInternalKey = this.viewModel.convertPublicToInternalKey("resource", resourceKey);
        const taskInternalKey = this.viewModel.convertPublicToInternalKey("task", taskKey);
        if(isDefined(resourceInternalKey) && isDefined(taskInternalKey))
            this.commandManager.assignResourceCommand.execute(resourceInternalKey, taskInternalKey);
    }
    unassignResourceFromTask(resourceKey: any, taskKey: any): void {
        const assignment = this.viewModel.findAssignment(resourceKey, taskKey);
        if(assignment)
            this.commandManager.deassignResourceCommand.execute(assignment.internalId);
    }
    unassignAllResourcesFromTask(taskPublicKey: any): void {
        const taskInternalKey = this.viewModel.convertPublicToInternalKey("task", taskPublicKey);
        const assignments = this.viewModel.findAllTaskAssignments(taskInternalKey);
        assignments.forEach(assignment => this.commandManager.deassignResourceCommand.execute(assignment.internalId));
    }
    getResourceData(key: any): any {
        return this.viewModel.getResourceObjectForDataSource(key);
    }
    getResourceAssignmentData(key: any): any {
        return this.viewModel.getResourceAssignmentObjectForDataSource(key);
    }
    getTaskResources(key): Array<Resource> {
        const model = this.viewModel;
        const task = model.getItemByPublicId("task", key) as Task;
        return task && model.getAssignedResources(task).items;
    }
    getVisibleTaskKeys(): Array<any> { return this.viewModel.getVisibleTasks().map(t => t.id); }
    getVisibleDependencyKeys(): Array<any> { return this.viewModel.getVisibleDependencies().map(d => d.id); }
    getVisibleResourceKeys(): Array<any> { return this.viewModel.getVisibleResources().map(r => r.id); }
    getVisibleResourceAssignmentKeys(): Array<any> { return this.viewModel.getVisibleResourceAssignments().map(a => a.id); }

    getTasksExpandedState(): Record<any, boolean> { return this.viewModel.getTasksExpandedState(); }
    applyTasksExpandedState(state: Record<any, boolean>): void { this.viewModel.applyTasksExpandedState(state); }
    updateWithDataReload(keepExpandState: boolean): void {
        if(this._updateWithModelReloadLockedCounter > 0) {
            this._pendingUpdateInfo = { keepExpandState: keepExpandState };
            return;
        }
        const state = keepExpandState && this.getTasksExpandedState();
        this.loadOptionsFromGanttOwner();

        if(keepExpandState)
            this.applyTasksExpandedState(state);
        else
            this.resetAndUpdate();

        const activeDialog = DialogBase.activeInstance;
        if(activeDialog && activeDialog.canRefresh && activeDialog.getDialogName() === "TaskEdit")
            activeDialog.refresh();
    }
    onBrowserWindowResize(): void {
        if(this.fullScreenModeHelper.isInFullScreenMode)
            this.fullScreenModeHelper.adjustControlInFullScreenMode();
        else
            this.adjustOwnerControl();
    }

    setTaskValue(id: string, fieldName: string, newValue: any): boolean {
        const command = this.commandManager.updateTaskCommand;
        const task = this.getTaskByPublicId(id);
        const data = { };
        if(task) {
            if(fieldName === "title")
                data[fieldName] = newValue ? newValue : "";
            if(fieldName === "progress")
                data[fieldName] = newValue;
            if(fieldName === "start")
                data[fieldName] = DateTimeUtils.getMinDate(newValue, task.end);
            if(fieldName === "end")
                data[fieldName] = DateTimeUtils.getMaxDate(newValue, task.start);
        }
        return Object.keys(data).length > 0 ? command.execute(task.internalId, data) : false;
    }

    getLastInsertedTaskId(): string {
        const createTaskItems = this.history.historyItems.filter(i => i instanceof CreateTaskHistoryItem);
        const lastItem = createTaskItems[createTaskItems.length - 1] as CreateTaskHistoryItem;
        return lastItem && lastItem.insertedKey;
    }

    getTaskByPublicId(id: string): Task {
        return this.viewModel.tasks.getItemByPublicId(id);
    }
    getPrevTask(taskId: string): Task {
        const item = this.viewModel.findItem(taskId);
        const parent = item.parent || this.viewModel.root;
        const index = parent.children.indexOf(item) - 1;
        return index > -1 ? item.parent.children[index].task : item.parent.task;
    }
    getTaskIdByInternalId(internalId : string): string {
        const item = this.viewModel.findItem(internalId);
        const task = item && item.task;
        return task ? task.id : null;
    }
    isTaskHasChildren(taskId: string): boolean {
        const item = this.viewModel.findItem(taskId);
        return item && item.children.length > 0;
    }
    requireFirstLoadParentAutoCalc(): boolean {
        const owner = this.ganttOwner;
        return owner.getRequireFirstLoadParentAutoCalc && owner.getRequireFirstLoadParentAutoCalc();
    }
    public updateOwnerInAutoParentMode(): void {
        if(this.viewModel.parentAutoCalc)
            this.dispatcher.notifyParentDataRecalculated(this.viewModel.getCurrentTaskData());
    }

    getOwnerControlMainElement(): HTMLElement {
        const owner = this.ganttOwner;
        return owner.getMainElement && owner.getMainElement();
    }
    adjustOwnerControl(): void {
        const owner = this.ganttOwner;
        if(owner.adjustControl)
            owner.adjustControl();
    }

    applySettings(settings: any, preventViewUpdate: boolean = false): void {
        const ganttSettings = Settings.parse(settings);
        const preventUpdate = preventViewUpdate || this.settings.equal(ganttSettings);
        this.settings = ganttSettings;
        if(!preventUpdate)
            this.resetAndUpdate();
    }

    getDataUpdateErrorCallback(): () => void {
        const history = this.history;
        const currentHistoryItemInfo = history.getCurrentProcessingItemInfo();
        return () => {
            this.dispatcher.lock();
            history.rollBackAndRemove(currentHistoryItemInfo);
            this.dispatcher.unlock();
            this.updateBarManager();
        };
    }

    setTaskTooltipContentTemplate(taskTooltipContentTemplate: () => void): void {
        this.settings.taskTooltipContentTemplate = taskTooltipContentTemplate;
    }

    setTaskProgressTooltipContentTemplate(taskProgressTooltipContentTemplate: () => void): void {
        this.settings.taskProgressTooltipContentTemplate = taskProgressTooltipContentTemplate;
    }

    setTaskTimeTooltipContentTemplate(taskTimeTooltipContentTemplate: () => void): void {
        this.settings.taskTimeTooltipContentTemplate = taskTimeTooltipContentTemplate;
    }

    setTaskContentTemplate(taskContentTemplate: () => boolean): void {
        this.settings.taskContentTemplate = taskContentTemplate;
    }

    updateBarManager(): void {
        this.barManager.updateItemsState([]);
    }

    onTaskAreaClick(rowIndex: number, evt: Event): boolean {
        const clickedItem = this.viewModel.items[rowIndex];
        return clickedItem && this.onTaskClick(clickedItem.task?.id, evt);
    }
    onTaskAreaDblClick(rowIndex: number, evt: Event): void {
        const clickedItem = this.viewModel.items[rowIndex];
        if(clickedItem && this.onTaskDblClick(clickedItem.task.id, evt))
            this.commandManager.showTaskEditDialog.execute(clickedItem.task);
    }
    onTaskAreaContextMenu(rowIndex: number, evt: Event, type: string): void {
        const isDependency = type === "dependency";
        const event = evt as MouseEvent | TouchEvent;
        const model = this.viewModel;
        const key = isDependency ? model.convertInternalToPublicKey("dependency", EvtUtils.getEventSource(evt).getAttribute("dependency-id")) : model.items[rowIndex]?.task?.id;
        if(this.onGanttViewContextMenu(evt, key, type)) {
            const info = {
                event: evt,
                type: type,
                key: key,
                position: new Point(EvtUtils.getEventX(event), EvtUtils.getEventY(event))
            };
            this.showPopupMenu(info);
        }
    }
    onTaskSelectionChanged(rowIndex: number, evt: Event): void {
        const clickedTask = this.viewModel.items[rowIndex];
        this.isFocus = DomUtils.isItParent(this.renderHelper.taskArea, EvtUtils.getEventSource(evt));
        if(clickedTask && this.isFocus && this.settings.allowSelectTask)
            setTimeout(() => { this.changeGanttTaskSelection(clickedTask.task.id, true); }, 0);
    }
    onTaskClick(key: any, evt: any): boolean {
        if(!this.ganttOwner.onTaskClick)
            return true;
        return this.ganttOwner.onTaskClick(key, evt);
    }
    onTaskDblClick(key: any, evt: any): boolean {
        if(!this.ganttOwner.onTaskDblClick)
            return true;
        return this.ganttOwner.onTaskDblClick(key, evt);
    }

    getDateFormat(date: Date): string {
        return this.ganttOwner.getFormattedDateText ? this.ganttOwner.getFormattedDateText(date) : this.getDefaultDateFormat(date);
    }
    getDefaultDateFormat(date: Date): string {
        return ("0" + date.getDate()).slice(-2) + "/" + ("0" + (date.getMonth() + 1)).slice(-2) + "/" + date.getFullYear() + " " + ("0" + date.getHours()).slice(-2) + ":" + ("0" + date.getMinutes()).slice(-2);
    }

    destroyTemplate(container: HTMLElement): void {
        this.ganttOwner.destroyTemplate ? this.ganttOwner.destroyTemplate(container) : container.innerHTML = "";
    }
    onTaskAreaSizeChanged(info: Record<string, any>): void {
        if(this.ganttOwner.onTaskAreaSizeChanged)
            this.ganttOwner.onTaskAreaSizeChanged(info);
    }

    showTaskEditDialog(): void {
        this.commandManager.showTaskEditDialog.execute();
    }

    showTaskDetailsDialog(taskPublicKey: any): void {
        const task = this.getTaskByPublicId(taskPublicKey);
        if(task)
            this.commandManager.showTaskEditDialog.execute(task, true);

    }
    showResourcesDialog(): void {
        this.commandManager.showResourcesDialog.execute();
    }
    getCommandByKey(key: GanttClientCommand): ICommand {
        return this.commandManager.getCommand(key);
    }
    private _getTaskDataForUpdate(data: Record<string, any>, task: Task): Record<string, any> {
        const result = { };
        if(task && data) {
            if(isDefined(data.title) && data.title !== task.title)
                result["title"] = data.title;
            if(isDefined(data.progress) && data.progress !== task.progress)
                result["progress"] = data.progress;
            if(isDefined(data.start) && data.start !== task.start)
                result["start"] = data.start;
            if(isDefined(data.end) && data.end !== task.end)
                result["end"] = data.end;
            if(isDefined(data.color) && data.color !== task.color)
                result["color"] = data.color;
        }
        return Object.keys(result).length > 0 ? result : null;
    }

    public updateViewDataRange(): void {
        const model = this.viewModel;
        const minStart = model.getTaskMinStart();
        const maxEnd = model.getTaskMaxEnd();
        const startChanged = minStart.getTime() < this.dataRange.start.getTime();
        const endChanged = maxEnd.getTime() > this.dataRange.end.getTime();
        if(startChanged)
            this.dataRange.start = minStart;
        if(endChanged)
            this.dataRange.end = maxEnd;
        if(startChanged || endChanged)
            this.resetAndUpdate();
    }
}
