import ngModule from '../../module';
import ModelProxy from '../model-proxy/model-proxy';
import {mergeWhere, mergeFilters} from 'vn-loopback/util/filter';

/**
 * Model that uses remote loopback model as datasource.
 *
 * @property {Array<String>} fields the list of fields to fetch
 */
export default class CrudModel extends ModelProxy {
    constructor($q, $http, $element, $scope) {
        super($element, $scope);
        this.$http = $http;
        this.$q = $q;
        this.primaryKey = 'id';
        this.autoLoad = false;
    }

    $onInit() {
        this.initialized = true;
        this.autoRefresh();
    }

    get isLoading() {
        return this.canceler != null;
    }

    /**
     * @type {String} The remote model URL
     */
    get url() {
        return this._url;
    }

    set url(url) {
        if (this._url === url) return;
        this._url = url;
        this.clear();

        if (this.initialized)
            this.autoRefresh();
    }

    autoRefresh() {
        if (this.autoLoad)
            return this.refresh();
        return this.$q.resolve();
    }

    buildFilter() {
        let order = this.order;

        if (typeof order === 'string')
            order = this.order.split(/\s*,\s*/);

        let myFilter = {
            fields: this.fields,
            where: mergeWhere(this.link, this.where),
            include: this.include,
            order: order,
            limit: this.limit
        };

        let filter = this.filter;
        filter = mergeFilters(myFilter, filter);
        filter = mergeFilters(this.userFilter, filter);
        return filter;
    }

    refresh() {
        if (!this._url)
            return this.$q.resolve();
        return this.sendRequest(this.buildFilter());
    }

    /**
     * Applies a new filter to the model.
     *
     * @param {Object} filter The Loopback filter
     * @param {Object} params Custom parameters
     * @return {Promise} The request promise
     */
    applyFilter(filter, params) {
        this.userFilter = filter;
        this.userParams = params;
        return this.refresh();
    }

    /**
     * Adds a filter to the model.
     *
     * @param {Object} filter The Loopback filter
     * @param {Object} params Custom parameters
     * @return {Promise} The request promise
     */
    addFilter(filter, params) {
        this.userFilter = mergeFilters(filter, this.userFilter);
        Object.assign(this.userParams, params);
        return this.refresh();
    }

    removeFilter() {
        return applyFilter(null, null);
    }

    /**
     * Cancels the current request, if any.
     */
    cancelRequest() {
        if (this.canceler) {
            this.canceler.resolve();
            this.canceler = null;
        }
    }

    loadMore() {
        if (!this.moreRows)
            return this.$q.resolve();

        let filter = Object.assign({}, this.currentFilter);
        filter.skip = this.orgData ? this.orgData.length : 0;
        return this.sendRequest(filter, true);
    }

    clear() {
        this.orgData = null;
        this.moreRows = null;
    }

    /**
     * Returns an object with the unsaved changes made to the model.
     *
     * @return {Object} The current changes
     */
    getChanges() {
        if (!this.isChanged)
            return null;

        let deletes = [];
        let updates = [];
        let creates = [];

        let pk = this.primaryKey;

        for (let row of this.removed)
            deletes.push(row.$orgRow[pk]);

        for (let row of this._data) {
            if (row.$isNew) {
                let data = {};
                for (let prop in row) {
                    if (prop.charAt(0) !== '$')
                        data[prop] = row[prop];
                }
                creates.push(data);
            } else if (row.$oldData) {
                let data = {};
                for (let prop in row.$oldData)
                    data[prop] = row[prop];
                updates.push({
                    data,
                    where: {[pk]: row.$orgRow[pk]}
                });
            }
        }

        let changes = {deletes, updates, creates};

        for (let prop in changes) {
            if (changes[prop].length === 0)
                changes[prop] = undefined;
        }

        return changes;
    }

    /**
     * Saves current changes on the server.
     *
     * @return {Promise} The save request promise
     */
    save() {
        let changes = this.getChanges();

        if (!changes)
            return this.$q.resolve();

        let url = this.saveUrl ? this.saveUrl : `${this._url}/crud`;
        return this.$http.post(url, changes)
            .then(() => {
                this.applyChanges();
                super.save();
            });
    }

    buildParams() {
        let params = {};

        if (this.params instanceof Object)
            Object.assign(params, this.params);
        if (this.userParams instanceof Object)
            Object.assign(params, this.userParams);

        return params;
    }

    sendRequest(filter, append) {
        this.cancelRequest();
        this.canceler = this.$q.defer();
        this.isRefreshing = !append;
        this.isPaging = append;

        let params = Object.assign(
            {filter},
            this.buildParams()
        );
        let options = {
            timeout: this.canceler.promise,
            params: params
        };

        return this.$http.get(this._url, options).then(
            json => this.onRemoteDone(json, filter, append),
            json => this.onRemoteError(json)
        ).finally(() => {
            this.isRefreshing = false;
            this.isPaging = false;
        });
    }

    onRemoteDone(json, filter, append) {
        let data = json.data;

        if (append)
            this.orgData = this.orgData.concat(data);
        else {
            this.orgData = data;
            this.currentFilter = filter;
        }

        this.data = this.proxiedData.slice();
        this.moreRows = filter.limit && data.length == filter.limit;
        this.onRequestEnd();
    }

    onRemoteError(err) {
        this.onRequestEnd();
        throw err;
    }

    onRequestEnd() {
        this.canceler = null;
    }

    undoChanges() {
        super.undoChanges();
        this.data = this.proxiedData.slice();
    }
}
CrudModel.$inject = ['$q', '$http', '$element', '$scope'];

ngModule.component('vnCrudModel', {
    controller: CrudModel,
    bindings: {
        orgData: '<?',
        data: '=?',
        onDataChange: '&?',
        link: '<?',
        url: '@?',
        saveUrl: '@?',
        fields: '<?',
        include: '<?',
        where: '<?',
        order: '@?',
        limit: '<?',
        filter: '<?',
        params: '<?',
        userFilter: '<?',
        userParams: '<?',
        primaryKey: '@?',
        autoLoad: '<?',
        autoSave: '<?'
    }
});