import AppMeta from '../objects/app-meta';
import {A as emberA, isArray} from '@ember/array';
import EmberObject from '@ember/object';
import Service, {inject} from '@ember/service';
import {isEmpty, isNone} from '@ember/utils';
import RSVP from 'rsvp';

/**
 * This service is a simple cache of app metas, as we need information from app
 * metas for a few inter app compoments.
 * This is done as a service instead of using ember data reduce dependencies and
 * prevent model conflicts (plus meta does not conform to a JSON model).
 *
 * After you inject this service, you have four public methods which can be used to retrieve
 * appMetas.
 *
 * @public
 * @class AppMetaService
 * @extends Ember.Service
 * @module Services
 */
export default Service.extend({
    ajax: inject(),
    config: inject(),

    /**
     * Cache of app metas previously fetched.
     * Should not be directly accessed, but can be watched
     * @protected
     * @property cache
     * @type {EmberObject}
     */
    cache: EmberObject.create(),

    /**
     * App promises that have not yet resolved
     * @private
     * @property _promises
     * @type {EmberObject}
     */
    _promises: EmberObject.create(),

    /**
     * Fetches the meta for an app
     * @public
     * @param  {String} app App ID
     * @return {Object}     Javascript object of app metadata, {size: 0} if no meta found, or null if not fetched
     */
    peekMeta(id) {
        return this.get('cache').get(id);
    },

    /**
     * Fetches the metadata for a list of apps
     * @param  {Array} ids Array of string app IDs
     * @return {Array}      Array of coorsponding app IDs
     */
    peekMetas(ids) {
        return emberA(ids).map((id) => this.peekMeta(id)).reject(isEmpty);
    },

    /**
     * Internal function to store an array of apps to the cache
     * @private
     * @param  {Array} apps Array of app meta
     * @param  {Array} ids  Array of IDs to ensure are stored
     */
    _cacheApps(apps, ids) {
        // store all passed in app data
        let metas = this.get('cache');
        if (isArray(apps)) {
            emberA(apps).forEach((app) => {
                metas.set(app.appId, AppMeta.create(app));
            });
        }
        // for all remaining IDs, store object of size 0 to say we tried to fetch but got nothing
        if (isArray(ids)) {
            emberA(ids).forEach((id) => {
                if (isNone(metas.get(id))) {
                    metas.set(id, AppMeta.create({appId: id, size: 0}));
                }
            });
        }
    },

    /**
     * Internal function to request an app meta. Prevents making multiple requests for the same piece of data
     * @private
     * @param  {String}  id  App ID to fetch
     * @return {Promise}     Promise that resolves to app meta
     */
    _requestMeta(id) {
        let promises = this.get('_promises');
        let current = promises.get(id);
        if (!isNone(current)) {
            return current;
        }

        // make the actual fetch
        let promise = this.get('ajax').request(this.get('config').formGCUrl('apps', 'meta', {ids: id})).then(({apps}) => {
            this._cacheApps(apps, [id]);
            promises.set(id, null);
            // normalize empty to null for this method
            let meta = this.peekMeta(id);
            return isEmpty(meta) ? null : meta;
        });
        promises.set(id, promise);
        return promise;
    },

    /**
     * Fetches the meta from the server for the specified app
     * @param  {String}  id             App ID to fetch
     * @param  {Boolean} [reload=false] If true, fetches the data even if loaded. False returns cached data if present
     * @return {Promise}                Promise that resolves to app meta, or resolves to null if no access to the app
     */
    findMeta(id, reload = false) {
        // if not requested to reload, return previously found meta (if it exists)
        if (!reload) {
            let meta = this.peekMeta(id);
            if (!isNone(meta)) {
                // empty means we fetched it before, but got nothing, so give null in that case
                return RSVP.resolve(isEmpty(meta) ? null : meta);
            }
        }

        // if we are reloading or did not find the meta, fetch it
        return this._requestMeta(id);
    },

    /**
     * Fetches the meta from the server for the specified app
     * @param  {Array}  ids            App IDs to fetch
     * @param  {Boolean} [reload=false] If true, fetches the data even if loaded. False returns cached data if present
     * @return {Promise}                Promise that resolves to app meta
     */
    findMetas(ids, reload = false) {
        // TODO: probably a good idea to validate the IDs are valid apps, not that this method is used outside of the app switcher now
        let idString;
        if (reload) {
            idString = ids.join(',');
        } else {
            // if we have no missing apps, return cached metas
            let missing = emberA(ids).filter((id) => isNone(this.peekMeta(id)));
            if (isEmpty(missing)) {
                return RSVP.resolve(this.peekMetas(ids));
            }
            // otherwise fetch just the missing metas
            idString = missing.join(',');
        }
        // fetch all missing metas and return the array of meta
        // does not use _requestMeta as that function is designed for a single metadata only
        return this.get('ajax').request(this.get('config').formGCUrl('apps', 'meta', {ids: idString})).then(({apps}) => {
            this._cacheApps(apps, ids);
            return this.peekMetas(ids);
        });
    }
});