/* Copyright 2018 Tecnativa - Jairo Llopis
 * Copyright 2018 Tecnativa - Sergey Shebanin
 * License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl). */

odoo.define("web_responsive", function (require) {
    "use strict";

    const ActionManager = require("web.ActionManager");
    const AbstractWebClient = require("web.AbstractWebClient");
    const AppsMenu = require("web.AppsMenu");
    const BasicController = require("web.BasicController");
    const config = require("web.config");
    const core = require("web.core");
    const FormRenderer = require("web.FormRenderer");
    const Menu = require("web.Menu");
    const RelationalFields = require("web.relational_fields");
    const ListRenderer = require("web.ListRenderer");
    const CalendarRenderer = require("web.CalendarRenderer");
    const patchMixin = require("web.patchMixin");
    const AttachmentViewer = require("mail/static/src/components/attachment_viewer/attachment_viewer.js");
    const PatchableAttachmentViewer = patchMixin(AttachmentViewer);
    const ControlPanel = require("web.ControlPanel");
    const SearchPanel = require("web/static/src/js/views/search_panel.js");
    const {QWeb, Context} = owl;
    const {useState, useContext} = owl.hooks;

    /* Hide AppDrawer in desktop and mobile modes.
     * To avoid delays in pages with a lot of DOM nodes we make
     * sub-groups' with 'querySelector' to improve the performance.
     */
    function closeAppDrawer() {
        _.defer(function () {
            // Need close AppDrawer?
            var menu_apps_dropdown = document.querySelector(".o_menu_apps .dropdown");
            $(menu_apps_dropdown)
                .has(".dropdown-menu.show")
                .find("> a")
                .dropdown("toggle");
            // Need close Sections Menu?
            // TODO: Change to 'hide' in modern Bootstrap >4.1
            var menu_sections = document.querySelector(
                ".o_menu_sections li.show .dropdown-toggle"
            );
            $(menu_sections).dropdown("toggle");
            // Need close Mobile?
            var menu_sections_mobile = document.querySelector(".o_menu_sections.show");
            $(menu_sections_mobile).collapse("hide");
        });
    }

    /**
     * Reduce menu data to a searchable format understandable by fuzzy.js
     *
     * `AppsMenu.init()` gets `menuData` in a format similar to this (only
     * relevant data is shown):
     *
     * ```js
     * {
     *  [...],
     *  children: [
     *    // This is a menu entry:
     *    {
     *      action: "ir.actions.client,94", // Or `false`
     *      children: [... similar to above "children" key],
     *      name: "Actions",
     *      parent_id: [146, "Settings/Technical/Actions"], // Or `false`
     *    },
     *    ...
     *  ]
     * }
     * ```
     *
     * This format is very hard to process to search matches, and it would
     * slow down the search algorithm, so we reduce it with this method to be
     * able to later implement a simpler search.
     *
     * @param {Object} memo
     * Reference to current result object, passed on recursive calls.
     *
     * @param {Object} menu
     * A menu entry, as described above.
     *
     * @returns {Object}
     * Reduced object, without entries that have no action, and with a
     * format like this:
     *
     * ```js
     * {
     *  "Discuss": {Menu entry Object},
     *  "Settings": {Menu entry Object},
     *  "Settings/Technical/Actions/Actions": {Menu entry Object},
     *  ...
     * }
     * ```
     */
    function findNames(memo, menu) {
        if (menu.action) {
            var key = menu.parent_id ? menu.parent_id[1] + "/" : "";
            memo[key + menu.name] = menu;
        }
        if (menu.children.length) {
            _.reduce(menu.children, findNames, memo);
        }
        return memo;
    }

    AppsMenu.include({
        events: _.extend(
            {
                "keydown .search-input input": "_searchResultsNavigate",
                "input .search-input input": "_searchMenusSchedule",
                "click .o-menu-search-result": "_searchResultChosen",
                "shown.bs.dropdown": "_searchFocus",
                "hidden.bs.dropdown": "_searchReset",
                "hide.bs.dropdown": "_hideAppsMenu",
            },
            AppsMenu.prototype.events
        ),

        /**
         * Rescue some menu data stripped out in original method.
         *
         * @override
         */
        init: function (parent, menuData) {
            this._super.apply(this, arguments);
            // Keep base64 icon for main menus
            for (const n in this._apps) {
                this._apps[n].web_icon_data = menuData.children[n].web_icon_data;
            }
            // This is handy to be kept
            this.menuData = menuData;
            // Store menu data in a format searchable by fuzzy.js
            this._searchableMenus = _.reduce(menuData.children, findNames, {});
            // Search only after timeout, for fast typers
            this._search_def = false;
        },

        /**
         * @override
         */
        start: function () {
            this.$search_container = this.$(".search-container");
            this.$search_input = this.$(".search-input input");
            this.$search_results = this.$(".search-results");
            return this._super.apply(this, arguments);
        },

        /**
         * Prevent the menu from being opened twice
         *
         * @override
         */
        _onAppsMenuItemClicked: function (ev) {
            this._super.apply(this, arguments);
            ev.preventDefault();
            ev.stopPropagation();
        },

        /**
         * Get all info for a given menu.
         *
         * @param {String} key
         * Full path to requested menu.
         *
         * @returns {Object}
         * Menu definition, plus extra needed keys.
         */
        _menuInfo: function (key) {
            const original = this._searchableMenus[key];
            return _.extend(
                {
                    action_id: parseInt(original.action.split(",")[1], 10),
                },
                original
            );
        },

        /**
         * Autofocus on search field on big screens.
         */
        _searchFocus: function () {
            if (!config.device.isMobile) {
                // This timeout is necessary since the menu has a 100ms fading animation
                setTimeout(() => this.$search_input.focus(), 100);
            }
        },

        /**
         * Reset search input and results
         */
        _searchReset: function () {
            this.$search_container.removeClass("has-results");
            this.$search_results.empty();
            this.$search_input.val("");
        },

        /**
         * Schedule a search on current menu items.
         */
        _searchMenusSchedule: function () {
            this._search_def = new Promise((resolve) => {
                setTimeout(resolve, 50);
            });
            this._search_def.then(this._searchMenus.bind(this));
        },

        /**
         * Search among available menu items, and render that search.
         */
        _searchMenus: function () {
            const query = this.$search_input.val();
            if (query === "") {
                this.$search_container.removeClass("has-results");
                this.$search_results.empty();
                return;
            }
            var results = fuzzy.filter(query, _.keys(this._searchableMenus), {
                pre: "<b>",
                post: "</b>",
            });
            this.$search_container.toggleClass("has-results", Boolean(results.length));
            this.$search_results.html(
                core.qweb.render("web_responsive.MenuSearchResults", {
                    results: results,
                    widget: this,
                })
            );
        },

        /**
         * Filters which menu tree object contains a given subelement
         *
         * @param {Array|Object} menu containing the tree to search for
         * @param {Number} id The id to find
         * @returns {Object}
         */
        _isInMenuTree: function (menu, id) {
            let tree = menu;
            if (tree instanceof Object && tree.children) {
                tree = tree.children;
            }
            for (const node of tree) {
                if (node.id === id) {
                    return node;
                } else if (node.children.length === 0) {
                    continue;
                } else {
                    const root = this._isInMenuTree(node.children, id);
                    if (root) {
                        return root;
                    }
                }
            }
            return false;
        },

        /**
         * Use chooses a search result, so we navigate to that menu
         *
         * @param {jQuery.Event} event
         */
        _searchResultChosen: function (event) {
            event.preventDefault();
            event.stopPropagation();
            const $result = $(event.currentTarget);
            const data = $result.data();
            // Load the menu view
            this.trigger_up("menu_clicked", {
                action_id: data.actionId,
                id: data.menuId,
                previous_menu_id: data.parentId,
            });
            // Find app that owns the chosen menu
            const app = this.menuData.children.find((app_menu) => {
                if (app_menu.id == data.menuId) {
                    return true;
                }
                return this._isInMenuTree(app_menu, data.menuId);
            });
            if (!app) {
                return;
            }
            // Update navbar menus
            core.bus.trigger("change_menu_section", app.id);
        },

        /**
         * Navigate among search results
         *
         * @param {jQuery.Event} event
         */
        _searchResultsNavigate: function (event) {
            // Find current results and active element (1st by default)
            const all = this.$search_results.find(".o-menu-search-result"),
                pre_focused = all.filter(".active") || $(all[0]);
            let offset = all.index(pre_focused),
                key = event.key;
            // Keyboard navigation only supports search results
            if (!all.length) {
                return;
            }
            // Transform tab presses in arrow presses
            if (key === "Tab") {
                event.preventDefault();
                key = event.shiftKey ? "ArrowUp" : "ArrowDown";
            }
            switch (key) {
                // Pressing enter is the same as clicking on the active element
                case "Enter":
                    pre_focused.click();
                    break;
                // Navigate up or down
                case "ArrowUp":
                    offset--;
                    break;
                case "ArrowDown":
                    offset++;
                    break;
                default:
                    // Other keys are useless in this event
                    return;
            }
            // Allow looping on results
            if (offset < 0) {
                offset = all.length + offset;
            } else if (offset >= all.length) {
                offset -= all.length;
            }
            // Switch active element
            const new_focused = $(all[offset]);
            pre_focused.removeClass("active");
            new_focused.addClass("active");
            this.$search_results.scrollTo(new_focused, {
                offset: {
                    top: this.$search_results.height() * -0.5,
                },
            });
        },

        /*
         * Control if AppDrawer can be closed
         */
        _hideAppsMenu: function () {
            return !this.$("input").is(":focus");
        },
    });

    BasicController.include({
        /**
         * Close the AppDrawer if the data set is dirty and a discard dialog
         * is opened
         *
         * @override
         */
        canBeDiscarded: function (recordID) {
            if (this.model.isDirty(recordID || this.handle)) {
                closeAppDrawer();
            }
            return this._super.apply(this, arguments);
        },
    });

    Menu.include({
        events: _.extend(
            {
                // Clicking a hamburger menu item should close the hamburger
                "click .o_menu_sections [role=menuitem]": "_onClickMenuItem",
                // Opening any dropdown in the navbar should hide the hamburger
                "show.bs.dropdown .o_menu_systray, .o_menu_apps": "_hideMobileSubmenus",
            },
            Menu.prototype.events
        ),

        start: function () {
            this.$menu_toggle = this.$(".o-menu-toggle");
            return this._super.apply(this, arguments);
        },

        /**
         * Hide menus for current app if you're in mobile
         */
        _hideMobileSubmenus: function () {
            if (
                config.device.isMobile &&
                this.$menu_toggle.is(":visible") &&
                this.$section_placeholder.is(":visible")
            ) {
                this.$section_placeholder.collapse("hide");
            }
        },

        /**
         * Prevent hide the menu (should be closed when action is loaded)
         *
         * @param {ClickEvent} ev
         */
        _onClickMenuItem: function (ev) {
            ev.stopPropagation();
        },

        /**
         * No menu brand in mobiles
         *
         * @override
         */
        _updateMenuBrand: function () {
            if (!config.device.isMobile) {
                return this._super.apply(this, arguments);
            }
        },
    });

    RelationalFields.FieldStatus.include({
        /**
         * Fold all on mobiles.
         *
         * @override
         */
        _setState: function () {
            this._super.apply(this, arguments);
            if (config.device.isMobile) {
                _.map(this.status_information, (value) => {
                    value.fold = true;
                });
            }
        },
    });

    // Sticky Column Selector
    ListRenderer.include({
        _renderView: function () {
            const self = this;
            return this._super.apply(this, arguments).then(() => {
                const $col_selector = self.$el.find(
                    ".o_optional_columns_dropdown_toggle"
                );
                if ($col_selector.length !== 0) {
                    const $th = self.$el.find("thead>tr:first>th:last");
                    $col_selector.appendTo($th);
                }
            });
        },

        _onToggleOptionalColumnDropdown: function (ev) {
            // FIXME: For some strange reason the 'stopPropagation' call
            // in the main method don't work. Invoking here the same method
            // does the expected behavior... O_O!
            // This prevents the action of sorting the column from being
            // launched.
            ev.stopPropagation();
            this._super.apply(this, arguments);
        },
    });

    // Responsive view "action" buttons
    FormRenderer.include({
        /**
         * In mobiles, put all statusbar buttons in a dropdown.
         *
         * @override
         */
        _renderHeaderButtons: function () {
            const $buttons = this._super.apply(this, arguments);
            if (
                !config.device.isMobile ||
                $buttons.children("button:not(.o_invisible_modifier)").length <= 2
            ) {
                return $buttons;
            }

            // $buttons must be appended by JS because all events are bound
            const $dropdown = $(
                core.qweb.render("web_responsive.MenuStatusbarButtons")
            );
            $buttons.addClass("dropdown-menu").appendTo($dropdown);
            return $dropdown;
        },
    });

    CalendarRenderer.include({
        _getFullCalendarOptions: function () {
            var options = this._super.apply(this, arguments);
            if (config.device.isMobile) {
                options.views.dayGridMonth.columnHeaderFormat = "ddd";
            }
            return options;
        },
    });

    // Hide AppDrawer or Menu when the action has been completed
    ActionManager.include({
        /**
         * @override
         */
        _appendController: function () {
            this._super.apply(this, arguments);
            closeAppDrawer();
        },
    });

    /**
     * Use ALT+SHIFT instead of ALT as hotkey triggerer.
     *
     * HACK https://github.com/odoo/odoo/issues/30068 - See it to know why.
     *
     * Cannot patch in `KeyboardNavigationMixin` directly because it's a mixin,
     * not a `Class`, and altering a mixin's `prototype` doesn't alter it where
     * it has already been used.
     *
     * Instead, we provide an additional mixin to be used wherever you need to
     * enable this behavior.
     */
    var KeyboardNavigationShiftAltMixin = {
        /**
         * Alter the key event to require pressing Shift.
         *
         * This will produce a mocked event object where it will seem that
         * `Alt` is not pressed if `Shift` is not pressed.
         *
         * The reason for this is that original upstream code, found in
         * `KeyboardNavigationMixin` is very hardcoded against the `Alt` key,
         * so it is more maintainable to mock its input than to rewrite it
         * completely.
         *
         * @param {keyEvent} keyEvent
         * Original event object
         *
         * @returns {keyEvent}
         * Altered event object
         */
        _shiftPressed: function (keyEvent) {
            const alt = keyEvent.altKey || keyEvent.key === "Alt",
                newEvent = _.extend({}, keyEvent),
                shift = keyEvent.shiftKey || keyEvent.key === "Shift";
            // Mock event to make it seem like Alt is not pressed
            if (alt && !shift) {
                newEvent.altKey = false;
                if (newEvent.key === "Alt") {
                    newEvent.key = "Shift";
                }
            }
            return newEvent;
        },

        _onKeyDown: function (keyDownEvent) {
            return this._super(this._shiftPressed(keyDownEvent));
        },

        _onKeyUp: function (keyUpEvent) {
            return this._super(this._shiftPressed(keyUpEvent));
        },
    };

    // Include the SHIFT+ALT mixin wherever
    // `KeyboardNavigationMixin` is used upstream
    AbstractWebClient.include(KeyboardNavigationShiftAltMixin);

    // TODO: use default odoo device context when it will be realized
    const deviceContext = new Context({
        isMobile: config.device.isMobile,
        size_class: config.device.size_class,
        SIZES: config.device.SIZES,
    });
    window.addEventListener(
        "resize",
        owl.utils.debounce(() => {
            const state = deviceContext.state;
            if (state.isMobile !== config.device.isMobile) {
                state.isMobile = !state.isMobile;
            }
            if (state.size_class !== config.device.size_class) {
                state.size_class = config.device.size_class;
            }
        }, 15)
    );
    // Patch attachment viewer to add min/max buttons capability
    PatchableAttachmentViewer.patch("web_responsive.AttachmentViewer", (T) => {
        class AttachmentViewerPatchResponsive extends T {
            constructor() {
                super(...arguments);
                this.state = useState({
                    maximized: false,
                });
            }
            // Disable auto-close to allow to use form in edit mode.
            isCloseable() {
                return false;
            }
        }
        return AttachmentViewerPatchResponsive;
    });
    QWeb.components.AttachmentViewer = PatchableAttachmentViewer;

    // Patch control panel to add states for mobile quick search
    ControlPanel.patch("web_responsive.ControlPanelMobile", (T) => {
        class ControlPanelPatchResponsive extends T {
            constructor() {
                super(...arguments);
                this.state = useState({
                    mobileSearchMode: "",
                });
                this.device = useContext(deviceContext);
            }
        }
        return ControlPanelPatchResponsive;
    });
    // Patch search panel to add functionality for mobile view
    SearchPanel.patch("web_responsive.SearchPanelMobile", (T) => {
        class SearchPanelPatchResponsive extends T {
            constructor() {
                super(...arguments);
                this.state.mobileSearch = false;
                this.device = useContext(deviceContext);
            }
            getActiveSummary() {
                const selection = [];
                for (const filter of this.model.get("sections")) {
                    let filterValues = [];
                    if (filter.type === "category") {
                        if (filter.activeValueId) {
                            const parentIds = this._getAncestorValueIds(
                                filter,
                                filter.activeValueId
                            );
                            filterValues = [...parentIds, filter.activeValueId].map(
                                (valueId) => filter.values.get(valueId).display_name
                            );
                        }
                    } else {
                        let values = [];
                        if (filter.groups) {
                            values = Array.from(
                                filter.groups.values(),
                                (g) => g.values
                            ).flat();
                        }
                        if (filter.values) {
                            values = [...filter.values.values()];
                        }
                        filterValues = values
                            .filter((v) => v.checked)
                            .map((v) => v.display_name);
                    }
                    if (filterValues.length) {
                        selection.push({
                            values: filterValues,
                            icon: filter.icon,
                            color: filter.color,
                            type: filter.type,
                        });
                    }
                }
                return selection;
            }
        }
        return SearchPanelPatchResponsive;
    });
    return {
        deviceContext: deviceContext,
    };
});
