import RecycleItemPool from "../utils/RecycleItemPool";
import { Dimension, BaseLayoutProvider } from "./dependencies/LayoutProvider";
import CustomError from "./exceptions/CustomError";
import RecyclerListViewExceptions from "./exceptions/RecyclerListViewExceptions";
import { Point, LayoutManager } from "./layoutmanager/LayoutManager";
import ViewabilityTracker, { TOnItemStatusChanged, WindowCorrection } from "./ViewabilityTracker";
import { ObjectUtil, Default } from "ts-object-utils";
import TSCast from "../utils/TSCast";
import { BaseDataProvider } from "./dependencies/DataProvider";

/***
 * Renderer which keeps track of recyclable items and the currently rendered items. Notifies list view to re render if something changes, like scroll offset
 */
export interface RenderStackItem {
    dataIndex?: number;
}
export interface StableIdMapItem {
    key: string;
    type: string | number;
}
export interface RenderStack { [key: string]: RenderStackItem; }

export interface RenderStackParams {
    isHorizontal?: boolean;
    itemCount: number;
    initialOffset?: number;
    initialRenderIndex?: number;
    renderAheadOffset?: number;
}

export type StableIdProvider = (index: number) => string;

export default class VirtualRenderer {

    private onVisibleItemsChanged: TOnItemStatusChanged | null;

    private _scrollOnNextUpdate: (point: Point) => void;
    private _stableIdToRenderKeyMap: { [key: string]: StableIdMapItem | undefined };
    private _engagedIndexes: { [key: number]: number | undefined };
    private _renderStack: RenderStack;
    private _renderStackChanged: (renderStack: RenderStack) => void;
    private _fetchStableId: StableIdProvider;
    private _isRecyclingEnabled: boolean;
    private _isViewTrackerRunning: boolean;
    private _markDirty: boolean;
    private _startKey: number;
    private _layoutProvider: BaseLayoutProvider = TSCast.cast<BaseLayoutProvider>(null); //TSI
    private _recyclePool: RecycleItemPool = TSCast.cast<RecycleItemPool>(null); //TSI

    private _params: RenderStackParams | null;
    private _layoutManager: LayoutManager | null = null;
    private _viewabilityTracker: ViewabilityTracker | null = null;
    private _dimensions: Dimension | null;
    private _optimizeForAnimations: boolean = false;

    constructor(renderStackChanged: (renderStack: RenderStack) => void,
                scrollOnNextUpdate: (point: Point) => void,
                fetchStableId: StableIdProvider,
                isRecyclingEnabled: boolean) {
        //Keeps track of items that need to be rendered in the next render cycle
        this._renderStack = {};

        this._fetchStableId = fetchStableId;

        //Keeps track of keys of all the currently rendered indexes, can eventually replace renderStack as well if no new use cases come up
        this._stableIdToRenderKeyMap = {};
        this._engagedIndexes = {};
        this._renderStackChanged = renderStackChanged;
        this._scrollOnNextUpdate = scrollOnNextUpdate;
        this._dimensions = null;
        this._params = null;
        this._isRecyclingEnabled = isRecyclingEnabled;

        this._isViewTrackerRunning = false;
        this._markDirty = false;

        //Would be surprised if someone exceeds this
        this._startKey = 0;

        this.onVisibleItemsChanged = null;
    }

    public getLayoutDimension(): Dimension {
        if (this._layoutManager) {
            return this._layoutManager.getContentDimension();
        }
        return { height: 0, width: 0 };
    }

    public setOptimizeForAnimations(shouldOptimize: boolean): void {
        this._optimizeForAnimations = shouldOptimize;
    }

    public hasPendingAnimationOptimization(): boolean {
        return this._optimizeForAnimations;
    }

    public updateOffset(offsetX: number, offsetY: number, isActual: boolean, correction: WindowCorrection): void {
        if (this._viewabilityTracker) {
            const offset = this._params && this._params.isHorizontal ? offsetX : offsetY;
            if (!this._isViewTrackerRunning) {
                if (isActual) {
                    this._viewabilityTracker.setActualOffset(offset);
                }
                this.startViewabilityTracker(correction);
            }
            this._viewabilityTracker.updateOffset(offset, isActual, correction);
        }
    }

    public attachVisibleItemsListener(callback: TOnItemStatusChanged): void {
        this.onVisibleItemsChanged = callback;
    }

    public removeVisibleItemsListener(): void {
        this.onVisibleItemsChanged = null;

        if (this._viewabilityTracker) {
            this._viewabilityTracker.onVisibleRowsChanged = null;
        }
    }

    public getLayoutManager(): LayoutManager | null {
        return this._layoutManager;
    }

    public setParamsAndDimensions(params: RenderStackParams, dim: Dimension): void {
        this._params = params;
        this._dimensions = dim;
    }

    public setLayoutManager(layoutManager: LayoutManager): void {
        this._layoutManager = layoutManager;
        if (this._params) {
            this._layoutManager.relayoutFromIndex(0, this._params.itemCount);
        }
    }

    public setLayoutProvider(layoutProvider: BaseLayoutProvider): void {
        this._layoutProvider = layoutProvider;
    }

    public getViewabilityTracker(): ViewabilityTracker | null {
        return this._viewabilityTracker;
    }

    public refreshWithAnchor(): void {
        if (this._viewabilityTracker) {
            let firstVisibleIndex = this._viewabilityTracker.findFirstLogicallyVisibleIndex();
            this._prepareViewabilityTracker();
            let offset = 0;
            if (this._layoutManager && this._params) {
                firstVisibleIndex = Math.min(this._params.itemCount - 1, firstVisibleIndex);
                const point = this._layoutManager.getOffsetForIndex(firstVisibleIndex);
                this._scrollOnNextUpdate(point);
                offset = this._params.isHorizontal ? point.x : point.y;
            }
            this._viewabilityTracker.forceRefreshWithOffset(offset);
        }
    }

    public refresh(): void {
        if (this._viewabilityTracker) {
            this._prepareViewabilityTracker();
            this._viewabilityTracker.forceRefresh();
        }
    }

    public getInitialOffset(): Point {
        let offset = { x: 0, y: 0 };
        if (this._params) {
            const initialRenderIndex = Default.value<number>(this._params.initialRenderIndex, 0);
            if (initialRenderIndex > 0 && this._layoutManager) {
                offset = this._layoutManager.getOffsetForIndex(initialRenderIndex);
                this._params.initialOffset = this._params.isHorizontal ? offset.x : offset.y;
            } else {
                if (this._params.isHorizontal) {
                    offset.x = Default.value<number>(this._params.initialOffset, 0);
                    offset.y = 0;
                } else {
                    offset.y = Default.value<number>(this._params.initialOffset, 0);
                    offset.x = 0;
                }
            }
        }
        return offset;
    }

    public init(): void {
        this.getInitialOffset();
        this._recyclePool = new RecycleItemPool();
        if (this._params) {
            this._viewabilityTracker = new ViewabilityTracker(
                Default.value<number>(this._params.renderAheadOffset, 0),
                Default.value<number>(this._params.initialOffset, 0));
        } else {
            this._viewabilityTracker = new ViewabilityTracker(0, 0);
        }
        this._prepareViewabilityTracker();
    }

    public startViewabilityTracker(windowCorrection: WindowCorrection): void {
        if (this._viewabilityTracker) {
            this._isViewTrackerRunning = true;
            this._viewabilityTracker.init(windowCorrection);
        }
    }

    public syncAndGetKey(index: number, overrideStableIdProvider?: StableIdProvider,
                         newRenderStack?: RenderStack,
                         keyToStableIdMap?: { [key: string]: string } ): string {
        const getStableId = overrideStableIdProvider ? overrideStableIdProvider : this._fetchStableId;
        const renderStack = newRenderStack ? newRenderStack : this._renderStack;
        const stableIdItem = this._stableIdToRenderKeyMap[getStableId(index)];
        let key = stableIdItem ? stableIdItem.key : undefined;

        if (ObjectUtil.isNullOrUndefined(key)) {
            const type = this._layoutProvider.getLayoutTypeForIndex(index);
            key = this._recyclePool.getRecycledObject(type);
            if (!ObjectUtil.isNullOrUndefined(key)) {
                const itemMeta = renderStack[key];
                if (itemMeta) {
                    const oldIndex = itemMeta.dataIndex;
                    itemMeta.dataIndex = index;
                    if (!ObjectUtil.isNullOrUndefined(oldIndex) && oldIndex !== index) {
                        delete this._stableIdToRenderKeyMap[getStableId(oldIndex)];
                    }
                } else {
                    renderStack[key] = { dataIndex: index };
                    if (keyToStableIdMap && keyToStableIdMap[key]) {
                        delete this._stableIdToRenderKeyMap[keyToStableIdMap[key]];
                    }
                }
            } else {
                key = getStableId(index);
                if (renderStack[key]) {
                    //Probable collision, warn and avoid
                    //TODO: Disabled incorrectly triggering in some cases
                    //console.warn("Possible stableId collision @", index); //tslint:disable-line
                    key = this._getCollisionAvoidingKey();
                }
                renderStack[key] = { dataIndex: index };
            }
            this._markDirty = true;
            this._stableIdToRenderKeyMap[getStableId(index)] = { key, type };
        }
        if (!ObjectUtil.isNullOrUndefined(this._engagedIndexes[index])) {
            this._recyclePool.removeFromPool(key);
        }
        const stackItem = renderStack[key];
        if (stackItem && stackItem.dataIndex !== index) {
            //Probable collision, warn
            console.warn("Possible stableId collision @", index); //tslint:disable-line
        }
        return key;
    }

    //Further optimize in later revision, pretty fast for now considering this is a low frequency event
    public handleDataSetChange(newDataProvider: BaseDataProvider): void {
        const getStableId = newDataProvider.getStableId;
        const maxIndex = newDataProvider.getSize() - 1;
        const activeStableIds: { [key: string]: number } = {};
        const newRenderStack: RenderStack = {};
        const keyToStableIdMap: { [key: string]: string } = {};

        // Do not use recycle pool so that elements don't fly top to bottom or vice versa
        // Doing this is expensive and can draw extra items
        if (this._optimizeForAnimations && this._recyclePool) {
            this._recyclePool.clearAll();
        }

        //Compute active stable ids and stale active keys and resync render stack
        for (const key in this._renderStack) {
            if (this._renderStack.hasOwnProperty(key)) {
                const index = this._renderStack[key].dataIndex;
                if (!ObjectUtil.isNullOrUndefined(index)) {
                    if (index <= maxIndex) {
                        const stableId = getStableId(index);
                        activeStableIds[stableId] = 1;
                    }
                }
            }
        }

        //Clean stable id to key map
        const oldActiveStableIds = Object.keys(this._stableIdToRenderKeyMap);
        const oldActiveStableIdsCount = oldActiveStableIds.length;
        for (let i = 0; i < oldActiveStableIdsCount; i++) {
            const key = oldActiveStableIds[i];
            const stableIdItem = this._stableIdToRenderKeyMap[key];
            if (stableIdItem) {
                if (!activeStableIds[key]) {
                    if (!this._optimizeForAnimations && this._isRecyclingEnabled) {
                        this._recyclePool.putRecycledObject(stableIdItem.type, stableIdItem.key);
                    }
                    delete this._stableIdToRenderKeyMap[key];

                    const stackItem = this._renderStack[stableIdItem.key];
                    const dataIndex = stackItem ? stackItem.dataIndex : undefined;
                    if (!ObjectUtil.isNullOrUndefined(dataIndex) && dataIndex <= maxIndex && this._layoutManager) {
                        this._layoutManager.removeLayout(dataIndex);
                    }
                } else {
                    keyToStableIdMap[stableIdItem.key] = key;
                }
            }
        }
        const renderStackKeys = Object.keys(this._renderStack).sort((a, b) => {
            const firstItem = this._renderStack[a];
            const secondItem = this._renderStack[b];
            if (firstItem && firstItem.dataIndex && secondItem && secondItem.dataIndex) {
                return firstItem.dataIndex - secondItem.dataIndex;
            }
            return 1;
        });
        const renderStackLength = renderStackKeys.length;
        for (let i = 0; i < renderStackLength; i++) {
            const key = renderStackKeys[i];
            const index = this._renderStack[key].dataIndex;
            if (!ObjectUtil.isNullOrUndefined(index)) {
                if (index <= maxIndex) {
                    const newKey = this.syncAndGetKey(index, getStableId, newRenderStack, keyToStableIdMap);
                    const newStackItem = newRenderStack[newKey];
                    if (!newStackItem) {
                        newRenderStack[newKey] = { dataIndex: index };
                    } else if (newStackItem.dataIndex !== index) {
                        const cllKey = this._getCollisionAvoidingKey();
                        newRenderStack[cllKey] = { dataIndex: index };
                        this._stableIdToRenderKeyMap[getStableId(index)] = {
                            key: cllKey, type: this._layoutProvider.getLayoutTypeForIndex(index),
                        };
                    }
                }
            }
            delete this._renderStack[key];
        }

        Object.assign(this._renderStack, newRenderStack);

        for (const key in this._renderStack) {
            if (this._renderStack.hasOwnProperty(key)) {
                const index = this._renderStack[key].dataIndex;
                if (!ObjectUtil.isNullOrUndefined(index) && ObjectUtil.isNullOrUndefined(this._engagedIndexes[index])) {
                    const type = this._layoutProvider.getLayoutTypeForIndex(index);
                    this._recyclePool.putRecycledObject(type, key);
                }
            }
        }
    }

    private _getCollisionAvoidingKey(): string {
        return "#" + this._startKey++ + "_rlv_c";
    }

    private _prepareViewabilityTracker(): void {
        if (this._viewabilityTracker && this._layoutManager && this._dimensions && this._params) {
            this._viewabilityTracker.onEngagedRowsChanged = this._onEngagedItemsChanged;
            if (this.onVisibleItemsChanged) {
                this._viewabilityTracker.onVisibleRowsChanged = this._onVisibleItemsChanged;
            }
            this._viewabilityTracker.setLayouts(this._layoutManager.getLayouts(), this._params.isHorizontal ?
                this._layoutManager.getContentDimension().width :
                this._layoutManager.getContentDimension().height);
            this._viewabilityTracker.setDimensions({
                height: this._dimensions.height,
                width: this._dimensions.width,
            }, Default.value<boolean>(this._params.isHorizontal, false));
        } else {
            throw new CustomError(RecyclerListViewExceptions.initializationException);
        }
    }

    private _onVisibleItemsChanged = (all: number[], now: number[], notNow: number[]): void => {
        if (this.onVisibleItemsChanged) {
            this.onVisibleItemsChanged(all, now, notNow);
        }
    }

    private _onEngagedItemsChanged = (all: number[], now: number[], notNow: number[]): void => {
        const count = notNow.length;
        let resolvedKey;
        let disengagedIndex = 0;
        if (this._isRecyclingEnabled) {
            for (let i = 0; i < count; i++) {
                disengagedIndex = notNow[i];
                delete this._engagedIndexes[disengagedIndex];
                if (this._params && disengagedIndex < this._params.itemCount) {
                    //All the items which are now not visible can go to the recycle pool, the pool only needs to maintain keys since
                    //react can link a view to a key automatically
                    resolvedKey = this._stableIdToRenderKeyMap[this._fetchStableId(disengagedIndex)];
                    if (!ObjectUtil.isNullOrUndefined(resolvedKey)) {
                        this._recyclePool.putRecycledObject(this._layoutProvider.getLayoutTypeForIndex(disengagedIndex), resolvedKey.key);
                    }
                }
            }
        }
        if (this._updateRenderStack(now)) {
            //Ask Recycler View to update itself
            this._renderStackChanged(this._renderStack);
        }
    }

    //Updates render stack and reports whether anything has changed
    private _updateRenderStack(itemIndexes: number[]): boolean {
        this._markDirty = false;
        const count = itemIndexes.length;
        let index = 0;
        let hasRenderStackChanged = false;
        for (let i = 0; i < count; i++) {
            index = itemIndexes[i];
            this._engagedIndexes[index] = 1;
            this.syncAndGetKey(index);
            hasRenderStackChanged = this._markDirty;
        }
        this._markDirty = false;
        return hasRenderStackChanged;
    }
}
