/*
 * Copyright (c) 2012, 2023, Oracle and/or its affiliates. All rights reserved.
 * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
 *
 * This code is free software; you can redistribute it and/or modify it
 * under the terms of the GNU General Public License version 2 only, as
 * published by the Free Software Foundation.  Oracle designates this
 * particular file as subject to the "Classpath" exception as provided
 * by Oracle in the LICENSE file that accompanied this code.
 *
 * This code 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
 * version 2 for more details (a copy is included in the LICENSE file that
 * accompanied this code).
 *
 * You should have received a copy of the GNU General Public License version
 * 2 along with this work; if not, write to the Free Software Foundation,
 * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
 *
 * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
 * or visit www.oracle.com if you need additional information or have any
 * questions.
 */

package com.sun.javafx.tk.quantum;

import com.sun.javafx.menu.CheckMenuItemBase;
import com.sun.javafx.menu.MenuBase;
import com.sun.javafx.menu.MenuItemBase;
import com.sun.javafx.menu.RadioMenuItemBase;
import com.sun.javafx.menu.SeparatorMenuItemBase;
import com.sun.javafx.PlatformUtil;
import com.sun.javafx.tk.TKSystemMenu;
import com.sun.glass.events.KeyEvent;
import com.sun.glass.ui.Application;
import com.sun.glass.ui.Menu;
import com.sun.glass.ui.MenuBar;
import com.sun.glass.ui.MenuItem;
import com.sun.glass.ui.Pixels;
import com.sun.javafx.tk.Toolkit;

import java.util.HashMap;
import java.util.Map;
import java.util.List;

import javafx.collections.ListChangeListener;
import javafx.collections.ObservableList;
import javafx.beans.InvalidationListener;
import javafx.beans.property.BooleanProperty;
import javafx.beans.property.SimpleBooleanProperty;
import javafx.collections.transformation.FilteredList;
import javafx.scene.image.Image;
import javafx.scene.image.ImageView;
import javafx.scene.input.KeyCode;
import javafx.scene.input.KeyCombination;
import javafx.scene.input.KeyCharacterCombination;
import javafx.scene.input.KeyCodeCombination;

class GlassSystemMenu implements TKSystemMenu {

    private List<MenuBase>      systemMenus = null;
    private MenuBar             glassSystemMenuBar = null;
    private final Map<Menu, ListChangeListener<MenuItemBase>> menuListeners = new HashMap<>();
    private final Map<ListChangeListener<MenuItemBase>, ObservableList<MenuItemBase>> listenerItems = new HashMap<>();
    private BooleanProperty active;

    private InvalidationListener visibilityListener = valueModel -> {
        if (systemMenus != null) {
            setMenus(systemMenus);
        }
    };

    protected void createMenuBar() {
        if (glassSystemMenuBar == null) {
            Application app = Application.GetApplication();
            glassSystemMenuBar = app.createMenuBar();
            app.installDefaultMenus(glassSystemMenuBar);

            if (systemMenus != null) {
                setMenus(systemMenus);
            }
        }
    }

    protected MenuBar getMenuBar() {
        return glassSystemMenuBar;
    }

    @Override public boolean isSupported() {
        return Application.GetApplication().supportsSystemMenu();
    }

    @Override public void setMenus(List<MenuBase> menus) {
        if (active != null) {
            active.set(false);
        }
        active = new SimpleBooleanProperty(true);
        systemMenus = menus;
        if (glassSystemMenuBar != null) {

            /*
             * Remove existing menus
             */
            List<Menu> existingMenus = glassSystemMenuBar.getMenus();
            int existingSize = existingMenus.size();

            /*
             * Leave the Apple menu in place
             */
            for (int index = existingSize - 1; index >= 1; index--) {
                Menu menu = existingMenus.get(index);
                clearMenu(menu);
                glassSystemMenuBar.remove(index);
            }

            for (MenuBase menu : menus) {
                addMenu(null, menu);
            }
        }
    }

    // Clear the menu to prevent a memory leak, as outlined in RT-34779
    private void clearMenu(Menu menu) {
        ListChangeListener<MenuItemBase> lcl = menuListeners.get(menu);
        if (lcl != null) {
            ObservableList<MenuItemBase> target = listenerItems.get(lcl);
            target.removeListener(lcl);
            menuListeners.remove(menu);
            listenerItems.remove(lcl);
        }

        for (int i = menu.getItems().size() - 1; i >= 0; i--) {
            Object o = menu.getItems().get(i);
            if (o instanceof MenuItem) {
                ((MenuItem)o).setCallback(null);
                menu.remove(i);
            } else if (o instanceof Menu) {
                clearMenu((Menu) o);
            }
        }
        menu.setEventHandler(null);
    }

    private void addMenu(final Menu parent, final MenuBase mb) {
        if (parent != null) {
            insertMenu(parent, mb, parent.getItems().size());
        } else {
            insertMenu(parent, mb, glassSystemMenuBar.getMenus().size());
        }
    }

    private void insertMenu(final Menu parent, final MenuBase mb, int pos) {
        Application app = Application.GetApplication();
        final Menu glassMenu = app.createMenu(parseText(mb), ! mb.isDisable());
        glassMenu.setEventHandler(new GlassMenuEventHandler(mb));

        // There is no way of knowing if listener was already added.
        mb.visibleProperty().removeListener(visibilityListener);
        mb.visibleProperty().addListener(visibilityListener);

        if (!mb.isVisible()) {
            return;
        }

        final ObservableList<MenuItemBase> items = mb.getItemsBase();

        final FilteredList<MenuItemBase> filteredItems = items.filtered(x -> x.isVisible());

        ListChangeListener<MenuItemBase> menuItemListener = createListener(glassMenu);
        filteredItems.addListener(menuItemListener);
        menuListeners.put(glassMenu, menuItemListener);
        listenerItems.put(menuItemListener, filteredItems);

        for (MenuItemBase item : items) {
            if (item instanceof MenuBase baseItem) {
                // submenu
                addMenu(glassMenu, baseItem);
            } else {
                // menu item
                addMenuItem(glassMenu, item);
            }
        }
        glassMenu.setPixels(getPixels(mb));

        setMenuBindings(glassMenu, mb);

        if (parent != null) {
            parent.insert(glassMenu, pos);
        } else {
            glassSystemMenuBar.insert(glassMenu, pos);
        }
    }

    private ListChangeListener<MenuItemBase> createListener(final Menu glassMenu) {
        return (ListChangeListener.Change<? extends MenuItemBase> change) -> {
            while (change.next()) {
                int from = change.getFrom();
                int to = change.getTo();
                List<? extends MenuItemBase> removed = change.getRemoved();

                for (int i = from + removed.size() - 1; i >= from ; i--) {
                    List<Object> menuItemList = glassMenu.getItems();
                    if (i >= 0 && menuItemList.size() > i) {
                        Object item = menuItemList.get(i);
                        if (item instanceof Menu menu) clearMenu(menu);
                        glassMenu.remove(i);
                    }
                }
                for (int i = from; i < to; i++) {
                    MenuItemBase item = change.getList().get(i);
                    if (item instanceof MenuBase) {
                        insertMenu(glassMenu, (MenuBase)item, i);
                    } else {
                        insertMenuItem(glassMenu, item, i);
                    }
                }
            }
        };
    }


    protected void setMenuBindings(final Menu glassMenu, final MenuBase mb) {
        mb.textProperty().when(active).subscribe(valueModel -> glassMenu.setTitle(parseText(mb)));
        mb.disableProperty().when(active).subscribe(valueModel -> glassMenu.setEnabled(!mb.isDisable()));
        mb.mnemonicParsingProperty().when(active).subscribe(valueModel -> glassMenu.setTitle(parseText(mb)));
    }

    private void addMenuItem(Menu parent, final MenuItemBase menuitem) {
        insertMenuItem(parent, menuitem, parent.getItems().size());
    }

    private void insertMenuItem(final Menu parent, final MenuItemBase menuitem, int pos) {
        Application app = Application.GetApplication();

        // There is no way of knowing if listener was already added.
        menuitem.visibleProperty().removeListener(visibilityListener);
        menuitem.visibleProperty().addListener(visibilityListener);

        if (!menuitem.isVisible()) {
            return;
        }

        if (menuitem instanceof SeparatorMenuItemBase) {
            if (menuitem.isVisible()) {
                parent.insert(MenuItem.Separator, pos);
            }
        } else {
            MenuItem.Callback callback = new MenuItem.Callback() {
                @Override public void action() {
                    // toggle state of check or radio items (from ContextMenuContent.java)
                    if (menuitem instanceof CheckMenuItemBase) {
                        CheckMenuItemBase checkItem = (CheckMenuItemBase)menuitem;
                        checkItem.setSelected(!checkItem.isSelected());
                    } else if (menuitem instanceof RadioMenuItemBase) {
                        // this is a radio button. If there is a toggleGroup specified, we
                        // simply set selected to true. If no toggleGroup is specified, we
                        // toggle the selected state, as there is no assumption of mutual
                        // exclusivity when no toggleGroup is set.
                        RadioMenuItemBase radioItem = (RadioMenuItemBase)menuitem;
                        // Note: The ToggleGroup is not exposed for RadioMenuItemBase,
                        // so we just assume that one has been set at this point.
                        //radioItem.setSelected(radioItem.getToggleGroup() != null ? true : !radioItem.isSelected());
                        radioItem.setSelected(true);
                    }
                    menuitem.fire();
                }
                @Override public void validate() {
                    Menu.EventHandler     meh  = parent.getEventHandler();
                    GlassMenuEventHandler gmeh = (GlassMenuEventHandler)meh;

                    if (gmeh.isMenuOpen()) {
                        return;
                    }
                    menuitem.fireValidation();
                }
            };

            final MenuItem glassSubMenuItem = app.createMenuItem(parseText(menuitem), callback);

            menuitem.textProperty().addListener(valueModel -> glassSubMenuItem.setTitle(parseText(menuitem)));

            glassSubMenuItem.setPixels(getPixels(menuitem));
            menuitem.graphicProperty().addListener(valueModel -> {
                glassSubMenuItem.setPixels(getPixels(menuitem));
            });

            glassSubMenuItem.setEnabled(!menuitem.isDisable());
            menuitem.disableProperty().addListener(valueModel -> glassSubMenuItem.setEnabled(!menuitem.isDisable()));

            setShortcut(glassSubMenuItem, menuitem);
            menuitem.acceleratorProperty().addListener(valueModel -> setShortcut(glassSubMenuItem, menuitem));

            menuitem.mnemonicParsingProperty().addListener(valueModel -> glassSubMenuItem.setTitle(parseText(menuitem)));

            if (menuitem instanceof CheckMenuItemBase) {
                final CheckMenuItemBase checkItem = (CheckMenuItemBase) menuitem;
                glassSubMenuItem.setChecked(checkItem.isSelected());
                checkItem.selectedProperty().addListener(valueModel -> glassSubMenuItem.setChecked(checkItem.isSelected()));
            } else if (menuitem instanceof RadioMenuItemBase) {
                final RadioMenuItemBase radioItem = (RadioMenuItemBase) menuitem;
                glassSubMenuItem.setChecked(radioItem.isSelected());
                radioItem.selectedProperty().addListener(valueModel -> glassSubMenuItem.setChecked(radioItem.isSelected()));
            }

            parent.insert(glassSubMenuItem, pos);
        }
    }

    private String parseText(MenuItemBase menuItem) {
        String text = menuItem.getText();
        if (text == null) {
            // don't pass null strings to Glass
            return "";
        } else if (!text.isEmpty() && menuItem.isMnemonicParsing()) {
            // \ufffc is a placeholder character
            //return text.replace("__", "\ufffc").replace("_", "").replace("\ufffc", "_");
            return text.replaceFirst("_([^_])", "$1");
        } else {
            return text;
        }
    }

    private Pixels getPixels(MenuItemBase menuItem) {
        if (menuItem.getGraphic() instanceof ImageView) {
            ImageView iv = (ImageView)menuItem.getGraphic();
            Image     im = iv.getImage();
            if (im == null) return null;

            String    url          = im.getUrl();

            if (url == null || PixelUtils.supportedFormatType(url)) {
                com.sun.prism.Image pi = (com.sun.prism.Image) Toolkit.getImageAccessor().getPlatformImage(im);

                return pi == null ? null : PixelUtils.imageToPixels(pi);
            }
        }
        return (null);
    }

    private void setShortcut(MenuItem glassSubMenuItem, MenuItemBase menuItem) {
        final KeyCombination accelerator = menuItem.getAccelerator();
        if (accelerator == null) {
            glassSubMenuItem.setShortcut(0, 0);
        } else if (accelerator instanceof KeyCodeCombination) {
            KeyCodeCombination kcc  = (KeyCodeCombination)accelerator;
            KeyCode            code = kcc.getCode();
            assert PlatformUtil.isMac() || PlatformUtil.isLinux();
            int modifier = glassModifiers(kcc);
            if (PlatformUtil.isMac()) {
                int finalCode = code.isLetterKey() ? code.getChar().toUpperCase().charAt(0)
                        : code.getCode();
                glassSubMenuItem.setShortcut(finalCode, modifier);
            } else if (PlatformUtil.isLinux()) {
                String lower = code.getChar().toLowerCase();
                if ((modifier & KeyEvent.MODIFIER_CONTROL) != 0) {
                    glassSubMenuItem.setShortcut(lower.charAt(0), modifier);
                } else {
                    glassSubMenuItem.setShortcut(0, 0);
                }
            } else {
                glassSubMenuItem.setShortcut(0, 0);
            }
        } else if (accelerator instanceof KeyCharacterCombination) {
            KeyCharacterCombination kcc = (KeyCharacterCombination)accelerator;
            String kchar = kcc.getCharacter();
            glassSubMenuItem.setShortcut(kchar.charAt(0), glassModifiers(kcc));
        }
    }

    private int glassModifiers(KeyCombination kcc) {
        int ret = 0;
        if (kcc.getShift() == KeyCombination.ModifierValue.DOWN) {
            ret += KeyEvent.MODIFIER_SHIFT;
        }
        if (kcc.getControl() == KeyCombination.ModifierValue.DOWN) {
            ret += KeyEvent.MODIFIER_CONTROL;
        }
        if (kcc.getAlt() == KeyCombination.ModifierValue.DOWN) {
            ret += KeyEvent.MODIFIER_ALT;
        }
        if (kcc.getShortcut() == KeyCombination.ModifierValue.DOWN) {
            if (PlatformUtil.isLinux()) {
                ret += KeyEvent.MODIFIER_CONTROL;
            } else if (PlatformUtil.isMac()) {
                ret += KeyEvent.MODIFIER_COMMAND;
            }
        }
        if (kcc.getMeta() == KeyCombination.ModifierValue.DOWN) {
            if (PlatformUtil.isLinux()) {
                ret += KeyEvent.MODIFIER_WINDOWS;   // RT-19326 - Linux shortcut support
            } else if (PlatformUtil.isMac()) {
                ret += KeyEvent.MODIFIER_COMMAND;
            }
        }

        if (kcc instanceof KeyCodeCombination) {
            KeyCode kcode = ((KeyCodeCombination)kcc).getCode();
            int     code  = kcode.getCode();

            if (((code >= KeyCode.F1.getCode())  && (code <= KeyCode.F12.getCode())) ||
                ((code >= KeyCode.F13.getCode()) && (code <= KeyCode.F24.getCode()))) {
                ret += KeyEvent.MODIFIER_FUNCTION;
            }
        }

        return (ret);
    }

}
