/**
 *############################################################################
 * A component of the Greenstone Librarian Interface, part of the Greenstone
 * digital library suite from the New Zealand Digital Library Project at the
 * University of Waikato, New Zealand.
 *
 * Author: Michael Dewsnip, NZDL Project, University of Waikato, NZ
 *
 * Copyright (C) 2005 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.metadata;


import java.io.*;
import java.util.*;
import org.greenstone.gatherer.DebugStream;
import org.greenstone.gatherer.Gatherer;
import org.greenstone.gatherer.collection.CollectionManager;
import org.greenstone.gatherer.collection.CollectionTreeNode;
import org.greenstone.gatherer.remote.RemoteGreenstoneServer;
import org.greenstone.gatherer.util.XMLTools;

import org.greenstone.gatherer.util.Utility;


/** This class is a static class that manages the metadata.xml files */
public class MetadataXMLFileManager
{
    static private ArrayList metadata_xml_files = new ArrayList();
    /** The objects listening for MetadataChanged events. */
    static private ArrayList metadata_changed_listeners = new ArrayList();
    /** Keep track of which metadata.xml files have been modified so we can upload them to the server */
    static private ArrayList modified_metadata_xml_files = new ArrayList();

  /** For non-accumulating metadata (gs.FilenameEncoding), need the metadata.xml files
   * sorted in top-down folder level order, which is achieved through this Comparator. 
   * By declaring a static class member here, we avoid recreating this object for every 
   * comparison, which would otherwise have resulted in a constructor call each time. */
  static private MetadataXMLFileComparator metadataXMLFileComparator = new MetadataXMLFileComparator();


    static public void addMetadata(CollectionTreeNode file_node, ArrayList metadata_values)
    {
	addMetadata(new CollectionTreeNode[] { file_node }, metadata_values);
    }


    static public void addMetadata(CollectionTreeNode[] file_nodes, MetadataValue metadata_value)
    {
	ArrayList metadata_values = new ArrayList();
	metadata_values.add(metadata_value);
	addMetadata(file_nodes, metadata_values);
    }


    static public void addMetadata(CollectionTreeNode[] file_nodes, ArrayList metadata_values)
    {
	// Check the list of metadata values is non-empty
	if (metadata_values.isEmpty()) {
	    return;
	}

	// Add the metadata to each file node in turn
	for (int i = 0; i < file_nodes.length; i++) {
	    File current_file = file_nodes[i].getFile();
	    DebugStream.println("Adding metadata to " + current_file.getAbsolutePath() + " - hex: " + Utility.debugUnicodeString(current_file.getAbsolutePath()));		

	    // Find which metadata.xml file needs editing
	    boolean applicable_metadata_xml_file_found = false;
	    File current_file_directory = (current_file.isDirectory() ? current_file : current_file.getParentFile());
	    String current_file_directory_path = current_file_directory.getAbsolutePath();
	    for (int j = 0; j < metadata_xml_files.size(); j++) {
		MetadataXMLFile metadata_xml_file = (MetadataXMLFile) metadata_xml_files.get(j);

		// This metadata.xml file is only applicable if it is at the same level as the file
		if (current_file_directory_path.equals(metadata_xml_file.getParentFile().getAbsolutePath())) {
		    applicable_metadata_xml_file_found = true;
		    metadata_xml_file.addMetadata(file_nodes[i], metadata_values);
		    if (!modified_metadata_xml_files.contains(metadata_xml_file)) {
			modified_metadata_xml_files.add(metadata_xml_file);
		    }
		}
	    }

	    // If no applicable metadata.xml file exists, we have to create a new one
	    if (!applicable_metadata_xml_file_found) {
		// Create a new (empty) metadata.xml file in the file's directory...
		File new_metadata_xml_file_file = new File(current_file_directory, "metadata.xml");
		XMLTools.writeXMLFile(new_metadata_xml_file_file, XMLTools.parseXMLFile("xml/metadata.xml", true));

		// ...load it...
		MetadataXMLFile new_metadata_xml_file = loadMetadataXMLFile(new_metadata_xml_file_file,true);

		// ...and add the metadata
		new_metadata_xml_file.addMetadata(file_nodes[i], metadata_values);
		if (!modified_metadata_xml_files.contains(new_metadata_xml_file)) {
		    modified_metadata_xml_files.add(new_metadata_xml_file);
		}
	    }
	}

	// Let any listeners know that the metadata has changed
	fireMetadataChangedEvent(file_nodes);
    }


    static public void addMetadataChangedListener(MetadataChangedListener metadata_changed_listener)
    {
	metadata_changed_listeners.add(metadata_changed_listener);
    }


    static public void clearMetadataXMLFiles()
    {
	metadata_xml_files.clear();
    }


    static private void fireMetadataChangedEvent(CollectionTreeNode[] file_nodes)
    {
	// Send the event off to all the MetadataChangedListeners
	for (int i = 0; i < metadata_changed_listeners.size(); i++) {
	    ((MetadataChangedListener) metadata_changed_listeners.get(i)).metadataChanged(file_nodes);
	}
    }


    /** Returns the metadata assigned to a file outside the collection, excluding folder-level/inherited metadata. */
    static public ArrayList getMetadataAssignedDirectlyToExternalFile(File file)
    {
	DebugStream.println("Getting metadata assigned directly to external file " + file + "...");

	// Build up a list of applicable metadata.xml files
	ArrayList applicable_metadata_xml_files = new ArrayList();

	File directory = (file.isDirectory() ? file : file.getParentFile());
	while (directory != null) {
	    File metadata_xml_file = new File(directory, "metadata.xml");
	    if (metadata_xml_file.exists() && !metadata_xml_file.isDirectory()) {
		// It is very important that shallower files come before deeper ones
		applicable_metadata_xml_files.add(0, new MetadataXMLFile(metadata_xml_file.getAbsolutePath()));
	    }

	    directory = directory.getParentFile();
	}

	// Get the metadata assigned to the specified file from the applicable metadata.xml files
	ArrayList assigned_metadata = getMetadataAssignedToFile(file, applicable_metadata_xml_files, false);

	// Remove any folder-level metadata
	for (int i = assigned_metadata.size() - 1; i >= 0; i--) {
	    if (((MetadataValue) assigned_metadata.get(i)).isInheritedMetadata()) {
		assigned_metadata.remove(i);
	    }
	}

	return assigned_metadata;
    }


	/** Returns the metadata assigned to a file inside the collection, excluding folder-level/inherited metadata. */
    static public ArrayList getMetadataAssignedDirectlyToFile(File file)
    {
		return getMetadataAssignedDirectlyToFile(file, false);
    }
	
    /** Returns the metadata assigned to a file inside the collection, excluding folder-level/inherited metadata. */
    static public ArrayList getMetadataAssignedDirectlyToFile(File file, boolean filenameEncodingMetaOnly)
    {
		
		// Get all the metadata assigned to the specified file...
		ArrayList assigned_metadata = getMetadataAssignedToFile(file, filenameEncodingMetaOnly);

		// ...then remove any folder-level metadata
		for (int i = assigned_metadata.size() - 1; i >= 0; i--) {
		    if (((MetadataValue) assigned_metadata.get(i)).isInheritedMetadata()) {
			assigned_metadata.remove(i);
		    }
		}

		return assigned_metadata;
		/*
		// Get all the metadata assigned to the specified file...
		// Build up a list of applicable metadata.xml files - which in this case 
		// is exclusively the metadata file at this file/folder's own level.
		ArrayList applicable_metadata_xml_files = new ArrayList();

		// Find the metadata.xml file (if any) that is at the same level as the file
		String file_directory_path = (file.isDirectory() ? file : file.getParentFile()).getAbsolutePath() + File.separator;
		for (int i = 0; i < metadata_xml_files.size(); i++) {
		    MetadataXMLFile metadata_xml_file = (MetadataXMLFile) metadata_xml_files.get(i);
		    
		    if (file_directory_path.equals(metadata_xml_file.getParentFile().getAbsolutePath() + File.separator)) {
				//System.err.println("Found metadata_xml_file: " + metadata_xml_file);
				applicable_metadata_xml_files.add(metadata_xml_file);
		    }
		}
		
		if(applicable_metadata_xml_files.size() == 0) {
		    return new ArrayList(0);
		}

		// Return the metadata assigned to the specified file from the applicable metadata.xml files
		return getMetadataAssignedToFile(file, applicable_metadata_xml_files, filenameEncodingMetaOnly);
		*/
    }
	

    /** Returns all the metadata assigned to a file inside the collection. */
    static public ArrayList getMetadataAssignedToFile(File file)
    {
	return getMetadataAssignedToFile(file, false);
    }
	
	/** Returns all the metadata assigned to a file inside the collection (or 
	 *	just gs.filenameEncoding if parameter filenameEncodingMetaOnly is true),
	 * 	including folder-level/inherited metadata.
	 */    
    static public ArrayList getMetadataAssignedToFile(File file, boolean filenameEncodingMetaOnly)
    {
	// Build up a list of applicable metadata.xml files
	ArrayList applicable_metadata_xml_files = new ArrayList();

	// Look at each loaded metadata.xml file to see if it is potentially applicable
	String file_directory_path = (file.isDirectory() ? file : file.getParentFile()).getAbsolutePath() + File.separator;
	for (int i = 0; i < metadata_xml_files.size(); i++) {
	    MetadataXMLFile metadata_xml_file = (MetadataXMLFile) metadata_xml_files.get(i);

	    // This metadata.xml file is only potentially applicable if it is above or at the same level as the file
	    if (file_directory_path.startsWith(metadata_xml_file.getParentFile().getAbsolutePath() + File.separator)) {
		applicable_metadata_xml_files.add(metadata_xml_file);
	    }
	}

	// Sort the metadataxml files in order starting from those in the 
	// topmost folders down to the one in the lowest level folder.
	Collections.sort(applicable_metadata_xml_files, metadataXMLFileComparator);
	// Return the metadata assigned to the specified file from the applicable metadata.xml files
	return getMetadataAssignedToFile(file, applicable_metadata_xml_files, filenameEncodingMetaOnly);
    }

	// package access method
    static ArrayList getMetadataAssignedToFile(File file, ArrayList applicable_metadata_xml_files,
						       boolean filenameEncodingMetaOnly)	
    {
	// Build up a list of metadata values assigned to this file
	ArrayList metadata_values_all = new ArrayList();

	// Look at each applicable metadata.xml file to see if it assigns metadata to this file
	for (int i = 0; i < applicable_metadata_xml_files.size(); i++) {
	    MetadataXMLFile metadata_xml_file = (MetadataXMLFile) applicable_metadata_xml_files.get(i);
	    DebugStream.println("Applicable metadata.xml file: " + metadata_xml_file);

	    ArrayList metadata_values = metadata_xml_file.getMetadataAssignedToFile(file, filenameEncodingMetaOnly);
	    for (int j = 0; j < metadata_values.size(); j++) {
		MetadataValue metadata_value = (MetadataValue) metadata_values.get(j);

		// Overriding metadata: remove any values with this metadata element
		if (metadata_value.isAccumulatingMetadata() == false) {
		    for (int k = metadata_values_all.size() - 1; k >= 0; k--) {
			if (((MetadataValue) metadata_values_all.get(k)).getMetadataElement().equals(metadata_value.getMetadataElement())) {
			    metadata_values_all.remove(k);
			}
		    }
		}

		metadata_values_all.add(metadata_value);
	    }
	}

	return metadata_values_all;
    }


    static public void loadMetadataXMLFiles(File directory, boolean skimfile)
    {
	// Make sure the directory (import) exists
	if (directory.exists() == false) {
	    return;
	}

	// Look recursively at each subfile of the directory for metadata.xml files
	File[] directory_files = directory.listFiles();
	for (int i = 0; i < directory_files.length; i++) {
	    File child_file = directory_files[i];
	    if (child_file.isDirectory()) {
		loadMetadataXMLFiles(child_file,skimfile);
	    }
	    else if (child_file.getName().equals("metadata.xml")) {
		loadMetadataXMLFile(child_file,skimfile);
	    }
	}
    }


    static private MetadataXMLFile loadMetadataXMLFile(File metadata_xml_file_file, boolean skimfile)
    {
	MetadataXMLFile metadata_xml_file = new MetadataXMLFile(metadata_xml_file_file.getAbsolutePath());
	if (metadata_xml_files.contains(metadata_xml_file)) {
	    // This metadata.xml file has already been loaded, so return the loaded object
	    return (MetadataXMLFile) metadata_xml_files.get(metadata_xml_files.indexOf(metadata_xml_file));
	}
	if(skimfile){
	   metadata_xml_file.skimFile();
	}
	metadata_xml_files.add(metadata_xml_file);
	return metadata_xml_file;
    }


    static public void removeMetadata(CollectionTreeNode file_node, ArrayList metadata_values)
    {
	removeMetadata(new CollectionTreeNode[] { file_node }, metadata_values);
    }


    static public void removeMetadata(CollectionTreeNode[] file_nodes, MetadataValue metadata_value)
    {
	ArrayList metadata_values = new ArrayList();
	metadata_values.add(metadata_value);
	removeMetadata(file_nodes, metadata_values);
    }


    static public void removeMetadata(CollectionTreeNode[] file_nodes, ArrayList metadata_values)
    {
	// Check the list of metadata values is non-empty
	if (metadata_values.isEmpty()) {
	    return;
	}

	// Remove the metadata from each file node in turn
	for (int i = 0; i < file_nodes.length; i++) {
	    File current_file = file_nodes[i].getFile();
	    DebugStream.println("Removing metadata from " + current_file.getAbsolutePath());

	    // Find which metadata.xml file needs editing
	    File current_file_directory = (current_file.isDirectory() ? current_file : current_file.getParentFile());
	    String current_file_directory_path = current_file_directory.getAbsolutePath();
	    for (int j = 0; j < metadata_xml_files.size(); j++) {
		MetadataXMLFile metadata_xml_file = (MetadataXMLFile) metadata_xml_files.get(j);

		// This metadata.xml file is only potentially applicable if it is above or at the same level as the file
		if (current_file_directory_path.startsWith(metadata_xml_file.getParentFile().getAbsolutePath())) {
		    metadata_xml_file.removeMetadata(file_nodes[i], metadata_values);
		    if (!modified_metadata_xml_files.contains(metadata_xml_file)) {
			modified_metadata_xml_files.add(metadata_xml_file);
		    }
		}
	    }
	}

	// Let any listeners know that the metadata has changed
	fireMetadataChangedEvent(file_nodes);
    }


    static public void removeMetadataChangedListener(MetadataChangedListener metadata_changed_listener)
    {
	metadata_changed_listeners.remove(metadata_changed_listener);
    }


    static public void replaceMetadata(CollectionTreeNode[] file_nodes, MetadataValue old_metadata_value, MetadataValue new_metadata_value)
    {
	// Replace the metadata in each file node in turn
	for (int i = 0; i < file_nodes.length; i++) {
	    File current_file = file_nodes[i].getFile();
	    DebugStream.println("Replacing metadata in " + current_file.getAbsolutePath());

	    // Find which metadata.xml file needs editing
	    File current_file_directory = (current_file.isDirectory() ? current_file : current_file.getParentFile());
	    String current_file_directory_path = current_file_directory.getAbsolutePath();
	    for (int j = 0; j < metadata_xml_files.size(); j++) {
		MetadataXMLFile metadata_xml_file = (MetadataXMLFile) metadata_xml_files.get(j);

		// This metadata.xml file is only applicable if it is at the same level as the file
		if (current_file_directory_path.equals(metadata_xml_file.getParentFile().getAbsolutePath())) {
		    metadata_xml_file.replaceMetadata(file_nodes[i], old_metadata_value, new_metadata_value);
		    if (!modified_metadata_xml_files.contains(metadata_xml_file)) {
			modified_metadata_xml_files.add(metadata_xml_file);
		    }
		}
	    }
	}

	// Let any listeners know that the metadata has changed
	fireMetadataChangedEvent(file_nodes);
    }


    /** Ensures that all the metadata is written to metadata.xml files. */
    static public void saveMetadataXMLFiles()
    {
	// Save the file currently loaded into memory out to disk
	MetadataXMLFile.saveLoadedFile();

	// If the collection is stored on a remote server, upload all the modified files now
	if (Gatherer.isGsdlRemote) {
	    if (modified_metadata_xml_files.isEmpty()) {
		DebugStream.println("No modified metadata.xml files to upload.");
		return;
	    }

	    // Upload the files modified since last time, then reset the list
	    Gatherer.remoteGreenstoneServer.uploadCollectionFiles(
                  CollectionManager.getLoadedCollectionName(), (File[]) modified_metadata_xml_files.toArray(new File[0]));
	    modified_metadata_xml_files.clear();
	}
    }


    static public void unloadMetadataXMLFile(File metadata_xml_file_file)
    {
	DebugStream.println("Unloading metadata.xml file " + metadata_xml_file_file);

	// Find the metadata.xml file in the list of loaded files, and remove it
	for (int i = 0; i < metadata_xml_files.size(); i++) {
	    MetadataXMLFile metadata_xml_file = (MetadataXMLFile) metadata_xml_files.get(i);
	    if (metadata_xml_file_file.getAbsolutePath().equals(metadata_xml_file.getAbsolutePath())) {
		metadata_xml_files.remove(i);
		break;
	    }
	}
    }

    static public void clearAllMetadataInCollection() {
	for (int j = 0; j < metadata_xml_files.size(); j++) {
	    MetadataXMLFile metadata_xml_file = (MetadataXMLFile) metadata_xml_files.get(j);
	    metadata_xml_file.clearAllMetadataInFile();
	    
	    // Let any listeners know that the metadata has changed
	    //fireMetadataChangedEvent(file_nodes); // sadly don't have file_nodes needed to do this

	    // all are modified	    
	    if (!modified_metadata_xml_files.contains(metadata_xml_file)) {		
		modified_metadata_xml_files.add(metadata_xml_file);
	    }
	    saveMetadataXMLFiles(); // saves final modified metaXML and then takes care of uploading all modified metaXML files to remote gsdl	
	}
    }
		
	/**
	  * Comparator to order MetadataXMLFiles in ascending order from
	  * those in a higher level folder to those in a lower level folder
	  * It is based on the assumption that all MetadataXMLFiles sent to
	  * it to compare will be linear descendants of one toplevel folder
	  * E.g. /A/metadata.xml, /A/B/metadata.xml, /A/B/C/D/metadata.xml.
	  * In other words, that each is a substring of one of the others until
	  * the toplevel folder is reached.
	*/
	private static class MetadataXMLFileComparator implements Comparator {
	    
	    public int compare(Object o1, Object o2) {
			if(!(o1 instanceof MetadataXMLFile)) {
				return -1;
			} else if (!(o2 instanceof MetadataXMLFile)) {
				return 1;
			} 

			// Both are MetadataXMLFiles objects. Remove the terminating
			// "metadata.xml" from their filenames to get their containing folder
			String filename1 = ((MetadataXMLFile)o1).getParentFile().getAbsolutePath();
			String filename2 = ((MetadataXMLFile)o2).getParentFile().getAbsolutePath();

			// if 1 is a prefix of 2, then 1 < 2 in the ordering (1 comes before 2)
			if(filename2.startsWith(filename1)) {
				return -1;
			} else if(filename1.startsWith(filename2)) {
				return 1;
			} else { 
				// unlikely that the metadata.xml files will be the same
				// or that neither is a prefix of the other
				return filename1.compareTo(filename2); // sorts in ascending order
			}      

		}

		public boolean equals(Object obj) {
			if(!(obj instanceof MetadataXMLFileComparator)) {
				return false;
			} 

			// else it is the same sort of comparator
			return true;
		}

	}

}
