/**
 *#########################################################################
 *
 * A component of the Gatherer application, part of the Greenstone digital
 * library suite from the New Zealand Digital Library Project at the
 * University of Waikato, New Zealand.
 *
 * Author: John Thompson, Greenstone Digital Library, University of Waikato
 *
 * Copyright (C) 1999 New Zealand Digital Library Project
 *
 * This program is free software; you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation; either version 2 of the License, or
 * (at your option) any later version.
 *
 * This program 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 for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program; if not, write to the Free Software
 * Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
 *########################################################################
 */
package org.greenstone.gatherer.cdm;

import java.awt.*;
import java.awt.event.*;
import java.io.*;
import java.util.*;
import javax.swing.*;
import org.greenstone.gatherer.DebugStream;
import org.greenstone.gatherer.Gatherer;
import org.greenstone.gatherer.collection.CollectionManager;
import org.greenstone.gatherer.greenstone.LocalLibraryServer;
import org.greenstone.gatherer.gui.GLIButton;
import org.greenstone.gatherer.remote.RemoteGreenstoneServer;
import org.greenstone.gatherer.util.DOMTree;
import org.greenstone.gatherer.util.StaticStrings;
import org.greenstone.gatherer.util.XMLTools;
import org.greenstone.gatherer.util.Utility;
import org.w3c.dom.*;

/**
 * This class provides access to an xml-type view of the collect.cfg file. This
 * is useful as it allows the manipulation and free form editing of a
 * collect.cfg file while still allowing the various CDM data managers to base
 * themselves directly on this model (whereas they used to be independant
 * ListModels which clobbered the ordering of unparsed commands).
 * 
 * @author John Thompson, Greenstone Digital Library, University of Waikato
 */
public class CollectionConfiguration
{
	static final public String ENCODING = "UTF-8";
	static final public String NEWLINE_ELEMENT = "NewLine";

	static private Document document;
	static private String saved_config_file_string = null;

	// may be collec.cfg (GS2) or collectionConfig.xml (GS3)
	private File collect_config_file;
	private String collect_config_filename;

	// This method is initilised in CollectionDesignManager.java constructor
	public CollectionConfiguration(File collect_config_file)
	{	    
	    load(collect_config_file);
	}

	private void load(File collect_config_file) {
		this.collect_config_file = collect_config_file;
		this.collect_config_filename = collect_config_file.getName();
		// parse the XML template
		document = XMLTools.parseXMLFile("xml/CollectionConfig.xml", true);
		String filename = collect_config_filename.toLowerCase();

		if (filename.endsWith(".cfg"))
		{
			saved_config_file_string = CollectCfgReadWrite.parse(collect_config_file, document);
		}
		else if (filename.endsWith(".xml"))
		{
			CollectionConfigXMLReadWrite.parse(collect_config_file, document);
		}

		//XMLTools.printXMLNode(document.getDocumentElement());
	}


	public void reload() {
	    load(this.collect_config_file);
	}

	static public Element createElement(String element_name)
	{
		return document.createElement(element_name);
	}

	/** Gives the preferred ordering of commands */
    static final public String[] COMMAND_ORDER = { StaticStrings.COLLECTIONMETADATA_CREATOR_ELEMENT, StaticStrings.COLLECTIONMETADATA_MAINTAINER_ELEMENT, StaticStrings.COLLECTIONMETADATA_PUBLIC_ELEMENT, StaticStrings.BUILDTYPE_ELEMENT, StaticStrings.PLUGIN_ELEMENT, StaticStrings.INDEXES_ELEMENT, StaticStrings.INDEX_DEFAULT_ELEMENT, StaticStrings.SORTS_ELEMENT, StaticStrings.SORT_DEFAULT_ELEMENT, StaticStrings.FACETS_ELEMENT, StaticStrings.INDEXOPTIONS_ELEMENT, StaticStrings.INDEXOPTION_DEFAULT_ELEMENT, StaticStrings.LANGUAGES_ELEMENT, StaticStrings.LANGUAGE_DEFAULT_ELEMENT, StaticStrings.LANGUAGE_METADATA_ELEMENT, StaticStrings.SUBCOLLECTION_ELEMENT, StaticStrings.SUBCOLLECTION_INDEXES_ELEMENT, StaticStrings.SUBCOLLECTION_DEFAULT_INDEX_ELEMENT, StaticStrings.SUPERCOLLECTION_ELEMENT, StaticStrings.CLASSIFY_ELEMENT, StaticStrings.FORMAT_ELEMENT, StaticStrings.SEARCHMETADATA_ELEMENT, StaticStrings.COLLECTIONMETADATA_ELEMENT };

	/**
	 * Find the best insertion position for the given DOM Element. This should
	 * try to match command tag, and if found should then try to group by name
	 * or type (eg CollectionMeta), or append to end is no such grouping exists
	 * (eg Plugins). Failing a command match it will check against the command
	 * order for the best insertion location.
	 * 
	 * @param target_element
	 *            the command Element to be inserted
	 * @return the Element which the given command should be inserted before, or
	 *         null to append to end of list
	 */
	static public Node findInsertionPoint(Element target_element)
	{
		///ystem.err.println("Find insertion point: " + target_element.getNodeName());
		String target_element_name = target_element.getNodeName();
		Element document_element = document.getDocumentElement();
		// Try to find commands with the same tag.
		NodeList matching_elements = document_element.getElementsByTagName(target_element_name);
		// If we found matching elements, then we have our most likely insertion location, so check within for groupings
		if (matching_elements.getLength() != 0)
		{
			///ystem.err.println("Found matching elements.");
			// Only CollectionMeta are grouped.
			if (target_element_name.equals(StaticStrings.COLLECTIONMETADATA_ELEMENT))
			{
				///ystem.err.println("Dealing with collection metadata");
				// Special case: CollectionMeta can be added at either the start or end of a collection configuration file. However the start position is reserved for special metadata, so if no non-special metadata can be found we must append to the end.
				// So if the command to be added is special add it immediately after any other special command
				if (target_element.getAttribute(StaticStrings.SPECIAL_ATTRIBUTE).equals(StaticStrings.TRUE_STR))
				{
					int index = 0;
					Element matched_element = (Element) matching_elements.item(index);
					Element sibling_element = (Element) matched_element.getNextSibling();
					while (sibling_element.getAttribute(StaticStrings.SPECIAL_ATTRIBUTE).equals(StaticStrings.TRUE_STR))
					{
						index++;
						matched_element = (Element) matching_elements.item(index);
						sibling_element = (Element) matched_element.getNextSibling();
					}
					if (sibling_element.getNodeName().equals(NEWLINE_ELEMENT))
					{
						Element newline_element = document.createElement(NEWLINE_ELEMENT);
						document_element.insertBefore(newline_element, sibling_element);
					}
					return sibling_element;
				}
				// Otherwise try to find a matching 'name' and add after the last one in that group.
				else
				{
					int index = 0;
					target_element_name = target_element.getAttribute(StaticStrings.NAME_ATTRIBUTE);
					boolean found = false;
					// Skip all of the special metadata
					Element matched_element = (Element) matching_elements.item(index);
					while (matched_element.getAttribute(StaticStrings.SPECIAL_ATTRIBUTE).equals(StaticStrings.TRUE_STR))
					{
						index++;
						matched_element = (Element) matching_elements.item(index);
					}
					// Begin search
					while (!found && matched_element != null)
					{
						if (matched_element.getAttribute(StaticStrings.NAME_ATTRIBUTE).equals(target_element_name))
						{
							found = true;
						}
						else
						{
							index++;
							matched_element = (Element) matching_elements.item(index);
						}
					}
					// If we found a match, we need to continue checking until we find the last name match.
					if (found)
					{
						index++;
						Element previous_sibling = matched_element;
						Element sibling_element = (Element) matching_elements.item(index);
						while (sibling_element != null && sibling_element.getAttribute(StaticStrings.NAME_ATTRIBUTE).equals(target_element_name))
						{
							previous_sibling = sibling_element;
							index++;
							sibling_element = (Element) matching_elements.item(index);
						}
						// Previous sibling now holds the command immediately before where we want to add, so find its next sibling and add to that. In this one case we can ignore new lines!
						return previous_sibling.getNextSibling();
					}
					// If not found we just add after last metadata element
					else
					{
						Element last_element = (Element) matching_elements.item(matching_elements.getLength() - 1);
						return last_element.getNextSibling();
					}
				}

			}
			else
			{
				///ystem.err.println("Not dealing with collection meta.");
				Element matched_element = (Element) matching_elements.item(matching_elements.getLength() - 1);
				// One final quick test. If the matched element is immediately followed by a NewLine command, then we insert another NewLine after the matched command, then return the NewLine instead (thus the about to be inserted command will be placed between the two NewLines)
				Node sibling_element = matched_element.getNextSibling();
				if (sibling_element != null && sibling_element.getNodeName().equals(NEWLINE_ELEMENT))
				{
					Element newline_element = document.createElement(NEWLINE_ELEMENT);
					document_element.insertBefore(newline_element, sibling_element);
				}
				return sibling_element; // Note that this may be null
			}
		}
		///ystem.err.println("No matching elements found.");
		// Locate where this command is in the ordering
		int command_index = -1;
		for (int i = 0; command_index == -1 && i < COMMAND_ORDER.length; i++)
		{
			if (COMMAND_ORDER[i].equals(target_element_name))
			{
				command_index = i;
			}
		}
		///ystem.err.println("Command index is: " + command_index);
		// Now move forward, checking for existing elements in each of the preceeding command orders.
		int preceeding_index = command_index - 1;
		///ystem.err.println("Searching before the target command.");
		while (preceeding_index >= 0)
		{
			matching_elements = document_element.getElementsByTagName(COMMAND_ORDER[preceeding_index]);
			// If we've found a match
			if (matching_elements.getLength() > 0)
			{
				// We add after the last element
				Element matched_element = (Element) matching_elements.item(matching_elements.getLength() - 1);
				// One final quick test. If the matched element is immediately followed by a NewLine command, then we insert another NewLine after the matched command, then return the NewLine instead (thus the about to be inserted command will be placed between the two NewLines)
				Node sibling_element = matched_element.getNextSibling();
				if (sibling_element != null && sibling_element.getNodeName().equals(NEWLINE_ELEMENT))
				{
					Element newline_element = document.createElement(NEWLINE_ELEMENT);
					document_element.insertBefore(newline_element, sibling_element);
				}
				return sibling_element; // Note that this may be null
			}
			preceeding_index--;
		}
		// If all that fails, we now move backwards through the commands
		int susceeding_index = command_index + 1;
		///ystem.err.println("Searching after the target command.");
		while (susceeding_index < COMMAND_ORDER.length)
		{
			matching_elements = document_element.getElementsByTagName(COMMAND_ORDER[susceeding_index]);
			// If we've found a match
			if (matching_elements.getLength() > 0)
			{
				// We add before the first element
				Element matched_element = (Element) matching_elements.item(0);
				// One final quick test. If the matched element is immediately preceeded by a NewLine command, then we insert another NewLine before the matched command, then return this new NewLine instead (thus the about to be inserted command will be placed between the two NewLines)
				Node sibling_element = matched_element.getPreviousSibling();
				if (sibling_element != null && sibling_element.getNodeName().equals(NEWLINE_ELEMENT))
				{
					Element newline_element = document.createElement(NEWLINE_ELEMENT);
					document_element.insertBefore(newline_element, sibling_element);
				}
				return sibling_element; // Note that this may be null
			}
			susceeding_index++;
		}
		// Well. Apparently there are no other commands in this collection configuration. So append away...
		return null;
	}

	static public NodeList getElementsByTagName(String element_name)
	{
		return document.getDocumentElement().getElementsByTagName(element_name);
	}

	public Element getDocumentElement()
	{
		return document.getDocumentElement();
	}

	/**
	 * This debug facility shows the currently loaded collect.cfg or
	 * CollectConfig.xml file as a DOM tree.
	 */
	public void display()
	{
		JDialog dialog = new JDialog(Gatherer.g_man, "Collection Configuration", false);
		dialog.setSize(400, 400);
		JPanel content_pane = (JPanel) dialog.getContentPane();
		final DOMTree tree = new DOMTree(document);
		JButton refresh_button = new GLIButton("Refresh Tree");
		refresh_button.addActionListener(new ActionListener()
		{
			public void actionPerformed(ActionEvent event)
			{
				tree.setDocument(document);
			}
		});
		content_pane.setBorder(BorderFactory.createEmptyBorder(5, 5, 5, 5));
		content_pane.setLayout(new BorderLayout());
		content_pane.add(new JScrollPane(tree), BorderLayout.CENTER);
		content_pane.add(refresh_button, BorderLayout.SOUTH);
		dialog.setVisible(true);
	}

	public File getFile()
	{
		return collect_config_file;
	}

	public Element getCreator()
	{
		Element element = getOrCreateElementByTagName(StaticStrings.COLLECTIONMETADATA_CREATOR_ELEMENT, null, null);
		element.setAttribute(StaticStrings.NAME_ATTRIBUTE, StaticStrings.COLLECTIONMETADATA_CREATOR_STR);
		element.setAttribute(StaticStrings.SPECIAL_ATTRIBUTE, StaticStrings.TRUE_STR);
		return element;
	}

	public Element getMaintainer()
	{
		Element element = getOrCreateElementByTagName(StaticStrings.COLLECTIONMETADATA_MAINTAINER_ELEMENT, null, null);
		element.setAttribute(StaticStrings.NAME_ATTRIBUTE, StaticStrings.COLLECTIONMETADATA_MAINTAINER_STR);
		element.setAttribute(StaticStrings.SPECIAL_ATTRIBUTE, StaticStrings.TRUE_STR);
		return element;
	}

	/** Retrieve or create the languages Element. */
	public Element getLanguages()
	{
		return getOrCreateElementByTagName(StaticStrings.LANGUAGES_ELEMENT, null, null);
	}

	public Element getLanguageMetadata()
	{
		return getOrCreateElementByTagName(StaticStrings.LANGUAGE_METADATA_ELEMENT, null, null);
	}

	public Element getLevels()
	{
		return getOrCreateElementByTagName(StaticStrings.INDEXOPTIONS_ELEMENT, StaticStrings.NAME_ATTRIBUTE, StaticStrings.LEVELS_STR);
	}

	public Element getLevelDefault()
	{
		return getOrCreateElementByTagName(StaticStrings.INDEXOPTION_DEFAULT_ELEMENT, StaticStrings.NAME_ATTRIBUTE, StaticStrings.LEVEL_DEFAULT_STR);
	}

	public Element getIndexOptions()
	{
		return getOrCreateElementByTagName(StaticStrings.INDEXOPTIONS_ELEMENT, StaticStrings.NAME_ATTRIBUTE, StaticStrings.INDEXOPTIONS_STR);
	}

	/**
	 * Retrieve or create the indexes Element. Note that this method behaves
	 * differently from the other getBlah methods, in that it also has to keep
	 * in mind that indexes come in two flavours, MG and MGPP.
	 */
	public Element getMGIndexes()
	{
		return getOrCreateElementByTagName(StaticStrings.INDEXES_ELEMENT, StaticStrings.MGPP_ATTRIBUTE, StaticStrings.FALSE_STR);
	}

	public Element getMGPPIndexes()
	{
		return getOrCreateElementByTagName(StaticStrings.INDEXES_ELEMENT, StaticStrings.MGPP_ATTRIBUTE, StaticStrings.TRUE_STR);
	}

    public Element getSorts() 
    {
	return getOrCreateElementByTagName(StaticStrings.SORTS_ELEMENT, null, null);
    }
    public Element getFacets() 
    {
	return getOrCreateElementByTagName(StaticStrings.FACETS_ELEMENT, null, null);
    }
	public Element getPublic()
	{
		Element element = getOrCreateElementByTagName(StaticStrings.COLLECTIONMETADATA_PUBLIC_ELEMENT, null, null);
		element.setAttribute(StaticStrings.NAME_ATTRIBUTE, StaticStrings.COLLECTIONMETADATA_PUBLIC_STR);
		element.setAttribute(StaticStrings.SPECIAL_ATTRIBUTE, StaticStrings.TRUE_STR);
		return element;
	}

	public Element getBuildType()
	{
		Element element = getOrCreateElementByTagName(StaticStrings.BUILDTYPE_ELEMENT, null, null);
		element.setAttribute(StaticStrings.NAME_ATTRIBUTE, StaticStrings.BUILDTYPE_STR);
		element.setAttribute(StaticStrings.SPECIAL_ATTRIBUTE, StaticStrings.TRUE_STR);
		return element;

	}

	public Element getDatabaseType()
	{
		Element element = getOrCreateElementByTagName(StaticStrings.DATABASETYPE_ELEMENT, null, null);
		element.setAttribute(StaticStrings.NAME_ATTRIBUTE, StaticStrings.DATABASETYPE_STR);
		element.setAttribute(StaticStrings.SPECIAL_ATTRIBUTE, StaticStrings.TRUE_STR);
		return element;

	}

	/** Retrieve or create the subindexes Element. */
	public Element getSubIndexes()
	{
		return getOrCreateElementByTagName(StaticStrings.SUBCOLLECTION_INDEXES_ELEMENT, null, null);
	}

	/** Retrieve or create the supercollections Element. */
	public Element getSuperCollection()
	{
		return getOrCreateElementByTagName(StaticStrings.SUPERCOLLECTION_ELEMENT, null, null);
	}

	public boolean ready()
	{
		return document != null;
	}

	/** ************************** Private Methods ***************************/

	/** Retrieve or create the indexes Element. */
	static private Element getOrCreateElementByTagName(String name, String conditional_attribute, String required_value)
	{
		Element document_element = document.getDocumentElement();
		NodeList elements = document_element.getElementsByTagName(name);
		int elements_length = elements.getLength();
		if (elements_length > 0)
		{
			if (conditional_attribute == null)
			{
				document_element = null;
				return (Element) elements.item(0);
			}
			else
			{
				for (int i = 0; i < elements_length; i++)
				{
					Element element = (Element) elements.item(i);
					if (element.getAttribute(conditional_attribute).equals(required_value))
					{
						document_element = null;
						return element;
					}
					element = null;
				}
			}
		}
		// Create the element
		Element element = document.createElement(name);
		// If there was a property set it
		if (conditional_attribute != null)
		{
			element.setAttribute(conditional_attribute, required_value);
		}
		Node target_node = findInsertionPoint(element);
		if (target_node != null)
		{
			document_element.insertBefore(element, target_node);
		}
		else
		{
			document_element.appendChild(element);
		}
		document_element = null;
		return element;
	}

	/**
	 * Write the text to the buffer. This is used so we don't have to worry
	 * about storing intermediate String values just so we can calaulate length
	 * and offset.
	 * 
	 * @param writer
	 *            the BufferedWriter to which the str will be written
	 * @param str
	 *            the String to be written
	 */
	private void write(BufferedWriter writer, String str) throws IOException
	{
		writer.write(str, 0, str.length());
	}

	public void saveIfNecessary()
	{

		// Generate a string version of internal document
		String config_file_string = null;
		if (Gatherer.GS3)
		{
			config_file_string = CollectionConfigXMLReadWrite.generateStringVersion(document);
		}
		else
		{
			config_file_string = CollectCfgReadWrite.generateStringVersion(document);
		}
		// compare to saved version
		if (saved_config_file_string != null)
		{
			if (saved_config_file_string.equals(config_file_string))
			{
				DebugStream.println(collect_config_filename + " file hasn't changed so no save necessary...");
				return;
			}
		}

		// We need to save...
		DebugStream.println(collect_config_filename + " file has changed, saving now...");

		// If we're using the Local Library we must release the collection before writing to the collect.cfg file
		String collection_name = CollectionManager.getLoadedCollectionName(true); // url style slash
		boolean collection_released = false;
		if (Gatherer.c_man.built() && LocalLibraryServer.isRunning() == true)
		{
			// Release the collection
			LocalLibraryServer.releaseCollection(collection_name);
			collection_released = true;
		}

		// Make a backup of the existing config file
		if (collect_config_file.exists())
		{
			String config_filename;
			String backup_filename;
			if (Gatherer.GS3)
			{
				config_filename = Utility.COLLECTION_CONFIG_XML;
				backup_filename = Utility.COLLECTION_CONFIG_BAK;
			}
			else
			{
				config_filename = StaticStrings.COLLECT_CFG;
				backup_filename = Utility.COLLECT_BAK;
			}
			File original_file = new File(collect_config_file.getParentFile(), config_filename);
			File backup_file = new File(collect_config_file.getParentFile(), backup_filename);
			if (backup_file.exists())
			{
				backup_file.delete();
			}
			if (!original_file.renameTo(backup_file))
			{
				System.err.println("Warning: can't rename " + config_filename + " to " + backup_filename);
			}
		}

		// now save the file
		if (Gatherer.GS3)
		{
			CollectionConfigXMLReadWrite.save(collect_config_file, document);
		}
		else
		{
			// we have already converted to string, so save here
			try
			{
				OutputStream ostream = new FileOutputStream(collect_config_file);
				Writer file_writer = new OutputStreamWriter(ostream, ENCODING);
				BufferedWriter buffered_writer = new BufferedWriter(file_writer);
				buffered_writer.write(config_file_string);
				buffered_writer.close();
			}
			catch (Exception exception)
			{
				DebugStream.println("Error in CollectionConfiguration.save(): " + exception);
				DebugStream.printStackTrace(exception);
			}

		}

		// save the string version
		saved_config_file_string = config_file_string;

		// If we're using a remote Greenstone server, upload the new collect.cfg file
		if (Gatherer.isGsdlRemote)
		{
			Gatherer.remoteGreenstoneServer.uploadCollectionFile(collection_name, collect_config_file);
		}

		// Now re-add the collection to the Local Library server
		if (collection_released)
		{
			LocalLibraryServer.addCollection(collection_name);
		}
	}

}
