import { MetricsConfig } from '@amp-metrics/mt-client-config';
import { reflect } from '@amp-metrics/mt-metricskit-utils-private';

/*
 *  src/delegate.js
 *  mt-metricskit-delegates-core
 *
 *  Copyright © 2022 Apple Inc. All rights reserved.
 *
 */

/**
 * Abstract class for delegates.
 * All delegate implementations should extend from this class.
 * @param {String} topic - Defines the AMP Analytics "topic" for events to be stored under
 * @param {Object} platformImpls - A map that include the platform-based components.
 * @param {Environment} platformImpls.environment - An Environment that provide the event field accessor for the platform
 * @param {EventRecorder} platformImpls.eventRecorder - An EventRecorder that provide the EventRecorder implementation for the platform
 * @constructor
 */
var Delegates = function Delegates(topic, platformImpls) {
    if (!reflect.isDefinedNonNullNonEmpty(topic) || !reflect.isString(topic)) {
        throw new Error('No valid topic was provided to Delegates.');
    }

    this.config = this.getOrCreateConfig(topic);
    // TODO change platformImpls as an required argument in the next major version or supporting Typescript
    // since every platforms should provide their own impl for the Environment and EventHandler to the Delegates.
    if (reflect.isDefinedNonNull(platformImpls)) {
        this.environment = platformImpls.environment;
        this.eventRecorder = platformImpls.eventRecorder;
    }
    // A flag to indicate whether using the original execution context when calling the delegate methods. Default is false
    // TODO Consider to change this to true by default in the next major release 
    //      because using the execution context of the delegate methods may not necessary as accessing the the delegate methods' execution context is straightforward.
    this._useOrginalContextForDelegateFunc = false;
};

/**
 * Initializes the delegate by setting up the config.
 * @param delegates
 * @return {Promise}
 */
Delegates.prototype.init = function () {
    if (!reflect.isDefinedNonNull(this.environment)) {
        throw new Error('No environment was provided to Delegate options.');
    }

    if (!reflect.isDefinedNonNull(this.eventRecorder)) {
        throw new Error('No eventRecorder was provided to Delegate options.');
    }

    this.config.environment.setDelegate(this.environment);
    this.config.logger.setDelegate(this.logger);
    this.config.network.setDelegate(this.network);

    var configSources = reflect.isFunction(this.configSources) ? this.configSources.bind(this) : null;

    return this.config.init(configSources);
};

/**
 * Merge the provided delegates into the current Delegate.
 * NOTE: Any delegates which already exist in the Delegate won't be merged.
 * @param {Object} delegates - A key/value map of the system delegates
 */
Delegates.prototype.mergeDelegates = function (delegates) {
    var self = this;
    for (var key in delegates) {
        if (!self[key]) {
            self[key] = delegates[key];
        }
    }

    this.config.setDelegate(delegates.config);
};

/**
 * Cleans up resources used by the delegate.
 */
Delegates.prototype.cleanup = function cleanup() {
    if (reflect.isFunction(this.eventRecorder.cleanup)) {
        this.eventRecorder.cleanup();
    }

    this.config = null;
    this.eventRecorder = null;
    this.environment = null;
};

/**
 * Overrides the platform-specific event recorder for the Delegate.
 * @param {object} eventRecorderDelegates
 * @returns {Delegates}
 */
Delegates.prototype.setEventRecorder = function setEventRecorder(eventRecorderDelegates) {
    if (reflect.isDefinedNonNull(eventRecorderDelegates)) {
        if (!reflect.isDefinedNonNull(this.eventRecorder)) {
            this.eventRecorder = eventRecorderDelegates;
        } else {
            reflect.setDelegates(this.eventRecorder, eventRecorderDelegates);
            this.eventRecorder.setDelegate(eventRecorderDelegates);
        }
    }

    return this;
};

/**
 * Overrides the existing environment implementations.
 * @param {object} environment
 * @return {Delegates}
 */
Delegates.prototype.setEnvironment = function setEnvironment(environment) {
    if (!reflect.isDefinedNonNull(this.environment)) {
        this.environment = environment;
    } else {
        var newEnvironment = Object.create(this.environment);
        reflect.extend(newEnvironment, environment);
        this.environment = newEnvironment;
    }

    return this;
};

/**
 * Overrides the config methods
 * @param config
 * @return {Delegates}
 */
Delegates.prototype.setConfig = function setConfig(config) {
    this.config.setDelegate(config);

    return this;
};

/**
 * Access the config instance from the child delegates before/during calling Delegates.constructor,
 * this method will create new config instance and set it to the Delegates instance and return the config when the config doesn't exist
 * @param {String} topic - The topic to create the metrics config instance
 * @return {MetricsConfig}
 */
Delegates.prototype.getOrCreateConfig = function getOrCreateConfig(topic) {
    if (!reflect.isDefinedNonNull(this.config)) {
        this.config = new MetricsConfig(topic);
    }

    return this.config;
};

/**
 * Retrieves the config sources. This method must be implemented by subdelegates.
 * @abstract
 * @return {Promise}
 */
Delegates.prototype.configSources = function configSources() {
    throw new Error('This method should be implemented by subdelegates.');
};

/*
 *  src/abstract_event_recorder.js
 *  mt-metricskit-delegates-core
 *
 *  Copyright © 2022 Apple Inc. All rights reserved.
 *
 */

/**
 * An abstract event recorder to provide common logic across platform event recorders
 * @constructor
 */
function AbstractEventRecorder() {
    this._operationPromiseChain = Promise.resolve();
}

/**
 * Allows replacement of one or more of this class' functions
 * Any method on the passed-in object which matches a method that this class has will be called instead of the built-in class method.
 * To replace *all* methods of his class, simply have your delegate implement all the methods of this class
 * Your delegate can be a true object instance, an anonymous object, or a class object.
 * Your delegate is free to have as many additional non-matching methods as it likes.
 * It can even act as a delegate for multiple MetricsKit objects, though that is not recommended.
 *
 * "setDelegate()" may be called repeatedly, with the functions in the most-recently set delegates replacing any functions matching those in the earlier delegates, as well as any as-yet unreplaced functions.
 * This allows callers to use "canned" delegates to get most of their functionality, but still replace some number of methods that need custom implementations.
 * If, for example, a client wants to use the "canned" itml/environment delegate with the exception of, say, the "appVersion" method, they can set itml/environment as the delegate, and
 * then call "setDelegate()" again with their own delegate containing only a single method of "appVersion" as the delegate, which would leave all the other "replaced" methods intact,
 * but override the "appVersion" method again, this time with their own supplied delegate.
 *
 * NOTE: when the delegate function is called, it will include an additional final parameter representing the original function that it replaced (the callee would typically name this parameter "replacedFunction").
 * This allows the delegate to, essentially, call "super" before or after it does some work.
 * If a replaced method is overridden again with a subsequent "setDelegate()" call, the "replacedFunction" parameter will be the previous delegate.
 * @example:
 * To override one or more methods, in place:
 *      eventRecorder.setDelegate({recordEvent: itms.recordEvent});
 * To override one or more methods with a separate object:
 *      eventRecorder.setDelegate(eventRecorderDelegate);
 *      (where "eventRecorderDelegate" might be defined elsewhere as, e.g.:
 *          var eventRecorderDelegate = {recordEvent: itms.recordEvent,
 *                                       sendMethod: 'itms'};
 * To override one or more methods with an instantiated object from a class definition:
 *      eventRecorder.setDelegate(new EventRecorderDelegate());
 *      (where "EventRecorderDelegate" might be defined elsewhere as, e.g.:
 *          function EventRecorderDelegate() {
 *         }
 *         EventRecorderDelegate.prototype.recordEvent = itms.recordEvent;
 *         EventRecorderDelegate.prototype.sendMethod = function sendMethod() {
 *                                                          return 'itms';
 *                                                      };
 * To override one or more methods with a class object (with "static" methods):
 *      eventRecorder.setDelegate(EventRecorderDelegate);
 *      (where "EventRecorderDelegate" might be defined elsewhere as, e.g.:
 *          function EventRecorderDelegate() {
 *         }
 *         EventRecorderDelegate.recordEvent = itms.recordEvent;
 *         EventRecorderDelegate.sendMethod = function sendMethod() {
 *                                                return 'itms';
 *                                            };
 * @param {Object} Object or Class with delegate method(s) to be called instead of default (built-in) methods.
 * @returns {Boolean} true if one or more methods on the delegate object match one or more methods on the default object,
 * otherwise returns false.
 */
AbstractEventRecorder.prototype.setDelegate = function setDelegate(delegate) {
    return reflect.attachDelegate(this, delegate);
};

/**
 * Public method to interact with Processors to record an event
 * @param {String} topic - an 'override' topic which will override the main topic.
 * @param {Promise|Object} eventFields - a Promise/JavaScript object which will be converted to a JSON string and sent to AMP Analytics.
 * @return {Promise}
 */
AbstractEventRecorder.prototype.recordEvent = function recordEvent(topic, eventFields) {
    var vargs = Array.prototype.slice.call(arguments, 2);
    var self = this;
    this._operationPromiseChain = this._operationPromiseChain.then(function () {
        return Promise.resolve(eventFields).then(function (eventFields) {
            return self._recordEvent.apply(self, [topic, eventFields].concat(vargs));
        });
    });

    return this._operationPromiseChain;
};

/**
 * Sends any remaining events in the queue, then clears it
 * This is typically called on page / app close when the JS context is about to disappear and thus we will
 * not know if the events made it to the server
 * @param {Boolean} appIsExiting - Pass true if events are being flushed due to your app exiting or page going away
 * (the send method will be different in order to attempt to post events prior to actual termination)
 * @param {String} appExitSendMethod (optional) the send method for how events will be flushed when the app is exiting.
 * Possible options are enumerated in the `eventRecorder.SEND_METHOD` object.
 * Note: This argument will be ignored if appIsExiting is false.
 * @returns {Promise}
 */
AbstractEventRecorder.prototype.flushUnreportedEvents = function flushUnreportedEvents() {
    var args = Array.prototype.slice.call(arguments);
    var self = this;
    return this._operationPromiseChain.then(function () {
        // Reset the promise chain
        self._operationPromiseChain = Promise.resolve();
        return self._flushUnreportedEvents.apply(self, args);
    });
};

/**
 * Abstract recordEvent method
 * Subclasses implement this method to handle how to record an event
 * @abstract
 * @protected
 * @param {String} topic - an 'override' topic which will override the main topic.
 * @param {Object} eventFields - a JavaScript object which will be converted to a JSON string and sent to AMP Analytics.
 * @returns {Promise}
 */
AbstractEventRecorder.prototype._recordEvent = function _recordEvent(topic, eventFields) {};

/**
 * Abstract flushUnreportedEvents method
 * Subclasses implement this method to handle how to flush the cache events
 * @abstract
 * @protected
 * @param {Boolean} appIsExiting - if events are being flushed due to your app exiting (or page going away for web-apps), pass "true".
 *                  This allows MetricsKit to modify its flush strategy to attempt to post events prior to actual termination.
 *                  In cases where appIsExiting==false, the parameter may be omitted.
 * @returns {Promise}
 */
AbstractEventRecorder.prototype._flushUnreportedEvents = function _flushUnreportedEvents(appIsExiting) {};

export { AbstractEventRecorder, Delegates };
