Source: Models/FunctionParameter.js

'use strict';

/*global require*/
var arraysAreEqual = require('../Core/arraysAreEqual');
var defaultValue = require('terriajs-cesium/Source/Core/defaultValue');
var defined = require('terriajs-cesium/Source/Core/defined');
var defineProperties = require('terriajs-cesium/Source/Core/defineProperties');
var DeveloperError = require('terriajs-cesium/Source/Core/DeveloperError');
var knockout = require('terriajs-cesium/Source/ThirdParty/knockout');
var runLater = require('../Core/runLater');
var serializeToJson = require('../Core/serializeToJson');
var updateFromJson = require('../Core/updateFromJson');
var TerriaError = require('../Core/TerriaError');

/**
 * A parameter to a {@link CatalogFunction}.
 *
 * @alias FunctionParameter
 * @constructor
 * @abstract
 *
 * @param {Object} [options] Object with the following properties:
 * @param {Terria} options.terria The Terria instance.
 * @param {CatalogFunction} options.catalogFunction The function that this is a parameter to.
 * @param {String} options.id The unique ID of this parameter.
 * @param {String} [options.name] The name of this parameter.  If not specified, the ID is used as the name.
 * @param {String} [options.description] The description of the parameter.
 * @param {Boolean} [options.isRequired] True if this parameter is required, false if it is optional.
 * @param {Object} [options.value] The initial value of the parameter.
 */
var FunctionParameter = function(options) {
    if (!defined(options) || !defined(options.terria)) {
        throw new DeveloperError('options.terria is required.');
    }

    if (!defined(options.catalogFunction)) {
        throw new DeveloperError('options.catalogFunction is required.');
    }

    if (!defined(options.id)) {
        throw new DeveloperError('options.id is required.');
    }

    this._terria = options.terria;
    this._catalogFunction = options.catalogFunction;
    this._id = options.id;

    this._loadingPromise = undefined;
    this._lastLoadInfluencingValues = undefined;

    /**
     * Gets or sets a value indicating whether the parameter is currently loading.  This property
     * is observable.
     * @type {Boolean}
     */
    this.isLoading = false;

    /**
     * Gets or sets the name of the parameter.
     * @type {String}
     */
    this.name = defaultValue(options.name, options.id);

    /**
     * Gets or sets the description of the parameter.
     * @type {String}
     */
    this.description = options.description;

    /**
     * Gets or sets a value indicating whether this parameter is required.
     * @type {Boolean}
     * @default false
     */
    this.isRequired = defaultValue(options.isRequired, false);

    /**
     * A converter that can be used to convert this parameter for use with a {@link CatalogFunction}.
     * The actual type and content of this property is defined by the catalog function.
     * @type {Any}
     */
    this.converter = undefined;

    /**
     * Gets or sets the formatter (from the set defined by {@link FunctionParameter#availableFormatters})
     * to use to format this parameter to pass to the {@link CatalogFunction}.
     * @type {String}
     */
    this.formatter = 'default';

    /**
     * Gets the default value for this parameter, or undefined if there is no default value.
     */
    this._defaultValue = options.defaultValue;

    /**
     * Gets or sets the current value of this parameter.
     */
    this.value = options.value;

    knockout.track(this, ['_value', 'formatter', '_defaultValue']);

    /**
     * Gets or sets the current value of this parameter.
     * @member {*} value
     * @memberof FunctionParameter.prototype
     */
    knockout.defineProperty(this, 'value', {
        get: FunctionParameter.defaultValueGetter,
        set: FunctionParameter.defaultValueSetter
    });

    /**
     * Gets the default value for this parameter, or undefined if there is no default value.
     * @member {*} defaultValue
     * @memberof FunctionParameter.prototype
     */
    knockout.defineProperty(this, 'defaultValue', {
        get: function() { return this._defaultValue; },
        set: function(value) { this._defaultValue = value; }
    });
};

FunctionParameter.defaultValueGetter = function() {
    if (!defined(this._value)) {
        return this.defaultValue;
    }
    return this._value;
};

FunctionParameter.defaultValueSetter = function(value) {
    this._value = value;
};

defineProperties(FunctionParameter.prototype, {
    /**
     * Gets the type of this parameter.
     * @memberof FunctionParameter.prototype
     * @type {String}
     */
    type: {
        get: function() {
            throw new DeveloperError('FunctionParameter.type must be overridden in derived classes.');
        }
    },

    /**
     * Gets the Terria instance associated with this parameter.
     * @memberof FunctionParameter.prototype
     * @type {Terria}
     */
    terria: {
        get: function() {
            return this._terria;
        }
    },

    /**
     * Gets the function to which this is a parameter.
     * @memberof FunctionParameter.prototype
     * @type {CatalogFunction}
     */
    catalogFunction: {
        get: function() {
            return this._catalogFunction;
        }
    },

    /**
     * Gets the ID of the parameter.
     * @memberof FunctionParameter.prototype
     * @type {String}
     */
    id: {
        get: function() {
            return this._id;
        }
    },

    /**
     * Gets the formatters that are available to format the parameter's value.
     * @memberof FunctionParameter.prototype
     */
    availableFormatters: {
        get: function() {
            if (this.constructor && this.constructor.AvailableFormatters) {
                return this.constructor.AvailableFormatters;
            } else {
                return {
                    default: this.formatValueAsString.bind(this)
                };
            }
        }
    }
});

FunctionParameter.prototype.load = function() {
    if (!defined(this._load)) {
        // No loading required.
        return undefined;
    }

    if (defined(this._loadingPromise)) {
        // Load already in progress.
        return this._loadingPromise;
    }

    var loadInfluencingValues = [];
    if (defined(this._getValuesThatInfluenceLoad)) {
        loadInfluencingValues = this._getValuesThatInfluenceLoad();
    }

    if (arraysAreEqual(loadInfluencingValues, this._lastLoadInfluencingValues)) {
        // Already loaded, and nothing has changed to force a re-load.
        return undefined;
    }

    this.isLoading = true;

    var that = this;
    this._loadingPromise = runLater(function() {
        that._lastLoadInfluencingValues = [];
        if (defined(that._getValuesThatInfluenceLoad)) {
            that._lastLoadInfluencingValues = that._getValuesThatInfluenceLoad();
        }

        return that._load();
    }).then(function() {
        that._loadingPromise = undefined;
        that.isLoading = false;
    }).otherwise(function(e) {
        that._lastLoadInfluencingValues = undefined;
        that._loadingPromise = undefined;
        that.isLoading = false;
        throw e;
    });

    return this._loadingPromise;
};

/**
 * Represents value as string.
 * @param {Object} [value] Value to format as string. If not specified, {@link FunctionParameter#value} is used.
 * @return {String} String representation of the value.
 */
FunctionParameter.prototype.formatValueAsString = function(value) {
    value = defaultValue(value, this._value);
    return defined(value) ? value.toString() : '-';
};

/**
 * Formats this value to pass to a service. The format is controlled by the
 * {@link FunctionParameter#formatter} property.
 *
 * @param {Object} [value] Value to format as string. If not specified, {@link FunctionParameter#value} is used.
 * @return {Any} The formatted value.
 */
FunctionParameter.prototype.formatForService = function(value) {
    const formatter = this.availableFormatters[this.formatter];
    if (!formatter) {
        throw new TerriaError({
            sender: this,
            title: 'Unknown formatter',
            message: `The function parameter formatter ${this.formatter} is unknown.\n\n` +
                     'Valid formatters are:\n\n' +
                     '   * ' + Object.keys(this.availableFormatters).join('\n   * ')
        });
    }

    value = defaultValue(value, this._value);
    return formatter(value);
};

/**
 * Updates the function parameter from a JSON object-literal description of it.
 *
 * @param {Object} json The JSON description.  The JSON should be in the form of an object literal, not a string.
  * @returns {Promise} A promise that resolves when the update is complete.
*/
FunctionParameter.prototype.updateFromJson = function(json) {
    return updateFromJson(this, json);
};

/**
 * Serializes the data item to JSON.
 *
 * @return {Object} The serialized JSON object-literal.
 */
FunctionParameter.prototype.serializeToJson = function() {
    var result = serializeToJson(this);
    result.type = this.type;
    return result;
};

module.exports = FunctionParameter;