import { Doc, Field, FieldResult } from '../../../../fields/Doc';
import { Id, ToString } from '../../../../fields/FieldSymbols';
import { ObjectField } from '../../../../fields/ObjectField';
import { RefField } from '../../../../fields/RefField';
import { listSpec } from '../../../../fields/Schema';
import { Cast, NumCast, StrCast } from '../../../../fields/Types';
import { aggregateBounds } from '../../../../Utils';
import * as React from 'react';

export interface ViewDefBounds {
    type: string;
    payload: any;
    x: number;
    y: number;
    z?: number;
    rotation?: number;
    text?: string;
    zIndex?: number;
    width?: number;
    height?: number;
    transition?: string;
    fontSize?: number;
    highlight?: boolean;
    color?: string;
    opacity?: number;
    replica?: string;
    pair?: { layout: Doc; data?: Doc };
}

export interface PoolData {
    pair: { layout: Doc; data?: Doc };
    replica: string;
    x: number;
    y: number;
    z?: number;
    rotation?: number;
    zIndex?: number;
    width?: number;
    height?: number;
    autoDim?: number; // 1 for set to Panel dims, 0 for use width/height as entered
    backgroundColor?: string;
    color?: string;
    opacity?: number;
    transition?: string;
    highlight?: boolean;
    pointerEvents?: string;
}

export interface ViewDefResult {
    ele: JSX.Element;
    bounds?: ViewDefBounds;
    inkMask?: number; //sort elements into either the mask layer (which has a mixedBlendMode appropriate for transparent masks), or the regular documents layer;   -1 = no mask, 0 = mask layer but stroke is transparent (hidden, as in during a presentation when you want to smoothly animate it into being a mask), >0 = mask layer and not hidden
}
function toLabel(target: FieldResult<Field>) {
    if (typeof target === 'number' || Number(target)) {
        const truncated = Number(Number(target).toFixed(0));
        const precise = Number(Number(target).toFixed(2));
        return truncated === precise ? Number(target).toFixed(0) : Number(target).toFixed(2);
    }
    if (target instanceof ObjectField || target instanceof RefField) {
        return target[ToString]();
    }
    return String(target);
}
/**
 * Uses canvas.measureText to compute and return the width of the given text of given font in pixels.
 *
 * @param {String} text The text to be rendered.
 * @param {String} font The css font descriptor that text is to be rendered with (e.g. "bold 14px verdana").
 *
 * @see https://stackoverflow.com/questions/118241/calculate-text-width-with-javascript/21015393#21015393
 */
function getTextWidth(text: string, font: string): number {
    // re-use canvas object for better performance
    const canvas = (getTextWidth as any).canvas || ((getTextWidth as any).canvas = document.createElement('canvas'));
    const context = canvas.getContext('2d');
    context.font = font;
    const metrics = context.measureText(text);
    return metrics.width;
}

interface PivotColumn {
    docs: Doc[];
    replicas: string[];
    filters: string[];
}

export function computePassLayout(poolData: Map<string, PoolData>, pivotDoc: Doc, childPairs: { layout: Doc; data?: Doc }[], panelDim: number[], viewDefsToJSX: (views: ViewDefBounds[]) => ViewDefResult[], engineProps: any) {
    const docMap = new Map<string, PoolData>();
    childPairs.forEach(({ layout, data }, i) => {
        docMap.set(layout[Id], {
            x: NumCast(layout.x),
            y: NumCast(layout.y),
            width: NumCast(layout._width),
            height: NumCast(layout._height),
            pair: { layout, data },
            transition: 'all .3s',
            replica: '',
        });
    });
    return normalizeResults(panelDim, 12, docMap, poolData, viewDefsToJSX, [], 0, []);
}

export function computeStarburstLayout(poolData: Map<string, PoolData>, pivotDoc: Doc, childPairs: { layout: Doc; data?: Doc }[], panelDim: number[], viewDefsToJSX: (views: ViewDefBounds[]) => ViewDefResult[], engineProps: any) {
    const docMap = new Map<string, PoolData>();
    const burstDiam = [NumCast(pivotDoc._width), NumCast(pivotDoc._height)];
    const burstScale = NumCast(pivotDoc._starburstDocScale, 1);
    childPairs.forEach(({ layout, data }, i) => {
        const aspect = NumCast(layout._height) / NumCast(layout._width);
        const docSize = Math.min(Math.min(400, NumCast(layout._width)), Math.min(400, NumCast(layout._width)) / aspect) * burstScale;
        const deg = (i / childPairs.length) * Math.PI * 2;
        docMap.set(layout[Id], {
            x: Math.min(burstDiam[0] / 2 - docSize, Math.max(-burstDiam[0] / 2, (Math.cos(deg) * burstDiam[0]) / 2 - docSize / 2)),
            y: Math.min(burstDiam[1] / 2 - docSize * aspect, Math.max(-burstDiam[1] / 2, (Math.sin(deg) * burstDiam[1]) / 2 - (docSize / 2) * aspect)),
            width: docSize,
            height: docSize * aspect,
            zIndex: NumCast(layout.zIndex),
            pair: { layout, data },
            replica: '',
            color: 'white',
            backgroundColor: 'white',
            transition: 'all 0.3s',
        });
    });
    const divider = { type: 'div', color: 'transparent', x: -burstDiam[0] / 2, y: -burstDiam[1] / 2, width: 15, height: 15, payload: undefined };
    return normalizeResults(burstDiam, 12, docMap, poolData, viewDefsToJSX, [], 0, [divider]);
}

export function computePivotLayout(poolData: Map<string, PoolData>, pivotDoc: Doc, childPairs: { layout: Doc; data?: Doc }[], panelDim: number[], viewDefsToJSX: (views: ViewDefBounds[]) => ViewDefResult[], engineProps: any) {
    const docMap = new Map<string, PoolData>();
    const fieldKey = 'data';
    const pivotColumnGroups = new Map<FieldResult<Field>, PivotColumn>();

    let nonNumbers = 0;
    const pivotFieldKey = toLabel(engineProps?.pivotField ?? pivotDoc._pivotField) || 'author';
    childPairs.map(pair => {
        const listValue = Cast(pair.layout[pivotFieldKey], listSpec('string'), null);

        const num = toNumber(pair.layout[pivotFieldKey]);
        if (num === undefined || Number.isNaN(num)) {
            nonNumbers++;
        }
        const val = Field.toString(pair.layout[pivotFieldKey] as Field);
        if (listValue) {
            listValue.forEach((val, i) => {
                !pivotColumnGroups.get(val) && pivotColumnGroups.set(val, { docs: [], filters: [val], replicas: [] });
                pivotColumnGroups.get(val)!.docs.push(pair.layout);
                pivotColumnGroups.get(val)!.replicas.push(i.toString());
            });
        } else if (val) {
            !pivotColumnGroups.get(val) && pivotColumnGroups.set(val, { docs: [], filters: [val], replicas: [] });
            pivotColumnGroups.get(val)!.docs.push(pair.layout);
            pivotColumnGroups.get(val)!.replicas.push('');
        } else {
            docMap.set(pair.layout[Id], {
                x: 0,
                y: 0,
                zIndex: 0,
                width: 0, // should make doc hidden in CollectionFreeFormDocumentView
                height: 0,
                pair,
                replica: '',
            });
        }
    });
    const pivotNumbers = nonNumbers / childPairs.length < 0.1;
    if (pivotColumnGroups.size > 10) {
        const arrayofKeys = Array.from(pivotColumnGroups.keys());
        const sortedKeys = pivotNumbers ? arrayofKeys.sort((n1: FieldResult, n2: FieldResult) => toNumber(n1)! - toNumber(n2)!) : arrayofKeys.sort();
        const clusterSize = Math.ceil(pivotColumnGroups.size / 10);
        const numClusters = Math.ceil(sortedKeys.length / clusterSize);
        for (let i = 0; i < numClusters; i++) {
            for (let j = i * clusterSize + 1; j < Math.min(sortedKeys.length, (i + 1) * clusterSize); j++) {
                const curgrp = pivotColumnGroups.get(sortedKeys[i * clusterSize])!;
                const newgrp = pivotColumnGroups.get(sortedKeys[j])!;
                curgrp.docs.push(...newgrp.docs);
                curgrp.filters.push(...newgrp.filters);
                curgrp.replicas.push(...newgrp.replicas);
                pivotColumnGroups.delete(sortedKeys[j]);
            }
        }
    }
    const fontSize = NumCast(pivotDoc[fieldKey + '-timelineFontSize'], panelDim[1] > 58 ? 20 : Math.max(7, panelDim[1] / 3));
    const desc = `${fontSize}px ${getComputedStyle(document.body).fontFamily}`;
    const textlen = Array.from(pivotColumnGroups.keys())
        .map(c => getTextWidth(toLabel(c), desc))
        .reduce((p, c) => Math.max(p, c), 0 as number);
    const max_text = Math.min(Math.ceil(textlen / 120) * 28, panelDim[1] / 2);
    const maxInColumn = Array.from(pivotColumnGroups.values()).reduce((p, s) => Math.max(p, s.docs.length), 1);

    const colWidth = panelDim[0] / pivotColumnGroups.size;
    const colHeight = panelDim[1] - max_text;
    let numCols = 0;
    let bestArea = 0;
    let pivotAxisWidth = 0;
    for (let i = 1; i < 10; i++) {
        const numInCol = Math.ceil(maxInColumn / i);
        const hd = colHeight / numInCol;
        const wd = colWidth / i;
        const dim = Math.min(hd, wd);
        if (dim > bestArea) {
            bestArea = dim;
            numCols = i;
            pivotAxisWidth = dim;
        }
    }

    const groupNames: ViewDefBounds[] = [];

    const expander = 1.05;
    const gap = 0.15;
    const maxColHeight = pivotAxisWidth * expander * Math.ceil(maxInColumn / numCols);
    let x = 0;
    const sortedPivotKeys = pivotNumbers ? Array.from(pivotColumnGroups.keys()).sort((n1: FieldResult, n2: FieldResult) => toNumber(n1)! - toNumber(n2)!) : Array.from(pivotColumnGroups.keys()).sort();
    sortedPivotKeys.forEach(key => {
        const val = pivotColumnGroups.get(key)!;
        let y = 0;
        let xCount = 0;
        const text = toLabel(key);
        groupNames.push({
            type: 'text',
            text,
            x,
            y: pivotAxisWidth,
            width: pivotAxisWidth * expander * numCols,
            height: max_text,
            fontSize,
            payload: val,
        });
        val.docs.forEach((doc, i) => {
            const layoutDoc = Doc.Layout(doc);
            let wid = pivotAxisWidth;
            let hgt = pivotAxisWidth / (Doc.NativeAspect(layoutDoc) || 1);
            if (hgt > pivotAxisWidth) {
                hgt = pivotAxisWidth;
                wid = (Doc.NativeAspect(layoutDoc) || 1) * pivotAxisWidth;
            }
            docMap.set(doc[Id] + (val.replicas || ''), {
                x: x + xCount * pivotAxisWidth * expander + (pivotAxisWidth - wid) / 2 + (val.docs.length < numCols ? ((numCols - val.docs.length) * pivotAxisWidth) / 2 : 0),
                y: -y + (pivotAxisWidth - hgt) / 2,
                width: wid,
                height: hgt,
                pair: { layout: doc },
                replica: val.replicas[i],
            });
            xCount++;
            if (xCount >= numCols) {
                xCount = 0;
                y += pivotAxisWidth * expander;
            }
        });
        x += pivotAxisWidth * (numCols * expander + gap);
    });

    const dividers = sortedPivotKeys.map((key, i) => ({
        type: 'div',
        color: 'lightGray',
        x: i * pivotAxisWidth * (numCols * expander + gap) - (pivotAxisWidth * (expander - 1)) / 2,
        y: -maxColHeight + pivotAxisWidth,
        width: pivotAxisWidth * numCols * expander,
        height: maxColHeight,
        payload: pivotColumnGroups.get(key)!.filters,
    }));
    groupNames.push(...dividers);
    return normalizeResults(panelDim, max_text, docMap, poolData, viewDefsToJSX, groupNames, 0, []);
}

function toNumber(val: FieldResult<Field>) {
    return val === undefined ? undefined : NumCast(val, Number(StrCast(val)));
}

export function computeTimelineLayout(poolData: Map<string, PoolData>, pivotDoc: Doc, childPairs: { layout: Doc; data?: Doc }[], panelDim: number[], viewDefsToJSX: (views: ViewDefBounds[]) => ViewDefResult[], engineProps?: any) {
    const fieldKey = 'data';
    const pivotDateGroups = new Map<number, Doc[]>();
    const docMap = new Map<string, PoolData>();
    const groupNames: ViewDefBounds[] = [];
    const timelineFieldKey = Field.toString(pivotDoc._pivotField as Field);
    const curTime = toNumber(pivotDoc[fieldKey + '-timelineCur']);
    const curTimeSpan = Cast(pivotDoc[fieldKey + '-timelineSpan'], 'number', null);
    const minTimeReq = curTimeSpan === undefined ? Cast(pivotDoc[fieldKey + '-timelineMinReq'], 'number', null) : curTime && curTime - curTimeSpan;
    const maxTimeReq = curTimeSpan === undefined ? Cast(pivotDoc[fieldKey + '-timelineMaxReq'], 'number', null) : curTime && curTime + curTimeSpan;
    const fontSize = NumCast(pivotDoc[fieldKey + '-timelineFontSize'], panelDim[1] > 58 ? 20 : Math.max(7, panelDim[1] / 3));
    const fontHeight = panelDim[1] > 58 ? 30 : panelDim[1] / 2;
    const findStack = (time: number, stack: number[]) => {
        const index = stack.findIndex(val => val === undefined || val < x);
        return index === -1 ? stack.length : index;
    };

    let minTime = minTimeReq === undefined ? Number.MAX_VALUE : minTimeReq;
    let maxTime = maxTimeReq === undefined ? -Number.MAX_VALUE : maxTimeReq;
    childPairs.forEach(pair => {
        const num = NumCast(pair.layout[timelineFieldKey], Number(StrCast(pair.layout[timelineFieldKey])));
        if (!Number.isNaN(num) && (!minTimeReq || num >= minTimeReq) && (!maxTimeReq || num <= maxTimeReq)) {
            !pivotDateGroups.get(num) && pivotDateGroups.set(num, []);
            pivotDateGroups.get(num)!.push(pair.layout);
            minTime = Math.min(num, minTime);
            maxTime = Math.max(num, maxTime);
        }
    });
    if (curTime !== undefined) {
        if (curTime > maxTime || curTime - minTime > maxTime - curTime) {
            maxTime = curTime + (curTime - minTime);
        } else {
            minTime = curTime - (maxTime - curTime);
        }
    }
    setTimeout(() => {
        pivotDoc[fieldKey + '-timelineMin'] = minTime = minTimeReq ? Math.min(minTimeReq, minTime) : minTime;
        pivotDoc[fieldKey + '-timelineMax'] = maxTime = maxTimeReq ? Math.max(maxTimeReq, maxTime) : maxTime;
    }, 0);

    if (maxTime === minTime) {
        maxTime = minTime + 1;
    }

    const arrayofKeys = Array.from(pivotDateGroups.keys());
    const sortedKeys = arrayofKeys.sort((n1, n2) => n1 - n2);
    const scaling = panelDim[0] / (maxTime - minTime);
    let x = 0;
    let prevKey = Math.floor(minTime);

    if (sortedKeys.length && scaling * (sortedKeys[0] - prevKey) > 25) {
        groupNames.push({ type: 'text', text: toLabel(prevKey), x: x, y: 0, height: fontHeight, fontSize, payload: undefined });
    }
    if (!sortedKeys.length && curTime !== undefined) {
        groupNames.push({ type: 'text', text: toLabel(curTime), x: (curTime - minTime) * scaling, zIndex: 1000, color: 'orange', y: 0, height: fontHeight, fontSize, payload: undefined });
    }

    const pivotAxisWidth = NumCast(pivotDoc.pivotTimeWidth, panelDim[1] / 2.5);
    const stacking: number[] = [];
    let zind = 0;
    sortedKeys.forEach(key => {
        if (curTime !== undefined && curTime > prevKey && curTime <= key) {
            groupNames.push({ type: 'text', text: toLabel(curTime), x: (curTime - minTime) * scaling, y: 0, zIndex: 1000, color: 'orange', height: fontHeight, fontSize, payload: key });
        }
        const keyDocs = pivotDateGroups.get(key)!;
        x += scaling * (key - prevKey);
        const stack = findStack(x, stacking);
        prevKey = key;
        if (!stack && (curTime === undefined || Math.abs(x - (curTime - minTime) * scaling) > pivotAxisWidth)) {
            groupNames.push({ type: 'text', text: toLabel(key), x: x, y: stack * 25, height: fontHeight, fontSize, payload: undefined });
        }
        layoutDocsAtTime(keyDocs, key);
    });
    if (sortedKeys.length && curTime !== undefined && curTime > sortedKeys[sortedKeys.length - 1]) {
        x = (curTime - minTime) * scaling;
        groupNames.push({ type: 'text', text: toLabel(curTime), x: x, y: 0, zIndex: 1000, color: 'orange', height: fontHeight, fontSize, payload: undefined });
    }
    if (Math.ceil(maxTime - minTime) * scaling > x + 25) {
        groupNames.push({ type: 'text', text: toLabel(Math.ceil(maxTime)), x: Math.ceil(maxTime - minTime) * scaling, y: 0, height: fontHeight, fontSize, payload: undefined });
    }

    const divider = { type: 'div', color: 'black', x: 0, y: 0, width: panelDim[0], height: -1, payload: undefined };
    return normalizeResults(panelDim, fontHeight, docMap, poolData, viewDefsToJSX, groupNames, (maxTime - minTime) * scaling, [divider]);

    function layoutDocsAtTime(keyDocs: Doc[], key: number) {
        keyDocs.forEach(doc => {
            const stack = findStack(x, stacking);
            const layoutDoc = Doc.Layout(doc);
            let wid = pivotAxisWidth;
            let hgt = pivotAxisWidth / (Doc.NativeAspect(layoutDoc) || 1);
            if (hgt > pivotAxisWidth) {
                hgt = pivotAxisWidth;
                wid = (Doc.NativeAspect(layoutDoc) || 1) * pivotAxisWidth;
            }
            docMap.set(doc[Id], {
                x: x,
                y: (-Math.sqrt(stack) * pivotAxisWidth) / 2 - pivotAxisWidth + (pivotAxisWidth - hgt) / 2,
                zIndex: curTime === key ? 1000 : zind++,
                highlight: curTime === key,
                width: wid / Math.max(stack, 1),
                height: hgt / Math.max(stack, 1),
                pair: { layout: doc },
                replica: '',
            });
            stacking[stack] = x + pivotAxisWidth;
        });
    }
}

function normalizeResults(
    panelDim: number[],
    fontHeight: number,
    docMap: Map<string, PoolData>,
    poolData: Map<string, PoolData>,
    viewDefsToJSX: (views: ViewDefBounds[]) => ViewDefResult[],
    groupNames: ViewDefBounds[],
    minWidth: number,
    extras: ViewDefBounds[]
): ViewDefResult[] {
    const grpEles = groupNames.map(gn => ({ x: gn.x, y: gn.y, width: gn.width, height: gn.height }) as ViewDefBounds);
    const docEles = Array.from(docMap.entries()).map(ele => ele[1]);
    const aggBounds = aggregateBounds(
        extras.concat(grpEles.concat(docEles.map(de => ({ ...de, type: 'doc', payload: '' })))).filter(e => e.zIndex !== -99),
        0,
        0
    );
    aggBounds.r = aggBounds.x + Math.max(minWidth, aggBounds.r - aggBounds.x);
    const width = aggBounds.r - aggBounds.x === 0 ? 1 : aggBounds.r - aggBounds.x;
    const height = aggBounds.b - aggBounds.y === 0 ? 1 : aggBounds.b - aggBounds.y;
    const wscale = panelDim[0] / width;
    let scale = wscale * height > panelDim[1] ? panelDim[1] / height : wscale;
    if (Number.isNaN(scale)) scale = 1;

    Array.from(docMap.entries())
        .filter(ele => ele[1].pair)
        .map(ele => {
            const newPosRaw = ele[1];
            if (newPosRaw) {
                const newPos: PoolData = {
                    x: newPosRaw.x * scale,
                    y: newPosRaw.y * scale,
                    z: newPosRaw.z,
                    replica: newPosRaw.replica,
                    highlight: newPosRaw.highlight,
                    zIndex: newPosRaw.zIndex,
                    width: (newPosRaw.width || 0) * scale,
                    height: newPosRaw.height! * scale,
                    backgroundColor: newPosRaw.backgroundColor,
                    opacity: newPosRaw.opacity,
                    color: newPosRaw.color,
                    pair: ele[1].pair,
                };
                if (newPosRaw.transition) newPos.transition = newPosRaw.transition;
                poolData.set(newPos.pair.layout[Id] + (newPos.replica || ''), { transition: 'all 1s', ...newPos });
            }
        });

    return viewDefsToJSX(
        extras.concat(groupNames).map(gname => ({
            type: gname.type,
            text: gname.text,
            x: gname.x * scale,
            y: gname.y * scale,
            color: gname.color,
            width: gname.width === undefined ? undefined : gname.width * scale,
            height: gname.height === -1 ? 1 : gname.type === 'text' ? Math.max(fontHeight * scale, (gname.height || 0) * scale) : (gname.height || 0) * scale,
            fontSize: gname.fontSize,
            payload: gname.payload,
        }))
    );
}
