/**
 *############################################################################
 * 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.CollectionTreeNode;
import org.greenstone.gatherer.remote.RemoteGreenstoneServer;
import org.greenstone.gatherer.util.XMLTools;


/** 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();


    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());

	    // 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(current_file, 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);

		// ...and add the metadata
		new_metadata_xml_file.addMetadata(current_file, 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);

	// 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)
    {
	// Get all the metadata assigned to the specified file...
	ArrayList assigned_metadata = getMetadataAssignedToFile(file);

	// ...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;
    }


    /** Returns all the metadata assigned to a file inside the collection. */
    static public ArrayList getMetadataAssignedToFile(File file)
    {
	// 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);
	    }
	}

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


    static private ArrayList getMetadataAssignedToFile(File file, ArrayList applicable_metadata_xml_files)
    {
	// 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);
	    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)
    {
	// 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);
	    }
	    else if (child_file.getName().equals("metadata.xml")) {
		loadMetadataXMLFile(child_file);
	    }
	}
    }


    static private MetadataXMLFile loadMetadataXMLFile(File metadata_xml_file_file)
    {
	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));
	}

	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(current_file, 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(current_file, 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);
    }


    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 uploadModifiedMetadataXMLFiles()
    {
	// This is only necessary when the collection is stored on a remote server
	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
	    RemoteGreenstoneServer.uploadCollectionFiles(Gatherer.c_man.getCollection().getName(), (File[]) modified_metadata_xml_files.toArray(new File[0]));
	    modified_metadata_xml_files.clear();
	}
    }
}
