/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements.  See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership.  The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License.  You may obtain a copy of the License at
*
*   http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied.  See the License for the
* specific language governing permissions and limitations
* under the License.
*/

// TODO
// ??? refactor? check the outer usage of data provider.
// merge with defaultDimValueGetter?

import {isTypedArray, extend, assert, each, isObject, bind} from 'zrender/src/core/util';
import {getDataItemValue} from '../../util/model';
import { createSourceFromSeriesDataOption, Source, isSourceInstance } from '../Source';
import {ArrayLike, Dictionary} from 'zrender/src/core/types';
import {
    SOURCE_FORMAT_ORIGINAL,
    SOURCE_FORMAT_OBJECT_ROWS,
    SOURCE_FORMAT_KEYED_COLUMNS,
    SOURCE_FORMAT_TYPED_ARRAY,
    SOURCE_FORMAT_ARRAY_ROWS,
    SERIES_LAYOUT_BY_COLUMN,
    SERIES_LAYOUT_BY_ROW,
    DimensionName, DimensionIndex, OptionSourceData,
    OptionDataItem, OptionDataValue, SourceFormat, SeriesLayoutBy, ParsedValue, DimensionLoose, NullUndefined
} from '../../util/types';
import SeriesData from '../SeriesData';

export interface DataProvider {
    /**
     * true: all of the value are in primitive type (in type `OptionDataValue`).
     * false: Not sure whether any of them is non primitive type (in type `OptionDataItemObject`).
     *     Like `data: [ { value: xx, itemStyle: {...} }, ...]`
     *     At present it only happen in `SOURCE_FORMAT_ORIGINAL`.
     */
    pure?: boolean;
    /**
     * If data is persistent and will not be released after use.
     */
    persistent?: boolean;

    getSource(): Source;
    count(): number;
    getItem(idx: number, out?: OptionDataItem): OptionDataItem;
    fillStorage?(
        start: number,
        end: number,
        out: ArrayLike<ParsedValue>[],
        extent: number[][]
    ): void
    appendData?(newData: ArrayLike<OptionDataItem>): void;
    clean?(): void;
}


let providerMethods: Dictionary<any>;
let mountMethods: (provider: DefaultDataProvider, data: OptionSourceData, source: Source) => void;

export interface DefaultDataProvider {
    fillStorage?(
        start: number,
        end: number,
        out: ArrayLike<ParsedValue>[],
        extent: number[][]
    ): void
}
/**
 * If normal array used, mutable chunk size is supported.
 * If typed array used, chunk size must be fixed.
 */
export class DefaultDataProvider implements DataProvider {

    private _source: Source;

    private _data: OptionSourceData;

    private _offset: number;

    private _dimSize: number;

    pure: boolean;

    persistent: boolean;

    static protoInitialize = (function () {
        // PENDING: To avoid potential incompat (e.g., prototype
        // is visited somewhere), still init them on prototype.
        const proto = DefaultDataProvider.prototype;
        proto.pure = false;
        proto.persistent = true;
    })();


    constructor(sourceParam: Source | OptionSourceData, dimSize?: number) {
        // let source: Source;
        const source: Source = !isSourceInstance(sourceParam)
            ? createSourceFromSeriesDataOption(sourceParam as OptionSourceData)
            : sourceParam as Source;

        // declare source is Source;
        this._source = source;
        const data = this._data = source.data;

        // Typed array. TODO IE10+?
        if (source.sourceFormat === SOURCE_FORMAT_TYPED_ARRAY) {
            if (__DEV__) {
                if (dimSize == null) {
                    throw new Error('Typed array data must specify dimension size');
                }
            }
            this._offset = 0;
            this._dimSize = dimSize;
            this._data = data;
        }

        mountMethods(this, data, source);
    }

    getSource(): Source {
        return this._source;
    }

    count(): number {
        return 0;
    }

    getItem(idx: number, out?: ArrayLike<OptionDataValue>): OptionDataItem {
        return;
    }

    appendData(newData: OptionSourceData): void {
    }

    clean(): void {
    }

    private static internalField = (function () {

        mountMethods = function (provider, data, source) {
            const sourceFormat = source.sourceFormat;
            const seriesLayoutBy = source.seriesLayoutBy;
            const startIndex = source.startIndex;
            const dimsDef = source.dimensionsDefine;

            const methods = providerMethods[getMethodMapKey(sourceFormat, seriesLayoutBy)];
            if (__DEV__) {
                assert(methods, 'Invalide sourceFormat: ' + sourceFormat);
            }

            extend(provider, methods);

            if (sourceFormat === SOURCE_FORMAT_TYPED_ARRAY) {
                provider.getItem = getItemForTypedArray;
                provider.count = countForTypedArray;
                provider.fillStorage = fillStorageForTypedArray;
            }
            else {
                const rawItemGetter = getRawSourceItemGetter(sourceFormat, seriesLayoutBy);
                provider.getItem = bind(rawItemGetter, null, data, startIndex, dimsDef);
                const rawCounter = getRawSourceDataCounter(sourceFormat, seriesLayoutBy);
                provider.count = bind(rawCounter, null, data, startIndex, dimsDef);
            }
        };

        const getItemForTypedArray: DefaultDataProvider['getItem'] = function (
            this: DefaultDataProvider, idx: number, out: ArrayLike<number>
        ): ArrayLike<number> {
            idx = idx - this._offset;
            out = out || [];
            const data = this._data;
            const dimSize = this._dimSize;
            const offset = dimSize * idx;
            for (let i = 0; i < dimSize; i++) {
                out[i] = (data as ArrayLike<number>)[offset + i];
            }
            return out;
        };

        const fillStorageForTypedArray: DefaultDataProvider['fillStorage'] = function (
            this: DefaultDataProvider, start: number, end: number, storage: ArrayLike<ParsedValue>[], extent: number[][]
        ) {
            const data = this._data as ArrayLike<number>;
            const dimSize = this._dimSize;

            for (let dim = 0; dim < dimSize; dim++) {
                const dimExtent = extent[dim];
                let min = dimExtent[0] == null ? Infinity : dimExtent[0];
                let max = dimExtent[1] == null ? -Infinity : dimExtent[1];
                const count = end - start;
                const arr = storage[dim];
                for (let i = 0; i < count; i++) {
                    // appendData with TypedArray will always do replace in provider.
                    const val = data[i * dimSize + dim];
                    arr[start + i] = val;
                    val < min && (min = val);
                    val > max && (max = val);
                }
                dimExtent[0] = min;
                dimExtent[1] = max;
            }
        };

        const countForTypedArray: DefaultDataProvider['count'] = function (
            this: DefaultDataProvider
        ) {
            return this._data ? ((this._data as ArrayLike<number>).length / this._dimSize) : 0;
        };

        providerMethods = {

            [SOURCE_FORMAT_ARRAY_ROWS + '_' + SERIES_LAYOUT_BY_COLUMN]: {
                pure: true,
                appendData: appendDataSimply
            },

            [SOURCE_FORMAT_ARRAY_ROWS + '_' + SERIES_LAYOUT_BY_ROW]: {
                pure: true,
                appendData: function () {
                    throw new Error('Do not support appendData when set seriesLayoutBy: "row".');
                }
            },

            [SOURCE_FORMAT_OBJECT_ROWS]: {
                pure: true,
                appendData: appendDataSimply
            },

            [SOURCE_FORMAT_KEYED_COLUMNS]: {
                pure: true,
                appendData: function (this: DefaultDataProvider, newData: Dictionary<OptionDataValue[]>) {
                    const data = this._data as Dictionary<OptionDataValue[]>;
                    each(newData, function (newCol, key) {
                        const oldCol = data[key] || (data[key] = []);
                        for (let i = 0; i < (newCol || []).length; i++) {
                            oldCol.push(newCol[i]);
                        }
                    });
                }
            },

            [SOURCE_FORMAT_ORIGINAL]: {
                appendData: appendDataSimply
            },

            [SOURCE_FORMAT_TYPED_ARRAY]: {
                persistent: false,
                pure: true,
                appendData: function (this: DefaultDataProvider, newData: ArrayLike<number>): void {
                    if (__DEV__) {
                        assert(
                            isTypedArray(newData),
                            'Added data must be TypedArray if data in initialization is TypedArray'
                        );
                    }
                    this._data = newData;
                },

                // Clean self if data is already used.
                clean: function (this: DefaultDataProvider): void {
                    // PENDING
                    this._offset += this.count();
                    this._data = null;
                }
            }
        };

        function appendDataSimply(this: DefaultDataProvider, newData: ArrayLike<OptionDataItem>): void {
            for (let i = 0; i < newData.length; i++) {
                (this._data as any[]).push(newData[i]);
            }
        }

    })();
}



type RawSourceItemGetter = (
    rawData: OptionSourceData,
    startIndex: number,
    dimsDef: { name?: DimensionName }[],
    idx: number,
    // Only used in SOURCE_FORMAT_ARRAY_ROWS + '_' + SERIES_LAYOUT_BY_ROW and SOURCE_FORMAT_KEYED_COLUMNS
    // to avoid create a new [] if `out` is provided.
    out?: ArrayLike<OptionDataValue>
) => OptionDataItem | ArrayLike<OptionDataValue>;

const getItemSimply: RawSourceItemGetter = function (
    rawData, startIndex, dimsDef, idx
): OptionDataItem {
    return (rawData as [])[idx];
};

const rawSourceItemGetterMap: Dictionary<RawSourceItemGetter> = {
    [SOURCE_FORMAT_ARRAY_ROWS + '_' + SERIES_LAYOUT_BY_COLUMN]: function (
        rawData, startIndex, dimsDef, idx
    ) {
        return (rawData as OptionDataValue[][])[idx + startIndex];
    },
    [SOURCE_FORMAT_ARRAY_ROWS + '_' + SERIES_LAYOUT_BY_ROW]: function (
        rawData, startIndex, dimsDef, idx, out
    ) {
        idx += startIndex;
        const item = out || [];
        const data = rawData as OptionDataValue[][];
        for (let i = 0; i < data.length; i++) {
            const row = data[i];
            item[i] = row ? row[idx] : null;
        }
        return item;
    },
    [SOURCE_FORMAT_OBJECT_ROWS]: getItemSimply,
    [SOURCE_FORMAT_KEYED_COLUMNS]: function (
        rawData, startIndex, dimsDef, idx, out
    ) {
        const item = out || [];
        for (let i = 0; i < dimsDef.length; i++) {
            const dimName = dimsDef[i].name;
            if (__DEV__) {
                if (dimName == null) {
                    throw new Error();
                }
            }
            const col = (rawData as Dictionary<OptionDataValue[]>)[dimName];
            item[i] = col ? col[idx] : null;
        }
        return item;
    },
    [SOURCE_FORMAT_ORIGINAL]: getItemSimply
};

export function getRawSourceItemGetter(
    sourceFormat: SourceFormat, seriesLayoutBy: SeriesLayoutBy
): RawSourceItemGetter {
    const method = rawSourceItemGetterMap[getMethodMapKey(sourceFormat, seriesLayoutBy)];
    if (__DEV__) {
        assert(method, 'Do not support get item on "' + sourceFormat + '", "' + seriesLayoutBy + '".');
    }
    return method;
}




type RawSourceDataCounter = (
    rawData: OptionSourceData,
    startIndex: number,
    dimsDef: { name?: DimensionName }[]
) => number;

const countSimply: RawSourceDataCounter = function (
    rawData, startIndex, dimsDef
) {
    return (rawData as []).length;
};

const rawSourceDataCounterMap: Dictionary<RawSourceDataCounter> = {
    [SOURCE_FORMAT_ARRAY_ROWS + '_' + SERIES_LAYOUT_BY_COLUMN]: function (
        rawData, startIndex, dimsDef
    ) {
        return Math.max(0, (rawData as OptionDataItem[][]).length - startIndex);
    },
    [SOURCE_FORMAT_ARRAY_ROWS + '_' + SERIES_LAYOUT_BY_ROW]: function (
        rawData, startIndex, dimsDef
    ) {
        const row = (rawData as OptionDataValue[][])[0];
        return row ? Math.max(0, row.length - startIndex) : 0;
    },
    [SOURCE_FORMAT_OBJECT_ROWS]: countSimply,
    [SOURCE_FORMAT_KEYED_COLUMNS]: function (
        rawData, startIndex, dimsDef
    ) {
        const dimName = dimsDef[0].name;
        if (__DEV__) {
            if (dimName == null) {
                throw new Error();
            }
        }
        const col = (rawData as Dictionary<OptionDataValue[]>)[dimName];
        return col ? col.length : 0;
    },
    [SOURCE_FORMAT_ORIGINAL]: countSimply
};

export function getRawSourceDataCounter(
    sourceFormat: SourceFormat, seriesLayoutBy: SeriesLayoutBy
): RawSourceDataCounter {
    const method = rawSourceDataCounterMap[getMethodMapKey(sourceFormat, seriesLayoutBy)];
    if (__DEV__) {
        assert(method, 'Do not suppport count on "' + sourceFormat + '", "' + seriesLayoutBy + '".');
    }
    return method;
}


type RawSourceValueGetter = (
    dataItem: OptionDataItem,
    dimIndex: DimensionIndex,
    property: DimensionName
) => OptionDataValue;

const getRawValueSimply = function (
    dataItem: ArrayLike<OptionDataValue>, dimIndex: number, property: string
): OptionDataValue {
    return dataItem[dimIndex];
};

const rawSourceValueGetterMap: Partial<Record<SourceFormat, RawSourceValueGetter>> = {

    [SOURCE_FORMAT_ARRAY_ROWS]: getRawValueSimply,

    [SOURCE_FORMAT_OBJECT_ROWS]: function (
        dataItem: Dictionary<OptionDataValue>, dimIndex: number, property: string
    ): OptionDataValue {
        return dataItem[property];
    },

    [SOURCE_FORMAT_KEYED_COLUMNS]: getRawValueSimply,

    [SOURCE_FORMAT_ORIGINAL]: function (
        dataItem: OptionDataItem, dimIndex: number, property: string
    ): OptionDataValue {
        // FIXME: In some case (markpoint in geo (geo-map.html)),
        // dataItem is {coord: [...]}
        const value = getDataItemValue(dataItem);
        return !(value instanceof Array)
            ? value
            : value[dimIndex];
    },

    [SOURCE_FORMAT_TYPED_ARRAY]: getRawValueSimply
};

export function getRawSourceValueGetter(sourceFormat: SourceFormat): RawSourceValueGetter {
    const method = rawSourceValueGetterMap[sourceFormat];
    if (__DEV__) {
        assert(method, 'Do not suppport get value on "' + sourceFormat + '".');
    }
    return method;
}


function getMethodMapKey(sourceFormat: SourceFormat, seriesLayoutBy: SeriesLayoutBy): string {
    return sourceFormat === SOURCE_FORMAT_ARRAY_ROWS
        ? sourceFormat + '_' + seriesLayoutBy
        : sourceFormat;
}


// ??? FIXME can these logic be more neat: getRawValue, getRawDataItem,
// Consider persistent.
// Caution: why use raw value to display on label or tooltip?
// A reason is to avoid format. For example time value we do not know
// how to format is expected. More over, if stack is used, calculated
// value may be 0.91000000001, which have brings trouble to display.
// TODO: consider how to treat null/undefined/NaN when display?
export function retrieveRawValue(
    data: SeriesData, dataIndex: number,
    // If dimIndex is null/undefined, return OptionDataItem.
    // Otherwise, return OptionDataValue.
    dim?: DimensionLoose | NullUndefined
): OptionDataValue | OptionDataItem {
    if (!data) {
        return;
    }

    // Consider data may be not persistent.
    const dataItem = data.getRawDataItem(dataIndex);

    if (dataItem == null) {
        return;
    }

    const store = data.getStore();
    const sourceFormat = store.getSource().sourceFormat;

    if (dim != null) {
        const dimIndex = data.getDimensionIndex(dim);
        const property = store.getDimensionProperty(dimIndex);

        return getRawSourceValueGetter(sourceFormat)(dataItem, dimIndex, property);
    }
    else {
        let result = dataItem;
        if (sourceFormat === SOURCE_FORMAT_ORIGINAL) {
            result = getDataItemValue(dataItem);
        }
        return result;
    }
}


/**
 * Compatible with some cases (in pie, map) like:
 * data: [{name: 'xx', value: 5, selected: true}, ...]
 * where only sourceFormat is 'original' and 'objectRows' supported.
 *
 * // TODO
 * Supported detail options in data item when using 'arrayRows'.
 *
 * @param data
 * @param dataIndex
 * @param attr like 'selected'
 */
export function retrieveRawAttr(data: SeriesData, dataIndex: number, attr: string): any {
    if (!data) {
        return;
    }

    const sourceFormat = data.getStore().getSource().sourceFormat;

    if (sourceFormat !== SOURCE_FORMAT_ORIGINAL
        && sourceFormat !== SOURCE_FORMAT_OBJECT_ROWS
    ) {
        return;
    }

    let dataItem = data.getRawDataItem(dataIndex);
    if (sourceFormat === SOURCE_FORMAT_ORIGINAL && !isObject(dataItem)) {
        dataItem = null;
    }
    if (dataItem) {
        return (dataItem as Dictionary<OptionDataValue>)[attr];
    }
}
