// ==========================================================================
//  
//  Author:   jsandoe
//
//  Copyright (c) 2016 by The Concord Consortium, Inc. All rights reserved.
//
//  Licensed under the Apache License, Version 2.0 (the "License");
//  you may not use this file except in compliance with the License.
//  You may obtain a copy of the License at
//
//    http://www.apache.org/licenses/LICENSE-2.0
//
//  Unless required by applicable law or agreed to in writing, software
//  distributed under the License is distributed on an "AS IS" BASIS,
//  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
//  See the License for the specific language governing permissions and
//  limitations under the License.
// ==========================================================================

/**
 * This class is intended to provide an abstraction layer for managing
 * a CODAP Data Interactive's connection with CODAP. It is not required. It is
 * certainly possible for a data interactive, for example, to use only the
 * iFramePhone library, which manages the connection at a lower level.
 *
 * This object provides the following services:
 *   1. Initiates the iFramePhone interaction with CODAP.
 *   2. Provides information on the status of the connection.
 *   3. Provides a sendRequest method. It accepts a callback or returns a Promise
 *      for handling the results from CODAP.
 *   4. Provides a subscriber interface to receive selected notifications from
 *      CODAP.
 *   5. Provides automatic handling of Data Interactive State. Prior to saving
 *      a document CODAP requests state from the Data Interactive, where state
 *      is an arbitrary serializable object containing whatever the data
 *      interactive needs to retain. It returns this state when the document
 *      is reopened.
 *   6. Provides a utility to parse a resource selector into its component parts.
 *
 * @type {Object}
 *
 * Usage Notes:
 *
 *  * With plain ol' script tags:
 *
 *    * In HTML
 *
 *      <script src=".../iframe-phone.js"></script>
 *      <script src=".../CodapInterface.js"></script>
 *      ...
 *
 *    * In Javascript
 *
 *      codapInterface.init({...});
 *      ...
 *
 *  * In a module packager (e.g. webpack):
 *
 *    * Installation
 *
 *      % npm install --save iframe-phone
 *
 *    * In calling module (e.g. my-module.js)
 *
 *      var iframePhone = require('.../CodapInterface');
 *      ...
 *      codapInterface.init({...});
 *
 *    * Packaging
 *
 *      % webpack my-module.js app.js
 *
 */

/*global require, iframePhone, Promise, module */

(function (global) {

    var iframePh = (typeof module !== 'undefined')? require('iframe-phone') : iframePhone;

    var config = null;

    /**
     * The CODAP Connection
     * @param {iframePhone.IframePhoneRpcEndpoint}
     */
    var connection = null;

    var connectionState = 'preinit';

    var stats = {
        countDiReq: 0,
        countDiRplSuccess: 0,
        countDiRplFail: 0,
        countDiRplTimeout: 0,
        countCodapReq: 0,
        countCodapUnhandledReq: 0,
        countCodapRplSuccess: 0,
        countCodapRplFail: 0,
        timeDiFirstReq: null,
        timeDiLastReq: null,
        timeCodapFirstReq: null,
        timeCodapLastReq: null
    };

    /**
     * A serializable object shared with CODAP. This is saved as a part of the
     * CODAP document. It is intended for the data interactive's use to store
     * any information it may need to reestablish itself when a CODAP document
     * is saved and restored.
     *
     * This object will be initially empty. It will be updated during the process
     * initiated by the init method if CODAP was started from a previously saved
     * document.
     */
    var interactiveState = {};

    /**
     * A list of subscribers to messages from CODAP
     * @param {[{actionSpec: {RegExp}, resourceSpec: {RegExp}, handler: {function}}]}
     */
    var notificationSubscribers = [];

    function matchResource(resourceName, resourceSpec) {
        return resourceSpec === '*' || resourceName === resourceSpec;
    }

    function notificationHandler (request, callback) {
        var action = request.action;
        var resource = request.resource;
        var requestValues = request.values;
        var returnMessage = {success: true};

        connectionState = 'active';
        stats.countCodapReq += 1;
        stats.timeCodapLastReq = new Date();
        if (!stats.timeCodapFirstReq) {
            stats.timeCodapFirstReq = stats.timeCodapLastReq;
        }

        if (action === 'notify' && !Array.isArray(requestValues)) {
            requestValues = [requestValues];
        }

        var handled = false;
        var success = true;

        if (action === 'get') {
            // get assumes only one subscriber because it expects only one response.
            notificationSubscribers.some(function (subscription) {
                var result = false;
                try {
                    if ((subscription.actionSpec === action) &&
                        matchResource(resource, subscription.resourceSpec)) {
                        var rtn = subscription.handler(request);
                        if (rtn && rtn.success) { stats.countCodapRplSuccess++; } else{ stats.countCodapRplFail++; }
                        returnMessage = rtn;
                        result = true;
                    }
                } catch (ex) {
                    console.log('DI Plugin notification handler exception: ' + ex);
                    result = true;
                }
                return result;
            });
            if (!handled) {
                stats.countCodapUnhandledReq++;
            }
        } else if (action === 'notify') {
            requestValues.forEach(function (value) {
                notificationSubscribers.forEach(function (subscription) {
                    // pass this notification to matching subscriptions
                    handled = false;
                    if ((subscription.actionSpec === action) && matchResource(resource,
                            subscription.resourceSpec) && (!subscription.operation ||
                        (subscription.operation === value.operation) && subscription.handler)) {
                        var rtn = subscription.handler(
                            {action: action, resource: resource, values: value});
                        if (rtn && rtn.success) { stats.countCodapRplSuccess++; } else{ stats.countCodapRplFail++; }
                        success = (success && (rtn ? rtn.success : false));
                        handled = true;
                    }
                });
                if (!handled) {
                    stats.countCodapUnhandledReq++;
                }
            });
        } else {
            console.log("DI Plugin received unknown message: " + JSON.stringify(request));
        }
        return callback(returnMessage);
    }

    var codapInterface = {
        /**
         * Connection statistics
         */
        stats: stats,

        /**
         * Initialize connection.
         *
         * Start connection. Request interactiveFrame to get prior state, if any.
         * Update interactive frame to set name and dimensions and other configuration
         * information.
         *
         * @param iConfig {object} Configuration. Optional properties: title {string},
         *                        version {string}, dimensions {object}
         *
         * @param iCallback {function(interactiveState)}
         * @return {Promise} Promise of interactiveState;
         */
        init: function (iConfig, iCallback) {
            return new Promise(function (resolve, reject) {
                function getFrameRespHandler(resp) {
                    var success = resp && resp[1] && resp[1].success;
                    var receivedFrame = success && resp[1].values;
                    var savedState = receivedFrame && receivedFrame.savedState;
                    this_.updateInteractiveState(savedState);
                    if (success) {
                        // deprecated way of conveying state
                        if (iConfig.stateHandler) {
                            iConfig.stateHandler(savedState);
                        }
                        resolve(savedState);
                    } else {
                        if (!resp) {
                            reject('Connection request to CODAP timed out.');
                        } else {
                            reject(
                                (resp[1] && resp[1].values && resp[1].values.error) ||
                                'unknown failure');
                        }
                    }
                    if (iCallback) {
                        iCallback(savedState);
                    }
                }

                var getFrameReq = {action: 'get', resource: 'interactiveFrame'};
                var newFrame = {
                    name: iConfig.name,
                    title: iConfig.title,
                    version: iConfig.version,
                    dimensions: iConfig.dimensions,
                    preventBringToFront: iConfig.preventBringToFront
                };
                var updateFrameReq = {
                    action: 'update',
                    resource: 'interactiveFrame',
                    values: newFrame
                };
                var this_ = this;

                config = iConfig;

                // initialize connection
                connection = new iframePh.IframePhoneRpcEndpoint(
                    notificationHandler, "data-interactive", window.parent);

                this.on('get', 'interactiveState', function () {
                    return ({success: true, values: this.getInteractiveState()});
                }.bind(this));

                console.log('sending interactiveState: ' + JSON.stringify(this.getInteractiveState));
                // update, then get the interactiveFrame.
                return this.sendRequest([updateFrameReq, getFrameReq])
                    .then(getFrameRespHandler, reject);
            }.bind(this));
        },

        /**
         * Current known state of the connection
         * @param {'preinit' || 'init' || 'active' || 'inactive' || 'closed'}
         */
        getConnectionState: function () {return connectionState;},

        getStats: function () {
            return stats;
        },

        getConfig: function () {
            return config;
        },

        /**
         * Returns the interactive state.
         *
         * @returns {object}
         */
        getInteractiveState: function () {
            return interactiveState;
        },

        /**
         * Updates the interactive state.
         * @param iInteractiveState {Object}
         */
        updateInteractiveState: function (iInteractiveState) {
            if (!iInteractiveState) {
                return;
            }
            interactiveState = Object.assign(interactiveState, iInteractiveState);
        },

        destroy: function () {
            // todo : more to do?
            connection = null;
        },

        /**
         * Sends a request to CODAP. The format of the message is as defined in
         * {@link https://github.com/concord-consortium/codap/wiki/CODAP-Data-Interactive-API}.
         *
         * @param message {String|Object}
         * @param [callback] {function(response, request)} Optional callback to handle
         *    the CODAP response. Note both the response and the initial request will
         *    sent.
         *
         * @return {Promise} The promise of the response from CODAP.
         */
        sendRequest: function (message, callback) {
            return new Promise(function (resolve, reject){
                var handled = false;
                function handleResponse (request, response, callback) {
                    if (response === undefined) {
                        console.warn('handleResponse: CODAP request timed out');
                        // If we get a request timeout we wait before issuing it because we
                        // may get a response
                        setTimeout(function () {
                            if (!handled) {
                                reject('handleResponse: CODAP request timed out: ' + JSON.stringify(request));
                            } else {
                                console.warn('IFramephone Timeout on handled request');
                            }
                        }, 10000);
                        stats.countDiRplTimeout++;

                    } else {
                        connectionState = 'active';
                        handled = true;
                        if (response.success) { stats.countDiRplSuccess++; } else { stats.countDiRplFail++; }
                        resolve(response);
                        if (callback) {
                            callback(response, request);
                        }
                    }
                }
                switch (connectionState) {
                    case 'closed': // log the message and ignore
                        console.warn('sendRequest on closed CODAP connection: ' + JSON.stringify(message));
                        reject('sendRequest on closed CODAP connection: ' + JSON.stringify(message));
                        break;
                    case 'preinit': // warn, but issue request.
                        console.log('sendRequest on not yet initialized CODAP connection: ' +
                            JSON.stringify(message));
                    /* falls through */
                    default:
                        if (connection) {
                            stats.countDiReq++;
                            stats.timeDiLastReq = new Date();
                            if (!stats.timeDiFirstReq) {
                                stats.timeCodapFirstReq = stats.timeDiLastReq;
                            }

                            connection.call(message, function (response) {
                                handleResponse(message, response, callback);
                            });
                        } else {
                            console.error('sendRequest on non-existent CODAP connection');
                        }
                }
            });
        },

        /**
         * Registers a handler to respond to CODAP-initiated requests and
         * notifications. See {@link https://github.com/concord-consortium/codap/wiki/CODAP-Data-Interactive-API#codap-initiated-actions}
         *
         * @param actionSpec {'get' || 'notify'} (optional) Action to handle. Defaults to 'notify'.
         * @param resourceSpec {String} A resource string.
         * @param [operation] {String} (optional) name of operation, e.g. 'create', 'delete',
         *   'move', 'resize', .... If not specified, all operations will be reported.
         * @param handler {Function} A handler to receive the notifications.
         */
        on: function (actionSpec, resourceSpec, operation, handler) { // eslint-disable-line no-unused-vars
            var as = 'notify',
                rs,
                os,
                hn;
            var args = Array.prototype.slice.call(arguments);
            if (args[0] === 'get' || args[0] === 'notify') {
                as = args.shift();
            }
            rs = args.shift();
            if (typeof args[0] !== 'function') {
                os = args.shift();
            }
            hn = args.shift();

            notificationSubscribers.push({
                actionSpec: as,
                resourceSpec: rs,
                operation: os,
                handler: hn
            });
        },

        /**
         * Parses a resource selector returning a hash of named resource names to
         * resource values. The last clause is identified as the resource type.
         * E.g. converts 'dataContext[abc].collection[def].case'
         * to {dataContext: 'abc', collection: 'def', type: 'case'}
         *
         * @param {String} iResource
         * @return {Object}
         */
        parseResourceSelector: function (iResource) {
            var selectorRE = /([A-Za-z0-9_-]+)\[([^\]]+)]/;
            var result = {};
            var selectors = iResource.split('.');
            selectors.forEach(function (selector) {
                var resourceType, resourceName;
                var match = selectorRE.exec(selector);
                if (selectorRE.test(selector)) {
                    resourceType = match[1];
                    resourceName = match[2];
                    result[resourceType] = resourceName;
                    result.type = resourceType;
                } else {
                    result.type = selector;
                }
            });

            return result;
        }
    };

    if (typeof module !== 'undefined') {
        module.exports = codapInterface;
    } else {
        global.codapInterface = codapInterface;
    }
}(this));
