import { Size } from "@devexpress/utils/lib/geometry/size";
import { DomUtils } from "@devexpress/utils/lib/utils/dom";
import { ITaskAreaContainer } from "../../Interfaces/ITaskAreaContainer";
import { DateRange } from "../../Model/WorkingTime/DateRange";
import { GanttView } from "../../View/GanttView";
import { ViewType } from "../../View/Helpers/Enums";
import { GridElementInfo } from "../../View/Helpers/GridElementInfo";
import { GridLayoutCalculator } from "../../View/Helpers/GridLayoutCalculator";
import { Settings } from "../../View/Settings/Settings";
import { DateUtils } from "../../View/Utils/DateUtils";
import { IPdfExportRangeProps } from "./Settings/Props";
import { Color } from "./Table/Color";
import { DataExportMode } from "./Settings/Enums";
import { PdfDataRange } from "./Settings/DataRange";

export class TaskAreaExportHelper {
    private _owner: GanttView;

    private _visibleLeft: number;
    private _visibleRight: number;
    private _visibleTop: number;
    private _visibleBottom: number;
    private _scaleLeft: number;
    private _scaleRight: number;
    private _scaleTopWidths: Array<number>;
    private _scaleBottomWidths: Array<number>;
    private _scaleRanges: number[][];
    private _offsetLeft: number;
    private _offsetTop: number;
    private _headerRowHeight: number;
    private _arrowWidth: number;
    private _customRangeLeftOffset: number;
    private _visibleTaskIndices: number[];
    private _visibleDependencyKeys: string[];

    private _connectorLines: Array<GridElementInfo>;
    private _resourcesElements: Array<HTMLElement>;

    private _stripLinesElements: Array<HTMLElement>;
    private _noWorkingIntervalsElements: Array<HTMLElement>;

    private _scaleTableStyle: Record<string, any>
    private _chartMainTableStyle: Record<string, any>
    private _parentRowBackColor: string;
    private _dependencyColor: string;

    private _props: IPdfExportRangeProps;

    private _layoutCalculator: GridLayoutCalculator;

    constructor(owner: GanttView, props: IPdfExportRangeProps) {
        this._owner = owner;
        this._props = props;
    }

    public get customRangeLeftOffset(): number {
        this._customRangeLeftOffset ??= this.layoutCalculator.getWidthByDateRange(this.startDate, this.ownerStartDate);
        return this._customRangeLeftOffset;
    }
    public get baseCellSize(): Size { return this._owner.tickSize; }
    public get objectsLeftDelta(): number {
        return this.renderedScaleLeft;
    }
    public get objectsTopDelta(): number {
        if(!this.hasTasks)
            return 0;
        const firstIndex = this.visibleTaskIndices[0];
        return this.getCellTop(firstIndex) + this.getTaskCellOffsetTop(firstIndex);
    }
    public get offsetLeft(): number {
        this._offsetLeft ??= Math.max(this.visibleLeft - this.renderedScaleLeft, 0);
        return this._offsetLeft;
    }
    public get offsetTop(): number {
        this._offsetTop ??= this.getOffsetTop();
        return this._offsetTop;
    }
    public get scales(): number[] {
        const viewType = this.settings.viewType;
        return [DateUtils.ViewTypeToScaleMap[viewType], viewType];
    }
    public get scaleRanges(): number[][] {
        this._scaleRanges ??= this.layoutCalculator.getScaleRangesInArea(this.scaleLeft, this.scaleRight);
        return this._scaleRanges;
    }
    public get scaleBottomStartIndex(): number {
        return this.scaleRanges[1][0];
    }
    public get scaleBottomEndIndex(): number {
        return this.scaleRanges[1][1];
    }
    public get scaleTopStartIndex(): number {
        return this.scaleRanges[0][0];
    }
    public get scaleTopEndIndex(): number {
        return this.scaleRanges[0][1];
    }
    public get scaleTopWidths(): Array<number> {
        this._scaleTopWidths ??= this.getScaleTopWidths();
        return this._scaleTopWidths;
    }
    public get scaleBottomWidths(): Array<number> {
        this._scaleBottomWidths ??= this.getScaleBottomWidths();
        return this._scaleBottomWidths;
    }

    public get headerRowHeight(): number {
        if(!this._headerRowHeight) {
            const element = this.scaleElements[0].filter(el => !!el)[0];
            this._headerRowHeight = element?.offsetHeight;
        }
        return this._headerRowHeight;
    }
    public get visibleTaskIndices(): number[] {
        this._visibleTaskIndices ??= this.getTaskIndices();
        return this._visibleTaskIndices;
    }
    public get baseCellHeight(): number {
        return this.hasTasks ? this.baseCellSize.height : this.taskAreaHeight;
    }
    public get taskAreaHeight(): number {
        return this.hasTasks ? this.visibleTaskIndices.length * this.baseCellHeight : this._owner.renderHelper.taskArea.offsetHeight;
    }

    public get scaleTableStyle(): Record<string, any> {
        this._scaleTableStyle ??= this.getScaleTableStyle();
        return this._scaleTableStyle;
    }
    public get chartMainTableStyle(): Record<string, any> {
        this._chartMainTableStyle ??= this.getChartMainTableStyle();
        return this._chartMainTableStyle;
    }
    public get parentRowBackColor(): string {
        this._parentRowBackColor ??= this.getParentBackColor();
        return this._parentRowBackColor;
    }
    public get arrowWidth(): number {
        this._arrowWidth ??= this.getArrowWidth();
        return this._arrowWidth;
    }
    public get dependencyColor(): string {
        this._dependencyColor ??= this.getDependencyColor();
        return this._dependencyColor;
    }

    public getTaskElementBackColor(taskIndex: number, className: string): string {
        const style = this.getTaskElementStyle(taskIndex, className);
        return style?.backgroundColor;
    }
    public getTaskElementStyle(taskIndex: number, className: string): CSSStyleDeclaration {
        const taskWrapper = this.getTaskWrapper(taskIndex);
        return this.getElementStyle(taskWrapper.getElementsByClassName(className)[0]);
    }

    protected get hasTasks(): boolean {
        return this.visibleTaskIndices.length > 0;
    }
    protected get visibleLeft(): number {
        this._visibleLeft ??= this.isVisibleMode ? this.container.scrollLeft : 0;
        return this._visibleLeft;
    }
    protected get visibleTop(): number {
        this._visibleTop ??= this.isVisibleMode ? this.container.scrollTop : 0;
        return this._visibleTop;
    }
    protected get visibleRight(): number {
        this._visibleRight ??= this.getVisibleRight();
        return this._visibleRight;
    }
    protected getVisibleRight(): number {
        const width = this.container.getElement().offsetWidth;
        return this.visibleLeft + width;
    }
    protected get visibleBottom(): number {
        this._visibleBottom ??= this.getVisibleBottom();
        return this._visibleBottom;
    }
    protected getVisibleBottom(): number {
        if(!this.isVisibleMode)
            return this.visibleTaskIndices.length * this.baseCellSize.height;
        return this.visibleTop + this.container.getHeight();
    }

    protected get scaleLeft(): number {
        this._scaleLeft ??= this.isVisibleMode ? this.visibleLeft : this.getPosByDate(this.startDate);
        return this._scaleLeft;
    }
    protected get scaleRight(): number {
        this._scaleRight ??= this.isVisibleMode ? this.visibleRight : this.getPosByDate(this.endDate) - 1;
        return this._scaleRight;
    }

    protected getScaleTopWidths(): Array<number> {
        const widths = this.getScaleWidths(this.scaleTopStartIndex, this.scaleTopEndIndex, this.scales[0]);
        const calc = this.layoutCalculator;

        const firstBottom = calc.getScaleItemInfo(this.scaleBottomStartIndex, this.scales[1]);
        const firstTop = calc.getScaleItemInfo(this.scaleTopStartIndex, this.scales[0]);
        let delta = Math.max(firstBottom.position.x - firstTop.position.x, 0);
        widths[this.scaleTopStartIndex] -= delta;

        const lastTop = calc.getScaleItemInfo(this.scaleTopEndIndex, this.scales[0]);
        const lastBottom = calc.getScaleItemInfo(this.scaleBottomEndIndex, this.scales[1]);
        delta = Math.max(lastTop.position.x + lastTop.size.width - lastBottom.position.x - lastBottom.size.width, 0);
        widths[this.scaleTopEndIndex] -= delta;
        return widths;
    }

    protected getScaleBottomWidths(): Array<number> {
        return this.getScaleWidths(this.scaleBottomStartIndex, this.scaleBottomEndIndex, this.scales[1]);
    }
    protected getScaleWidths(start: number, end: number, scaleType: ViewType): Array<number> {
        const widths = new Array<number>();
        for(let i = start; i <= end; i++)
            widths[i] = this.layoutCalculator.getScaleItemInfo(i, scaleType).size.width;
        return widths;
    }
    protected getOffsetTop(): number {
        return this.isVisibleMode && this.hasTasks ? this.getTaskCellOffsetTop(this.visibleTaskIndices[0]) : 0;
    }
    protected get renderedScaleLeft(): number {
        return this.getCellLeft(this.scaleBottomStartIndex);
    }
    protected getTaskCellOffsetTop(taskIndex: number): number {
        const point = this.getCellTop(taskIndex);
        return Math.max(this.visibleTop - point, 0);
    }
    protected getCellTop(index: number): number {
        const point = this.layoutCalculator.getGridBorderPosition(index - 1, false);
        return point.y;
    }
    protected getCellLeft(index: number): number {
        const point = this.layoutCalculator.getScaleItemInfo(index, this.scales[1]).position;
        return point.x;
    }
    protected getTaskIndices(): Array<number> {
        if(this.dataMode === DataExportMode.all || this.exportRange)
            return this._owner.getAllVisibleTaskIndices(this.exportRange?.startIndex, this.exportRange?.endIndex);
        return this.getVisibleTaskIndices();
    }
    protected getVisibleTaskIndices(): Array<number> {
        const indices = [];
        this.taskElements.forEach((t, i) => {
            if(t) {
                const taskTop = DomUtils.pxToInt(t.style.top);
                const taskBottom = taskTop + t.offsetHeight;
                const topInArea = taskTop >= this.visibleTop && taskTop <= this.visibleBottom;
                const bottomInArea = taskBottom >= this.visibleTop && taskBottom <= this.visibleBottom;
                if(topInArea || bottomInArea)
                    indices.push(i);
            }
        });
        return indices;
    }

    protected get scaleElements(): Array<Array<HTMLElement>> {
        return this._owner.renderHelper.scaleElements.slice();
    }
    protected get scaleBorders(): Array<Array<HTMLElement>> {
        return this._owner.renderHelper.scaleBorders.slice();
    }
    protected get hlRowElements(): Array<HTMLElement> {
        return this._owner.renderHelper.hlRowElements.filter(el => !!el);
    }
    protected get selectionElements(): Array<HTMLElement> {
        return this._owner.renderHelper.selectionElements.filter(el => !!el);
    }
    protected get taskElements(): Array<HTMLElement> {
        return this._owner.renderHelper.taskElements;
    }
    public get connectorLines(): Array<GridElementInfo> {
        this._connectorLines ??= this._owner.renderHelper.allConnectorLines.filter(l => this.isLineVisible(l));
        return this._connectorLines;
    }
    protected isLineVisible(line: GridElementInfo): boolean {
        if(this.dataMode === DataExportMode.all)
            return true;
        const id = line.attr["dependency-id"];
        return this.visibleDependencyKeys.indexOf(id) > -1;
    }
    protected get visibleDependencyKeys(): string[] {
        this._visibleDependencyKeys ??= this._owner.getVisibleDependencyKeysByTaskRange(this.visibleTaskIndices);
        return this._visibleDependencyKeys;
    }
    public get resourcesElements(): Array<HTMLElement> {
        this._resourcesElements ??= this.visibleTaskIndices.map(tIndex => this._owner.renderHelper.resourcesElements[tIndex]).filter(r => r && r.parentElement); 
        return this._resourcesElements;
    }
    public get stripLinesElements(): Array<HTMLElement> {
        if(!this._stripLinesElements) {
            const elements = this._owner.renderHelper.stripLinesMap.filter(s =>!!s).map(s => s as unknown);
            this._stripLinesElements = elements.map(e => e as HTMLElement);
        }
        return this._stripLinesElements;
    }
    public get noWorkingIntervalsElements(): Array<HTMLElement> {
        if(!this._noWorkingIntervalsElements) {
            this._noWorkingIntervalsElements = [ ];
            const hash = this._owner.renderHelper.noWorkingIntervalsToElementsMap;
            for(const key in hash) {
                if(!Object.prototype.hasOwnProperty.call(hash, key))
                    continue;
                this._noWorkingIntervalsElements.push(hash[key]);
            }
        }
        return this._noWorkingIntervalsElements;
    }
    protected get taskArea(): HTMLElement {
        return this._owner.renderHelper.taskArea;
    }

    protected get settings(): Settings { return this._owner.settings; }

    protected get dataMode(): DataExportMode {
        return this._props?.exportDataMode;
    }
    protected get exportRange(): PdfDataRange {
        return this._props?.dateRange;
    }
    protected get isVisibleMode(): boolean {
        return this.dataMode === DataExportMode.visible && !this.exportRange;
    }
    protected get ownerStartDate(): Date {
        return this._owner.range.start;
    }
    protected get ownerEndDate(): Date {
        return this._owner.range.end;
    }
    protected get startDate(): Date {
        if(this.exportRange?.startDate && this.exportRange?.endDate) {
            const min = Math.min(this.exportRange?.startDate.getTime(), this.exportRange?.endDate.getTime());
            return new Date(min);
        }
        return this.ownerStartDate;
    }
    protected get endDate(): Date {
        if(this.exportRange?.startDate && this.exportRange?.endDate) {
            const max = Math.max(this.exportRange?.startDate.getTime(), this.exportRange?.endDate.getTime());
            return new Date(max);
        }
        return this.ownerEndDate;
    }
    protected get hasCustomRangeOutOfRender(): boolean {
        return this.startDate.getTime() !== this.ownerStartDate.getTime() || this.endDate.getTime() !== this.ownerEndDate.getTime();
    }

    public get layoutCalculator(): GridLayoutCalculator {
        if(!this._layoutCalculator) {
            const calc = this._owner.renderHelper.gridLayoutCalculator;
            if(this.hasCustomRangeOutOfRender) {
                this._layoutCalculator = new GridLayoutCalculator();
                this._layoutCalculator.setSettings(
                    calc.visibleTaskAreaSize,
                    calc.tickSize,
                    calc.elementSizeValues,
                    new DateRange(this.startDate, this.endDate),
                    calc.viewModel,
                    calc.viewType,
                    calc.scrollBarHeight,
                    this._owner.settings.firstDayOfWeek
                );
            }
            else
                this._layoutCalculator = calc;
        }
        return this._layoutCalculator;
    }
    protected get container(): ITaskAreaContainer {
        return this._owner.renderHelper.taskAreaContainer;
    }
    protected getPosByDate(date: Date): number {
        return this.layoutCalculator.getPosByDate(date);
    }
    protected getScaleTableStyle(): Record<string, any> { 
        const result = { };

        const visibleScaleItem = this.scaleElements[0].filter(el => !!el)[0];
        const style = this.getElementStyle(visibleScaleItem);

        result["backgroundColor"] = this.findElementBackColor(visibleScaleItem);
        result["borderColor"] = this.getChartTableBorderColor();
        result["verticalAlign"] = "middle";
        result["textAlign"] = "center";

        result["fontSize"] = style.fontSize;
        result["fontFamily"] = style.fontFamily;
        result["fontWeight"] = style.fontWeight;
        result["fontStyle"] = style.fontStyle;
        result["color"] = style.color;

        return result;
    }
    protected getChartMainTableStyle(): Record<string, any> {
        const result = { };
        result["backgroundColor"] = this.findElementBackColor(this.taskArea);
        result["borderColor"] = this.getChartTableBorderColor();
        return result;
    }
    protected findElementBackColor(element: Element): Color {
        if(!element)
            return null;
        let parent = element;
        const color = new Color("transparent");
        while(color.opacity === 0 && parent) {
            const style = this.getElementStyle(parent);
            color.assign(style.backgroundColor);
            parent = parent.parentElement;
        }
        return color;
    }
    protected getChartTableBorderColor(): string {
        const style = this.getElementStyle(this.scaleBorders[0].filter(el => !!el)[0]);
        return style?.borderColor;
    }
    protected getParentBackColor(): string {
        const style = this.getElementStyle(this.hlRowElements[0]);
        return style?.backgroundColor;
    }

    protected getArrowWidth(): number {
        const style = this.getDependencyLineStyle(GridLayoutCalculator.arrowClassName);
        const borderWidth = style.borderWidth || style.borderLeftWidth || style.borderRightWidth || style.borderTopWidth || style.borderBottomWidth;
        return style && DomUtils.pxToInt(borderWidth);
    }
    protected getDependencyColor(): string {
        const style = this.getDependencyLineStyle(GridLayoutCalculator.CLASSNAMES.CONNECTOR_HORIZONTAL);
        return style?.borderColor;
    }
    protected getDependencyLineStyle(className: string): CSSStyleDeclaration {
        return this.getElementStyle(this.taskArea.getElementsByClassName(className)[0]);
    }
    protected getElementStyle(element: Element): CSSStyleDeclaration {
        return element && getComputedStyle(element);
    }
    protected getTaskWrapper(index: number): HTMLElement {
        if(this.isTaskTemplateMode)
            return this._owner.renderHelper.fakeTaskWrapper;
        if(!this.taskElements[index])
            this._owner.renderHelper.createDefaultTaskElement(index);
        return this.taskElements[index];
    }
    protected get isTaskTemplateMode(): boolean {
        return !!this._owner.settings.taskContentTemplate;
    }
}
