/**
 * ---------------------------------------------------------------------
 *
 * GLPI - Gestionnaire Libre de Parc Informatique
 *
 * http://glpi-project.org
 *
 * @copyright 2015-2024 Teclib' and contributors.
 * @copyright 2003-2014 by the INDEPNET Development Team.
 * @licence   https://www.gnu.org/licenses/gpl-3.0.html
 *
 * ---------------------------------------------------------------------
 *
 * LICENSE
 *
 * This file is part of GLPI.
 *
 * 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, either version 3 of the License, or
 * (at your option) any later version.
 *
 * 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, see <https://www.gnu.org/licenses/>.
 *
 * ---------------------------------------------------------------------
 */

/* global escapeMarkupText */

import SearchTokenizer from "./SearchTokenizer.js";

/**
 * @typedef SearchInputOptions
 * @property {{}} [popover] Popover options
 * @property {'edit'|'remove'} [backspace_action='edit'] The action when pressing the backspace key at the start of the input
 * @property {function} [on_result_change] Callback when the result changes
 * @property {TokenizerOptions} [tokenizer_options] Tokenizer options
 * @property {boolean} filter_on_type Whether to filter the suggestions on typing
 * @property {{}} [input_options] Options for the new input element
 * @property {[]|'copy'} [input_options.classes] Classes for the new input element. If set to "copy", the classes of the original input will be copied
 * @property {{}|'copy'} [input_options.attributes] Attributes for the new input element. If set to "copy", the attributes of the original input will be copied
 * @property {{}|'copy'} [input_options.data] Data for the new input element. If set to "copy", the attributes of the original input will be copied
 */

export default class SearchInput {

    constructor(input, options) {
        /**
       * @type {jQuery}
       */
        this.original_input = $(input);

        /**
       * @type {SearchInputOptions}
       */
        this.options = Object.assign({
            backspace_action: 'edit',
            tokenizer_options: {},
            filter_on_type: true,
            input_options: {
                classes: [],
                attributes: {},
                data: {}
            }
        }, options || {});
        this.tokenizer = new SearchTokenizer(this.options.allowed_tags || {}, this.options.drop_unallowed_tags || false, this.options.tokenizer_options);

        this.displayed_input = $(`
         <div class="form-control search-input d-flex overflow-auto" tabindex="0"></div>
      `).insertBefore(input);
        this.displayed_input.append(`<span class="search-input-tag-input flex-grow-1" contenteditable="true"></span>`);
        this.applyInputOptions();

        this.original_input.hide();

        this.last_result = null;

        this.registerListeners();
    }

    applyInputOptions() {
        let new_attrs = {};

        if (typeof this.options.input_options.attributes === 'object') {
            new_attrs = this.options.input_options.attributes;
        } else if (this.options.input_options.attributes === 'copy') {
            const original_attr = this.original_input.get(0).attributes;
            for (let i = 0; i < original_attr.length; i++) {
                // Get only non-data attributes
                if (!original_attr[i].name.startsWith('data-') && original_attr[i].name !== 'class') {
                    new_attrs[original_attr[i].name] = original_attr[i].value;
                }
            }
        }

        let new_data = {};
        let old_data_attrs = {};
        if (typeof this.options.input_options.data === 'object') {
            new_data = this.options.input_options.data;
        } else if (this.options.input_options.data === 'copy') {
            new_data = this.original_input.data();
            const original_attr = this.original_input.get(0).attributes;
            // Get data attributes in case they aren't in jQuery data
            for (let i = 0; i < original_attr.length; i++) {
                // Get only data attributes
                if (original_attr[i].name.startsWith('data-')) {
                    old_data_attrs[original_attr[i].name] = original_attr[i].value;
                }
            }
        }

        // Add data attributes. We don't use $.data() because having the DOM attribute may be needed and using $.data doesn't add them.
        // Information from $.data will override any data attributes of the same name
        new_attrs = Object.assign(old_data_attrs, Object.keys(new_data).reduce((obj, key) => {
            obj['data-' + key] = new_data[key];
            return obj;
        }, new_attrs));

        // Apply attributes including data attributes
        this.displayed_input.attr(new_attrs);

        // Apply classes
        if (Array.isArray(this.options.input_options.classes)) {
            this.displayed_input.addClass(this.options.input_options.classes.join(' '));
        } else if (this.options.input_options.classes === 'copy') {
            this.displayed_input.addClass(this.original_input.attr('class'));
        }
    }

    registerListeners() {

        const input = this.displayed_input;

        input.on('input change', () => {
            if (this.isSelectionUntagged()) {
                this.refreshPopover();
            }
        });

        input.popover(Object.assign({
            trigger: 'manual',
            html: true,
            container: this.displayed_input.parent(),
            customClass: 'search-input-popover shadow',
            placement: 'bottom', // Option from Bootstrap (fallback)
            popperConfig: {
                placement: 'bottom-start', // Option only available directly in popper.js (Preferred)
            },
            delay: {
                hide: 300
            },
            sanitize: false,
            content: () => {
                return this.getPopoverContent();
            }
        }, this.options.popover || {}));

        input.parent().on('mousedown', '.search-input-popover', (e) => {
            e.preventDefault();
        });

        input.parent().on('click', '.search-input-popover .tags-list li', (e) => {
            e.preventDefault();
            e.stopPropagation();
            const tag = $(e.target).closest('li').attr('data-tag');
            const node = $('<span class="search-input-tag-input">'+tag.trim()+':</span>').insertBefore($('.search-input-tag-input:last-of-type'));
            //Clear selected node's text
            const selected_node = this.getSelectedNode();
            $(selected_node).text('');
            const new_node = this.tagifyInputNode(node);
            this.makeTagEditable(new_node);
            new_node.focus();
        });

        input.parent().on('click', '.search-input-popover .tags-list li button.tag-prefix', (e) => {
            e.preventDefault();
            e.stopPropagation();
            const prefix = $(e.target).closest('button.tag-prefix').attr('data-prefix');
            const tag = $(e.target).closest('li').attr('data-tag');
            const node = $('<span class="search-input-tag-input">'+(prefix || '')+tag.trim()+':</span>').insertBefore($('.search-input-tag-input:last-of-type'));
            //Clear selected node's text
            const selected_node = this.getSelectedNode();
            $(selected_node).text('');
            const new_node = this.tagifyInputNode(node);
            this.makeTagEditable(new_node);
            new_node.focus();
        });

        input.parent().on('click', '.search-input-popover .term-suggestions-list li', (e) => {
            e.preventDefault();
            const li = $(e.target).closest('li');
            const tag = li.closest('ul').attr('data-tag');
            const selected_term = li.text().trim();
            const editing_node = input.find('.search-input-tag-input[data-tag="'+tag+'"]');
            editing_node.text(`${tag}:${selected_term}`);
            this.tagifyInputNode(editing_node);
            this.placeCaretInDefaultInput();
        });

        input.on('input click focus', () => {
            this.refreshPopover();
            input.popover('show');
        });

        $(document.body).on('click', (e) => {
            if ($(e.target).closest(input, this.original_input, input.parent().find('.search-input-popover')).length === 0) {
                input.popover('hide');
            }
        });

        input.on('blur', '.search-input-tag-input', (e) => {
            const tag_input = $(e.target).closest('.search-input-tag-input');
            if (tag_input.length > 0 && tag_input.text().trim().length > 0) {
                this.tagifyInputNode(tag_input);
            }
        });

        input.on('keydown', '.search-input-tag-input', (e) => {
            if (e.keyCode === 9) { // Tab
            // Prevent losing focus when pressing tab key
                e.preventDefault();
            } else if (e.keyCode === 8) { // Backspace
                const selected_node = this.getSelectedNode();
                if (!selected_node || selected_node.classList.contains('search-input-tag-input')) {
                    const selection = document.getSelection();
                    if (!selection.anchorNode.isSameNode(selection.focusNode)) {
                        // Prevent removing the input placeholder tag
                        e.preventDefault();
                    }
                    // if end selection is at the beginning of the input, do the backspace_action
                    if (selection.anchorOffset === 0) {
                        if (this.options.backspace_action === 'remove') {
                            // Remove tag element before the selected_node
                            const prev_node = selected_node.previousSibling;
                            if (prev_node) {
                                prev_node.remove();
                                this.displayed_input.trigger('result_change');
                            }
                        } else if (this.options.backspace_action === 'edit') {
                            // Make the tag element before the selected_node editable
                            const prev_node = $(selected_node.previousSibling);
                            if (prev_node) {
                                this.makeTagEditable(prev_node);
                                e.preventDefault();
                            }
                        }
                    }
                }
            } else if (e.keyCode === 13) { // Enter
                e.preventDefault();
                const popover_list = input.parent().find('.search-input-popover ul');
                if (popover_list.length > 0) {
                    const active_item = popover_list.find('li.active');
                    if (active_item.length > 0) {
                        const active_tag_prefix = active_item.find('button.tag-prefix.active');
                        if (active_tag_prefix.length > 0) {
                            active_tag_prefix.click();
                        } else {
                            active_item.click();
                        }
                    } else {
                        this.tagifySelectedNode();
                    }
                } else {
                    this.tagifySelectedNode();
                }
            } else if (e.keyCode === 40) { // Down arrow
                const popover_list = input.parent().find('.search-input-popover ul');
                if (popover_list.length > 0) {
                    const active_item = popover_list.find('li.active');
                    if (active_item.length === 0) {
                        popover_list.find('li:first-of-type').addClass('active');
                    } else {
                        const next_item = active_item.next();
                        if (next_item.length > 0) {
                            active_item.removeClass('active');
                            next_item.addClass('active');
                        }
                    }
                    //deactivate all tag-prefix buttons in the popover
                    popover_list.find('button.tag-prefix').removeClass('active');
                }
            } else if (e.keyCode === 38) { // Up arrow
                const popover_list = input.parent().find('.search-input-popover ul');
                if (popover_list.length > 0) {
                    const active_item = popover_list.find('li.active');
                    if (active_item.length === 0) {
                        popover_list.find('li:last-of-type').addClass('active');
                    } else {
                        const prev_item = active_item.prev();
                        if (prev_item.length > 0) {
                            active_item.removeClass('active');
                            prev_item.addClass('active');
                        }
                    }
                    //deactivate all tag-prefix buttons in the popover
                    popover_list.find('button.tag-prefix').removeClass('active');
                }
            } else if (e.keyCode === 37) { // Left arrow
                const popover_list = input.parent().find('.search-input-popover ul');
                if (popover_list.length > 0) {
                    const active_item = popover_list.find('li.active');
                    if (active_item.length > 0) {
                        const active_tag_prefix = active_item.find('button.tag-prefix.active');
                        if (active_tag_prefix.length === 0) {
                            active_item.find('button.tag-prefix:last-of-type').addClass('active');
                        } else {
                            const prev_tag_prefix = active_tag_prefix.prev();
                            active_tag_prefix.removeClass('active');
                            if (prev_tag_prefix.length > 0) {
                                prev_tag_prefix.addClass('active');
                            }
                        }
                    }
                }
            } else if (e.keyCode === 39) { // Right arrow
                const popover_list = input.parent().find('.search-input-popover ul');
                if (popover_list.length > 0) {
                    const active_item = popover_list.find('li.active');
                    if (active_item.length > 0) {
                        const active_tag_prefix = active_item.find('button.tag-prefix.active');
                        if (active_tag_prefix.length === 0) {
                            active_item.find('button.tag-prefix:first-of-type').addClass('active');
                        } else {
                            const next_tag_prefix = active_tag_prefix.next();
                            active_tag_prefix.removeClass('active');
                            if (next_tag_prefix.length > 0) {
                                next_tag_prefix.addClass('active');
                            }
                        }
                    }
                }
            }
        });

        input.on('keypress', '.search-input-tag-input', (e) => {
            // Prevent default behavior of the enter key
            if (e.keyCode === 13) {
                e.preventDefault();
            }
        });

        input.on('keyup', 'search-input-tag-input', (e) => {
            if (e.keyCode === 9) { // Tab
                e.preventDefault();
                this.tagifySelectedNode();
            }
        });

        input.on('click', '.search-input-tag', (e) => {
            const tag = $(e.target).closest('.search-input-tag');
            this.makeTagEditable(tag);
        });

        input.on('click', '.search-input-tag i', (e) => {
            $(e.target).closest('.search-input-tag').remove();
            this.displayed_input.trigger('result_change');
        });

        input.on('result_change', (e) => {
            let text = this.getRawInput();

            const result = this.tokenizer.tokenize(text);
            const result_changed = JSON.stringify(result) !== JSON.stringify(this.last_result);
            if (this.options.on_result_change && result_changed) {
                this.options.on_result_change(e, result);
            }
            this.last_result = result;
        });
    }

    tagifySelectedNode() {
        const selected_node = $(this.getSelectedNode());
        if (selected_node && this.isSelectionUntagged()) {
            return this.tagifyInputNode(selected_node);
        }
        return null;
    }

    /**
    *
    * @param {SearchToken} token
    */
    tokenToTagHtml(token) {
        const tag_display = token.tag ? `<b>${token.exclusion ? this.tokenizer.EXCLUSION_PREFIX : ''}${token.prefix ? token.prefix : ''}${token.tag}</b>:` : '';
        let tag_color_override = null;
        if (this.tokenizer.options.custom_prefixes[token.prefix]) {
            tag_color_override = this.tokenizer.options.custom_prefixes[token.prefix].token_color || null;
        } else if (token.exclusion) {
            tag_color_override = '#80000080';
        }
        const dark_mode = $('html').css('--is-dark').trim() === 'true';
        const text_color = $(document.body).css('color');
        let style_overrides = '';
        if (!token.tag) {
            tag_color_override = text_color;
        }
        if (dark_mode) {
            tag_color_override = tag_color_override || '#b3b3b3';
            // Remove alpha from hex color
            if (tag_color_override.indexOf('#') === 0) {
                tag_color_override = tag_color_override.replace(/[^#]*#([0-9a-f]{6})([0-9a-f]{2})?/i, '#$1');
            }
            style_overrides = tag_color_override ? `style="border-color: ${tag_color_override} !important; background-color: unset !important;"` : '';
        } else {
            style_overrides = tag_color_override ? `style="background-color: ${tag_color_override} !important"` : '';
        }
        return `<span class="search-input-tag badge bg-secondary me-1" contenteditable="false" data-tag="${token.tag}" ${style_overrides}>
                  <span class="search-input-tag-value" contenteditable="false">${tag_display}${escapeMarkupText(token.term) || ''}</span>
                  <i class="ti ti-x cursor-pointer ms-1" title="${__('Delete')}" contenteditable="false"></i>
               </span>`;
    }

    tagifyInputNode(node) {
        const tokenized = this.tokenizer.tokenize(node.text());
        const tagged_tokens = tokenized.getTaggedTerms();
        const untagged_tokens = tokenized.getUntaggedTerms();

        let last_inserted = null;
        for (let i = 0; i < tagged_tokens.length; i++) {
            const t = tagged_tokens[i];
            last_inserted = $(this.tokenToTagHtml(t)).insertBefore(node);
            last_inserted.data('token', t);
            this.transformTagTermFromAutocomplete(last_inserted);
        }

        if (node.data('token') !== undefined && node.data('token').tag) {
            const untagged_text = tokenized.getFullPhrase();
            node.text(untagged_text);
        } else {
            for (let i = 0; i < untagged_tokens.length; i++) {
                const t = untagged_tokens[i];
                last_inserted = $(this.tokenToTagHtml(t)).insertBefore(node);
                last_inserted.data('token', t);
            }
            node.text('');
        }

        if (node.text().length === 0) {
            // if node is the last child of the container, empty it. Otherwise, remove the selected node
            if (node.is(':last-child')) {
                node.empty();
            } else {
                try {
                    node.remove();
                } catch (e) {
                    // node is already removed. In some cases, this can be attempted to be removed twice
                }
            }
            if (last_inserted) {
                this.displayed_input.find('.search-input-tag-input:last-of-type').focus();
                this.refreshPopover();
            }
        } else {
            // place cursor at end of the selected_node text
            this.placeCaretAtEndOfNode(node.get(0));
        }

        this.displayed_input.trigger('result_change');

        return last_inserted;
    }

    transformTagTermFromAutocomplete(node) {
        const tokenized = this.tokenizer.tokenize(node.text());
        const tagged_tokens = tokenized.getTaggedTerms();
        const last_token = tagged_tokens[tagged_tokens.length - 1];
        const autocomplete_info = this.tokenizer.getAutocomplete(last_token.tag);

        if (autocomplete_info) {
            autocomplete_info.forEach((t) => {
                const autocomplete_value = $(`<span>${t}</span>`).text();
                const term_text = $(`<span>${last_token.term}</span>`).text();
                if (autocomplete_value.localeCompare(term_text, undefined, { sensitivity: 'accent' }) === 0) {
                    last_token.term = t;
                    const replacement = $(this.tokenToTagHtml(last_token));
                    replacement.data('token', last_token);
                    node.replaceWith(replacement);
                }
            });
        }
    }

    makeTagEditable(tag) {
        if (tag && tag.hasClass('search-input-tag')) {
            tag.removeClass('search-input-tag');
            tag.addClass('search-input-tag-input');
            tag.attr('contenteditable', 'true');
            const token = tag.data('token');
            tag.empty();
            tag.text(token.raw);
            tag.focus();
            // place cursor at end of the tag text
            this.placeCaretAtEndOfNode(tag.get(0));
            // Refresh popover to get up to date suggestions
            this.refreshPopover();
            this.displayed_input.trigger('result_change');
        }
    }

    getSelectedNode() {
        const selection = document.getSelection();
        let result = null;
        if (selection) {
            result = selection.anchorNode;
            if (result && result.nodeType === Node.TEXT_NODE) {
                result = result.parentNode;
            }
        }
        return result || null;
    }

    isSelectionUntagged() {
        const node = this.getSelectedNode();
        return node !== null && node.classList.contains('search-input-tag-input');
    }

    placeCaretAfterNode(node) {
        if (!node || !node.parentNode) {
            return;
        }
        const nextSibling = node.nextSibling;
        const sel = document.getSelection();
        const range = sel.getRangeAt(0);

        if (sel.rangeCount) {
            range.setStartAfter(nextSibling || node);
            range.collapse(true);

            sel.removeAllRanges();
            sel.addRange(range);

            this.refreshPopover();
        }
    }

    placeCaretAtStartOfNode(node) {
        if (!node || !node.parentNode) {
            return;
        }
        const sel = document.getSelection();
        const range = sel.getRangeAt(0);

        if (sel.rangeCount) {
            range.setStart(node, 0);
            range.collapse(true);

            sel.removeAllRanges();
            sel.addRange(range);

            this.refreshPopover();
        }
    }

    placeCaretAtEndOfNode(node) {
        const selection = document.getSelection();
        const range = document.createRange();

        if (node.lastChild && node.lastChild.nodeType === Node.TEXT_NODE) {
            range.setStart(node.lastChild, node.lastChild.length);
        } else {
            range.setStart(node, node.childNodes.length);
        }
        selection.removeAllRanges();
        selection.addRange(range);

        this.refreshPopover();
    }

    placeCaretInDefaultInput() {
        const default_input = this.displayed_input.find('.search-input-tag-input:last-of-type');
        if (default_input.length > 0) {
            this.placeCaretAtStartOfNode(default_input.get(0));
        }
    }

    getRawInput() {
        let raw_input = '';
        this.displayed_input.find('.search-input-tag').each((i, node) => {
            const n = $(node);
            if (n.data('token') !== undefined) {
                raw_input += n.data('token').raw + ' ';
            }
        });
        return raw_input.trim();
    }

    refreshPopover() {
        const content = this.getPopoverContent();
        this.displayed_input.parent().find('.popover-body').html(content);
    }

    getPopoverContent() {
        const input = this.displayed_input;

        const selected = $(this.getSelectedNode());
        let last_token = null;

        if (this.isSelectionUntagged()) {
            if (selected.closest(input)) {
                const text = selected.text();
                const cursor_pos = document.getSelection().anchorOffset;

                const t = text.slice(0, cursor_pos);
                if (t.endsWith(' ')) {
                    return this.getTagsHelperContent();
                }
                const tokens = this.tokenizer.tokenize(t).tokens;
                const max = Math.max.apply(Math, tokens.map((token) => {
                    return token.position;
                }));
                last_token = tokens.find((token) => {
                    return token.position === max;
                });
            }
        }

        return (last_token && last_token.tag) ? this.getAutocompleteHelperContent(last_token.tag) : this.getTagsHelperContent();
    }

    getTagsHelperContent() {
        const tags = this.tokenizer.allowed_tags;
        const selected = $(this.getSelectedNode());
        let selected_text = (selected ? selected.text() : '').trim();

        const selected_phrases = selected_text.match(/(?:[^\s"]+|"[^"]*")+/g);
        selected_text = selected_phrases ? selected_phrases[selected_phrases.length - 1] : '';

        let helper = '';
        if (Object.keys(tags).length > 0) {
            helper += '<ul class="list-group tags-list">';
        }
        $.each(tags, (name, info) => {
            if ((this.options.filter_on_type && selected_text.length > 0) && !name.toLowerCase().startsWith(selected_text.toLowerCase())) {
                return; // continue
            }
            const description = info.description || '';
            let prefix_content = '';
            const prefix_count = Object.keys(info.supported_prefixes || {}).length;

            $.each(info.supported_prefixes, (i, prefix) => {
                const custom_prefix = this.tokenizer.options.custom_prefixes[prefix];
                let label = custom_prefix ? (custom_prefix.label || prefix) : prefix;
                if (prefix === this.tokenizer.EXCLUSION_PREFIX) {
                    label = __('Exclude');
                }
                prefix_content += `<button type="button" class="btn btn-outline-secondary btn-sm ${prefix_count > 1 ? 'ms-1' : ''} tag-prefix" title="${label}" data-prefix="${prefix}">${prefix}</button>`;
            });
            helper += `
            <li class="list-group-item list-group-item-action" style="cursor: pointer" data-tag="${name}">
                <div class="d-flex flex-grow-1 justify-content-between">
                   <b>${name}</b>
                   <span>${prefix_content}</span>
                </div>
                <div class="text-muted fst-italic">${description}</div>
            </li>
         `;
        });
        if (Object.keys(tags).length > 0) {
            helper += '</ul>';
        }
        return helper;
    }

    getAutocompleteHelperContent(tag_name) {
        tag_name = tag_name.toLowerCase();
        const tag = this.tokenizer.allowed_tags[tag_name];
        if (tag === undefined) {
            return null;
        }
        const selected = $(this.getSelectedNode());
        const selected_text = (selected ? selected.text() : '').trim();
        const tokens = this.tokenizer.tokenize(selected_text).getTaggedTerms();
        const current_term = (tokens.length > 0 ? tokens[0].term : '').trim();

        let helper = '';
        const autocomplete_values = this.tokenizer.getAutocomplete(tag_name);
        if (autocomplete_values.length > 0) {
            helper += `<ul class="list-group term-suggestions-list" data-tag="${tag_name}">`;
        } else {
            helper = `${tag_name.toLowerCase()}: ${tag.description}`;
        }
        $.each(autocomplete_values, (i, v) => {
            if ((this.options.filter_on_type && selected_text.length > 0) && !v.toLowerCase().startsWith(current_term.toLowerCase())) {
                return; // continue
            }
            helper += `<li class="list-group-item list-group-item-action" style="cursor: pointer">${v}</li>`;
        });
        if (autocomplete_values.length > 0) {
            helper += '</ul>';
        }
        return helper;
    }
}
