/*

Siesta 5.1.0
Copyright(c) 2009-2018 Bryntum AB
https://bryntum.com/contact
https://bryntum.com/products/siesta/license

*/
Ext.define('Siesta.Recorder.UI.RecorderPanel', {
    extend : 'Ext.tree.Panel',

    alias : 'widget.recorderpanel',

    requires : [
        'Ext.grid.plugin.CellEditing',

        'Siesta.Recorder.UI.ActionColumn',
        'Siesta.Recorder.UI.ActionIconColumn',
        'Siesta.Recorder.UI.TargetColumn',

        'Siesta.Recorder.UI.ContextMenu',
        'Siesta.Recorder.UI.Store.Action'
    ],

    mixins : [
        'Siesta.Project.Browser.UI.CanCopyToClipboard'
    ],

    buttonAlign : 'left',
    border      : false,
    cls         : 'siesta-recorderpanel',
    selModel    : {
        mode : 'MULTI'
    },

    store : {
        type : 'actionstore'
    },

    rootVisible : false,

    viewConfig : {
        markDirty  : false,
        stripeRows : false,
        allowCopy  : true,
        plugins    : {
            ptype : 'treeviewdragdrop'
        }
    },


    newActionDefaults : {
        action : 'click'
    },
    lines             : false,

    test              : null,
    recorder          : null,
    project           : null,
    domContainer      : null,

    recorderConfig    : null,

    editing           : null,

    bufferedRenderer  : false,

    enableColumnMove  : false,
    enableContextMenu : true,
    enableEditing     : true,

    showToolbars      : true,

    playbackOnly      : false,

    /**
     * @event startrecord
     * Fires when a recording is started
     * @param {Siesta.Recorder.UI.RecorderPanel} this
     * @param {Siesta.Test} test The test instance to which the recorder is attached
     */
    /**
     * @event stoprecord
     * Fires when a recording is stopped
     * @param {Siesta.Recorder.UI.RecorderPanel} this
     */
    /**
     * @event play
     * Fires when a recording is being played back
     * @param {Siesta.Recorder.UI.RecorderPanel} this
     */

    initComponent : function () {
        var me             = this;
        var R              = Siesta.Resource('Siesta.Recorder.UI.RecorderPanel');

        var recorderConfig = this.recorderConfig || {};

        if (!this.playbackOnly) {
            var recorderClass = this.project && this.project.recorderClass || recorderConfig.recorderClass || Siesta.Recorder.ExtJS;

            if (typeof recorderClass === 'string') {
                recorderClass = Joose.S.strToClass(recorderClass);
            }

            var recorder = me.recorder = me.recorder || new recorderClass(recorderConfig);

            recorder.on("actionadd", this.onActionAdded, this)
            recorder.on("actionremove", this.onActionRemoved, this)
            recorder.on("actionupdate", this.onActionUpdated, this)
            recorder.on("clear", this.onRecorderClear, this)

            recorder.on("start", this.onRecorderStart, this)
            recorder.on("stop", this.onRecorderStop, this)
        }


        if (this.enableEditing) {
            me.plugins = me.plugins ? [].concat(me.plugins) : [];

            me.editing = me.editing || new Ext.grid.plugin.CellEditing({
                clicksToEdit : 1
            });

            me.editing.on({
                beforeedit   : me.onBeforeEdit,
                validateedit : me.onValidateEdit,
                edit         : me.afterEdit,
                canceledit   : me.afterEdit,
                scope        : me
            });

            this.relayEvents(me.editing, ['beforeedit', 'afteredit', 'validateedit'])

            me.plugins.push(me.editing);

            this.on('hide', function () {
                if (this.editing) {
                    this.editing.completeEdit();
                }
            });

            this.on({
                afteredit    : this.onAfterEdit,
                validateedit : this.onAfterEdit,
                canceledit   : this.onAfterEdit,
                scope        : this,
                buffer       : 200
            });
        }

        Ext.applyIf(me, {

            columns : [
                {
                    xtype : 'recorderactioniconcolumn'
                },
                {
                    xtype : 'recorderactioncolumn'
                },
                {
                    xtype           : 'targetcolumn',
                    highlightTarget : this.highlightTarget.bind(this)
                }
            ].concat(
                {
                    header       : R.get('offsetColumnHeader'),
                    dataIndex    : '__offset__',
                    width        : 60,
                    sortable     : false,
                    menuDisabled : true,
                    tdCls        : 'siesta-recorderpanel-offsetcolumn',
                    renderer     : function (value, meta, record) {
                        var target = record.getTarget()

                        if (target && target.offset) {
                            return target.offset + '<div class="siesta-recorderpanel-clearoffset fa fa-close"></div>'
                        }
                    },
                    editor       : 'textfield'
                }
            ).concat({
                xtype        : 'actioncolumn',
                width        : 55,
                align        : 'center',
                sortable     : false,
                menuDisabled : true,
                tdCls        : 'siesta-recorderpanel-actioncolumn',

                items : [
                    {
                        iconCls : 'step-icon fa fa-close icon-delete-row',
                        tooltip : 'Delete this action',
                        handler : this.onDeleteStepClick,
                        scope   : this
                    },
                    {
                        iconCls : 'step-icon fa fa-play icon-play-row',
                        tooltip : 'Play this action',
                        handler : this.onPlaySingleStepClick,
                        scope   : this
                    },
                    {
                        iconCls : 'step-icon fa fa-forward icon-play-from-row',
                        tooltip : 'Play from this action',
                        handler : this.onPlayFromStepClick,
                        scope   : this
                    }
                ]
            })
        });

        if (this.showToolbars) {
            me.createToolbars();
        }

        me.callParent();

        this.mon(Ext.getBody(), 'mousedown', this.onBodyMouseDown, this, { delegate : '.target-inspector-label' })

        if (this.enableContextMenu) {
            this.contextMenu = new Siesta.Recorder.UI.ContextMenu({ panel : this });
        }
    },


    onAfterEdit : function () {
        if (!this.editing.editing) {
            this.hideHighlighter();
        }
    },


    onBodyMouseDown : function (e, t) {
        var focusedEl = document.activeElement;

        if (Ext.fly(focusedEl).up('.siesta-targeteditor')) {
            e.stopEvent();
            e.preventDefault();
            focusedEl.value = Ext.htmlDecode(t.innerHTML);
        }
    },


    onRecorderStart : function () {
        this.fireEvent('startrecord', this, this.test);

        this.addCls('recorder-recording');
    },


    onRecorderStop : function () {
        this.fireEvent('stoprecord', this);

        this.removeCls('recorder-recording');
    },


    hideHighlighter : function () {
        if (this.test && this.domContainer) {
            this.domContainer.clearHighlight();
        }
    },


    highlightTarget : function (target, offset) {
        if (!target) {
            // Pass no target => simply hide highlighter
            this.hideHighlighter();
            return;
        }

        var test = this.test;

        if (!test) {
            this.hideHighlighter();
            return { success : true }
        }

        var R = Siesta.Resource('Siesta.Recorder.UI.RecorderPanel');
        var resolved, el

        try {
            resolved = this.test.normalizeElement(target, true, true, true);

            el = resolved.el
        } catch (e) {
            // sizzle may break on various characters in the query (=, $, etc)
        } finally {
            if (!el) {
                return { success : false, warning : R.get('queryMatchesNothing') }
            }
        }

        var warning = resolved.matchingMultiple ? R.get('queryMatchesMultiple') : ''

        if (test.isElementVisible(el) && this.domContainer) {
            var pointToVisualize = Ext.isArray(target) ? target : (offset || ['50%', '50%']);

            this.domContainer.highlightTarget(el, '<span class="target-inspector-label">' + target + '</span>', pointToVisualize);
        } else {
            // If target was provided but no element could be located, return false so
            // caller can get a hint there is potential trouble
            warning = warning || R.get('noVisibleElsFound')
        }

        return {
            success : !warning,
            message : warning
        };
    },


    createToolbars : function () {
        var me = this;
        var R  = Siesta.Resource('Siesta.Recorder.UI.RecorderPanel');

        me.dockedItems = [
            {
                xtype   : 'toolbar',
                padding : '3 5',
                cls     : 'siesta-toolbar recorder-toolbar',
                dock    : 'top',
                height  : 45,
                style   : 'border-color:transparent',
                items   : [
                    {
                        xtype      : 'textfield',
                        itemId     : 'recording-name',
                        fieldLabel : 'Name',
                        height     : 30,
                        width      : 200,
                        labelWidth : 50,
                        value      : R.get('newRecording')
                    },
                    {
                        xtype           : 'textfield',
                        itemId          : 'pageUrl',
                        height          : 30,
                        flex            : 1,
                        labelWidth      : 70,
                        fieldLabel      : R.get('pageUrl'),
                        enableKeyEvents : true,
                        listeners       : {
                            keyup : function (field, e) {
                                if (e.getKey() == e.ENTER) {
                                    this.onPageUrlFieldEnterKey();
                                }
                            },
                            scope : this
                        }
                    }
                ]
            },
            {
                xtype    : 'toolbar',
                cls      : 'siesta-toolbar siesta-recorder-button-toolbar',
                dock     : 'top',
                height   : 45,
                defaults : {
                    scale       : 'medium',
                    tooltipType : 'title',
                    scope       : me
                },
                items    : [
                    {
                        iconCls  : 'fa fa-circle icon-record',
                        action   : 'recorder-start',
                        cls      : 'recorder-tool',
                        whenIdle : true,
                        tooltip  : R.get('recordTooltip'),
                        handler  : me.onRecordClick
                    },
                    {
                        iconCls : 'fa fa-square',
                        cls     : 'recorder-tool',
                        action  : 'recorder-stop',
                        handler : me.stop,
                        tooltip : R.get('stopTooltip')
                    },
                    {
                        iconCls : 'fa fa-play',
                        action  : 'recorder-play',
                        cls     : 'recorder-tool',
                        handler : me.onPlayClick,
                        tooltip : R.get('playTooltip')
                    },
                    {
                        iconCls : 'fa fa-close',
                        action  : 'recorder-remove-all',
                        cls     : 'recorder-tool icon-clear',
                        handler : function () {
                            var me = this;

                            if (me.store.getCount() === 0) return;

                            Ext.Msg.confirm(R.get('removeAllPromptTitle'), R.get('removeAllPromptMessage'), function (btn) {
                                if (btn == 'yes') {
                                    // process text value and close...
                                    me.clear();
                                }
                            });
                        },
                        tooltip : R.get('clearTooltip')
                    },
                    {
                        iconCls     : 'fa fa-plus',
                        action      : 'recorder-add-step',
                        tooltip     : R.get('addNewTooltip'),
                        cls         : 'recorder-tool',
                        tooltipType : 'title',
                        scope       : me,
                        handler     : function () {
                            var store    = me.store;
                            var selected = me.getSelectionModel().selected.first();
                            var model    = new store.model(Ext.apply({}, this.newActionDefaults));

                            if (selected && selected.isVisible()) {
                                selected.parentNode.insertChild(selected.get('index') + 1, model);
                            } else {
                                store.getRootNode().appendChild(model);
                            }

                            me.editing.startEdit(model, 1);
                        }
                    },
                    '->',
                    {
                        xtype   : 'splitbutton',
                        text    : 'Show source',
                        cls     : 'recorder-tool',
                        action  : 'recorder-generate-code',
                        handler : this.onGenerateCodeClick,
                        scope   : this,
                        menu    : {
                            items : [
                                {
                                    text    : R.get('showSourceInNewWindow'),
                                    scope   : this,
                                    handler : function () {
                                        var win           = window.open(null);
                                        var body          = win.document.body;
                                        var recordingName = this.getRecordingName();
                                        var code          = this.store.generateCode(recordingName);

                                        body.innerHTML = '<pre>' + code + '</pre>';
                                    }
                                }
                            ]
                        }
                    },
                    window.document.queryCommandSupported('copy')
                        ?
                            {
                                iconCls : 'fa fa-clipboard',
                                cls     : 'recorder-tool',
                                action  : 'recorder-copy-to-clipboard',
                                handler : me.onCopyToClipboard,
                                tooltip : 'Copy generated code to clipboard'
                            }
                        :
                            null,
                    me.closeButton
                ]
            }];

        me.bbar = {
            xtype  : 'component',
            cls    : 'cheatsheet',
            // height : 70,
            html   : this.getCheatSheetText()
        };
    },


    getCheatSheetText : function () {
        var text    = '<table><tr><td class="cheatsheet-type">CSS Query:</td><td class="cheatsheet-sample"> .x-btn</td></tr>'

        if (this.recorder instanceof Siesta.Recorder.ExtJS) {
            text    += '<tr><td class="cheatsheet-type">Component Query:</td><td class="cheatsheet-sample"> &gt;&gt;toolbar button</td></tr>' +
                '<tr><td class="cheatsheet-type">Composite Query:</td><td class="cheatsheet-sample"> toolbar =&gt; .x-btn</td></tr>'
        }

        text    += '</table>'

        return text
    },


    // Attach to a test (and optionally a specific iframe, only used for testing)
    attachTo : function (test, iframe) {
        var me       = this;
        var doClear  = me.test && me.test.url !== test.url;
        var recorder = this.recorder

        this.setTest(test);

        var recWindow = (iframe && iframe.contentWindow) || (test.scopeProvider && test.scopeProvider.scope);

        if (recWindow) {
            me.recorder.attach(recWindow);
        }

        if (doClear) me.clear();
    },


    onPageUrlFieldEnterKey : function () {
        var descriptor = this.getRecorderTestDescriptor();

        if (descriptor && (descriptor.hostPageUrl || descriptor.pageUrl)) {
            this.domContainer.expand();

            this.project.startSingle(descriptor);

            this.project.on('teststart', function (event, test) {

                if (test.url === descriptor.url) {
                    // To ensure test is visible
                    this.fireEvent('play', this, test, 0);
                }

            }, this, { single : true });
        }
    },


    onRecordClick : function () {
        var test            = this.test

        if (!test) return;

        var R               = Siesta.Resource('Siesta.Recorder.UI.RecorderPanel');
        var descriptor      = this.getRecorderTestDescriptor();

        if (descriptor) {

            if (this.isRecording()) {
                this.stop();
            } else {
                var project = this.project;
                var pageUrl = this.down('#pageUrl').getValue();

                // Grab a new test reference from project in case test was rerun while recorder was open
                test        = project.getTestByURL(test.url) || test;

                this.setTest(test);

                var scopeProvider = test.scopeProvider;

                // If we're recording a new URL, or the test has no window (already was cleaned up) -
                // first launch the test to load the URL into the test iframe
                if (!scopeProvider || pageUrl && scopeProvider.sourceURL !== pageUrl) {
                    project.on('teststart', function (event, test) {

                        if (test.url === descriptor.url) {
                            this.attachTo(test);
                            this.recorder.start();
                        }

                    }, this, { single : true });

                    project.startSingle(descriptor);
                } else if (test && test.global) {
                    this.attachTo(test);
                    this.recorder.start();
                }
            }

        } else {
            Ext.Msg.alert('Error', R.get('noTestStarted'))
        }
    },


    onPlayClick : function () {
        var me         = this;
        var descriptor = this.getRecorderTestDescriptor();

        if (descriptor) {
            me.stop();

            var testStartListener = function (ev, runningTestInstance) {
                if (runningTestInstance.url === descriptor.url) {
                    runningTestInstance.on('beforetestfinalizeearly', testFinalizeListener, null, { single : true });
                }
            };

            var testFinalizeListener = function (ev, test2) {
                // important, need to update our reference to the test
                me.setTest(test2);

                // Run test first, and before it ends - fire off the recorded steps
                me.playSteps();
            };

            var project = me.project;

            project.on('teststart', testStartListener, null, { single : true });

            if (this.store.getCount() > 0) {
                this.scrollRecordIntoView( this.store.getAt(0));
            }

            project.startSingle(descriptor);
        }
    },


    onCopyToClipboard : function (button) {
        var recordingName   = this.getRecordingName()

        var success         = this.copyToClipboard(this.store.generateCode(recordingName))

        Ext.Msg.show({
            animateTarget   : button.getEl(),
            message         : success ? 'Code copied successfully' : 'Something went wrong, code was not copied',
            title           : 'Copy to clipboard',
            modal           : false
        })

        setTimeout(function () {
            Ext.Msg.hide()
        }, 800)
    },


    stop : function () {
        this.recorder.stop();
    },


    clear : function () {
        this.recorder.clear();
    },


    onRecorderClear : function () {
        this.store.getRootNode().removeAll();
    },


    getRecorderTestDescriptor : function () {
        var project     = this.project;
        var pageUrl     = this.down('#pageUrl').getValue();

        var test        = this.test;
        var descriptor  = test && project.getScriptDescriptor(test.url);
        var testHostUrl = descriptor && project.getDescriptorConfig(descriptor, 'pageUrl');

        // If user changes target page URL - use an empty virtual test descriptor
        return pageUrl && testHostUrl !== pageUrl ? {
            url             : '/',
            enablePageRedirect : true,
            testCode        : 'StartTest(function(t) {})',
            pageUrl         : pageUrl
        } : (test ? descriptor : null);
    },


    setTest : function (test) {
        if (this.test) {
            this.test.un('beforechainstep', this.onBeforeActionExecute, this)
        }

        this.test = test

        if (test) {
            test.on('beforechainstep', this.onBeforeActionExecute, this)

            var field   = this.down('#pageUrl')

            if (field && test.scopeProvider) field.setValue(test.scopeProvider.sourceURL || '')

            test.on('testfinalize', function () {
                if (test.scopeProvider && test.scopeProvider.crossOriginFailed) {
                    Ext.Msg.alert('Error', 'Recorder attached to the page from different origin - can\'t record anything')
                }
            })
        }
    },


    generateSteps : function (events) {
        var me    = this;
        var t     = me.test;

        var steps = (events || this.store.getRange()).map(function (action, index) {
            if (action.isLeaf()) {
                var step = action.asStep(t);
                // wait for targets when playing entire test,
                // and
                // when playing actions manually from a certain step, wait for all steps but the first one
                if (action.getTarget()) step.waitForTarget = !events || index > 0;

                return [
                    function (next) {
                        index = events ? me.store.indexOf(action) : index;
                        t.fireEvent('beforechainstep', t, index, step);
                        next();
                    },
                    step
                ]
            } else {
                return me.generateSteps(action.childNodes);
            }
        });

        return steps;
    },


    onBeforeActionExecute : function(event, test, index, step) {
        var count = this.store.getCount();

        this.getSelectionModel().select(index);

        if (index < count - 2) {
            this.scrollRecordIntoView( this.store.getAt(index + 2));
        }
    },


    onActionAdded : function (event, action) {
        var root         = this.store.getRootNode();
        var targetParent = (root.lastChild && root.lastChild.parentNode) || root;

        var newRecord = targetParent.appendChild(action);

        this.scrollRecordIntoView(newRecord)
    },


    onActionRemoved : function (event, action) {
        this.store.getNodeById(action.id).remove();
    },


    onActionUpdated : function (event, action) {
        var model = this.store.getNodeById(action.id);
        model.callJoined('afterEdit', [['target', 'action', '__offset__']])
    },


    getActions : function (asJooseInstances) {
        var actionModels = this.store.getRange()

        return asJooseInstances ? Ext.Array.pluck(actionModels, 'data') : actionModels
    },


    onDestroy : function () {
        if (this.recorder) {
            this.recorder.stop();
        }

        this.callParent(arguments);
    },


    scrollRecordIntoView : function (record) {
        if (this.view.rendered) {
            this.ensureVisible(record, { animate : { duration : 100 } });
        }
    },


    onBeforeEdit : function (cellEditing, editingContext) {

        var column = editingContext.column;
        var editor;

        if (column.xtype === "targetcolumn") {
            cellEditing.completeEdit();

            column.setTargetEditor(editingContext.record);

            editingContext.value = editingContext.record.get(column.dataIndex);
        } else {
            editor = editingContext.column.getEditor();

            if (editor.xtype === 'typeeditor') {
                // Can't populate until we have a test bound
                editor.populate(this.test);
            }
        }

        if (editingContext.column.dataIndex === 'target' && this.domContainer) {
            this.domContainer.startInspection(false);
        }

        // Offset only relevant for mouseinput actions
        return editingContext.field !== '__offset__' || editingContext.record.isMouseAction();
    },


    afterEdit : function (plug, e) {
        if (e.field === 'action') {
            var store = e.column.field.store;
            store.clearFilter();

            if (store.getById(e.value).get('type') !== store.getById(e.originalValue).get('type')) {
                e.record.resetValues();
            }
        }
    },


    onValidateEdit : function (plug, e) {
        var value = e.value;

        if (e.field === 'action' && !value) return false;

        if (e.field === '__offset__') {
            e.cancel = true;

            if (value) {
                var parsed = e.record.parseOffset(value);

                if (parsed) {
                    e.record.setTargetOffset(parsed);
                }
            } else {
                e.record.clearTargetOffset();
            }
        } else if (e.column.getEditor().applyChanges) {
            e.cancel = true;

            e.column.getEditor().applyChanges(e.record);
        }

        // Trigger manual refresh of node when 'set' operation is more complex
        if (e.cancel) {
            this.afterEdit(plug, e);
            this.getView().refreshNode(e.record);
        }
    },


    afterRender : function () {
        this.callParent(arguments);

        var view = this.getView();

        view.el.on({
            mousedown : function (e, t) {
                var record = view.getRecord(view.findItemByChild(t));

                record.clearTargetOffset()
                view.refreshNode(record);

                e.stopEvent();
            },
            delegate  : '.siesta-recorderpanel-clearoffset'
        })
    },


    isRecording : function () {
        return this.recorder.active;
    },


    onGenerateCodeClick : function () {
        var R = Siesta.Resource('Siesta.Recorder.UI.RecorderPanel');

        var win = new Ext.Window({
            title       : R.get('codeWindowTitle'),
            layout      : 'fit',
            id          : 'codeWindow',
            itemId      : 'codeWindow',
            cls         : 'si-recorder-sourcewindow',
            height      : 400,
            width       : 600,
            autoScroll  : true,
            autoShow    : true,
            constrain   : true,
            modal       : true,
            closeAction : 'destroy',
            stateful    : true,
            items       : {
                xtype : 'jseditor',
                mode  : 'text/javascript'
            }
        });

        var field         = win.items.first();
        var recordingName = this.getRecordingName();
        var code          = this.store.generateCode(recordingName);

        field.setValue(code);
        field.editor.focus();
    },


    getRecordingName : function () {
        return this.down('#recording-name').getValue();
    },


    onDeleteStepClick : function (grid, rowIndex, colIndex, item, e, record) {
        this.editing && this.editing.completeEdit();

        record.remove();
    },


    onPlaySingleStepClick : function (cmp, rowIndex) {
        this.playSingle(rowIndex);
    },


    onPlayFromStepClick : function (cmp, rowIndex) {
        this.playFromStep(rowIndex);
    },


    playSingle : function(index) {
        if (this.test) {
            var action = this.store.getAt(index);
            var steps  = this.generateSteps([action]);

            this.playSteps(steps);
        }
    },


    playFromStep : function (startIndex) {
        if (this.test) {
            this.playRange(startIndex);
        }
    },


    playRange : function (startIndex, endIndex) {
        if (this.test) {
            var steps = this.generateSteps(this.store.getRange(startIndex, endIndex));

            this.playSteps(steps);
        }
    },


    playSteps : function (steps) {
        var test    = this.test

        if (test) {
            var me  = this;

            steps   = steps || this.generateSteps();

            steps   = steps instanceof Array ? steps : [ steps ];

            test.chain(
                function (next) {
                    me.fireEvent('play', me, test);
                    next();
                },

                steps,

                function (next) {
                    me.fireEvent('stop', me, test);
                    next();
                }
            );
        }
    },


    getRecorder : function () {
        return this.recorder;
    }
});