import ngModule from '../../module';
import Component from '../../lib/component';
import getModifiedData from '../../lib/modified';
import isEqual from '../../lib/equals';
import isFullEmpty from '../../lib/full-empty';
import UserError from '../../lib/user-error';
import {mergeFilters} from 'vn-loopback/util/filter';

/**
 * Component that checks for changes on a specific model property and asks the
 * user to save or discard it when the state changes.
 * Also it can save the data to the server when the @url and @idField properties
 * are provided.
 *
 * @property {String} idField The id field name, 'id' if not specified
 * @property {*} idValue The id field value
 * @property {Boolean} isNew Whether is a new instance
 * @property {Boolean} insertMode Whether to enable insert mode
 * @property {String} url The base HTTP request path
 * @property {Boolean} get Whether to fetch initial data
 */
export default class Watcher extends Component {
    constructor(...args) {
        super(...args);
        this.idField = 'id';
        this.get = true;
        this.insertMode = false;
        this.state = null;
        this.deregisterCallback = this.$transitions.onStart({},
            transition => this.callback(transition));
        this.snapshot();
    }

    $onInit() {
        let fetch = !this.insertMode
            && this.get
            && this.url
            && this.idValue;

        if (fetch)
            this.fetch();
        else {
            this.isNew = !!this.insertMode;
            this.snapshot();
        }
    }

    $onDestroy() {
        this.deregisterCallback();
    }

    /**
     * @type {Booelan} The computed instance HTTP path
     */
    get instanceUrl() {
        return `${this.url}/${this.idValue}`;
    }

    /**
     * @type {Object} The instance data
     */
    get data() {
        return this._data;
    }

    set data(value) {
        this._data = value;
        this.isNew = !!this.insertMode;
        this.snapshot();
    }

    /**
     * @type {Booelan} Whether it's popullated with data
     */
    get hasData() {
        return !!this.data;
    }

    set hasData(value) {
        if (value)
            this.fill();
        else
            this.delete();
    }

    /**
     * @type {Booelan} Whether instance data have been modified
     */
    get dirty() {
        return this.form && this.form.$dirty || this.dataChanged();
    }

    fetch() {
        let filter = mergeFilters({
            fields: this.fields,
            where: this.where,
            include: this.include
        }, this.filter);

        let params = filter ? {filter} : null;

        return this.$http.get(this.instanceUrl, params)
            .then(json => {
                this.overwrite(json.data);
                this.isNew = false;
                this.snapshot();
            })
            .catch(err => {
                if (!(err.name == 'HttpError' && err.status == 404))
                    throw err;

                if (this.autoFill) {
                    this.insert();
                    this.snapshot();
                }
            });
    }

    insert(data) {
        this.assign({[this.idField]: this.idValue}, data);
        this.isNew = true;
        this.deleted = null;
    }

    delete() {
        if (!this.hasData) return;
        this.deleted = this.makeSnapshot();
        this.clear();
        this.isNew = false;
    }

    recover() {
        if (!this.deleted) return;
        this.restoreSnapshot(this.deleted);
    }

    fill() {
        if (this.hasData)
            return;

        if (this.deleted)
            this.recover();
        else if (this.original && this.original.data)
            this.reset();
        else
            this.insert();
    }

    reset() {
        this.restoreSnapshot(this.original);
        this.setPristine();
    }

    snapshot() {
        const snapshot = this.makeSnapshot();

        if (snapshot.data) {
            const idValue = snapshot.data[this.idField];
            if (idValue) this.idValue = idValue;
        }

        this.original = snapshot;
        this.orgData = snapshot.data;
        this.deleted = null;
        this.setPristine();
    }

    makeSnapshot() {
        return {
            data: this.copyData(),
            isNew: this.isNew,
            ref: this.data
        };
    }

    restoreSnapshot(snapshot) {
        if (!snapshot) return;
        this._data = snapshot.ref;
        this.overwrite(snapshot.data);
        this.isNew = snapshot.isNew;
        this.deleted = null;
    }

    writeData(res) {
        if (this.hasData)
            this.assign(res.data);
        this.isNew = false;
        this.snapshot();
        return res;
    }

    clear() {
        this._data = null;
    }

    overwrite(data) {
        if (data) {
            if (!this.data) this._data = {};
            overwrite(this.data, data);
        } else
            this._data = null;
    }

    assign(...args) {
        this._data = Object.assign(this.data || {}, ...args);
    }

    copyData() {
        return copyObject(this.data);
    }

    refresh() {
        return this.fetch();
    }

    dataChanged() {
        return !isEqual(this.orgData, this.copyData());
    }

    /**
     * Submits the data and goes back in the history.
     *
     * @return {Promise} The request promise
     */
    submitBack() {
        return this.submit().then(res => {
            this.window.history.back();
            return res;
        });
    }

    /**
     * Submits the data and goes another state.
     *
     * @param {String} state The state name
     * @param {Object} params The request params
     * @return {Promise} The request promise
     */
    submitGo(state, params) {
        return this.submit().then(res => {
            this.$state.go(state, params || {});
            return res;
        });
    }

    /**
     * Submits the data to the server.
     *
     * @return {Promise} The http request promise
     */
    submit() {
        try {
            if (this.isNew)
                this.isInvalid();
            else
                this.check();
        } catch (err) {
            return this.$q.reject(err);
        }

        return this.realSubmit().then(res => {
            this.notifySaved();
            return res;
        });
    }

    /**
     * Submits the data without checking data validity or changes.
     *
     * @return {Promise} The http request promise
     */
    realSubmit() {
        if (this.form)
            this.form.$setSubmitted();

        if (!this.dataChanged() && !this.isNew) {
            this.snapshot();
            return this.$q.resolve();
        }

        let changedData = this.isNew
            ? this.data
            : getModifiedData(this.data, this.orgData);

        // If watcher is associated to mgCrud

        if (this.save && this.save.accept) {
            this.save.model = changedData;
            return this.save.accept()
                .then(json => this.writeData({data: json}));
        }

        // When mgCrud is not used

        let req;

        if (this.deleted)
            req = this.$http.delete(this.instanceUrl);
        else if (this.isNew)
            req = this.$http.post(this.url, changedData);
        else
            req = this.$http.patch(this.instanceUrl, changedData);

        return req.then(res => this.writeData(res));
    }

    /**
     * Checks if data is ready to send.
     */
    check() {
        this.isInvalid();
        if (!this.dirty)
            throw new UserError('No changes to save');
    }

    /**
     * Checks if the form is valid.
     */
    isInvalid() {
        if (this.form && this.form.$invalid)
            throw new UserError('Some fields are invalid');
    }

    /**
     * Notifies the user that the data has been saved.
     */
    notifySaved() {
        this.vnApp.showSuccess(this.$t('Data saved!'));
    }

    setPristine() {
        if (this.form) this.form.$setPristine();
    }

    setDirty() {
        if (this.form) this.form.$setDirty();
    }

    callback(transition) {
        if (!this.state && this.dirty) {
            this.state = transition.to().name;
            this.$.confirm.show();
            return false;
        }

        return true;
    }

    onConfirmResponse(response) {
        if (response === 'accept') {
            this.reset();
            this.$state.go(this.state);
        } else
            this.state = null;
    }

    /**
     * @deprecated Use reset()
     */
    loadOriginalData() {
        this.reset();
    }

    /**
     * @deprecated Use snapshot()
     */
    updateOriginalData() {
        this.snapshot();
    }
}
Watcher.$inject = ['$element', '$scope'];

function copyObject(data) {
    let newCopy;

    if (data && typeof data === 'object') {
        newCopy = {};
        Object.keys(data).forEach(
            key => {
                let value = data[key];
                if (value instanceof Date)
                    newCopy[key] = new Date(value.getTime());
                else if (!isFullEmpty(value)) {
                    if (typeof value === 'object')
                        newCopy[key] = copyObject(value);
                    else
                        newCopy[key] = value;
                }
            }
        );
    } else
        newCopy = data;

    return newCopy;
}

function clearObject(obj) {
    if (!obj) return;
    for (let key in obj) {
        if (obj.hasOwnProperty(key))
            delete obj[key];
    }
}

function overwrite(obj, data) {
    if (!obj) return;
    clearObject(obj);
    Object.assign(obj, data);
}

ngModule.vnComponent('vnWatcher', {
    template: require('./watcher.html'),
    bindings: {
        url: '@?',
        idField: '@?',
        idValue: '<?',
        data: '=',
        form: '<',
        save: '<?',
        get: '<?',
        insertMode: '<?',
        autoFill: '<?',
        filter: '<?',
        fields: '<?',
        where: '<?',
        include: '<?'
    },
    controller: Watcher
});