/*
 * This program is free software; you can redistribute it and/or
 * modify it under the terms of the GNU General Public License
 * as published by the Free Software Foundation; under version 2
 * of the License (non-upgradable).
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program; if not, write to the Free Software
 * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
 *
 * Copyright (c) 2015 (original work) Open Assessment Technologies SA ;
 *
 */

/**
 * This module allows adding extra buttons in the action bar of the test runner
 *
 */
define([
    'jquery',
    'lodash',
    'core/errorHandler',
    'core/promise'
], function ($, _, errorHandler, Promise) {

    'use strict';

    /**
     * Events namespace
     * @type {String}
     * @private
     */
    var _ns = '.actionBarHook';

    /**
     * We need to access the root document to listen for some events
     * @type {jQuery}
     * @private
     */
    var $doc = $(document);

    /**
     * List of loaded and visible hooks
     * @type {Object}
     * @private
     */
    var tools = {};

    /**
     * Flag set to true when the item is loaded
     * @type {Boolean}
     * @private
     */
    var itemIsLoaded = false;

    // catch the item loaded event
    $doc.off(_ns).on('serviceloaded' + _ns, function() {
        itemIsLoaded = true;
        _.forEach(tools, function(tool) {
            triggerItemLoaded(tool);
        });
    });

    /**
     * Check that the toolConfig is correct
     *
     * @param {Object} toolconfig
     * @param {String} toolconfig.hook - the amd module to be loaded to initialize the button
     * @param {String} [toolconfig.label] - the label to be displayed in the button
     * @param {String} [toolconfig.icon] - the icon to be displayed in the button
     * @param {String} [toolconfig.title] - the title to be displayed in the button
     * @param {Array} [toolconfig.items] - an optional list of menu items
     * @returns {Boolean}
     */
    function isValidConfig(toolconfig) {
        return !!(_.isObject(toolconfig) && toolconfig.hook);
    }

    /**
     * Triggers the itemLoaded event inside the provided actionBar hook
     * @param {Object} tool
     */
    function triggerItemLoaded(tool) {
        if (tool && tool.itemLoaded) {
            tool.itemLoaded();
        }
    }

    /**
     * Init a test runner button from its config
     *
     * @param {String} id
     * @param {Object|String} toolconfig
     * @param {String} toolconfig.hook - the amd module to be loaded to initialize the button
     * @param {String} [toolconfig.label] - the label to be displayed in the button
     * @param {String} [toolconfig.icon] - the icon to be displayed in the button
     * @param {String} [toolconfig.title] - the title to be displayed in the button
     * @param {Array} [toolconfig.items] - an optional list of menu items
     * @param {Object} testContext - the complete state of the test
     * @param {Object} testRunner - the test runner instance
     * @fires ready.actionBarHook when the hook has been initialized
     * @returns {Promise}
     */
    function initQtiTool($toolsContainer, id, toolconfig, testContext, testRunner) {

        // the tool is always initialized before the item is loaded, so we can safely false the flag
        itemIsLoaded = false;
        tools[id] = null;

        if (_.isString(toolconfig)) {
            toolconfig = {
                hook: toolconfig
            };
        }

        return new Promise(function(resolve) {
            if (isValidConfig(toolconfig)) {

                require([toolconfig.hook], function (hook) {

                    var $button;
                    var $existingBtn;

                    if (isValidHook(hook)) {
                        //init the control
                        hook.init(id, toolconfig, testContext, testRunner);

                        //if an instance of the tool is already attached, remove it:
                        $existingBtn = $toolsContainer.children('[data-control="' + id + '"]');
                        if ($existingBtn.length) {
                            hook.clear($existingBtn);
                            $existingBtn.remove();
                        }

                        //check if the tool is to be available
                        if (hook.isVisible()) {
                            //keep access to the tool
                            tools[id] = hook;

                            // renders the button from the config
                            $button = hook.render();

                            //only attach the button to the dom when everything is ready
                            _appendInOrder($toolsContainer, $button);

                            //ready !
                            $button.trigger('ready' + _ns, [hook]);

                            //fires the itemLoaded event if the item has already been loaded
                            if (itemIsLoaded) {
                                triggerItemLoaded(hook);
                            }
                        }

                        resolve(hook);
                    } else {
                        errorHandler.throw(_ns, 'invalid hook format');
                        resolve(null);
                    }

                }, function (e) {
                    errorHandler.throw(_ns, 'the hook amd module cannot be found');
                    resolve(null);
                });

            } else {
                errorHandler.throw(_ns, 'invalid tool config format');
                resolve(null);
            }
        });
    }

    /**
     * Append a dom element $button to a $container in a specific order
     * The orders are provided by data-order attribute set to the $button
     *
     * @param {JQuery} $container
     * @param {JQuery} $button
     */
    function _appendInOrder($container, $button) {

        var $after, $before;
        var order = $button.data('order');

        if ('last' === order) {

            $container.append($button);

        } else if ('first' === order) {

            $container.prepend($button);

        } else {

            order = _.parseInt(order);
            if (!_.isNaN(order)) {

                $container.children('.action').each(function () {

                    var $btn = $(this),
                        _order = $btn.data('order');

                    if ('last' === _order) {

                        $before = $btn;
                        $after = null;

                    } else if ('first' === _order) {

                        $before = null;
                        $after = $btn;

                    } else {

                        _order = _.parseInt(_order);

                        if (_.isNaN(_order) || _order > order) {
                            $before = $btn;
                            $after = null;
                            //stops here because $container children returns the dom elements in the dom order
                            return false;
                        } else if (_order === order) {
                            $after = $btn;
                        } else if (_order < order) {
                            $after = $btn;
                            $before = null;
                        }

                    }

                });

                if ($after) {
                    $after.after($button);
                } else if ($before) {
                    $before.before($button);
                } else {
                    $container.append($button);
                }

            } else {
                //unordered buttons are append at the end (including when order equals 0)
                $container.append($button);
            }
        }
    }

    /**
     * Check if the hook object is valid
     *
     * @param {Object} hook
     * @param {Function} hook.init
     * @param {Function} hook.clear
     * @param {Function} hook.isVisible
     * @returns {Boolean}
     */
    function isValidHook(hook) {
        return (_.isObject(hook) && _(['init', 'render', 'clear', 'isVisible']).reduce(function (result, method) {
            return result && _.isFunction(hook[method]);
        }, true));
    }

    return {
        isValid: isValidConfig,
        initQtiTool: initQtiTool
    };
});