//License: GPL. Copyright 2007 by Immanuel Scholz and others
package org.openstreetmap.josm.tools;

import static org.openstreetmap.josm.tools.I18n.tr;

import java.awt.event.KeyEvent;
import java.util.Collection;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.Map;

import javax.swing.AbstractButton;
import javax.swing.JMenu;
import javax.swing.JOptionPane;
import javax.swing.KeyStroke;

import org.openstreetmap.josm.Main;

/**
 * Global shortcut class.
 *
 * Note: This class represents a single shortcut, contains the factory to obtain
 *       shortcut objects from, manages shortcuts and shortcut collisions, and
 *       finally manages loading and saving shortcuts to/from the preferences.
 *
 * Action authors: You only need the registerShortcut() factory. Ignore everything
 *                 else.
 *
 * All: Use only public methods that are also marked to be used. The others are
 *      public so the shortcut preferences can use them.
 *
 */
public class Shortcut {
    //  public static final int SHIFT = KeyEvent.SHIFT_DOWN_MASK;
    //  public static final int CTRL = KeyEvent.CTRL_DOWN_MASK;
    //  public static final int SHIFT_CTRL = KeyEvent.SHIFT_DOWN_MASK|KeyEvent.CTRL_DOWN_MASK;
    public static final int SHIFT_DEFAULT = 1;
    private String shortText;        // the unique ID of the shortcut
    private String longText;         // a human readable description that will be shown in the preferences
    private int requestedKey;        // the key, the caller requested
    private int requestedGroup;      // the group, the caller requested
    private int assignedKey;         // the key that actually is used
    private int assignedModifier;    // the modifiers that are used
    private boolean assignedDefault; // true if it got assigned what was requested. (Note: modifiers will be ignored in favour of group when loading it from the preferences then.)
    private boolean assignedUser;    // true if the user changed this shortcut
    private boolean automatic;       // true if the user cannot change this shortcut (Note: it also will not be saved into the preferences)
    private boolean reset;           // true if the user requested this shortcut to be set to its default value (will happen on next restart, as this shortcut will not be saved to the preferences)

    // simple constructor
    private Shortcut(String shortText, String longText, int requestedKey, int requestedGroup, int assignedKey, int assignedModifier, boolean assignedDefault, boolean assignedUser) {
        this.shortText = shortText;
        this.longText = longText;
        this.requestedKey = requestedKey;
        this.requestedGroup = requestedGroup;
        this.assignedKey = assignedKey;
        this.assignedModifier = assignedModifier;
        this.assignedDefault = assignedDefault;
        this.assignedUser = assignedUser;
        this.automatic = false;
        this.reset = false;
    }

    public String getShortText() {
        return shortText;
    }

    public String getLongText() {
        return longText;
    }

    // a shortcut will be renamed when it is handed out again, because the original name
    // may be a dummy
    private void setLongText(String longText) {
        this.longText = longText;
    }

    private int getRequestedKey() {
        return requestedKey;
    }

    public int getRequestedGroup() {
        return requestedGroup;
    }

    public int getAssignedKey() {
        return assignedKey;
    }

    public int getAssignedModifier() {
        return assignedModifier;
    }

    public boolean getAssignedDefault() {
        return assignedDefault;
    }

    public boolean getAssignedUser() {
        return assignedUser;
    }

    public boolean getAutomatic() {
        return automatic;
    }

    public boolean isChangeable() {
        return !automatic && !shortText.equals("core:none");
    }

    private boolean getReset() {
        return reset;
    }

    /**
     * FOR PREF PANE ONLY
     */
    public void setAutomatic() {
        automatic = true;
    }

    /**
     * FOR PREF PANE ONLY
     */
    public void setAssignedModifier(int assignedModifier) {
        this.assignedModifier = assignedModifier;
    }

    /**
     * FOR PREF PANE ONLY
     */
    public void setAssignedKey(int assignedKey) {
        this.assignedKey = assignedKey;
    }

    /**
     * FOR PREF PANE ONLY
     */
    public void setAssignedUser(boolean assignedUser) {
        this.reset = (!this.assignedUser && assignedUser);
        if (assignedUser) {
            assignedDefault = false;
        }
        this.assignedUser = assignedUser;
    }

    /**
     * Use this to register the shortcut with Swing
     */
    public KeyStroke getKeyStroke() {
        if (assignedModifier != -1)
            return KeyStroke.getKeyStroke(assignedKey, assignedModifier);
        return null;
    }

    private boolean isSame(int isKey, int isModifier) {
        // -1 --- an unassigned shortcut is different from any other shortcut
        return( isKey == assignedKey && isModifier == assignedModifier && assignedModifier != groups.get(GROUP_NONE));
    }

    // create a shortcut object from an string as saved in the preferences
    private Shortcut(String prefString) {
        String[] s = prefString.split(";");
        this.shortText = s[0];
        this.longText = s[1];
        this.requestedKey = Integer.parseInt(s[2]);
        this.requestedGroup = Integer.parseInt(s[3]);
        this.assignedKey = Integer.parseInt(s[4]);
        this.assignedModifier = Integer.parseInt(s[5]);
        this.assignedDefault = Boolean.parseBoolean(s[6]);
        this.assignedUser = Boolean.parseBoolean(s[7]);
    }

    // get a string that can be put into the preferences
    private String asPrefString() {
        return shortText + ";" + longText + ";" + requestedKey + ";" + requestedGroup + ";" + assignedKey + ";" + assignedModifier + ";" + assignedDefault + ";" + assignedUser;
    }

    private boolean isSame(Shortcut other) {
        return assignedKey == other.assignedKey && assignedModifier == other.assignedModifier;
    }

    /**
     * use this to set a menu's mnemonic
     */
    public void setMnemonic(JMenu menu) {
        if (requestedGroup == GROUP_MNEMONIC && assignedModifier == groups.get(requestedGroup + GROUPS_DEFAULT) && getKeyStroke() != null && KeyEvent.getKeyText(assignedKey).length() == 1) {
            menu.setMnemonic(KeyEvent.getKeyText(assignedKey).charAt(0)); //getKeyStroke().getKeyChar() seems not to work here
        }
    }
    /**
     * use this to set a buttons's mnemonic
     */
    public void setMnemonic(AbstractButton button) {
        if (requestedGroup == GROUP_MNEMONIC && assignedModifier == groups.get(requestedGroup + GROUPS_DEFAULT) && getKeyStroke() != null && KeyEvent.getKeyText(assignedKey).length() == 1) {
            button.setMnemonic(KeyEvent.getKeyText(assignedKey).charAt(0)); //getKeyStroke().getKeyChar() seems not to work here
        }
    }

    /**
     * use this to get a human readable text for your shortcut
     */
    public String getKeyText() {
        KeyStroke keyStroke = getKeyStroke();
        if (keyStroke == null) return "";
        String modifText = KeyEvent.getKeyModifiersText(keyStroke.getModifiers());
        if ("".equals (modifText)) return KeyEvent.getKeyText (keyStroke.getKeyCode ());
        return modifText + "+" + KeyEvent.getKeyText(keyStroke.getKeyCode ());
    }

    ///////////////////////////////
    // everything's static below //
    ///////////////////////////////

    // here we store our shortcuts
    private static Map<String, Shortcut> shortcuts = new LinkedHashMap<String, Shortcut>();

    // and here our modifier groups
    private static Map<Integer, Integer> groups = new HashMap<Integer, Integer>();

    // check if something collides with an existing shortcut
    private static Shortcut findShortcut(int requestedKey, int modifier) {
        if (modifier == groups.get(GROUP_NONE))
            return null;
        for (Shortcut sc : shortcuts.values()) {
            if (sc.isSame(requestedKey, modifier))
                return sc;
        }
        return null;
    }

    /**
     * FOR PREF PANE ONLY
     */
    public static Collection<Shortcut> listAll() {
        return shortcuts.values();
    }

    // try to find an unused shortcut
    private static Shortcut findRandomShortcut(String shortText, String longText, int requestedKey, int requestedGroup) {
        int[] mods = {groups.get(requestedGroup + GROUPS_DEFAULT), groups.get(requestedGroup + GROUPS_ALT1), groups.get(requestedGroup + GROUPS_ALT2)};
        for (int m : mods) {
            for (int k = KeyEvent.VK_A; k < KeyEvent.VK_Z; k++) { // we'll limit ourself to 100% safe keys
                if ( findShortcut(k, m) == null )
                    return new Shortcut(shortText, longText, requestedKey, requestedGroup, k, m, false, false);
            }
        }
        return new Shortcut(shortText, longText, requestedKey, requestedGroup, requestedKey, groups.get(GROUP_NONE), false, false);
    }

    // use these constants to request shortcuts
    public static final int GROUP_NONE = 0;          // no shortcut
    public static final int GROUP_HOTKEY = 1;        // a button action, will use another modifier than MENU on system with a meta key
    public static final int GROUP_MENU = 2;          // a menu action, e.g. "ctrl-e"/"cmd-e" (export)
    public static final int GROUP_EDIT = 3;          // direct edit key, e.g. "a" (add)
    public static final int GROUP_LAYER = 4;         // toggle one of the right-hand-side windows, e.g. "alt-l" (layers)
    public static final int GROUP_DIRECT = 5;        // for non-letter keys, preferable without modifier, e.g. F5
    public static final int GROUP_MNEMONIC = 6;      // for use with Menu.setMnemonic() only!
    public static final int GROUP__MAX = 7;
    public static final int GROUP_RESERVED = 1000;
    public static final int GROUPS_DEFAULT = 0;
    public static final int GROUPS_ALT1 = GROUP__MAX;
    public static final int GROUPS_ALT2 = GROUP__MAX * 2;

    // bootstrap
    private static boolean initdone = false;
    private static void doInit() {
        if (initdone) return;
        initdone = true;
        // if we have no modifier groups in the config, we have to create them
        if (Main.pref.get("shortcut.groups.configured", null) == null) {
            Main.platform.initShortcutGroups();
            Main.pref.put("shortcut.groups.configured", true);
        }
        // pull in the groups
        for (int i = GROUP_NONE; i < GROUP__MAX+GROUPS_ALT2*2; i++) { // fill more groups, so registering with e.g. ALT2+MNEMONIC won't NPE
            groups.put(new Integer(i), new Integer(Main.pref.getInteger("shortcut.groups."+i, -1)));
        }
        // (1) System reserved shortcuts
        Main.platform.initSystemShortcuts();
        // (2) User defined shortcuts
        int i = 0;
        String p = Main.pref.get("shortcut.shortcut."+i, null);
        while (p != null) {
            Shortcut sc = new Shortcut(p);
            if (sc.getAssignedUser()) {
                registerShortcut(sc);
            }
            i++;
            p = Main.pref.get("shortcut.shortcut."+i, null);
        }
        // Shortcuts at their default values
        i = 0;
        p = Main.pref.get("shortcut.shortcut."+i, null);
        while (p != null) {
            Shortcut sc = new Shortcut(p);
            if (!sc.getAssignedUser() && sc.getAssignedDefault()) {
                registerShortcut(sc);
            }
            i++;
            p = Main.pref.get("shortcut.shortcut."+i, null);
        }
        // Shortcuts that were automatically moved
        i = 0;
        p = Main.pref.get("shortcut.shortcut."+i, null);
        while (p != null) {
            Shortcut sc = new Shortcut(p);
            if (!sc.getAssignedUser() && !sc.getAssignedDefault()) {
                registerShortcut(sc);
            }
            i++;
            p = Main.pref.get("shortcut.shortcut."+i, null);
        }
    }

    // shutdown handling
    public static void savePrefs() {
        //      we save this directly from the preferences pane, so don't overwrite these values here
        //      for (int i = GROUP_NONE; i < GROUP__MAX+GROUPS_ALT2; i++) {
        //      Main.pref.put("shortcut.groups."+i, Groups.get(i).toString());
        //      }
        int i = 0;
        for (Shortcut sc : shortcuts.values()) {
            //          TODO: Remove sc.getAssignedUser() when we fixed all internal conflicts
            if (!sc.getAutomatic() && !sc.getReset() && sc.getAssignedUser()) {
                Main.pref.put("shortcut.shortcut."+i, sc.asPrefString());
                i++;
            }
        }
        Main.pref.put("shortcut.shortcut."+i, "");
    }

    // this is used to register a shortcut that was read from the preferences
    private static void registerShortcut(Shortcut sc) {
        // put a user configured shortcut in as-is -- unless there's a conflict
        if(sc.getAssignedUser() && findShortcut(sc.getAssignedKey(),
                sc.getAssignedModifier()) == null) {
            shortcuts.put(sc.getShortText(), sc);
        } else {
            registerShortcut(sc.getShortText(), sc.getLongText(), sc.getRequestedKey(),
                    sc.getRequestedGroup(), sc.getAssignedModifier(), sc);
        }
    }

    /**
     * FOR PLATFORMHOOK USE ONLY
     *
     * This registers a system shortcut. See PlatformHook for details.
     */
    public static Shortcut registerSystemShortcut(String shortText, String longText, int key, int modifier) {
        if (shortcuts.containsKey(shortText))
            return shortcuts.get(shortText);
        Shortcut potentialShortcut = findShortcut(key, modifier);
        if (potentialShortcut != null) {
            // this always is a logic error in the hook
            System.err.println("CONFLICT WITH SYSTEM KEY "+shortText);
            return null;
        }
        potentialShortcut = new Shortcut(shortText, longText, key, GROUP_RESERVED, key, modifier, true, false);
        shortcuts.put(shortText, potentialShortcut);
        return potentialShortcut;
    }

    /**
     * Register a shortcut.
     *
     * Here you get your shortcuts from. The parameters are:
     *
     * shortText - an ID. re-use a "system:*" ID if possible, else use something unique.
     *             "menu:*" is reserved for menu mnemonics, "core:*" is reserved for
     *             actions that are part of JOSM's core. Use something like
     *             <pluginname>+":"+<actionname>
     * longText - this will be displayed in the shortcut preferences dialog. Better
     *            use soomething the user will recognize...
     * requestedKey - the key you'd prefer. Use a KeyEvent.VK_* constant here.
     * requestedGroup - the group this shortcut fits best. This will determine the
     *                  modifiers your shortcut will get assigned. Use the GROUP_*
     *                  constants defined above.
     */
    public static Shortcut registerShortcut(String shortText, String longText, int requestedKey, int requestedGroup, int modifier) {
        return registerShortcut(shortText, longText, requestedKey, requestedGroup, modifier, null);
    }
    public static Shortcut registerShortcut(String shortText, String longText, int requestedKey, int requestedGroup) {
        return registerShortcut(shortText, longText, requestedKey, requestedGroup, null, null);
    }

    // and now the workhorse. same parameters as above, just one more: if originalShortcut is not null and
    // is different from the shortcut that will be assigned, a popup warning will be displayed to the user.
    // This is used when registering shortcuts that have been visible to the user before (read: have been
    // read from the preferences file). New shortcuts will never warn, even when they land on some funny
    // random fallback key like Ctrl+Alt+Shift+Z for "File Open..." <g>
    private static Shortcut registerShortcut(String shortText, String longText, int requestedKey, int requestedGroup, Integer modifier,
            Shortcut originalShortcut) {
        doInit();
        if (shortcuts.containsKey(shortText)) { // a re-register? maybe a sc already read from the preferences?
            Shortcut sc = shortcuts.get(shortText);
            sc.setLongText(longText); // or set by the platformHook, in this case the original longText doesn't match the real action
            return sc;
        }
        Integer defaultModifier = groups.get(requestedGroup + GROUPS_DEFAULT);
        if(modifier != null) {
            if(modifier == SHIFT_DEFAULT) {
                defaultModifier |= KeyEvent.SHIFT_DOWN_MASK;
            } else {
                defaultModifier = modifier;
            }
        }
        else if (defaultModifier == null) { // garbage in, no shortcut out
            defaultModifier = groups.get(GROUP_NONE + GROUPS_DEFAULT);
        }
        Shortcut conflictsWith = null;
        Shortcut potentialShortcut = findShortcut(requestedKey, defaultModifier);
        if (potentialShortcut != null) { // 3 stage conflict handling
            conflictsWith = potentialShortcut;
            defaultModifier = groups.get(requestedGroup + GROUPS_ALT1);
            if (defaultModifier == null) { // garbage in, no shortcurt out
                defaultModifier = groups.get(GROUP_NONE + GROUPS_DEFAULT);
            }
            potentialShortcut = findShortcut(requestedKey, defaultModifier);
            if (potentialShortcut != null) {
                defaultModifier = groups.get(requestedGroup + GROUPS_ALT2);
                if (defaultModifier == null) { // garbage in, no shortcurt out
                    defaultModifier = groups.get(GROUP_NONE + GROUPS_DEFAULT);
                }
                potentialShortcut = findShortcut(requestedKey, defaultModifier);
                if (potentialShortcut != null) { // if all 3 modifiers for a group are used, we give up
                    potentialShortcut = findRandomShortcut(shortText, longText, requestedKey, requestedGroup);
                } else {
                    potentialShortcut = new Shortcut(shortText, longText, requestedKey, requestedGroup, requestedKey, defaultModifier, false, false);
                }
            } else {
                potentialShortcut = new Shortcut(shortText, longText, requestedKey, requestedGroup, requestedKey, defaultModifier, false, false);
            }
            if (originalShortcut != null && !originalShortcut.isSame(potentialShortcut)) {
                displayWarning(conflictsWith, potentialShortcut, shortText, longText);
            } else if (originalShortcut == null) {
                System.out.println("Silent shortcut conflict: '"+shortText+"' moved by '"+conflictsWith.getShortText()+"' to '"+potentialShortcut.getKeyText()+"'.");
            }
        } else {
            potentialShortcut = new Shortcut(shortText, longText, requestedKey, requestedGroup, requestedKey, defaultModifier, true, false);
        }

        shortcuts.put(shortText, potentialShortcut);
        return potentialShortcut;
    }

    // a lengthy warning message
    private static void displayWarning(Shortcut conflictsWith, Shortcut potentialShortcut, String shortText, String longText) {
        JOptionPane.showMessageDialog(Main.parent,
                tr("Setting the keyboard shortcut ''{0}'' for the action ''{1}'' ({2}) failed\n"+
                        "because the shortcut is already taken by the action ''{3}'' ({4}).\n\n",
                        conflictsWith.getKeyText(), longText, shortText,
                        conflictsWith.getLongText(), conflictsWith.getShortText())+
                        (potentialShortcut.getKeyText().equals("") ?
                                tr("This action will have no shortcut.\n\n")
                                :
                                    tr("Using the shortcut ''{0}'' instead.\n\n", potentialShortcut.getKeyText())
                        )+
                        tr("(Hint: You can edit the shortcuts in the preferences.)"),
                        tr("Error"),
                        JOptionPane.ERROR_MESSAGE
        );
    }
}
