import ngModule from '../../module';
import Component from '../../lib/component';

/**
 * A data model. It allows to filter, sort and paginate data.
 *
 * @property {Boolean} autoLoad Whether to load data automatically when required attributes are setted
 * @event dataChange Emitted when data property changes
 */
export class DataModel extends Component {
    /**
     * @type {Array<Object>} A JavaScript array with the model data.
     */
    get data() {
        return null;
    }

    /**
     * Refresh the model data.
     *
     * @return {Promise} The request promise
     */
    refresh() {
        return Promise.resolve();
    }

    /**
     * Clears the model, removing all it's data.
     */
    clear() {}
}

ngModule.vnComponent('vnDataModel', {
    controller: DataModel,
    bindings: {
        data: '=?',
        autoLoad: '<?',
        order: '@?',
        limit: '<?'
    }
});

/**
 * A data model that monitorizes changes to the data.
 * It internally uses the JavaScript Proxy to track changes
 * made in model rows.
 *
 * @property {Boolean} autoSave Whether to save data automatically when model is modified
 * @event dataChange Emitted when data property changes
 * @event dataUpdate Emitted when data property changes
 * @event rowInsert Emitted when new row is inserted
 * @event rowRemove Emitted when row is removed
 * @event rowChange Emitted when row property is changed
 * @event save Emitted when data is saved
 */
export default class ModelProxy extends DataModel {
    constructor($element, $scope) {
        super($element, $scope);
        this.resetChanges();
        this.status = 'clear';
    }

    get orgData() {
        return this._orgData;
    }

    set orgData(value) {
        this._orgData = value;

        if (value) {
            this.proxiedData = new Array(value.length);

            for (let i = 0; i < value.length; i++)
                this.proxiedData[i] = this.createRow(Object.assign({}, value[i]), i, value[i]);
        } else
            this.proxiedData = null;

        this.data = null;
        this.resetChanges();
    }

    /**
     * @type {Array<Object>} A JavaScript array with the model data.
     * Rows data can be modified directly but for insertion or removing
     * rows use the remove() or insert() model methods, otherwise changes
     * are not detected by the model.
     */
    get data() {
        return this._data;
    }

    set data(value) {
        this._data = value;

        if (value == null)
            this.status = 'clear';
        else if (value.length)
            this.status = 'ready';
        else
            this.status = 'empty';

        this.emit('dataChange');
        this.emit('dataUpdate');
    }

    /**
     * Removes a row from the model and emits the 'rowRemove' event.
     *
     * @param {Number} index The row index
     * @return {Promise} The save request promise
     */
    remove(index) {
        let [row] = this.data.splice(index, 1);

        let proxiedIndex = this.proxiedData.indexOf(row);
        this.proxiedData.splice(proxiedIndex, 1);

        if (!row.$isNew)
            this.removed.push(row);

        this.isChanged = true;
        if (!this.data.length)
            this.status = 'empty';

        this.emit('rowRemove', index);
        this.emit('dataUpdate');

        if (this.autoSave)
            return this.save();
        else
            return this.$q.resolve();
    }

    /**
     * Removes a row from the model and emits the 'rowRemove' event.
     *
     * @param {Object} row The row object
     * @return {Promise} The save request promise
     */
    removeRow(row) {
        return this.remove(this.data.indexOf(row));
    }

    /**
     * Inserts a new row into the model and emits the 'rowInsert' event.
     *
     * @param {Object} data The initial data for the new row
     * @return {Number} The inserted row index
     */
    insert(data) {
        data = Object.assign(data || {}, this.link);
        let newRow = this.createRow(data, null);
        newRow.$isNew = true;
        let index = this.proxiedData.push(newRow) - 1;

        if (this.data)
            this.data.push(newRow);

        this.isChanged = true;
        this.status = 'ready';

        this.emit('rowInsert', index);
        this.emit('dataUpdate');

        return index;
    }

    createRow(obj, index, orgRow) {
        Object.assign(obj, {
            $orgIndex: index,
            $orgRow: orgRow,
            $oldData: null,
            $isNew: false
        });
        return new Proxy(obj, {
            set: (obj, prop, value) => {
                let changed = prop.charAt(0) !== '$' && value !== obj[prop];

                if (changed && !obj.$isNew) {
                    if (!obj.$oldData)
                        obj.$oldData = {};
                    if (!obj.$oldData[prop])
                        obj.$oldData[prop] = value;
                    this.isChanged = true;
                }

                let ret = Reflect.set(obj, prop, value);

                if (changed) {
                    this.emit('rowChange', {obj, prop, value});
                    this.emit('dataUpdate');

                    if (!obj.$isNew && this.autoSave)
                        this.save();
                }

                return ret;
            }
        });
    }

    resetChanges() {
        this.removed = [];
        this.isChanged = false;

        let data = this.proxiedData;
        if (data) {
            for (let row of data)
                row.$oldData = null;
        }
    }

    /**
     * Applies all changes made to the model into the original data source.
     */
    applyChanges() {
        let data = this.proxiedData;
        let orgData = this.orgData;
        if (!data) return;

        for (let row of data) {
            if (row.$isNew) {
                let orgRow = {};
                for (let prop in row) {
                    if (prop.charAt(0) !== '$')
                        orgRow[prop] = row[prop];
                }
                row.$orgIndex = orgData.push(orgRow) - 1;
                row.$orgRow = orgRow;
                row.$isNew = false;
            } else if (row.$oldData) {
                for (let prop in row.$oldData)
                    row.$orgRow[prop] = row[prop];
            }
        }

        let removed = this.removed;

        if (removed) {
            removed = removed.sort((a, b) => b.$orgIndex - a.$orgIndex);

            for (let row of this.removed)
                orgData.splice(row.$orgIndex, 1);
        }

        this.resetChanges();
    }

    /**
     * Should be implemented by child classes.
     */
    save() {
        this.emit('save');
    }

    /**
     * Undoes all changes made to the model data.
     */
    undoChanges() {
        let data = this.proxiedData;
        if (!data) return;

        for (let i = 0; i < data.length; i++) {
            let row = data[i];

            if (row.$oldData)
                Object.assign(row, row.$oldData);
            if (row.$isNew)
                data.splice(i--, 1);
        }

        let removed = this.removed;

        if (removed) {
            removed = removed.sort((a, b) => a.$orgIndex - b.$orgIndex);

            for (let row of this.removed)
                data.splice(row.$orgIndex, 0, row);
        }

        this.resetChanges();
    }
}

ngModule.vnComponent('vnModelProxy', {
    controller: ModelProxy,
    bindings: {
        orgData: '<?',
        data: '=?'
    }
});

/**
 * Interface used to filter data coming from different datasources.
 *
 * @property {Object} filter The base filter
 * @property {Object} params The base filter params
 */
export class Filtrable {
    /**
     * Applies a filter to the model clearing the previous ones.
     *
     * @param {Object} filter The filter parameters
     * @param {*} params Custom user parameters
     * @return {Promise} The request promise
     */
    applyFilter() {
        return Promise.resolve();
    }

    /**
     * Adds a filter to the model mixing it with the currently applied filters.
     *
     * @param {Object} filter The filter parameters
     * @param {*} params Custom user parameters
     * @return {Promise} The request promise
     */
    addFilter() {
        return Promise.resolve();
    }

    /**
     * Removes the currently applied filters.
     *
     * @return {Promise} The request promise
     */
    removeFilter() {
        return Promise.resolve();
    }
}

/**
 * Interface used to sort data coming from different datasources using
 * the same specification: columnName [ASC|DESC]
 *
 * @property {String|Array<String>|Function} order The sort specification.
 */
export class Sortable {}

/**
 * Interface used to paginate data coming from different datasources.
 *
 * @property {Number} limit The page size
 */
export class Paginable {
    /**
     * @type {Boolean} Whether the model is loading.
     */
    get isLoading() {
        return false;
    }

    /**
     * @type {ready|loading|clear|empty|error} The current model status.
     */
    get status() {
        return null;
    }

    /**
     * @type {Boolean} Whether the model is paging.
     */
    get isPaging() {
        return false;
    }

    /**
     * @type {Boolean} Whether the data is paginated and there are more rows to load.
     */
    get moreRows() {
        return false;
    }

    /**
     * When limit is enabled, loads the next set of rows.
     *
     * @return {Promise} The request promise
     */
    loadMore() {
        return Promise.resolve();
    }
}