import {DispBase, DispBaseT, DispType, PointI} from "../canvas-shapes/DispBase";
import {DispPolyline} from "../canvas-shapes/DispPolyline";
import {DispGroupPointerT} from "../canvas-shapes/DispGroupPointer";
import {DispPolygon} from "../canvas-shapes/DispPolygon";

// import 'rxjs/add/operator/takeUntil';


export interface PolylineEnd {
    isStart: boolean,
    disp: any
}

export interface DispFilterCallableT {
    (disp: DispBaseT): boolean;
}

/**
 * Peek Canvas Model
 *
 * This class stores and manages the model of the NodeCoord and ConnCoord
 * objects that are within the viewable area.
 *
 */

export class PeekCanvasModelQuery {


    constructor(private model) {


    };


// -------------------------------------------------------------------------------------
// Display Items
// -------------------------------------------------------------------------------------

    get viewableDisps(): any[] {
        return this.model.viewableDisps();
    }

    get selectableDisps(): any[] {
        return this.viewableDisps
            .filter(disp => DispBase.isSelectable(disp));
    }

    get selectedDisps(): any[] {
        return this.model.selection.selectedDisps();
    }

    sortBySelectionPriority(disps): any[] {
        function cmp(disp1, disp2): number {

            if (DispBase.typeOf(disp1) == DispType.groupPointer
                && DispBase.typeOf(disp2) != DispType.groupPointer)
                return 1;

            if (DispBase.typeOf(disp1) == DispType.polygon
                && DispBase.typeOf(disp2) != DispType.polygon)
                return 1;

            // Put all the null bounds at one end
            if (disp1.bounds && !disp2.bounds)
                return 1;

            if (!disp1.bounds && disp2.bounds)
                return -1;

            // Bigger bounds come first
            return disp1.bounds.area(disp2) - disp1.bounds.area(disp1);

        }

        return disps.sort(cmp);
    }

    sortByDistanceFromCenter(disps, point: PointI): any[] {
        const lazyDists = {};

        function lazyGetDist(disp) {
            if (disp.bounds == null)
                return 99999999;
            let dist = lazyDists[DispBase.id(disp)];
            if (dist != null)
                return dist;
            return lazyDists[DispBase.id(disp)] = disp.bounds.distanceFromPoint(point);
        }

        function cmp(disp1, disp2): number {
            return lazyGetDist(disp1) - lazyGetDist(disp2);
        }

        return disps.sort(cmp);
    }

    filterForVisibleDisps(disps, zoom, includeShapesWithNoColor = false): any[] {
        function check(disp): boolean {
            if (!includeShapesWithNoColor && !DispBase.hasColor(disp))
                return false;

            if (!DispBase.layer(disp).visible)
                return false;

            const level = DispBase.level(disp);
            return level.isVisibleAtZoom(zoom);
        }

        return disps.filter(check);
    }

    filterForDispsContainingPoint(disps, zoom: number, margin: number, point: PointI,
                                  useBoxContainsForPolygons: boolean): any[] {
        margin = margin / zoom;
        return disps.filter(d => {
                if (DispBase.typeOf(d) == DispType.polygon
                    && !useBoxContainsForPolygons) {
                    return DispPolygon.contains(d, point, margin);
                }
                if (DispBase.typeOf(d) == DispType.polyline) {
                    return DispPolyline.contains(d, point, margin);
                }
                return d.bounds && d.bounds.contains(point.x, point.y, margin);
            }
        );


    }

    keyOfDisps(disps: any[]): string[] {
        let keys = [];
        for (let disp of disps) {
            if (DispBase.key(disp) != null)
                keys.push(DispBase.key(disp));
        }
        return keys;
    }

    uniqueDisps(disps: any[]): string[] {
        const ids = {};
        return disps
            .filter(d => ids[d.id] === true ? false : ids[d.id] = true);
    }

    uniquePolylineEnds(ends: PolylineEnd[]): PolylineEnd[] {
        const ids = {};
        return ends.filter(d => ids[d.disp.id] === true
            ? false
            : ids[d.disp.id] = true);
    }

    dispsForKeys(keys: string[]): any[] {
        let keyDict = {};
        for (let key of keys)
            keyDict[key] = true;

        let resultDisps = [];
        for (let disp of this.viewableDisps) {
            if (keyDict[DispBase.key(disp)] != null)
                resultDisps.push(disp);
        }

        return resultDisps;
    }

    get dispsInSelectedGroups(): any[] {
        return this.dispsForGroups(this.selectedDisps);
    }


    /** Disp Group for Disp
     *
     * Return the DispGroupPointer that a disp belongs to.
     * NOTE: This is an O(N) approach
     * @param disp
     */
    dispGroupForDisp(disp: DispBaseT): DispGroupPointerT | null {
        let groupId = DispBase.groupId(disp);

        if (groupId == null)
            return null;

        for (let iterDisp of this.model.viewableDisps()) {
            if (DispBase.id(iterDisp) == groupId)
                return iterDisp;
        }
        return null;
    }

    dispsInSameGroup(refDisp): any[] {
        return this.dispsForGroups([refDisp]);
    }

    dispsForGroups(disps: any[]): any[] {
        const result = [];
        const selectedGroupIds = {};

        for (let disp of disps) {
            // DispGroup and DispGroupPtrs are not selectable
            selectedGroupIds[DispBase.id(disp)] = true;
            if (DispBase.groupId(disp) != null)
                selectedGroupIds[DispBase.groupId(disp)] = true;
        }

        const dispIdsAdded = {}; // return a unique list.
        for (let disp of this.viewableDisps) {
            if (dispIdsAdded[DispBase.id(disp)])
                continue;
            dispIdsAdded[DispBase.id(disp)] = true;

            if (selectedGroupIds[DispBase.groupId(disp)] === true)
                result.push(disp);

            else if (selectedGroupIds[DispBase.id(disp)] === true)
                result.push(disp);
        }

        return result;
    }

    decentAndAddDisps(disps: DispBaseT[], outDisps: DispBaseT[] = []): DispBaseT[] {
        for (let disp of disps) {
            outDisps.push(disp);
            if (disp["disps"] != null)
                this.decentAndAddDisps(disp["disps"], outDisps);
        }
        return outDisps;
    }

    polylinesConnectedToDispKey(keys: string[]): PolylineEnd[] {
        let result: PolylineEnd[] = [];
        let keysDict = {};

        for (let key of keys) {
            keysDict[key] = true;
        }

        for (let disp of this.viewableDisps) {
            let startKey = DispPolyline.startKey(disp);
            let endKey = DispPolyline.endKey(disp);

            if (startKey != null && keysDict[startKey] === true)
                result.push({isStart: true, disp: disp});

            else if (endKey != null && keysDict[endKey] === true)
                result.push({isStart: false, disp: disp});
        }


        return result;
    }

    polylinesConnectedToPoint(points: PointI[]): PolylineEnd[] {
        let result: PolylineEnd[] = [];
        let pointsDict = {};

        for (let point of points) {
            pointsDict[`${point.x}x${point.y}`] = true;
        }

        for (let disp of this.viewableDisps) {
            if (DispBase.typeOf(disp) != DispType.polyline)
                continue;

            let fp = DispPolyline.firstPoint(disp);
            let lp = DispPolyline.lastPoint(disp);

            if (pointsDict[`${fp.x}x${fp.y}`] === true)
                result.push({isStart: true, disp: disp});

            else if (pointsDict[`${lp.x}x${lp.y}`] === true)
                result.push({isStart: false, disp: disp});
        }


        return result;
    }

    closestDispToPoint(x, y, dispFiltCallable: DispFilterCallableT | null = null): DispBaseT | null {
        let closestDisp = null;
        let closestDispDistance = null;

        for (let disp of this.viewableDisps) {
            if (disp.bounds == null)
                continue;

            if (dispFiltCallable != null && !dispFiltCallable(disp))
                continue;

            let distance = disp.bounds.distanceFromPoint({x, y});
            if (closestDisp == null || distance < closestDispDistance) {
                closestDisp = disp;
                closestDispDistance = distance;
            }

        }

        return closestDisp;
    }


}
