package ij.plugin;
import java.awt.*;
import java.io.*;
import java.awt.event.*;
import ij.*;
import ij.gui.*;
import ij.macro.*;
import ij.text.*;
import ij.util.Tools;
import ij.io.*;
import ij.macro.MacroConstants;
import ij.plugin.frame.*;
                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                import java.util.*;                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                            

/** This plugin implements the Plugins/Macros/Install Macros command. It is also used by the Editor
    class to install macro in menus and by the ImageJ class to install macros at startup. */
public class MacroInstaller implements PlugIn, MacroConstants, ActionListener {

    public static final int MAX_SIZE = 28000, MAX_MACROS=100, XINC=10, YINC=18;
    public static final char commandPrefix = '^';
    static final String commandPrefixS = "^";
    static final int MACROS_MENU_COMMANDS = 6; // number of commands in Plugins>Macros submenu
    
    private int[] macroStarts;
    private String[] macroNames;
    private MenuBar mb = new MenuBar();
    private int nMacros;
    private Program pgm;
    private boolean firstEvent = true;
    private String shortcutsInUse;
    private int inUseCount;
    private int nShortcuts;
    private int toolCount;
    private String text;
    private String anonymousName;
    private Menu macrosMenu;
    private int autoRunCount, autoRunAndHideCount;
    private boolean openingStartupMacrosInEditor;
    private boolean installTools = true;
    
    private static String defaultDir, fileName;
    private static MacroInstaller instance, listener;
    private Thread macroToolThread;
    private ArrayList<Menu> subMenus = new ArrayList();
    
    public void run(String path) {
        if (path==null || path.equals(""))
            path = showDialog();
        if (path==null) return;
        openingStartupMacrosInEditor = path.indexOf("StartupMacros")!=-1;
        String text = open(path);
        if (text!=null) {
            String functions = Interpreter.getAdditionalFunctions();
            if (functions!=null) {
                if (!(text.endsWith("\n") || functions.startsWith("\n")))
                    text = text + "\n" + functions;
                else
                    text = text + functions;
            }
            install(text);
        }
    }
            
    void install() {
        subMenus.clear();
        if (text!=null) {
            Tokenizer tok = new Tokenizer();
            pgm = tok.tokenize(text);
        }
        if (macrosMenu!=null)
            IJ.showStatus("");
        int[] code = pgm.getCode();
        Symbol[] symbolTable = pgm.getSymbolTable();
        int count=0, token, nextToken, address;
        String name;
        Symbol symbol;
        shortcutsInUse = null;
        inUseCount = 0;
        nShortcuts = 0;
        toolCount = 0;
        macroStarts = new int[MAX_MACROS];
        macroNames = new String[MAX_MACROS];
        boolean isPluginsMacrosMenu = false;
        if (macrosMenu!=null) {
            int itemCount = macrosMenu.getItemCount();
            isPluginsMacrosMenu = macrosMenu==Menus.getMacrosMenu();
            int baseCount =isPluginsMacrosMenu?MACROS_MENU_COMMANDS:Editor.MACROS_MENU_ITEMS;
            if (itemCount>baseCount) {
                for (int i=itemCount-1; i>=baseCount; i--)
                    macrosMenu.remove(i);
            }
        }
        if (pgm.hasVars() && pgm.macroCount()>0 && pgm.getGlobals()==null)
            new Interpreter().saveGlobals(pgm);
        ArrayList tools = new ArrayList();
        for (int i=0; i<code.length; i++) {
            token = code[i]&TOK_MASK;
            if (token==MACRO) {
                nextToken = code[i+1]&TOK_MASK;
                if (nextToken==STRING_CONSTANT) {
                    if (count==MAX_MACROS) {
                        if (isPluginsMacrosMenu)
                            IJ.error("Macro Installer", "Macro sets are limited to "+MAX_MACROS+" macros.");
                        break;
                    }
                    address = code[i+1]>>TOK_SHIFT;
                    symbol = symbolTable[address];
                    name = symbol.str;
                    macroStarts[count] = i + 2;
                    macroNames[count] = name;
                    if (name.indexOf('-')!=-1 && (name.indexOf("Tool")!=-1||name.indexOf("tool")!=-1)) {
                        tools.add(name);
                        toolCount++;
                    } else if (name.startsWith("AutoRun")) {
                        if (autoRunCount==0 && !openingStartupMacrosInEditor) {
                            new MacroRunner(pgm, macroStarts[count], name, (String)null);
                            if (name.equals("AutoRunAndHide"))
                                autoRunAndHideCount++;
                        }
                        autoRunCount++;
                        count--;
                    } else if  (name.equals("Popup Menu"))
                        installPopupMenu(name, pgm);
                    else if (!name.endsWith("Tool Selected")) { 
                        if (macrosMenu!=null) {
                            addShortcut(name);
                            int pos = name.indexOf(">");
                            boolean inSubMenu = name.startsWith("<") && (pos>1);
                            if (inSubMenu) {
                                Menu parent = macrosMenu;
                                Menu subMenu = null;
                                String parentStr = name.substring(1, pos).trim();
                                String childStr = name.substring(pos + 1).trim();
                                MenuItem mnuItem = new MenuItem();
                                mnuItem.setActionCommand(name);
                                mnuItem.setLabel(childStr);
                                for (int jj = 0; jj < subMenus.size(); jj++) {
                                    String aName = subMenus.get(jj).getName();
                                    if (aName.equals(parentStr))
                                        subMenu = subMenus.get(jj);
                                }
                                if (subMenu==null) {
                                    subMenu = new Menu(parentStr);
                                    subMenu.setName(parentStr);
                                    subMenu.addActionListener(this);
                                    subMenus.add(subMenu);
                                    parent.add(subMenu);
                                }
                                subMenu.add(mnuItem);
                            } else
                                macrosMenu.add(new MenuItem(name));
                        }
                    }
                    //IJ.log(count+" "+name+" "+macroStarts[count]);
                    count++;
                }                   
            } else if (token==EOF)
                break;
        }
        nMacros = count;
        if (toolCount>0 && (isPluginsMacrosMenu||macrosMenu==null) && installTools) {
            Toolbar tb = Toolbar.getInstance();
            if (toolCount==1) 
                tb.addMacroTool((String)tools.get(0), this);
            else {
                for (int i=0; i<tools.size(); i++) {
                    String toolName = (String)tools.get(i);
                    if (toolName.startsWith("Abort Macro or Plugin") && toolCount>6)
                        toolName = "Unused "+toolName;
                    tb.addMacroTool(toolName, this, i);
                }
            }
            if (toolCount>1 && Toolbar.getToolId()>=Toolbar.CUSTOM1)
                tb.setTool(Toolbar.RECTANGLE);
            tb.repaint();
        }
        if (macrosMenu!=null)
            this.instance = this;
        if (shortcutsInUse!=null && text!=null)
            IJ.showMessage("Install Macros", (inUseCount==1?"This keyboard shortcut is":"These keyboard shortcuts are")
            + " already in use:"+shortcutsInUse);
        if (nMacros==0 && fileName!=null) {
            if (text==null||text.length()==0)
                return;
            int dotIndex = fileName.lastIndexOf('.');
            if (dotIndex>0)
                anonymousName = fileName.substring(0, dotIndex);
            else
                anonymousName =fileName;
            if (macrosMenu!=null)
                macrosMenu.add(new MenuItem(anonymousName));
            macroNames[0] = anonymousName;
            nMacros = 1;
        }
        String word = nMacros==1?" macro":" macros";
        if (isPluginsMacrosMenu)
            IJ.showStatus(nMacros + word + " installed");
    }
    
    public int install(String text) {
        if (text==null && pgm==null)
            return 0;
        this.text = text;
        macrosMenu = Menus.getMacrosMenu();
        if (listener!=null)
            macrosMenu.removeActionListener(listener);
        macrosMenu.addActionListener(this);
        listener = this;
        install();
        return nShortcuts;
    }
    
    public int install(String text, Menu menu) {
        this.text = text;
        macrosMenu = menu;
        install();
        int count = nShortcuts+toolCount;
        if (count==0 && nMacros>1)
            count = nMacros;
        return count;
    }

    public void installFile(String path) {
        String text = open(path);
        if (text==null) return;
        boolean isStartupMacros = path.contains("StartupMacros");
        if (isStartupMacros && !Toolbar.installStartupMacrosTools())
            installTools = false;
        //IJ.log("installFile: "+path+" "+isStartupMacros+" "+installTools);
        install(text);
        installTools = true;
        if (isStartupMacros) {
            Toolbar tb = Toolbar.getInstance();
            if (tb!=null)
                tb.installStartupTools();
        }
    }

    public void installTool(String path) {
        String text = open(path);
        if (text!=null)
            installSingleTool(text);
    }

    public void installLibrary(String path) {
            String text = open(path);
            if (text!=null)
                Interpreter.setAdditionalFunctions(text);
    }

     /** Installs a macro set contained in ij.jar. */
    public void installFromIJJar(String path) {
        String text = openFromIJJar(path);
        if (text==null) return;
        if (path.endsWith("StartupMacros.txt")) {
            if (Toolbar.installStartupMacrosTools())
                install(text);
            Toolbar tb = Toolbar.getInstance();
            if (tb!=null)
                tb.installStartupTools();
        } else
            installSingleTool(text);
    }

    public void installSingleTool(String text) {
        this.text = text;
        macrosMenu = null;
        install();
    }

    void installPopupMenu(String name, Program pgm) {
        Hashtable h = pgm.getMenus();
        if (h==null) return;
        String[] commands = (String[])h.get(name);
        if (commands==null) return;
        PopupMenu popup = Menus.getPopupMenu();
        if (popup==null) return;
        popup.removeAll();
        for (int i=0; i<commands.length; i++) {
            if (commands[i].equals("-"))
                popup.addSeparator();
            else {
                MenuItem mi = new MenuItem(commands[i]);
                mi.addActionListener(this);
                popup.add(mi);
            }
        }
    }

    void removeShortcuts() {
        Menus.getMacroShortcuts().clear();
        Hashtable shortcuts = Menus.getShortcuts();
        for (Enumeration en=shortcuts.keys(); en.hasMoreElements();) {
            Integer key = (Integer)en.nextElement();
            String value = (String)shortcuts.get(key);
            if (value.charAt(0)==commandPrefix)
                shortcuts.remove(key);
        }
    }

    void addShortcut(String name) {
        int index1 = name.indexOf('[');
        if (index1==-1)
            return;
        int index2 = name.lastIndexOf(']');
        if (index2<=(index1+1))
            return;
        String shortcut = name.substring(index1+1, index2);
        int len = shortcut.length();
        if (len>1)
            shortcut = shortcut.toUpperCase(Locale.US);;
        if (len>3 || (len>1&&shortcut.charAt(0)!='F'&&shortcut.charAt(0)!='N'))
            return;
        int code = Menus.convertShortcutToCode(shortcut);
        if (code==0)
            return;
        if (nShortcuts==0)
            removeShortcuts();
        // One character shortcuts go in a separate hash table to
        // avoid conflicts with ImageJ menu shortcuts.
        if (len==1 || shortcut.equals("N+") || shortcut.equals("N-") ) {
            Hashtable macroShortcuts = Menus.getMacroShortcuts();
            macroShortcuts.put(new Integer(code), commandPrefix+name);
            nShortcuts++;
            return;
        }
        Hashtable shortcuts = Menus.getShortcuts();
        if (shortcuts.get(new Integer(code))!=null) {
            if (shortcutsInUse==null)
                shortcutsInUse = "\n \n";
            shortcutsInUse += "   " + name + "\n";
            inUseCount++;
            return;
        }
        shortcuts.put(new Integer(code), commandPrefix+name);
        nShortcuts++;
        //IJ.log("addShortcut3: "+name+"      "+shortcut+"    "+code);
    }
    
     String showDialog() {
        if (defaultDir==null) defaultDir = Menus.getMacrosPath();
        OpenDialog od = new OpenDialog("Install Macros", defaultDir, fileName);
        String name = od.getFileName();
        if (name==null) return null;
        String dir = od.getDirectory();
        if (!(name.endsWith(".txt")||name.endsWith(".ijm"))) {
            IJ.showMessage("Macro Installer", "File name must end with \".txt\" or \".ijm\" .");
            return null;
        }
        fileName = name;
        defaultDir = dir;
        return dir+name;
    }

    String open(String path) {
        if (path==null) return null;
        try {
            StringBuffer sb = new StringBuffer(5000);
            BufferedReader r = new BufferedReader(new FileReader(path));
            while (true) {
                String s=r.readLine();
                if (s==null)
                    break;
                else
                    sb.append(s+"\n");
            }
            r.close();
            return new String(sb);
        }
        catch (Exception e) {
            IJ.error(e.getMessage());
            return null;
        }
    }
     
     /** Returns a text file contained in ij.jar. */
     public String openFromIJJar(String path) {
        String text = null;
          try {
            InputStream is = this.getClass().getResourceAsStream(path);
            //IJ.log(is+"   "+path);
            if (is==null) return null;
                InputStreamReader isr = new InputStreamReader(is);
                StringBuffer sb = new StringBuffer();
                char [] b = new char [8192];
                int n;
                while ((n = isr.read(b)) > 0)
                     sb.append(b,0, n);
                text = sb.toString();
          }
          catch (IOException e) {}
          return text;
    }
    
    //void runMacro() {
    // new MacroRunner(text);
    //}

    public boolean runMacroTool(String name) {
        for (int i=0; i<nMacros; i++) {
            if (macroNames[i].startsWith(name)) {
                if (macroToolThread!=null && macroToolThread.getName().indexOf(name)!=-1 && macroToolThread.isAlive())
                    return false; // do nothing if this tool is already running
                MacroRunner mw = new MacroRunner(pgm, macroStarts[i], name, (String)null);
                macroToolThread = mw.getThread();
                //IJ.log("runMacroTool: "+macroToolThread);
                return true;
            }
        }
        return false;
    }
    
    public boolean runMenuTool(String name, String command) {
        for (int i=0; i<nMacros; i++) {
            if (macroNames[i].startsWith(name)) {
                Recorder.recordInMacros = true;
                new MacroRunner(pgm, macroStarts[i], name, command);
                return true;
            }
        }
        return false;
    }

    /** Runs a command in the Plugins/Macros submenu on the current thread. */
    public static boolean runMacroCommand(String name) {
        if (instance==null)
            return false;
        //IJ.log("runMacroCommand: "+name+" "+instance.nMacros);
        if (name.startsWith(commandPrefixS))
            name = name.substring(1);
        for (int i=0; i<instance.nMacros; i++) {
            //IJ.log("  "+i+" "+instance.macroNames[i]);
            if (name.equals(instance.macroNames[i])) {
                MacroRunner mm = new MacroRunner();
                mm.run(instance.pgm, instance.macroStarts[i], name);
                return true;
            }
        }
        return false;
    }
    
    public static void runMacroShortcut(String name) {
        if (instance==null) return;
        if (name.startsWith(commandPrefixS))
            name = name.substring(1);
        for (int i=0; i<instance.nMacros; i++) {
            if (name.equals(instance.macroNames[i])) {
                (new MacroRunner()).runShortcut(instance.pgm, instance.macroStarts[i], name);
                return;
            }
        }
    }

    public void runMacro(String name) {
        runMacro(name, null);
    }

    public void runMacro(String name, Editor editor) {
        if (anonymousName!=null && name.equals(anonymousName)) {
            ImageJ.setCommandName(name);
            new MacroRunner(pgm, 0, anonymousName, editor);
            return;
        }
        for (int i=0; i<nMacros; i++)
            if (name.equals(macroNames[i])) {
                ImageJ.setCommandName(name);
                new MacroRunner(pgm, macroStarts[i], name, editor);
                return;
            }
    }
    
    public int getMacroCount() {
        return nMacros;
    }
    
    public Program getProgram() {
        return pgm;
    }

        
    /** Returns true if an "AutoRunAndHide" macro was run/installed. */
    public boolean isAutoRunAndHide() {
        return autoRunAndHideCount>0;
    }

    public void setFileName(String fileName) {
        this.fileName = fileName;
        openingStartupMacrosInEditor = fileName.startsWith("StartupMacros");
    }

    public static String getFileName() {
        return fileName;
    }
    
    public void actionPerformed(ActionEvent evt) {
        String cmd = evt.getActionCommand();
        ImageJ.setCommandName(cmd);
        MenuItem item = (MenuItem)evt.getSource();
        MenuContainer parent = item.getParent();
        if (parent instanceof PopupMenu) {
            for (int i=0; i<nMacros; i++) {
                if (macroNames[i].equals("Popup Menu")) {
                    new MacroRunner(pgm, macroStarts[i], "Popup Menu", cmd);
                    return;
                }
            }
        }
        runMacro(cmd);
    }

}