/*
 * Copyright (c) 2003 Sun Microsystems, Inc. All  Rights Reserved.
 * 
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions
 * are met:
 * 
 * -Redistributions of source code must retain the above copyright
 *  notice, this list of conditions and the following disclaimer.
 * 
 * -Redistribution in binary form must reproduct the above copyright
 *  notice, this list of conditions and the following disclaimer in
 *  the documentation and/or other materials provided with the distribution.
 * 
 * Neither the name of Sun Microsystems, Inc. or the names of contributors
 * may be used to endorse or promote products derived from this software
 * without specific prior written permission.
 * 
 * This software is provided "AS IS," without a warranty of any kind. ALL
 * EXPRESS OR IMPLIED CONDITIONS, REPRESENTATIONS AND WARRANTIES, INCLUDING
 * ANY IMPLIED WARRANTY OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE
 * OR NON-INFRINGEMENT, ARE HEREBY EXCLUDED. SUN AND ITS LICENSORS SHALL NOT
 * BE LIABLE FOR ANY DAMAGES OR LIABILITIES SUFFERED BY LICENSEE AS A RESULT
 * OF OR RELATING TO USE, MODIFICATION OR DISTRIBUTION OF THE SOFTWARE OR ITS
 * DERIVATIVES. IN NO EVENT WILL SUN OR ITS LICENSORS BE LIABLE FOR ANY LOST
 * REVENUE, PROFIT OR DATA, OR FOR DIRECT, INDIRECT, SPECIAL, CONSEQUENTIAL,
 * INCIDENTAL OR PUNITIVE DAMAGES, HOWEVER CAUSED AND REGARDLESS OF THE THEORY
 * OF LIABILITY, ARISING OUT OF THE USE OF OR INABILITY TO USE SOFTWARE, EVEN
 * IF SUN HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES.
 * 
 * You acknowledge that Software is not designed, licensed or intended for
 * use in the design, construction, operation or maintenance of any nuclear
 * facility.
 */

/*
 * @(#)Notepad.java	1.21 03/01/23
 */

import java.awt.*;
import java.awt.event.*;
import java.beans.*;
import java.io.*;
import java.net.URL;
import java.util.*;

import javax.swing.text.*;
import javax.swing.undo.*;
import javax.swing.event.*;
import javax.swing.*;

/**
 * Sample application using the simple text editor component that
 * supports only one font.
 *
 * @author  Timothy Prinzing
 * @version 1.21 01/23/03
 */
class Notepad extends JPanel {

    private static ResourceBundle resources;

    static {
        try {
            resources = ResourceBundle.getBundle("resources.Notepad", 
                                                 Locale.getDefault());
        } catch (MissingResourceException mre) {
            System.err.println("resources/Notepad.properties not found");
            System.exit(1);
        }
    }

    Notepad() {
	super(true);

	// Force SwingSet to come up in the Cross Platform L&F
	try {
	    UIManager.setLookAndFeel(UIManager.getCrossPlatformLookAndFeelClassName());
	    // If you want the System L&F instead, comment out the above line and
	    // uncomment the following:
	    // UIManager.setLookAndFeel(UIManager.getSystemLookAndFeelClassName());
	} catch (Exception exc) {
	    System.err.println("Error loading L&F: " + exc);
	}

	setBorder(BorderFactory.createEtchedBorder());
	setLayout(new BorderLayout());

	// create the embedded JTextComponent
	editor = createEditor();
	// Add this as a listener for undoable edits.
	editor.getDocument().addUndoableEditListener(undoHandler);

	// install the command table
	commands = new Hashtable();
	Action[] actions = getActions();
	for (int i = 0; i < actions.length; i++) {
	    Action a = actions[i];
	    //commands.put(a.getText(Action.NAME), a);
	    commands.put(a.getValue(Action.NAME), a);
	}
	
	JScrollPane scroller = new JScrollPane();
	JViewport port = scroller.getViewport();
	port.add(editor);
	try {
	    String vpFlag = resources.getString("ViewportBackingStore");
	    Boolean bs = new Boolean(vpFlag);
	    port.setBackingStoreEnabled(bs.booleanValue());
	} catch (MissingResourceException mre) {
	    // just use the viewport default
	}

	menuItems = new Hashtable();
	menubar = createMenubar();
	add("North", menubar);
	JPanel panel = new JPanel();
	panel.setLayout(new BorderLayout());	
	panel.add("North",createToolbar());
	panel.add("Center", scroller);
	add("Center", panel);
	add("South", createStatusbar());
    }

    public static void main(String[] args) {
        try {
        String vers = System.getProperty("java.version");
        if (vers.compareTo("1.1.2") < 0) {
            System.out.println("!!!WARNING: Swing must be run with a " +
                               "1.1.2 or higher version VM!!!");
        }
        JFrame frame = new JFrame();
        frame.setTitle(resources.getString("Title"));
	frame.setBackground(Color.lightGray);
	frame.getContentPane().setLayout(new BorderLayout());
	frame.getContentPane().add("Center", new Notepad());
	frame.addWindowListener(new AppCloser());
	frame.pack();
	frame.setSize(500, 600);
        frame.show();
        } catch (Throwable t) {
            System.out.println("uncaught exception: " + t);
            t.printStackTrace();
        }
    }

    /**
     * Fetch the list of actions supported by this
     * editor.  It is implemented to return the list
     * of actions supported by the embedded JTextComponent
     * augmented with the actions defined locally.
     */
    public Action[] getActions() {
	return TextAction.augmentList(editor.getActions(), defaultActions);
    }

    /**
     * Create an editor to represent the given document.  
     */
    protected JTextComponent createEditor() {
	JTextComponent c = new JTextArea();
	c.setDragEnabled(true);
	c.setFont(new Font("monospaced", Font.PLAIN, 12));
	return c;
    }

    /** 
     * Fetch the editor contained in this panel
     */
    protected JTextComponent getEditor() {
	return editor;
    }

    /**
     * To shutdown when run as an application.  This is a
     * fairly lame implementation.   A more self-respecting
     * implementation would at least check to see if a save
     * was needed.
     */
    protected static final class AppCloser extends WindowAdapter {
        public void windowClosing(WindowEvent e) {
	    System.exit(0);
	}
    }

    /**
     * Find the hosting frame, for the file-chooser dialog.
     */
    protected Frame getFrame() {
	for (Container p = getParent(); p != null; p = p.getParent()) {
	    if (p instanceof Frame) {
		return (Frame) p;
	    }
	}
	return null;
    }

    /**
     * This is the hook through which all menu items are
     * created.  It registers the result with the menuitem
     * hashtable so that it can be fetched with getMenuItem().
     * @see #getMenuItem
     */
    protected JMenuItem createMenuItem(String cmd) {
	JMenuItem mi = new JMenuItem(getResourceString(cmd + labelSuffix));
        URL url = getResource(cmd + imageSuffix);
	if (url != null) {
	    mi.setHorizontalTextPosition(JButton.RIGHT);
	    mi.setIcon(new ImageIcon(url));
	}
	String astr = getResourceString(cmd + actionSuffix);
	if (astr == null) {
	    astr = cmd;
	}
	mi.setActionCommand(astr);
	Action a = getAction(astr);
	if (a != null) {
	    mi.addActionListener(a);
	    a.addPropertyChangeListener(createActionChangeListener(mi));
	    mi.setEnabled(a.isEnabled());
	} else {
	    mi.setEnabled(false);
	}
	menuItems.put(cmd, mi);
	return mi;
    }

    /**
     * Fetch the menu item that was created for the given
     * command.
     * @param cmd  Name of the action.
     * @returns item created for the given command or null
     *  if one wasn't created.
     */
    protected JMenuItem getMenuItem(String cmd) {
	return (JMenuItem) menuItems.get(cmd);
    }

    protected Action getAction(String cmd) {
	return (Action) commands.get(cmd);
    }

    protected String getResourceString(String nm) {
	String str;
	try {
	    str = resources.getString(nm);
	} catch (MissingResourceException mre) {
	    str = null;
	}
	return str;
    }

    protected URL getResource(String key) {
	String name = getResourceString(key);
	if (name != null) {
	    URL url = this.getClass().getResource(name);
	    return url;
	}
	return null;
    }

    protected Container getToolbar() {
	return toolbar;
    }

    protected JMenuBar getMenubar() {
	return menubar;
    }

    /**
     * Create a status bar
     */
    protected Component createStatusbar() {
	// need to do something reasonable here
	status = new StatusBar();
	return status;
    }

    /**
     * Resets the undo manager.
     */
    protected void resetUndoManager() {
	undo.discardAllEdits();
	undoAction.update();
	redoAction.update();
    }

    /**
     * Create the toolbar.  By default this reads the 
     * resource file for the definition of the toolbar.
     */
    private Component createToolbar() {
	toolbar = new JToolBar();
	String[] toolKeys = tokenize(getResourceString("toolbar"));
	for (int i = 0; i < toolKeys.length; i++) {
	    if (toolKeys[i].equals("-")) {
		toolbar.add(Box.createHorizontalStrut(5));
	    } else {
		toolbar.add(createTool(toolKeys[i]));
	    }
	}
	toolbar.add(Box.createHorizontalGlue());
	return toolbar;
    }

    /**
     * Hook through which every toolbar item is created.
     */
    protected Component createTool(String key) {
	return createToolbarButton(key);
    }

    /**
     * Create a button to go inside of the toolbar.  By default this
     * will load an image resource.  The image filename is relative to
     * the classpath (including the '.' directory if its a part of the
     * classpath), and may either be in a JAR file or a separate file.
     * 
     * @param key The key in the resource file to serve as the basis
     *  of lookups.
     */
    protected JButton createToolbarButton(String key) {
	URL url = getResource(key + imageSuffix);
        JButton b = new JButton(new ImageIcon(url)) {
            public float getAlignmentY() { return 0.5f; }
	};
        b.setRequestFocusEnabled(false);
        b.setMargin(new Insets(1,1,1,1));

	String astr = getResourceString(key + actionSuffix);
	if (astr == null) {
	    astr = key;
	}
	Action a = getAction(astr);
	if (a != null) {
	    b.setActionCommand(astr);
	    b.addActionListener(a);
	} else {
	    b.setEnabled(false);
	}

	String tip = getResourceString(key + tipSuffix);
	if (tip != null) {
	    b.setToolTipText(tip);
	}
 
        return b;
    }

    /**
     * Take the given string and chop it up into a series
     * of strings on whitespace boundries.  This is useful
     * for trying to get an array of strings out of the
     * resource file.
     */
    protected String[] tokenize(String input) {
	Vector v = new Vector();
	StringTokenizer t = new StringTokenizer(input);
	String cmd[];

	while (t.hasMoreTokens())
	    v.addElement(t.nextToken());
	cmd = new String[v.size()];
	for (int i = 0; i < cmd.length; i++)
	    cmd[i] = (String) v.elementAt(i);

	return cmd;
    }

    /**
     * Create the menubar for the app.  By default this pulls the
     * definition of the menu from the associated resource file. 
     */
    protected JMenuBar createMenubar() {
	JMenuItem mi;
	JMenuBar mb = new JMenuBar();

	String[] menuKeys = tokenize(getResourceString("menubar"));
	for (int i = 0; i < menuKeys.length; i++) {
	    JMenu m = createMenu(menuKeys[i]);
	    if (m != null) {
		mb.add(m);
	    }
	}
	return mb;
    }

    /**
     * Create a menu for the app.  By default this pulls the
     * definition of the menu from the associated resource file.
     */
    protected JMenu createMenu(String key) {
	String[] itemKeys = tokenize(getResourceString(key));
	JMenu menu = new JMenu(getResourceString(key + "Label"));
	for (int i = 0; i < itemKeys.length; i++) {
	    if (itemKeys[i].equals("-")) {
		menu.addSeparator();
	    } else {
		JMenuItem mi = createMenuItem(itemKeys[i]);
		menu.add(mi);
	    }
	}
	return menu;
    }

    // Yarked from JMenu, ideally this would be public.
    protected PropertyChangeListener createActionChangeListener(JMenuItem b) {
	return new ActionChangedListener(b);
    }

    // Yarked from JMenu, ideally this would be public.
    private class ActionChangedListener implements PropertyChangeListener {
        JMenuItem menuItem;
        
        ActionChangedListener(JMenuItem mi) {
            super();
            this.menuItem = mi;
        }
        public void propertyChange(PropertyChangeEvent e) {
            String propertyName = e.getPropertyName();
            if (e.getPropertyName().equals(Action.NAME)) {
                String text = (String) e.getNewValue();
                menuItem.setText(text);
            } else if (propertyName.equals("enabled")) {
                Boolean enabledState = (Boolean) e.getNewValue();
                menuItem.setEnabled(enabledState.booleanValue());
            }
        }
    }

    private JTextComponent editor;
    private Hashtable commands;
    private Hashtable menuItems;
    private JMenuBar menubar;
    private JToolBar toolbar;
    private JComponent status;
    private JFrame elementTreeFrame;
    protected ElementTreePanel elementTreePanel;

    protected FileDialog fileDialog;

    /**
     * Listener for the edits on the current document.
     */
    protected UndoableEditListener undoHandler = new UndoHandler();

    /** UndoManager that we add edits to. */
    protected UndoManager undo = new UndoManager();

    /**
     * Suffix applied to the key used in resource file
     * lookups for an image.
     */
    public static final String imageSuffix = "Image";

    /**
     * Suffix applied to the key used in resource file
     * lookups for a label.
     */
    public static final String labelSuffix = "Label";

    /**
     * Suffix applied to the key used in resource file
     * lookups for an action.
     */
    public static final String actionSuffix = "Action";

    /**
     * Suffix applied to the key used in resource file
     * lookups for tooltip text.
     */
    public static final String tipSuffix = "Tooltip";

    public static final String openAction = "open";
    public static final String newAction  = "new";
    public static final String saveAction = "save";
    public static final String exitAction = "exit";
    public static final String showElementTreeAction = "showElementTree";

    class UndoHandler implements UndoableEditListener {

	/**
	 * Messaged when the Document has created an edit, the edit is
	 * added to <code>undo</code>, an instance of UndoManager.
	 */
        public void undoableEditHappened(UndoableEditEvent e) {
	    undo.addEdit(e.getEdit());
	    undoAction.update();
	    redoAction.update();
	}
    }

    /**
     * FIXME - I'm not very useful yet
     */
    class StatusBar extends JComponent {

        public StatusBar() {
	    super();
	    setLayout(new BoxLayout(this, BoxLayout.X_AXIS));
	}

        public void paint(Graphics g) {
	    super.paint(g);
	}

    }

    // --- action implementations -----------------------------------

    private UndoAction undoAction = new UndoAction();
    private RedoAction redoAction = new RedoAction();

    /**
     * Actions defined by the Notepad class
     */
    private Action[] defaultActions = {
	new NewAction(),
	new OpenAction(),
	new ExitAction(),
	new ShowElementTreeAction(),
        undoAction,
        redoAction
    };

    class UndoAction extends AbstractAction {
	public UndoAction() {
	    super("Undo");
	    setEnabled(false);
	}

	public void actionPerformed(ActionEvent e) {
	    try {
		undo.undo();
	    } catch (CannotUndoException ex) {
		System.out.println("Unable to undo: " + ex);
		ex.printStackTrace();
	    }
	    update();
	    redoAction.update();
	}

	protected void update() {
	    if(undo.canUndo()) {
		setEnabled(true);
		putValue(Action.NAME, undo.getUndoPresentationName());
	    }
	    else {
		setEnabled(false);
		putValue(Action.NAME, "Undo");
	    }
	}
    }

    class RedoAction extends AbstractAction {
	public RedoAction() {
	    super("Redo");
	    setEnabled(false);
	}

	public void actionPerformed(ActionEvent e) {
	    try {
		undo.redo();
	    } catch (CannotRedoException ex) {
		System.out.println("Unable to redo: " + ex);
		ex.printStackTrace();
	    }
	    update();
	    undoAction.update();
	}

	protected void update() {
	    if(undo.canRedo()) {
		setEnabled(true);
		putValue(Action.NAME, undo.getRedoPresentationName());
	    }
	    else {
		setEnabled(false);
		putValue(Action.NAME, "Redo");
	    }
	}
    }

    class OpenAction extends NewAction {

	OpenAction() {
	    super(openAction);
	}

        public void actionPerformed(ActionEvent e) {
	    Frame frame = getFrame();
	    if (fileDialog == null) {
		fileDialog = new FileDialog(frame);
	    }
	    fileDialog.setMode(FileDialog.LOAD);
	    fileDialog.show();

	    String file = fileDialog.getFile();
	    if (file == null) {
		return;
	    }
	    String directory = fileDialog.getDirectory();
	    File f = new File(directory, file);
	    if (f.exists()) {
		Document oldDoc = getEditor().getDocument();
		if(oldDoc != null)
		    oldDoc.removeUndoableEditListener(undoHandler);
		if (elementTreePanel != null) {
		    elementTreePanel.setEditor(null);
		}
		getEditor().setDocument(new PlainDocument());
		frame.setTitle(file);
		Thread loader = new FileLoader(f, editor.getDocument());
		loader.start();
	    }
	}
    }
    
    class NewAction extends AbstractAction {

	NewAction() {
	    super(newAction);
	}

	NewAction(String nm) {
	    super(nm);
	}

        public void actionPerformed(ActionEvent e) {
	    Document oldDoc = getEditor().getDocument();
	    if(oldDoc != null)
		oldDoc.removeUndoableEditListener(undoHandler);
	    getEditor().setDocument(new PlainDocument());
	    getEditor().getDocument().addUndoableEditListener(undoHandler);
	    resetUndoManager();
	    revalidate();
	}
    }

    /**
     * Really lame implementation of an exit command
     */
    class ExitAction extends AbstractAction {

	ExitAction() {
	    super(exitAction);
	}

        public void actionPerformed(ActionEvent e) {
	    System.exit(0);
	}
    }

    /**
     * Action that brings up a JFrame with a JTree showing the structure
     * of the document.
     */
    class ShowElementTreeAction extends AbstractAction {

	ShowElementTreeAction() {
	    super(showElementTreeAction);
	}

	ShowElementTreeAction(String nm) {
	    super(nm);
	}

        public void actionPerformed(ActionEvent e) {
	    if(elementTreeFrame == null) {
		// Create a frame containing an instance of 
		// ElementTreePanel.
		try {
		    String    title = resources.getString
			                ("ElementTreeFrameTitle");
		    elementTreeFrame = new JFrame(title);
		} catch (MissingResourceException mre) {
		    elementTreeFrame = new JFrame();
		}

		elementTreeFrame.addWindowListener(new WindowAdapter() {
		    public void windowClosing(WindowEvent weeee) {
			elementTreeFrame.setVisible(false);
		    }
		});
		Container fContentPane = elementTreeFrame.getContentPane();

		fContentPane.setLayout(new BorderLayout());
		elementTreePanel = new ElementTreePanel(getEditor());
		fContentPane.add(elementTreePanel);
		elementTreeFrame.pack();
	    }
	    elementTreeFrame.show();
	}
    }

    /**
     * Thread to load a file into the text storage model
     */
    class FileLoader extends Thread {

	FileLoader(File f, Document doc) {
	    setPriority(4);
	    this.f = f;
	    this.doc = doc;
	}

        public void run() {
	    try {
		// initialize the statusbar
		status.removeAll();
		JProgressBar progress = new JProgressBar();
		progress.setMinimum(0);
		progress.setMaximum((int) f.length());
		status.add(progress);
		status.revalidate();

		// try to start reading
		Reader in = new FileReader(f);
		char[] buff = new char[4096];
		int nch;
		while ((nch = in.read(buff, 0, buff.length)) != -1) {
		    doc.insertString(doc.getLength(), new String(buff, 0, nch), null);
		    progress.setValue(progress.getValue() + nch);
		}

		// we are done... get rid of progressbar
		doc.addUndoableEditListener(undoHandler);
		status.removeAll();
		status.revalidate();

		resetUndoManager();
	    }
	    catch (IOException e) {
		System.err.println(e.toString());
	    }
	    catch (BadLocationException e) {
		System.err.println(e.getMessage());
	    }
	    if (elementTreePanel != null) {
		SwingUtilities.invokeLater(new Runnable() {
		    public void run() {
			elementTreePanel.setEditor(getEditor());
		    }
		});
	    }
	}

	Document doc;
	File f;
    }

}
