/** @odoo-module **/

import { MockServer } from "@web/../tests/helpers/mock_server";
import testUtils from "@web/../tests/legacy/helpers/test_utils";
import { patch } from "@web/core/utils/patch";
import * as OdooEditorLib from "@web_editor/js/editor/odoo-editor/src/OdooEditor";
import { Wysiwyg } from '@web_editor/js/wysiwyg/wysiwyg';
import options from "@web_editor/js/editor/snippets.options";
import { TABLE_ATTRIBUTES, TABLE_STYLES } from '@web_editor/js/backend/convert_inline';

export const COLOR_PICKER_TEMPLATE = `
    <colorpicker>
        <div class="o_colorpicker_section" data-name="theme" data-display="Theme Colors" data-icon-class="fa fa-flask">
            <button data-color="o-color-1"/>
            <button data-color="o-color-2"/>
            <button data-color="o-color-3"/>
            <button data-color="o-color-4"/>
            <button data-color="o-color-5"/>
        </div>
        <div class="o_colorpicker_section" data-name="transparent_grayscale" data-display="Transparent Colors" data-icon-class="fa fa-eye-slash">
            <button class="o_btn_transparent"/>
            <button data-color="black-25"/>
            <button data-color="black-50"/>
            <button data-color="black-75"/>
            <button data-color="white-25"/>
            <button data-color="white-50"/>
            <button data-color="white-75"/>
        </div>
        <div class="o_colorpicker_section" data-name="common" data-display="Common Colors" data-icon-class="fa fa-paint-brush">
            <button data-color="black"></button>
            <button data-color="900"></button>
            <button data-color="800"></button>
            <button data-color="700" class="d-none"></button>
            <button data-color="600"></button>
            <button data-color="500" class="d-none"></button>
            <button data-color="400"></button>
            <button data-color="300" class="d-none"></button>
            <button data-color="200"></button>
            <button data-color="100"></button>
            <button data-color="white"></button>
        </div>
    </colorpicker>
`;
const SNIPPETS_TEMPLATE = `
    <h2 id="snippets_menu">Add blocks</h2>
    <div id="o_scroll">
        <div id="snippet_structure" class="o_panel">
            <div class="o_panel_header">First Panel</div>
            <div class="o_panel_body">
                <div name="Separator" data-oe-type="snippet" data-oe-thumbnail="/web_editor/static/src/img/snippets_thumbs/s_hr.svg">
                    <div class="s_hr pt32 pb32">
                        <hr class="s_hr_1px s_hr_solid w-100 mx-auto"/>
                    </div>
                </div>
                <div name="Content" data-oe-type="snippet" data-oe-thumbnail="/website/static/src/img/snippets_thumbs/s_text_block.png">
                    <section name="Content+Options" class="test_option_all pt32 pb32" data-oe-type="snippet" data-oe-thumbnail="/website/static/src/img/snippets_thumbs/s_text_block.png">
                        <div class="container">
                            <div class="row">
                                <div class="col-lg-10 offset-lg-1 pt32 pb32">
                                    <h2>Title</h2>
                                    <p class="lead o_default_snippet_text">Content</p>
                                </div>
                            </div>
                        </div>
                    </section>
                </div>
            </div>
        </div>
    </div>
    <div id="snippet_options" class="d-none">
        <div data-js="many2one" data-selector="[data-oe-many2one-model]:not([data-oe-readonly])" data-no-check="true"/>
        <div data-js="content"
            data-selector=".s_hr, .test_option_all"
            data-drop-in=".note-editable"
            data-drop-near="p, h1, h2, h3, blockquote, .s_hr"/>
        <div data-js="sizing_y" data-selector=".s_hr, .test_option_all"/>
        <div data-selector=".test_option_all">
            <we-colorpicker string="Background Color" data-select-style="true" data-css-property="background-color" data-color-prefix="bg-"/>
        </div>
        <div data-js="BackgroundImage" data-selector=".test_option_all">
            <we-button data-choose-image="true" data-no-preview="true">
                <i class="fa fa-picture-o"/> Background Image
            </we-button>
        </div>
        <div data-js="option_test" data-selector=".s_hr">
            <we-select string="Alignment">
                <we-button data-select-class="align-items-start">Top</we-button>
                <we-button data-select-class="align-items-center">Middle</we-button>
                <we-button data-select-class="align-items-end">Bottom</we-button>
                <we-button data-select-class="align-items-stretch">Equal height</we-button>
            </we-select>
        </div>
    </div>`;

patch(MockServer.prototype, {
    //--------------------------------------------------------------------------
    // Private
    //--------------------------------------------------------------------------

    /**
     * @override
     * @private
     * @returns {Promise}
     */
    async _performRPC(route, args) {
        if (args.model === "ir.ui.view" && args.method === 'render_public_asset') {
            if (args.args[0] === "web_editor.colorpicker") {
                return COLOR_PICKER_TEMPLATE;
            }
            if (args.args[0] === "web_editor.snippets") {
                return SNIPPETS_TEMPLATE;
            }
        }
        return super._performRPC(...arguments);
    },
});

/**
 * Options with animation and edition for test.
 */
options.registry.option_test = options.Class.extend({
    cleanForSave: function () {
        this.$target.addClass('cleanForSave');
    },
    onBuilt: function () {
        this.$target.addClass('built');
    },
    onBlur: function () {
        this.$target.removeClass('focus');
    },
    onClone: function () {
        this.$target.addClass('clone');
        this.$target.removeClass('focus');
    },
    onFocus: function () {
        this.$target.addClass('focus');
    },
    onMove: function () {
        this.$target.addClass('move');
    },
    onRemove: function () {
        this.$target.closest('.note-editable').addClass('snippet_has_removed');
    },
});

/**
 * @param {object} data
 * @returns {object}
 */
export function wysiwygData(data) {
    return Object.assign({
        'ir.ui.view': {
            fields: {
                display_name: {
                    string: "Displayed name",
                    type: "char",
                },
            },
            records: [],
            render_template(args) {
                if (args[0] === 'web_editor.colorpicker') {
                    return COLOR_PICKER_TEMPLATE;
                }
                if (args[0] === 'web_editor.snippets') {
                    return SNIPPETS_TEMPLATE;
                }
            },
        },
        'ir.attachment': {
            fields: {
                display_name: {
                    string: "display_name",
                    type: 'char',
                },
                description: {
                    string: "description",
                    type: 'char',
                },
                mimetype: {
                    string: "mimetype",
                    type: 'char',
                },
                checksum: {
                    string: "checksum",
                    type: 'char',
                },
                url: {
                    string: "url",
                    type: 'char',
                },
                type: {
                    string: "type",
                    type: 'char',
                },
                res_id: {
                    string: "res_id",
                    type: 'integer',
                },
                res_model: {
                    string: "res_model",
                    type: 'char',
                },
                public: {
                    string: "public",
                    type: 'boolean',
                },
                access_token: {
                    string: "access_token",
                    type: 'char',
                },
                image_src: {
                    string: "image_src",
                    type: 'char',
                },
                image_width: {
                    string: "image_width",
                    type: 'integer',
                },
                image_height: {
                    string: "image_height",
                    type: 'integer',
                },
                original_id: {
                    string: "original_id",
                    type: 'many2one',
                    relation: 'ir.attachment',
                },
            },
            records: [{
                id: 1,
                name: 'image',
                description: '',
                mimetype: 'image/png',
                checksum: false,
                url: '/web/image/123/transparent.png',
                type: 'url',
                res_id: 0,
                res_model: false,
                public: true,
                access_token: false,
                image_src: '/web/image/123/transparent.png',
                image_width: 256,
                image_height: 256,
            }],
            generate_access_token: function () {
                return;
            },
        },
    }, data);
}

/**
 * Perform a series of tests (`keyboardTests`) for using keyboard inputs.
 *
 * @see wysiwyg_keyboard_tests.js
 * @see wysiwyg_tests.js
 *
 * @param {jQuery} $editable
 * @param {object} assert
 * @param {object[]} keyboardTests
 * @param {string} keyboardTests.name
 * @param {string} keyboardTests.content
 * @param {object[]} keyboardTests.steps
 * @param {string} keyboardTests.steps.start
 * @param {string} [keyboardTests.steps.end] default: steps.start
 * @param {string} keyboardTests.steps.key
 * @param {object} keyboardTests.test
 * @param {string} [keyboardTests.test.content]
 * @param {string} [keyboardTests.test.start]
 * @param {string} [keyboardTests.test.end] default: steps.start
 * @param {function($editable, assert)} [keyboardTests.test.check]
 * @param {Number} addTests
 */
var testKeyboard = function ($editable, assert, keyboardTests, addTests) {
    var tests = keyboardTests.map((k) => k.test).map((x) => !!x);
    var testNumber =
        tests.map((test) => test.start).map((x) => !!x).length +
        tests.map((test) => test.content).map((x) => !!x).length +
        tests.map((test) => test.check.map((x) => !!x)).length +
        (addTests | 0);
    assert.expect(testNumber);

    function keydown(target, keypress) {
        var $target = $(target.tagName ? target : target.parentNode);
        var event = $.Event("keydown", keypress);
        $target.trigger(event);

        if (!event.isDefaultPrevented()) {
            if (keypress.key.length === 1) {
                textInput($target[0], keypress.key);
            } else {
                console.warn('Native "' + keypress.key + '" is not supported in test');
            }
        }
        $target.trigger($.Event("keyup", keypress));
        return $target;
    }

    function _select(selector) {
        // eg: ".class:contents()[0]->1" selects the first contents of the 'class' class, with an offset of 1
        var reDOMSelection = /^(.+?)(:contents(\(\)\[|\()([0-9]+)[\]|\)])?(->([0-9]+))?$/;
        var sel = selector.match(reDOMSelection);
        var $node = $editable.find(sel[1]);
        var point = {
            node: sel[3] ? $node.contents()[+sel[4]] : $node[0],
            offset: sel[5] ? +sel[6] : 0,
        };
        if (!point.node || point.offset > (point.node.tagName ? point.node.childNodes : point.node.textContent).length) {
            assert.notOk("Node not found: '" + selector + "' " + (point.node ? "(container: '" + (point.node.outerHTML || point.node.textContent) + "')" : ""));
        }
        return point;
    }

    function selectText(start, end) {
        start = _select(start);
        var target = start.node;
        $(target.tagName ? target : target.parentNode).trigger("mousedown");
        if (end) {
            end = _select(end);
            Wysiwyg.setRange(start.node, start.offset, end.node, end.offset);
        } else {
            Wysiwyg.setRange(start.node, start.offset);
        }
        target = end ? end.node : start.node;
        $(target.tagName ? target : target.parentNode).trigger('mouseup');
    }

    function nextPoint(point) {
        var node, offset;
        if (OdooEditorLib.nodeSize(point.node) === point.offset) {
            node = point.node.parentNode;
            offset = OdooEditorLib.childNodeIndex(point.node) + 1;
        } else if (point.node.hasChildNodes()) {
            node = point.node.childNodes[point.offset];
            offset = 0;
        } else {
            node = point.node;
            offset = point.offset + 1;
        }
        return {
            node: node,
            offset: offset
        };
    }

    function endOfAreaBetweenTwoNodes(point) {
        // move the position because some browser make the caret on the end of the previous area after normalize
        if (
            !point.node.tagName &&
            point.offset === point.node.textContent.length &&
            !/\S|\u00A0/.test(point.node.textContent)
        ) {
            point = nextPoint(nextPoint(point));
            while (point.node.tagName && point.node.textContent.length) {
                point = nextPoint(point);
            }
        }
        return point;
    }

    var defPollTest = Promise.resolve();

    function pollTest(test) {
        var def = Promise.resolve();
        $editable.data('wysiwyg').setValue(test.content);

        function poll(step) {
            var def = testUtils.makeTestPromise();
            if (step.start) {
                selectText(step.start, step.end);
                if (!Wysiwyg.getRange()) {
                    throw 'Wrong range! \n' +
                        'Test: ' + test.name + '\n' +
                        'Selection: ' + step.start + '" to "' + step.end + '"\n' +
                        'DOM: ' + $editable.html();
                }
            }
            setTimeout(function () {
                if (step.key) {
                    var target = Wysiwyg.getRange().ec;
                    if (window.location.search.indexOf('notrycatch') !== -1) {
                        keydown(target, {
                            key: step.key,
                            ctrlKey: !!step.ctrlKey,
                            shiftKey: !!step.shiftKey,
                            altKey: !!step.altKey,
                            metaKey: !!step.metaKey,
                        });
                    } else {
                        try {
                            keydown(target, {
                                key: step.key,
                                ctrlKey: !!step.ctrlKey,
                                shiftKey: !!step.shiftKey,
                                altKey: !!step.altKey,
                                metaKey: !!step.metaKey,
                            });
                        } catch (e) {
                            assert.notOk(e.name + '\n\n' + e.stack, test.name);
                        }
                    }
                }
                setTimeout(function () {
                    if (step.key) {
                        var $target = $(target.tagName ? target : target.parentNode);
                        $target.trigger($.Event('keyup', {
                            key: step.key,
                            ctrlKey: !!step.ctrlKey,
                            shiftKey: !!step.shiftKey,
                            altKey: !!step.altKey,
                            metaKey: !!step.metaKey,
                        }));
                    }
                    setTimeout(def.resolve.bind(def));
                });
            });
            return def;
        }
        while (test.steps.length) {
            def = def.then(poll.bind(null, test.steps.shift()));
        }

        return def.then(function () {
            if (!test.test) {
                return;
            }

            if (test.test.check) {
                test.test.check($editable, assert);
            }

            // test content
            if (test.test.content) {
                var value = $editable.data('wysiwyg').getValue({
                    keepPopover: true,
                });
                var allInvisible = /\u200B/g;
                value = value.replace(allInvisible, '&#8203;');
                var result = test.test.content.replace(allInvisible, '&#8203;');
                assert.strictEqual(value, result, test.name);

                if (test.test.start && value !== result) {
                    assert.notOk("Wrong DOM (see previous assert)", test.name + " (carret position)");
                    return;
                }
            }

            $editable[0].normalize();

            // test carret position
            if (test.test.start) {
                var start = _select(test.test.start);
                var range = Wysiwyg.getRange();
                if ((range.sc !== range.ec || range.so !== range.eo) && !test.test.end) {
                    assert.ok(false, test.name + ": the carret is not colapsed and the 'end' selector in test is missing");
                    return;
                }
                var end = test.test.end ? _select(test.test.end) : start;
                if (start.node && end.node) {
                    range = Wysiwyg.getRange();
                    var startPoint = endOfAreaBetweenTwoNodes({
                        node: range.sc,
                        offset: range.so,
                    });
                    var endPoint = endOfAreaBetweenTwoNodes({
                        node: range.ec,
                        offset: range.eo,
                    });
                    var sameDOM = (startPoint.node.outerHTML || startPoint.node.textContent) === (start.node.outerHTML || start.node.textContent);
                    var stringify = function (obj) {
                        if (!sameDOM) {
                            delete obj.sameDOMsameNode;
                        }
                        return JSON.stringify(obj, null, 2)
                            .replace(/"([^"\s-]+)":/g, "\$1:")
                            .replace(/([^\\])"/g, "\$1'")
                            .replace(/\\"/g, '"');
                    };
                    assert.deepEqual(stringify({
                            startNode: startPoint.node.outerHTML || startPoint.node.textContent,
                            startOffset: startPoint.offset,
                            endPoint: endPoint.node.outerHTML || endPoint.node.textContent,
                            endOffset: endPoint.offset,
                            sameDOMsameNode: sameDOM && startPoint.node === start.node,
                        }),
                        stringify({
                            startNode: start.node.outerHTML || start.node.textContent,
                            startOffset: start.offset,
                            endPoint: end.node.outerHTML || end.node.textContent,
                            endOffset: end.offset,
                            sameDOMsameNode: true,
                        }),
                        test.name + " (carret position)");
                }
            }
        });
    }
    while (keyboardTests.length) {
        defPollTest = defPollTest.then(pollTest.bind(null, keyboardTests.shift()));
    }

    return defPollTest;
};


/**
 * Select a node in the dom with is offset.
 *
 * @param {String} startSelector
 * @param {String} endSelector
 * @param {jQuery} $editable
 * @returns {Object} {sc, so, ec, eo}
 */
var select = (function () {
    var __select = function (selector, $editable) {
        var sel = selector.match(/^(.+?)(:contents\(\)\[([0-9]+)\]|:contents\(([0-9]+)\))?(->([0-9]+))?$/);
        var $node = $editable.find(sel[1]);
        return {
            node: sel[2] ? $node.contents()[sel[3] ? +sel[3] : +sel[4]] : $node[0],
            offset: sel[5] ? +sel[6] : 0,
        };
    };
    return function (startSelector, endSelector, $editable) {
        var start = __select(startSelector, $editable);
        var end = endSelector ? __select(endSelector, $editable) : start;
        return {
            sc: start.node,
            so: start.offset,
            ec: end.node,
            eo: end.offset,
        };
    };
})();

/**
 * Trigger a keydown event.
 *
 * @param {String or Number} key (name or code)
 * @param {jQuery} $editable
 * @param {Object} [options]
 * @param {Boolean} [options.firstDeselect] (default: false) true to deselect before pressing
 */
var keydown = function (key, $editable, options) {
    var keyPress = {};
    keyPress.key = key;
    var range = Wysiwyg.getRange();
    if (!range) {
        console.error("Editor have not any range");
        return;
    }
    if (options && options.firstDeselect) {
        range.sc = range.ec;
        range.so = range.eo;
        Wysiwyg.setRange(range.sc, range.so, range.ec, range.eo);
    }
    var target = range.ec;
    var $target = $(target.tagName ? target : target.parentNode);
    var event = $.Event("keydown", keyPress);
    $target.trigger(event);

    if (!event.isDefaultPrevented()) {
        if (keyPress.key.length === 1) {
            textInput($target[0], keyPress.key);
        } else {
            console.warn('Native "' + keyPress.key + '" is not supported in test');
        }
    }
};

var textInput = function (target, char) {
    var ev = new CustomEvent('textInput', {
        bubbles: true,
        cancelBubble: false,
        cancelable: true,
        composed: true,
        data: char,
        defaultPrevented: false,
        detail: 0,
        eventPhase: 3,
        isTrusted: true,
        returnValue: true,
        sourceCapabilities: null,
        type: "textInput",
        which: 0,
    });
    ev.data = char;
    target.dispatchEvent(ev);

    if (!ev.defaultPrevented) {
        document.execCommand("insertText", 0, ev.data);
    }
};

//--------------------------------------------------------------------------
// Convert Inline
//--------------------------------------------------------------------------

const tableAttributesString = Object.keys(TABLE_ATTRIBUTES).map(key => `${key}="${TABLE_ATTRIBUTES[key]}"`).join(' ');
const tableStylesString = Object.keys(TABLE_STYLES).map(key => `${key}: ${TABLE_STYLES[key]};`).join(' ');
/**
 * Take a matrix representing a grid and return an HTML string of the Bootstrap
 * grid. The matrix is an array of rows, with each row being an array of cells.
 * Each cell can be represented either by a 0 < number < 13 (col-#) or a falsy
 * value (col). Each cell has its coordinates `(row index, column index)` as
 * text content.
 * Eg: [                        // <div class="container">
 *      [                       //     <div class="row">
 *          1,                  //         <div class="col-1">(0, 0)</div>
 *          11,                 //         <div class="col-11">(0, 1)</div>
 *      ],                      //     </div>
 *      [                       //     <div class="row">
 *          false,              //         <div class="col">(1, 0)</div>
 *      ],                      //     </div>
 * ]                            // </div>
 *
 * @param {Array<Array<Number|null>>} matrix
 * @returns {string}
 */
export function getGridHtml(matrix) {
    return (
        `<div class="container">` +
        matrix.map((row, iRow) => (
            `<div class="row">` +
            row.map((col, iCol) => (
                `<div class="${col ? 'col-' + col : 'col'}">(${iRow}, ${iCol})</div>`
            )).join('') +
            `</div>`
        )).join('') +
        `</div>`
    );
}
export function getTdHtml(colspan, text, containerWidth) {
    return (
        `<td colspan="${colspan}"${
            containerWidth ? ' ' + `style="max-width: ${Math.round(containerWidth*colspan/12*100)/100}px;"`
                           : ''}>` +
            text +
        `</td>`
    );
}
/**
 * Take a matrix representing a table and return an HTML string of the table.
 * The matrix is an array of rows, with each row being an array of cells. Each
 * cell is represented by a tuple of numbers [colspan, width (in percent)]. A
 * cell can have a string as third value to represent its text content. The
 * default text content of each cell is its coordinates `(row index, column
 * index)`. If the cell has a number as third value, it will be used as the
 * max-width of the cell (in pixels).
 * Eg: [                        // <table> (note: extra attrs and styles apply)
 *      [                       //   <tr>
 *          [1, 8],             //     <td colspan="1" width="8%">(0, 0)</td>
 *          [11, 92]            //     <td colspan="11" width="92%">(0, 1)</td>
 *      ],                      //   </tr>
 *      [                       //   <tr>
 *          [2, 17, 'A'],       //     <td colspan="2" width="17%">A</td>
 *          [10, 83],           //     <td colspan="10" width="83%">(1, 1)</td>
 *      ],                      //   </tr>
 * ]                            // </table>
 *
 * @param {Array<Array<Array<[Number, Number, string?, number?]>>>} matrix
 * @param {Number} [containerWidth]
 * @returns {string}
 */
export function getTableHtml(matrix, containerWidth) {
    return (
        `<table ${tableAttributesString} style="width: 100% !important; ${tableStylesString}">` +
        matrix.map((row, iRow) => (
            `<tr>` +
            row.map((col, iCol) => (
                getTdHtml(col[0], typeof col[2] === 'string' ? col[2] : `(${iRow}, ${iCol})`, containerWidth)
            )).join('') +
            `</tr>`
        )).join('') +
        `</table>`
    );
}
/**
 * Take a number of rows and a number of columns (or number of columns per
 * individual row) and return an HTML string of the corresponding grid. Every
 * column is a regular Bootstrap "col" (no col-#).
 * Eg: [2, 3] <=> getGridHtml([[false, false, false], [false, false, false]])
 * Eg: [2, [2, 1]] <=> getGridHtml([[false, false], [false]])
 *
 * @see getGridHtml
 * @param {Number} nRows
 * @param {Number|Number[]} nCols
 * @returns {string}
 */
export function getRegularGridHtml(nRows, nCols) {
    const matrix = new Array(nRows).fill().map((_, iRow) => (
        new Array(Array.isArray(nCols) ? nCols[iRow] : nCols).fill()
    ));
    return getGridHtml(matrix);
};
/**
 * Take a number of rows, a number of columns (or number of columns per
 * individual row), a colspan (or colspan per individual row) and a width (or
 * width per individual row, in percent), and return an HTML string of the
 * corresponding table. Every cell in a row has the same colspan/width.
 * Eg: [2, 2, 6, 50] <=> getTableHtml([[[6, 50], [6, 50]], [[6, 50], [6, 50]]])
 * Eg: [2, [2, 1], [6, 12], [50, 100]] <=> getTableHtml([[[6, 50], [6, 50]], [[12, 100]]])
 *
 * @see getTableHtml
 * @param {Number} nRows
 * @param {Number|Number[]} nCols
 * @param {Number|Number[]} colspan
 * @param {Number|Number[]} width
 * @param {Number} containerWidth
 * @returns {string}
 */
export function getRegularTableHtml(nRows, nCols, colspan, width, containerWidth) {
    const matrix = new Array(nRows).fill().map((_, iRow) => (
        new Array(Array.isArray(nCols) ? nCols[iRow] : nCols).fill().map(() => ([
            Array.isArray(colspan) ? colspan[iRow] : colspan,
            Array.isArray(width) ? width[iRow] : width,
        ])))
    );
    return getTableHtml(matrix, containerWidth);
}
/**
 * Take an HTML string and returns that string stripped from any HTML comments.
 * By default, also removes the mso-hide class which is only there for outlook
 * to hide elements when we use mso conditional comments.
 *
 * @param {string} html
 * @param {boolean} [removeMsoHide=true]
 * @returns {string}
 */
export function removeComments(html, removeMsoHide=true) {
    const cleanHtml = html.replace(/<!--(.*?)-->/g, '');
    if (removeMsoHide) {
        return cleanHtml.replaceAll(' class="mso-hide"', '').replace(/\s*mso-hide/g, '').replace(/mso-hide\s*/g, '');
    } else {
        return cleanHtml;
    }
}

export default {
    wysiwygData: wysiwygData,
    testKeyboard: testKeyboard,
    select: select,
    keydown: keydown,
    getGridHtml: getGridHtml,
    getTableHtml: getTableHtml,
    getRegularGridHtml: getRegularGridHtml,
    getRegularTableHtml: getRegularTableHtml,
    getTdHtml: getTdHtml,
    removeComments: removeComments,
};
