/*
 * 09/16/2004
 *
 * Macro.java - A macro as recorded/played back by an RTextArea.
 * 
 * This library is distributed under a modified BSD license.  See the included
 * RSyntaxTextArea.License.txt file for details.
 */
package org.fife.ui.rtextarea;

import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import javax.xml.parsers.*;
import javax.xml.transform.*;
import javax.xml.transform.dom.*;
import javax.xml.transform.stream.*;
import org.w3c.dom.*;
import org.xml.sax.InputSource;

import org.fife.io.UnicodeReader;


/**
 * A macro as recorded/played back by an {@link RTextArea}.<p>
 * 
 * <code>Macro</code>s are static; when a Macro is loaded, it can be run by any
 * instance of <code>RTextArea</code> in the application.  To activate and play
 * back a macro, use the following methods:
 * 
 * <ul>
 *    <li>{@link RTextArea#loadMacro(Macro)}
 *    <li>{@link RTextArea#playbackLastMacro()}
 * </ul>
 *
 * To record and save a new macro, you'd use the following methods:
 * 
 * <ul>
 *    <li>{@link RTextArea#beginRecordingMacro()} (this discards the previous
 *        "current" macro, if any)
 *    <li>{@link RTextArea#endRecordingMacro()} (at this point, you could call
 *        <code>playbackLastMacro()</code> to play this macro immediately if
 *        desired)
 *    <li>{@link RTextArea#getCurrentMacro()}.{@link #saveToFile(File)}
 * </ul>
 * 
 * As <code>Macro</code>s save themselves as XML files, a common technique is
 * to save all macros in files named "<code>{@link #getName()}.xml</code>", and
 * place them all in a common directory.
 *
 * @author Robert Futrell
 * @version 0.1
 */
public class Macro {

	private String name;
	private ArrayList<MacroRecord> macroRecords;

	private static final String ROOT_ELEMENT			= "macro";
	private static final String MACRO_NAME				= "macroName";
	private static final String ACTION					= "action";
	private static final String ID					= "id";

	private static final String UNTITLED_MACRO_NAME		= "<Untitled>";

	private static final String FILE_ENCODING			= "UTF-8";


	/**
	 * Constructor.
	 */
	public Macro() {
		this(UNTITLED_MACRO_NAME);
	}


	/**
	 * Loads a macro from a file on disk.
	 *
	 * @param file The file from which to load the macro.
	 * @throws FileNotFoundException If the specified file does not exist, is
	 *         a directory instead of a regular file, or otherwise cannot be
	 *         opened.
	 * @throws IOException If an I/O exception occurs while reading the file.
	 * @see #saveToFile(String)
	 * @see #saveToFile(File)
	 */
	public Macro(File file) throws FileNotFoundException, IOException {

		DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance();
		DocumentBuilder db = null;
		Document doc = null;
		try {
			db = dbf.newDocumentBuilder();
			//InputSource is = new InputSource(new FileReader(file));
			InputSource is = new InputSource(new UnicodeReader(
								new FileInputStream(file), FILE_ENCODING));
			is.setEncoding(FILE_ENCODING);
			doc = db.parse(is);//db.parse(file);
		} catch (Exception e) {
			e.printStackTrace();
			String desc = e.getMessage();
			if (desc==null) {
				desc = e.toString();
			}
			throw new IOException("Error parsing XML: " + desc);
		}

		macroRecords = new ArrayList<MacroRecord>();

		// Traverse the XML tree.
		boolean parsedOK = initializeFromXMLFile(doc.getDocumentElement());
		if (parsedOK==false) {
			name = null;
			macroRecords.clear();
			macroRecords = null;
			throw new IOException("Error parsing XML!");
		}

	}


	/**
	 * Constructor.
	 *
	 * @param name The name of the macro.
	 */
	public Macro(String name) {
		this(name, null);
	}


	/**
	 * Constructor.
	 *
	 * @param name The name of the macro.
	 * @param records The initial records of the macro.
	 */
	public Macro(String name, List<MacroRecord> records) {
		
		this.name = name;

		if (records!=null) {
			macroRecords = new ArrayList<MacroRecord>(records.size());
			for (MacroRecord record : records) {
				macroRecords.add(record);
			}
		}
		else {
			macroRecords = new ArrayList<MacroRecord>(10);
		}
	
	}


	/**
	 * Adds a macro record to this macro.
	 *
	 * @param record The record to add.  If <code>null</code>, nothing happens.
	 * @see #getMacroRecords
	 */
	public void addMacroRecord(MacroRecord record) {
		if (record!=null)
			macroRecords.add(record);
	}


	/**
	 * Returns the macro records that make up this macro.
	 *
	 * @return The macro records.
	 * @see #addMacroRecord
	 */
	public List<MacroRecord> getMacroRecords() {
		return macroRecords;
	}


	/**
	 * Returns the name of this macro.  A macro's name is simply something to
	 * identify it with in a UI; it has nothing to do with the name of the file
	 * to save the macro to.
	 *
	 * @return The macro's name.
	 * @see #setName(String)
	 */
	public String getName() {
		return name;
	}


	/**
	 * Used in parsing an XML document containing a macro.  This method
	 * initializes this macro with the data contained in the passed-in node.
	 *
	 * @param node The root node of the parsed XML document.
	 * @return <code>true</code> if the macro initialization went okay;
	 *         <code>false</code> if an error occurred.
	 */
	private boolean initializeFromXMLFile(Element root) {

		/*
		 * This method expects the XML document to be in the following format:
		 *
		 * <?xml version="1.0" encoding="UTF-8" ?>
		 * <macro>
		 *    <macroName>test</macroName>
		 *    <action id="default-typed">abcdefg</action>
		 *    [<action id=...>...</action>]
		 *    ...
		 * </macro>
		 *
		 */

		NodeList childNodes = root.getChildNodes();
		int count = childNodes.getLength();

		for (int i=0; i<count; i++) {

			Node node = childNodes.item(i);
			int type = node.getNodeType();
			switch (type) {

				// Handle element nodes.
				case Node.ELEMENT_NODE:

					String nodeName = node.getNodeName();

					if (nodeName.equals(MACRO_NAME)) {
						NodeList childNodes2 = node.getChildNodes();
						name = UNTITLED_MACRO_NAME;
						if (childNodes2.getLength()>0) {
							node = childNodes2.item(0);
							int type2 = node.getNodeType();
							if (type2!=Node.CDATA_SECTION_NODE &&
									type2!=Node.TEXT_NODE) {
								return false;
							}
							name = node.getNodeValue().trim();
						}
						//System.err.println("Macro name==" + name);
					}

					else if (nodeName.equals(ACTION)) {
						NamedNodeMap attributes = node.getAttributes();
						if (attributes==null || attributes.getLength()!=1)
							return false;
						Node node2 = attributes.item(0);
						MacroRecord macroRecord = new MacroRecord();
						if (!node2.getNodeName().equals(ID)) {
							return false;
						}
						macroRecord.id = node2.getNodeValue();
						NodeList childNodes2 = node.getChildNodes();
						int length = childNodes2.getLength();
						if (length==0) { // Could be empty "" command.
							//System.err.println("... empty actionCommand");
							macroRecord.actionCommand = "";
							//System.err.println("... adding action: " + macroRecord);
							macroRecords.add(macroRecord);
							break;
						}
						else {
							node = childNodes2.item(0);
							int type2 = node.getNodeType();
							if (type2!=Node.CDATA_SECTION_NODE &&
									type2!=Node.TEXT_NODE) {
								return false;
							}
							macroRecord.actionCommand = node.getNodeValue();
							macroRecords.add(macroRecord);
						}

					}
					break;

				default:
					break; // Skip whitespace nodes, etc.

			}

		}

		// Everything went okay.
		return true;

	}


	/**
	 * Saves this macro to an XML file.  This file can later be read in by the
	 * constructor taking a <code>File</code> parameter; this is the mechanism
	 * for saving macros.
	 *
	 * @param file The file in which to save the macro.
	 * @throws IOException If an error occurs while generating the XML for
	 *         the output file.
	 * @see #saveToFile(String)
	 */
	public void saveToFile(File file) throws IOException {
		saveToFile(file.getAbsolutePath());
	}


	/**
	 * Saves this macro to a  file.  This file can later be read in by the
	 * constructor taking a <code>File</code> parameter; this is the mechanism
	 * for saving macros.
	 *
	 * @param fileName The name of the file in which to save the macro.
	 * @throws IOException If an error occurs while generating the XML for
	 *         the output file.
	 * @see #saveToFile(File)
	 */
	public void saveToFile(String fileName) throws IOException {

		/*
		 * This method writes the XML document in the following format:
		 *
		 * <?xml version="1.0" encoding="UTF-8" ?>
		 * <macro>
		 *    <macroName>test</macroName>
		 *    <action id="default-typed">abcdefg</action>
		 *    [<action id=...>...</action>]
		 *    ...
		 * </macro>
		 *
		 */

		try {

			DocumentBuilder db = DocumentBuilderFactory.newInstance().
											newDocumentBuilder();
			DOMImplementation impl = db.getDOMImplementation();

			Document doc = impl.createDocument(null, ROOT_ELEMENT, null);
			Element rootElement = doc.getDocumentElement();

			// Write the name of the macro.
			Element nameElement = doc.createElement(MACRO_NAME);
			rootElement.appendChild(nameElement);

			// Write all actions (the meat) in the macro.
			for (MacroRecord record : macroRecords) {
				Element actionElement = doc.createElement(ACTION);
				actionElement.setAttribute(ID, record.id);
				if (record.actionCommand!=null &&
						record.actionCommand.length()>0) {
					// Remove illegal characters.  I'm no XML expert, but
					// I'm not sure what I'm doing wrong.  If we don't
					// strip out chars with Unicode value < 32, our
					// generator will insert '&#<value>', which will cause
					// our parser to barf when reading the macro back in
					// (it says "Invalid XML character").  But why doesn't
					// our generator tell us the character is invalid too?
					String command = record.actionCommand;
					for (int j=0; j<command.length(); j++) {
						if (command.charAt(j)<32) {
							command = command.substring(0,j);
							if (j<command.length()-1)
								command += command.substring(j+1);
						}
					}
					Node n = doc.createCDATASection(command);
					actionElement.appendChild(n);
				}
				rootElement.appendChild(actionElement);
			}

			// Dump the XML out to the file.
			StreamResult result = new StreamResult(new File(fileName));
			DOMSource source = new DOMSource(doc);
			TransformerFactory transFac = TransformerFactory.newInstance();
			Transformer transformer = transFac.newTransformer();
			transformer.setOutputProperty(OutputKeys.INDENT, "yes");
			transformer.setOutputProperty(OutputKeys.ENCODING, FILE_ENCODING);
			transformer.transform(source, result);

		} catch (RuntimeException re) {
			throw re; // Keep FindBugs happy.
		} catch (Exception e) {
			throw new IOException("Error generating XML!");
		}

	}


	/**
	 * Sets the name of this macro.  A macro's name is simply something to
	 * identify it with in a UI; it has nothing to do with the name of the file
	 * to save the macro to.
	 *
	 * @param name The new name for the macro.
	 * @see #getName()
	 */
	public void setName(String name) {
		this.name = name;
	}


	/**
	 * A "record" of a macro is a single action in the macro (corresponding to
	 * a key type and some action in the editor, such as a letter inserted into
	 * the document, scrolling one page down, selecting the current line,
	 * etc.).
	 */
	static class MacroRecord {

		public String id;
		public String actionCommand;

		public MacroRecord() {
			this(null, null);
		}

		public MacroRecord(String id, String actionCommand) {
			this.id = id;
			this.actionCommand = actionCommand;
		}

	}


}