import $ from 'jquery';

import { NotImplementedError } from '../javascript-errors/javascript-errors';
import validator from '../json-validation/json-validation';
import Widget from '../widget/widget';

/**
 * The DataProvider is a relatively abstract class that provides some basic data fetching and management.
 * Each product should implement its own version of this to then provide to consumers (components).
 *
 * Data provided to a DataProvider is expect to at the least have an array of values along with some metadata.
 * The metadata will differ per product and this will be dealt with in the product specific implementations.
 *
 */
class DataProvider extends Widget {

    /**
     * @param {Object} options - The options for the Data Provider
     * @param {Object?} options.jsonDescriptor - If provided, transformed data will be validated against this shape.
     *                                           See the rules in declarative-validation.js and reference the unit tests.
                                                 If not provided, _validate must be implemented in a subclass.
     * @param {Object?} initialData - The initial data for this provider
     */
    constructor(options = {}, initialData = null) {
        super(options);
        this.initialData = initialData;

        if (this.options.jsonDescriptor) {
            this._validate = validator(this.options.jsonDescriptor);
        }
    }

    /**
     * If the data provider is currently fetching data with an XHR request, this will return true.
     *
     * Checks for the presence of the DataProvider#_requestPromise
     *
     * @returns {boolean}
     */
    get isFetching() {
        return !!this._requestPromise;
    }

    /**
     * Reset the data for this provider.
     */
    reset() {
        this.abort();
        this.currentData = null;
        delete this.initialData;
        this.trigger('reset');
    }

    /**
     * Cancel any in-flight requests and nullify the request promise
     */
    abort() {
        if (this._requestPromise) {
            this._requestPromise.abort();
            this._requestPromise = null;
            this.trigger('abort');
        }
    }

    /**
     * Perform the AJAX request for this DataProvider.
     * If called and there is currentData use the locally cached version.
     *
     * This means that {DataProvider#currentData} needs to be invalidated if
     * you want to force the fetching of data from the server on subsequent requests.
     *
     * @param {string?} url - the url to fetch data from. defaults to the base url
     *
     * @returns {Promise}
     */
    fetch(url = this.url) {

        // If a fetch is attempted before the previous fetch has returned,
        // then abort the previous fetch and continue with the new request.
        this.abort();

        // If there is initial data, upgrade the currentData to initialData
        if (this.initialData) {
            this.currentData = this.transform(this.initialData);
            // initialData is single use, delete it after it has been consumed
            delete this.initialData;
        }

        // If there is a set of currentData then return it.
        if (this.currentData) {
            return $.Deferred().resolve(this.currentData);
        }

        this.trigger('data-requested');

        this._requestPromise = this._fetch(url);

        // The promise from _fetch must be abortable.
        if (!this._requestPromise.abort) {
            throw new Error('no abort method on DataProvider#_requestPromise.');
        }

        return this._requestPromise
            .then(this.transform, (...args) => $.Deferred().reject(this.errorTransform(...args)))
            .done(transformedData => {
                this.trigger('data-loaded', transformedData);
                this.currentData = transformedData;
            })
            .fail(error => {
                this.trigger('data-request-failed', error);
            })
            .always(() => {
                this._requestPromise = null;
            });

    }

    /*eslint-disable valid-jsdoc*/ // Disable JSDoc validation for interface
    /**
     * Perform a request to fetch data.
     *
     * @returns {Promise} - a jqxhr promise. This promise should be abortable.
     * @protected
     */
    _fetch() {
        throw new NotImplementedError();
    }

    /**
     * Transform the results from a REST request to the format required
     * by a consumer of a component data provider.
     *
     * @returns {Object} The transformed results
     */
    transform(...args) {
        const out = this._transform(...args);
        this._validate(out);
        return out;
    }

    /**
     * @returns {Object}
     * @protected
     */
    _transform() {
        throw new NotImplementedError();
    }

    /**
     * Throw an error if the transformed data doesn't have the required shape.
     * If a jsonDescriptor is provided, it will be used for validation.
     * @protected
     *
     * @param {Object} data - The data to validate
     */
    _validate(data) { // eslint-disable-line no-unused-vars
        throw new NotImplementedError();
    }

    /**
     * Transform the errors from a REST request to the format required
     * by a consumer of a component data provider.
     *
     * @returns {Object|string}
     */
    errorTransform(...args) {
        return this._errorTransform(...args);
    }

    /**
     * @returns {Object|string}
     * @protected
     */
    _errorTransform() {
        throw new NotImplementedError();
    }

    /*eslint-enable valid-jsdoc*/
}

export default DataProvider;
