// Copyright (c) 2017, Rubén Rincón
// All rights reserved.
//
// Redistribution and use in source and binary forms, with or without
// modification, are permitted provided that the following conditions are met:
//
//     * Redistributions of source code must retain the above copyright notice,
//       this list of conditions and the following disclaimer.
//     * Redistributions in binary form must reproduce the above copyright
//       notice, this list of conditions and the following disclaimer in the
//       documentation and/or other materials provided with the distribution.
//
// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
// AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
// IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
// ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
// LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
// CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
// SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
// INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
// CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
// ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
// POSSIBILITY OF SUCH DAMAGE.

"use strict";

var options = require('../options');
var _ = require('underscore');
var $ = require('jquery');
var Promise = require('es6-promise').Promise;
var ga = require('../analytics');
var Components = require('../components');
var Libraries = require('../libs-widget');

require('selectize');

function Conformance(hub, container, state) {
    this.hub = hub;
    this.container = container;
    this.eventHub = hub.createEventHub();
    this.compilerService = hub.compilerService;
    this.domRoot = container.getElement();
    this.domRoot.html($('#conformance').html());
    this.editorId = state.editorid;
    this.maxCompilations = options.cvCompilerCountMax || 6;
    this.langId = state.langId || _.keys(options.languages)[0];
    this.source = state.source || "";
    this.sourceNeedsExpanding = true;
    this.expandedSource = null;

    this.status = {
        allowCompile: false,
        allowAdd: true
    };
    this.stateByLang = {};

    this.initButtons();
    this.initLibraries(state);
    this.initCallbacks();
    this.initFromState(state);
    this.handleToolbarUI();
    ga.proxy('send', {
        hitType: 'event',
        eventCategory: 'OpenViewPane',
        eventAction: 'Conformance'
    });

    // Dismiss the popover on escape.
    $(document).on('keyup.editable', _.bind(function (e) {
        if (e.which === 27) {
            this.libsButton.popover('hide');
        }
    }, this));

    // Dismiss on any click that isn't either in the opening element, inside
    // the popover or on any alert
    $(document).on('click', _.bind(function (e) {
        var elem = this.libsButton;
        var target = $(e.target);
        if (!target.is(elem) && elem.has(target).length === 0 && target.closest('.popover').length === 0) {
            elem.popover('hide');
        }
    }, this));
}

Conformance.prototype.onLibsChanged = function () {
    this.saveState();
    this.compileAll();
};

Conformance.prototype.initLibraries = function (state) {
    this.libsWidget = new Libraries.Widget(this.langId, null,
        this.libsButton, state, _.bind(this.onLibsChanged, this));
};

Conformance.prototype.initButtons = function () {
    this.selectorList = this.domRoot.find('.compiler-list');
    this.addCompilerButton = this.domRoot.find('.add-compiler');
    this.selectorTemplate = $('#compiler-selector').find('.form-row');
    this.topBar = this.domRoot.find('.top-bar');
    this.libsButton = this.topBar.find('.show-libs');
    this.hideable = this.domRoot.find('.hideable');
};

Conformance.prototype.initCallbacks = function () {
    this.container.on('destroy', function () {
        this.eventHub.unsubscribe();
        this.eventHub.emit("conformanceViewClose", this.editorId);
    }, this);

    this.container.on('destroy', this.close, this);
    this.container.on('open', function () {
        this.eventHub.emit("conformanceViewOpen", this.editorId);
    }, this);

    this.container.on('resize', this.resize, this);
    this.container.on('shown', this.resize, this);
    this.eventHub.on('resize', this.resize, this);
    this.eventHub.on('editorChange', this.onEditorChange, this);
    this.eventHub.on('editorClose', this.onEditorClose, this);
    this.eventHub.on('languageChange', this.onLanguageChange, this);

    this.addCompilerButton.on('click', _.bind(function () {
        this.addCompilerSelector();
        this.saveState();
    }, this));
};

Conformance.prototype.setTitle = function (compilerCount) {
    this.container.setTitle("Conformance viewer (Editor #" + this.editorId + ") " + (
        compilerCount !== 0 ? (compilerCount + "/" + this.maxCompilations) : ""
    ));
};

Conformance.prototype.addCompilerSelector = function (config) {
    if (!config) {
        config = {
            // Compiler id which is being used
            compilerId: "",
            // Options which are in use
            options: options.compileOptions[this.langId]
        };
    }

    var newEntry = this.selectorTemplate.clone();

    var onOptionsChange = _.debounce(_.bind(function () {
        this.saveState();
        this.compileChild(newEntry);
    }, this), 800);

    var optionsField = newEntry.find('.options')
        .val(config.options)
        .on("change", onOptionsChange)
        .on("keyup", onOptionsChange);

    newEntry.find('.close').not('.extract-compiler')
        .on("click", _.bind(function () {
            this.removeCompilerSelector(newEntry);
        }, this));

    this.selectorList.append(newEntry);

    var status = newEntry.find('.status-icon');
    var prependOptions = newEntry.find('.prepend-options');
    var popCompilerButton = newEntry.find('.extract-compiler');

    var onCompilerChange = _.bind(function (compilerId) {
        popCompilerButton.toggleClass('d-none', !compilerId);
        // Hide the results icon when a new compiler is selected
        this.handleStatusIcon(status, {code: 0});
        var compiler = this.compilerService.findCompiler(this.langId, compilerId);
        if (compiler) this.setCompilationOptionsPopover(prependOptions, compiler.options);
        this.updateLibraries();
    }, this);

    var compilerPicker = newEntry.find('.compiler-picker').selectize({
        sortField: this.compilerService.getSelectizerOrder(),
        valueField: 'id',
        labelField: 'name',
        searchField: ['name'],
        optgroupField: 'group',
        optgroups: this.compilerService.getGroupsInUse(this.langId),
        lockOptgroupOrder: true,
        options: _.map(this.getCurrentLangCompilers(), _.identity),
        items: config.compilerId ? [config.compilerId] : [],
        dropdownParent: 'body',
        closeAfterSelect: true
    }).on('change', _.bind(function (e) {
        onCompilerChange($(e.target).val());
        this.compileChild(newEntry);
    }, this));
    onCompilerChange(config.compilerId);


    var getCompilerConfig = _.bind(function () {
        return Components.getCompilerWith(
            this.editorId, undefined, optionsField.val(), compilerPicker.val(), this.langId, this.lastState.libs);
    }, this);

    this.container.layoutManager
        .createDragSource(popCompilerButton, getCompilerConfig);

    popCompilerButton.click(_.bind(function () {
        var insertPoint = this.hub.findParentRowOrColumn(this.container) ||
            this.container.layoutManager.root.contentItems[0];
        insertPoint.addChild(getCompilerConfig);
    }, this));

    this.handleToolbarUI();
    this.saveState();
};

Conformance.prototype.getCurrentLangCompilers = function () {
    return this.compilerService.getCompilersForLang(this.langId);
};

Conformance.prototype.setCompilationOptionsPopover = function (element, content) {
    element.popover('dispose');
    element.popover({
        content: content || 'No options in use',
        template: '<div class="popover' +
            (content ? ' compiler-options-popover' : '') +
            '" role="tooltip"><div class="arrow"></div>' +
            '<h3 class="popover-header"></h3><div class="popover-body"></div></div>'
    });
};

Conformance.prototype.removeCompilerSelector = function (element) {
    if (element) element.remove();
    this.updateLibraries();
    this.handleToolbarUI();
    this.saveState();
};

Conformance.prototype.expandSource = function () {
    if (this.sourceNeedsExpanding || !this.expandedSource) {
        return this.compilerService.expand(this.source).then(_.bind(function (expandedSource) {
            this.expandedSource = expandedSource;
            this.sourceNeedsExpanding = false;
            return expandedSource;
        }, this));
    }
    return Promise.resolve(this.expandedSource);
};

Conformance.prototype.onEditorChange = function (editorId, newSource, langId) {
    if (editorId === this.editorId) {
        this.langId = langId;
        this.source = newSource;
        this.sourceNeedsExpanding = true;
        this.compileAll();
    }
};

Conformance.prototype.onEditorClose = function (editorId) {
    if (editorId === this.editorId) {
        this.close();
        _.defer(function (self) {
            self.container.close();
        }, this);
    }
};

Conformance.prototype.onCompileResponse = function (child, result) {
    var allText = _.pluck((result.stdout || []).concat(result.stderr || []), 'text').join("\n");
    var failed = result.code !== 0;
    var warns = !failed && !!allText;

    this.setCompilationOptionsPopover(child.find('.prepend-options'),
        result.compilationOptions ? result.compilationOptions.join(' ') : '');
    child.find('.compiler-out')
        .prop('title', allText.replace(/\x1b\[[0-9;]*m(.\[K)?/g, ''))
        .toggleClass('d-none', !allText);
    this.handleStatusIcon(child.find('.status-icon'), {code: failed ? 3 : (warns ? 2 : 1), compilerOut: result.code});
    this.saveState();
};

Conformance.prototype.compileChild = function (child) {
    // Hide previous status icons
    var picker = child.find('.compiler-picker');

    if (!picker || !picker.val()) return;

    this.handleStatusIcon(child.find('.status-icon'), {code: 4});

    this.expandSource().then(_.bind(function (expandedSource) {
        var request = {
            source: expandedSource,
            compiler: picker.val(),
            options: {
                userArguments: child.find(".options").val() || "",
                filters: {},
                compilerOptions: {produceAst: false, produceOptInfo: false},
                libraries: []
            }
        };

        _.each(this.libsWidget.getLibsInUse(), function (item) {
            request.options.libraries.push({
                id: item.libId,
                version: item.versionId
            });
        });

        // This error function ensures that the user will know we had a problem (As we don't save asm)
        this.compilerService.submit(request)
            .then(_.bind(function (x) {
                this.onCompileResponse(child, x.result);
            }, this))
            .catch(_.bind(function (x) {
                this.onCompileResponse(child, {
                    asm: "",
                    code: -1,
                    stdout: "",
                    stderr: x.error
                });
            }, this));
    }, this));
};

Conformance.prototype.compileAll = function () {
    _.each(this.selectorList.children(), _.bind(function (child) {
        this.compileChild($(child));
    }, this));
};

Conformance.prototype.handleToolbarUI = function () {
    var compilerCount = this.selectorList.children().length;

    // Only allow new compilers if we allow for more
    this.addCompilerButton.prop("disabled", compilerCount >= this.maxCompilations);

    this.setTitle(compilerCount);
};

Conformance.prototype.handleStatusIcon = function (element, status) {
    if (!element) return;

    function ariaLabel() {
        // Compiling...
        if (status.code === 4) return "Compiling";
        if (status.compilerOut === 0) {
            // StdErr.length > 0
            if (status.code === 3) return "Compilation succeeded with errors";
            // StdOut.length > 0
            if (status.code === 2) return "Compilation succeeded with warnings";
            return "Compilation succeeded";
        } else {
            // StdErr.length > 0
            if (status.code === 3) return "Compilation failed with errors";
            // StdOut.length > 0
            if (status.code === 2) return "Compilation failed with warnings";
            return "Compilation failed";
        }
    }

    function color() {
        // Compiling...
        if (status.code === 4) return "black";
        if (status.compilerOut === 0) {
            // StdErr.length > 0
            if (status.code === 3) return "#FF6645";
            // StdOut.length > 0
            if (status.code === 2) return "#FF6500";
            return "#12BB12";
        } else {
            // StdErr.length > 0
            if (status.code === 3) return "#FF1212";
            // StdOut.length > 0
            if (status.code === 2) return "#BB8700";
            return "#FF6645";
        }
    }

    element
        .removeClass()
        .addClass('status-icon fas')
        .css('color', color())
        .toggle(status.code !== 0)
        .prop('aria-label', ariaLabel())
        .prop('data-status', status.code)
        .toggleClass('fa-spinner', status.code === 4)
        .toggleClass('fa-times-circle', status.code === 3)
        .toggleClass('fa-check-circle', status.code === 1 || status.code === 2);
};

Conformance.prototype.currentState = function () {
    var compilers = _.map(this.selectorList.children(), function (child) {
        child = $(child);
        return {
            compilerId: child.find('.compiler-picker').val() || "",
            options: child.find(".options").val() || ""
        };
    });
    return {
        editorid: this.editorId,
        langId: this.langId,
        compilers: compilers,
        libs: this.libsWidget.get()
    };
};

Conformance.prototype.saveState = function () {
    this.lastState = this.currentState();
    this.container.setState(this.lastState);
};

Conformance.prototype.resize = function () {
    this.updateHideables();
    this.selectorList.css("height", this.domRoot.height() - this.topBar.outerHeight(true));
};

Conformance.prototype.updateHideables = function () {
    this.hideable.toggle(this.domRoot.width() > this.addCompilerButton.width());
};

Conformance.prototype.getOverlappingLibraries = function (compilerIds) {
    var compilers = _.map(compilerIds, _.bind(function (compilerId) {
        return this.compilerService.findCompiler(this.langId, compilerId);
    }, this));

    var libraries = {};
    var first = true;
    _.forEach(compilers, function (compiler) {
        if (compiler) {
            if (first) {
                libraries = _.extend({}, compiler.libs);
                first = false;
            } else {
                var libsInCommon = _.intersection(_.keys(libraries),
                    _.keys(compiler.libs));
    
                _.forEach(libraries, function (lib, libkey) {
                    if (libsInCommon.includes(libkey)) {
                        var versionsInCommon = _.intersection(_.keys(lib.versions),
                            _.keys(compiler.libs[libkey].versions));

                        libraries[libkey].versions = _.pick(lib.versions,
                            function (version, versionkey) {
                                return versionsInCommon.includes(versionkey);
                            });
                    } else {
                        libraries[libkey] = false;
                    }
                });
    
                libraries = _.omit(libraries, function (lib) {
                    return !lib || _.isEmpty(lib.versions);
                });
            }
        }
    });

    return libraries;
};

Conformance.prototype.updateLibraries = function () {
    var compilerIds = _.uniq(
        _.filter(
            _.map(this.selectorList.children(), function (child) {
                return $(child).find('.compiler-picker').val();
            })
            , function (compilerId) {
                return compilerId !== "";
            })
    );

    var libraries = this.getOverlappingLibraries(compilerIds);

    this.libsWidget.setNewLangId(this.langId, compilerIds.join("|"), libraries);
};

Conformance.prototype.onLanguageChange = function (editorId, newLangId) {
    if (editorId === this.editorId && this.langId !== newLangId) {
        var oldLangId = this.langId;
        this.stateByLang[oldLangId] = this.currentState();

        this.langId = newLangId;
        this.selectorList.children().remove();
        var langState = this.stateByLang[newLangId];
        this.initFromState(langState);
        this.updateLibraries();
        this.handleToolbarUI();
        this.saveState();
    }
};

Conformance.prototype.close = function () {
    this.eventHub.unsubscribe();
    this.eventHub.emit("conformanceViewClose", this.editorId);
};

Conformance.prototype.initFromState = function (state) {
    if (state && state.compilers) {
        this.lastState = state;
        _.each(state.compilers, _.bind(this.addCompilerSelector, this));
    } else {
        this.lastState = this.currentState();
    }
};

module.exports = {
    Conformance: Conformance
};
