import ngModule from '../../module';
import Component from '../../lib/component';
import getModifiedData from '../../lib/modified';
import copyObject from '../../lib/copy';
import isEqual from '../../lib/equals';
import isFullEmpty from '../../lib/full-empty';
import UserError from '../../lib/user-error';

/**
 * 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.
 */
export default class Watcher extends Component {
    constructor($element, $, $state, $stateParams, $transitions, $http, vnApp, $translate, $attrs, $q) {
        super($element);
        Object.assign(this, {
            $,
            $state,
            $stateParams,
            $http,
            _: $translate,
            $attrs,
            vnApp,
            $q
        });

        this.state = null;
        this.deregisterCallback = $transitions.onStart({},
            transition => this.callback(transition));
        this.updateOriginalData();
    }

    $onInit() {
        if (this.get && this.url)
            this.fetchData();
        else if (this.get && !this.url)
            throw new Error('URL parameter ommitted');
    }

    $onChanges(changes) {
        if (this.data)
            this.updateOriginalData();
    }

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

    get dirty() {
        return this.form && this.form.$dirty || this.dataChanged();
    }

    dataChanged() {
        let data = this.copyInNewObject(this.data);
        return !isEqual(data, this.orgData);
    }

    fetchData() {
        let id = this.data[this.idField];
        return this.$http.get(`${this.url}/${id}`).then(
            json => {
                this.data = copyObject(json.data);
                this.updateOriginalData();
            }
        );
    }

    /**
     * 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 {
            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.updateOriginalData();
            return this.$q.resolve();
        }

        let isPost = (this.$attrs.save && this.$attrs.save.toLowerCase() === 'post');
        let changedData = isPost
            ? this.data
            : getModifiedData(this.data, this.orgData);

        let id = this.idField ? this.orgData[this.idField] : null;

        // If watcher is associated to mgCrud

        if (this.save && this.save.accept) {
            if (id)
                changedData[this.idField] = id;

            this.save.model = changedData;
            return this.$q((resolve, reject) => {
                this.save.accept().then(
                    json => this.writeData({data: json}, resolve),
                    reject
                );
            });
        }

        // When mgCrud is not used

        if (id) {
            return this.$q((resolve, reject) => {
                this.$http.patch(`${this.url}/${id}`, changedData).then(
                    json => this.writeData(json, resolve),
                    reject
                );
            });
        }


        return this.$q((resolve, reject) => {
            this.$http.post(this.url, changedData).then(
                json => this.writeData(json, resolve),
                reject
            );
        });
    }

    /**
     * Checks if data is ready to send.
     */
    check() {
        if (this.form && this.form.$invalid)
            throw new UserError('Some fields are invalid');
        if (!this.dirty)
            throw new UserError('No changes to save');
    }

    /**
     * Notifies the user that the data has been saved.
     */
    notifySaved() {
        this.vnApp.showSuccess(this._.instant('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') {
            if (this.data)
                Object.assign(this.data, this.orgData);
            this.$state.go(this.state);
        } else
            this.state = null;
    }

    writeData(json, resolve) {
        Object.assign(this.data, json.data);
        this.updateOriginalData();
        resolve(json);
    }

    updateOriginalData() {
        this.orgData = this.copyInNewObject(this.data);
        this.setPristine();
    }

    copyInNewObject(data) {
        let newCopy = {};
        if (data && typeof data === 'object') {
            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] = this.copyInNewObject(value);
                        else
                            newCopy[key] = value;
                    }
                }
            );
        }

        return newCopy;
    }
}
Watcher.$inject = ['$element', '$scope', '$state', '$stateParams', '$transitions', '$http', 'vnApp', '$translate', '$attrs', '$q'];

ngModule.component('vnWatcher', {
    template: require('./watcher.html'),
    bindings: {
        url: '@?',
        idField: '@?',
        data: '<',
        form: '<',
        save: '<',
        get: '<?'
    },
    controller: Watcher
});