/*
 * Code editor form field control
 *
 * Data attributes:
 * - data-control="codeeditor" - enables the code editor plugin
 * - data-vendor-path="/" - sets the path to find Ace editor files
 * - data-language="php" - set the coding language used
 * - data-theme="textmate" - the colour scheme and theme
 *
 * JavaScript API:
 * $('textarea').codeEditor({ vendorPath: '/', language: 'php '})
 *
 * Dependancies:
 * - Ace Editor (ace.js)
 */

+function ($) { "use strict";

    var Base = $.oc.foundation.base,
        BaseProto = Base.prototype

    // CODEEDITOR CLASS DEFINITION
    // ============================

    var CodeEditor = function(element, options) {
        Base.call(this);

        this.options   = options;
        this.$el       = $(element);
        this.$textarea = this.$el.find('>textarea:first');
        this.$toolbar  = this.$el.find('>.editor-toolbar:first');
        this.$code     = null;
        this.editor    = null;
        this.$form     = null;

        // Toolbar links
        this.isFullscreen = false;
        this.$fullscreenEnable = this.$toolbar.find('li.fullscreen-enable');
        this.$fullscreenDisable = this.$toolbar.find('li.fullscreen-disable');
        this.$searchboxEnable = this.$toolbar.find('li.searchbox-enable');
        this.$replaceboxEnable = this.$toolbar.find('li.replacebox-enable');

        $.oc.foundation.controlUtils.markDisposable(element);

        this.init();

        this.$el.trigger('oc.codeEditorReady');
    }

    CodeEditor.prototype = Object.create(BaseProto)
    CodeEditor.prototype.constructor = CodeEditor

    CodeEditor.DEFAULTS = {
        fontSize: 12,
        wordWrap: 'off',
        codeFolding: 'manual',
        autocompletion: 'manual',
        tabSize: 4,
        theme: 'textmate',
        showInvisibles: true,
        highlightActiveLine: true,
        useSoftTabs: true,
        autoCloseTags: true,
        showGutter: true,
        enableEmmet: true,
        language: 'php',
        margin: 0,
        vendorPath: '/',
        showPrintMargin: false,
        highlightSelectedWord: false,
        hScrollBarAlwaysVisible: false,
        readOnly: false
    }

    CodeEditor.prototype.init = function (){
        // Control must have an identifier
        if (!this.$el.attr('id')) {
            this.$el.attr('id', 'element-' + Math.random().toString(36).substring(7))
        }

        // Create code container
        this.$code = $('<div />')
            .addClass('editor-code')
            .attr('id', this.$el.attr('id') + '-code')
            .css({
                position: 'absolute',
                top: 0,
                right: 0,
                bottom: 0,
                left: 0
            })
            .appendTo(this.$el)

        // Initialize ACE editor
        var editor = this.editor = ace.edit(this.$code.attr('id')),
            options = this.options,
            $form = this.$el.closest('form');

        // Fixes a weird notice about scrolling
        editor.$blockScrolling = Infinity

        this.$form = $form

        this.$textarea.hide();
        editor.getSession().setValue(this.$textarea.val())

        editor.on('change', this.proxy(this.onChange))
        $form.on('oc.beforeRequest', this.proxy(this.onBeforeRequest))
        $(window).on('resize', this.proxy(this.onResize))
        $(window).on('oc.updateUi', this.proxy(this.onResize))
        this.$el.one('dispose-control', this.proxy(this.dispose))

        // Set theme, anticipated languages should be preloaded
        oc.AssetManager.load({
            js:[
                options.vendorPath + '/theme-' + options.theme + '.js'
            ]
        }, function() {
            if (editor) {
                editor.setTheme('ace/theme/' + options.theme);
                var inline = options.language === 'php';
                editor.getSession().setMode({ path: 'ace/mode/'+options.language, inline: inline });
            }
        });

        // Config editor
        editor.wrapper = this;
        editor.setShowInvisibles(options.showInvisibles);
        editor.setBehavioursEnabled(options.autoCloseTags);
        editor.setHighlightActiveLine(options.highlightActiveLine);
        editor.renderer.setShowGutter(options.showGutter);
        editor.renderer.setShowPrintMargin(options.showPrintMargin);
        editor.setHighlightSelectedWord(options.highlightSelectedWord);
        editor.renderer.setHScrollBarAlwaysVisible(options.hScrollBarAlwaysVisible);
        editor.setDisplayIndentGuides(options.displayIndentGuides);
        editor.getSession().setUseSoftTabs(options.useSoftTabs);
        editor.getSession().setTabSize(options.tabSize);
        editor.setReadOnly(options.readOnly);
        editor.getSession().setFoldStyle(options.codeFolding);
        editor.setFontSize(options.fontSize);
        editor.on('blur', this.proxy(this.onBlur));
        editor.on('focus', this.proxy(this.onFocus));
        this.setWordWrap(options.wordWrap);

        // Set the vendor path for Ace's require path
        ace.require('ace/config').set('basePath', this.options.vendorPath);

        editor.setOptions({
            enableEmmet: options.enableEmmet,
            enableBasicAutocompletion: options.autocompletion === 'basic',
            enableLiveAutocompletion: options.autocompletion === 'live'
        });

        editor.renderer.setScrollMargin(options.margin, options.margin, 0, 0);
        editor.renderer.setPadding(options.margin);

        // Toolbar
        this.$toolbar.find('>ul>li>a')
            .each(function(){
                var abbr = $(this).find('>abbr'),
                    label = abbr.text(),
                    help = abbr.attr('title'),
                    title = label + ' (<strong>' + help + '</strong>)';

                $(this).attr('title', title)
            })
            .tooltip({
                delay: 500,
                placement: 'top',
                html: true
            })
        ;

        this.$fullscreenDisable.hide()
        this.$fullscreenEnable.on('click.codeeditor', '>a', $.proxy(this.toggleFullscreen, this))
        this.$fullscreenDisable.on('click.codeeditor', '>a', $.proxy(this.toggleFullscreen, this))
        this.$searchboxEnable.on('click.codeeditor', '>a', $.proxy(this.showSearchbox, this))
        this.$replaceboxEnable.on('click.codeeditor', '>a', $.proxy(this.showReplacebox, this))

        // Hotkeys
        this.$el.hotKey({
            hotkey: 'esc',
            callback: this.proxy(this.onEscape)
        });

        editor.commands.addCommand({
            name: 'toggleFullscreen',
            bindKey: { win: 'Ctrl+Shift+F', mac: 'Ctrl+Shift+F' },
            exec: $.proxy(this.toggleFullscreen, this),
            readOnly: true
        });
    }

    CodeEditor.prototype.dispose = function() {
        if (this.$el === null) {
            return;
        }

        this.unregisterHandlers();
        this.disposeAttachedControls();

        this.$el = null;
        this.$textarea = null;
        this.$toolbar = null;
        this.$code = null;
        this.$fullscreenEnable = null;
        this.$fullscreenDisable = null;
        this.$searchboxEnable = null;
        this.$replaceboxEnable = null;
        this.$form = null;
        this.options = null;

        BaseProto.dispose.call(this);
    }

    CodeEditor.prototype.disposeAttachedControls = function() {
        this.editor.destroy();

        var keys = Object.keys(this.editor.renderer);
        for (var i=0, len=keys.length; i<len; i++) {
            this.editor.renderer[keys[i]] = null;
        }

        keys = Object.keys(this.editor);
        for (var i=0, len=keys.length; i<len; i++) {
            this.editor[keys[i]] = null;
        }

        this.editor = null;

        this.$toolbar.find('>ul>li>a').tooltip('dispose');
        this.$el.removeData('oc.codeEditor');
        this.$el.hotKey('dispose');
    }

    CodeEditor.prototype.unregisterHandlers = function() {
        this.editor.off('change', this.proxy(this.onChange));
        this.editor.off('blur', this.proxy(this.onBlur));
        this.editor.off('focus', this.proxy(this.onFocus));

        this.$fullscreenEnable.off('.codeeditor');
        this.$fullscreenDisable.off('.codeeditor');
        this.$form.off('oc.beforeRequest', this.proxy(this.onBeforeRequest));

        this.$el.off('dispose-control', this.proxy(this.dispose));

        $(window).off('resize', this.proxy(this.onResize));
        $(window).off('oc.updateUi', this.proxy(this.onResize));
    }

    CodeEditor.prototype.onBeforeRequest = function() {
        this.$textarea.val(this.editor.getSession().getValue())
    }

    CodeEditor.prototype.onChange = function() {
        this.$form.trigger('change')
        this.$textarea.trigger('oc.codeEditorChange')
    }

    CodeEditor.prototype.onResize = function() {
        this.editor.resize()
    }

    CodeEditor.prototype.onBlur = function() {
        this.$el.removeClass('editor-focus')
    }

    CodeEditor.prototype.onFocus = function() {
        this.$el.addClass('editor-focus')
    }

    CodeEditor.prototype.onEscape = function() {
        this.isFullscreen && this.toggleFullscreen()
    }

    CodeEditor.prototype.setWordWrap = function(mode) {
        var session = this.editor.getSession(),
            renderer = this.editor.renderer

        switch (mode + '') {
            default:
            case 'off':
                session.setUseWrapMode(false);
                renderer.setPrintMarginColumn(80);
            break
            case '40':
                session.setUseWrapMode(true);
                session.setWrapLimitRange(40, 40);
                renderer.setPrintMarginColumn(40);
            break
            case '80':
                session.setUseWrapMode(true);
                session.setWrapLimitRange(80, 80);
                renderer.setPrintMarginColumn(80);
            break
            case 'fluid':
                session.setUseWrapMode(true);
                session.setWrapLimitRange(null, null);
                renderer.setPrintMarginColumn(80);
            break
        }
    }

    CodeEditor.prototype.setTheme = function(theme) {
        var self = this;
        oc.AssetManager.load({
            js:[
                this.options.vendorPath + '/theme-' + theme + '.js'
            ]
        }, function(){
            self.editor.setTheme('ace/theme/' + theme)
        });
    }

    CodeEditor.prototype.getContent = function() {
        return this.editor.getSession().getValue();
    }

    CodeEditor.prototype.setContent = function(html) {
        this.editor.getSession().setValue(html);
    }

    CodeEditor.prototype.getEditorObject = function() {
        return this.editor;
    }

    CodeEditor.prototype.getToolbar = function() {
        return this.$toolbar;
    }

    CodeEditor.prototype.toggleFullscreen = function() {
        this.$el.toggleClass('editor-fullscreen');
        this.$fullscreenEnable.toggle();
        this.$fullscreenDisable.toggle();

        this.isFullscreen = this.$el.hasClass('editor-fullscreen');

        if (this.isFullscreen) {
            $('body').css({ overflow: 'hidden' });
        }
        else {
            $('body').css({ overflow: 'inherit' });
        }

        this.editor.resize();
        this.editor.focus();
    }

    CodeEditor.prototype.showSearchbox = function() {
        this.editor.execCommand('find');
        this.editor.resize();
        this.editor.focus();
    }

    CodeEditor.prototype.showReplacebox = function() {
        this.editor.execCommand('replace');
        this.editor.resize();
        this.editor.focus();
    }

    // CODEEDITOR PLUGIN DEFINITION
    // ============================

    var old = $.fn.codeEditor

    $.fn.codeEditor = function (option) {
        var args = Array.prototype.slice.call(arguments, 1), result
        this.each(function () {
            var $this   = $(this)
            var data    = $this.data('oc.codeEditor')
            var options = $.extend({}, CodeEditor.DEFAULTS, $this.data(), typeof option == 'object' && option)
            if (!data) $this.data('oc.codeEditor', (data = new CodeEditor(this, options)))
            if (typeof option == 'string') result = data[option].apply(data, args)
            if (typeof result != 'undefined') return false
        })

        return result ? result : this
    }

    $.fn.codeEditor.Constructor = CodeEditor

    if ($.oc === undefined)
        $.oc = {}

    $.oc.codeEditorExtensionModes = {
        'htm': 'html',
        'html': 'html',
        'md': 'markdown',
        'txt': 'plain_text',
        'js': 'javascript',
        'less': 'less',
        'scss': 'scss',
        'sass': 'sass',
        'css': 'css'
    }

    // CODEEDITOR NO CONFLICT
    // =================

    $.fn.codeEditor.noConflict = function () {
        $.fn.codeEditor = old
        return this
    }

    // CODEEDITOR DATA-API
    // ===============
    $(document).render(function () {
        $('[data-control="codeeditor"]').codeEditor()
    });

    // FIX EMMET HTML WHEN SYNTAX IS TWIG
    // ==================================

    +function (exports) {
        if (exports.ace && typeof exports.ace.require == 'function') {
            var emmetExt = exports.ace.require('ace/ext/emmet')

            if (emmetExt && emmetExt.AceEmmetEditor && emmetExt.AceEmmetEditor.prototype.getSyntax) {
                var coreGetSyntax = emmetExt.AceEmmetEditor.prototype.getSyntax

                emmetExt.AceEmmetEditor.prototype.getSyntax = function () {
                    var $syntax = $.proxy(coreGetSyntax, this)()
                    return $syntax == 'twig' ? 'html' : $syntax
                };
            }
        }
    }(window)

}(window.jQuery);
