/**
 *#########################################################################
 *
 * 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.net.*;
import java.util.*;
import java.util.jar.*;
import javax.swing.*;
import javax.swing.event.*;
import org.apache.xerces.parsers.*;
import org.greenstone.gatherer.Configuration;
import org.greenstone.gatherer.DebugStream;
import org.greenstone.gatherer.Dictionary;
import org.greenstone.gatherer.Gatherer;
import org.greenstone.gatherer.LocalGreenstone;
import org.greenstone.gatherer.greenstone.Classifiers;
import org.greenstone.gatherer.gui.DesignPaneHeader;
import org.greenstone.gatherer.gui.GComboBox;
import org.greenstone.gatherer.gui.GLIButton;
import org.greenstone.gatherer.remote.RemoteGreenstoneServer;
import org.greenstone.gatherer.util.JarTools;
import org.greenstone.gatherer.util.StaticStrings;
import org.greenstone.gatherer.util.Utility;
import org.greenstone.gatherer.util.XMLTools;
import org.w3c.dom.*;
import org.xml.sax.*;

/** This class is responsible for keeping track of all the classifiers assigned to this collection, and providing methods for adding and removing them.
 * @author John Thompson, Greenstone Digital Library, University of Waikato
 * @version 2.3
 */
public class ClassifierManager
    extends DOMProxyListModel
{
    /** The controls for editing the contents of this manager. */
    private Control controls = null;

    private DOMProxyListModel model;

    /** Constructor.
     * @see org.greenstone.gatherer.cdm.DynamicListModel
     * @see org.greenstone.gatherer.collection.CollectionManager
     */
    public ClassifierManager() {
	super(CollectionDesignManager.collect_config.getDocumentElement(), StaticStrings.CLASSIFY_ELEMENT, new Classifier());
	this.model = this;
	DebugStream.println("ClassifierManager: " + getSize() + " classifiers parsed.");

	// Force the assigned classifiers to be loaded and cached now
	for (int i = 0; i < getSize(); i++) {
	    getElementAt(i);
	}
    }


    /** Retrieve a list of the classifiers that are available to be added to the collection. */
    private Object[] getAvailableClassifiers()
    {
	ArrayList available = new ArrayList();

	// Add all the non-abstract core Greenstone classifiers
	ArrayList classifiers_list = Classifiers.getClassifiersList();
	for (int i = 0; i < classifiers_list.size(); i++) {
	    Classifier classifier = (Classifier) classifiers_list.get(i);
	    if (!classifier.isAbstract()) {
		available.add(classifier);
	    }
	}

	// Sort the available classifiers into alphabetical order
	Collections.sort(available);

	return available.toArray();
    }


    /** Method to assign a classifier.
     * @param classifier The base <strong>Classifier</strong> to assign.
     * @see org.greenstone.gatherer.cdm.DynamicListModel
     */
    private void assignClassifier(Classifier classifier) {
	if(!contains(classifier)) {
	    Element element = classifier.getElement();
	    // Locate where we should insert this new classifier.
	    Node target_node = CollectionConfiguration.findInsertionPoint(element);
	    add(root, classifier, target_node);
	}
    }


    /** Destructor. */
    public void destroy()
    {
	if (controls != null) {
	    controls.destroy();
	    controls = null;
	}
    }


    /** Method to retrieve the classifier with the given index.
     * @param index The index of the desired classifier as an <i>int</i>.
     * @return The requested Classifier or <i>null</i> if no such classifier exists.
     */
    public Classifier getClassifier(int index) {
	if(0 <= index && index < getSize()) {
	    return (Classifier) getElementAt(index);
	}
	return null;
    }

    /** Method to retrieve the control for this manager.
     * @return the Control for editing classifiers
     */
    public Control getControls() {
	if(controls == null) {
	    // Build controls
	    this.controls = new ClassifierControl();
	}
	return controls;
    }

    /** Called when the detail mode has changed which in turn may cause several design elements to be available/hidden
     * @param mode the new mode as an int
     */
    public void modeChanged(int mode) {

    }



    /** Determine if the Phind classifier has been assigned.
     * @return true if it has, false otherwise
     */
    public boolean isPhindClassifierAssigned() {
	for(int i = 0; i < getSize(); i++) {
	    Classifier classifier = (Classifier) getElementAt(i);
	    if(classifier.getName().equalsIgnoreCase(StaticStrings.PHIND_CLASSIFIER)) {
		return true;
	    }
	    classifier = null;
	}
	return false;
    }

    /** Determine if a DateList classifier has been assigned
     * @return true if it has, false otherwise
     */
    public boolean isDateListClassifierAssigned() {
	for(int i = 0; i < getSize(); i++) {
	    Classifier classifier = (Classifier) getElementAt(i);
	    if(classifier.getName().equalsIgnoreCase(StaticStrings.DATELIST_CLASSIFIER)) {
		return true;
	    }
	    classifier = null;
	}
	return false;
	
    }
    /** Method to move a classifier in the list order.
     * @param classifier the Classifier you want to move.
     * @param direction true to move the classifier up, false to move it down.
     * @param all true to move to move all the way, false for a single step.
     */
    private void moveClassifier(Classifier classifier, boolean direction, boolean all) {
	if(getSize() < 2) {
	    DebugStream.println("Not enough classifiers to allow moving.");
	    return;
	}
	if(all) {
	    // Move to top
	    if(direction) {
		// Remove the moving classifier
		remove(classifier);
		// Retrieve the first classifier
		Classifier first_classifier = (Classifier) getElementAt(0);
		// Add the moving classifier before the first classifier
		addBefore(classifier, first_classifier);
		first_classifier = null;
	    }
	    else {
		// Remove the moving classifier
		remove(classifier);
		// And add after last classifier
		add(getSize(), classifier);
	    }
	}
	else {
	    // Try to move the classifier one step in the desired direction.
	    int index = indexOf(classifier);
	    ///ystem.err.println("Index of " + classifier + " = " + index);
	    if(direction) {
		index--;
		if(index < 0) {
		    String args[] = new String[2];
		    args[0] = Dictionary.get("CDM.ClassifierManager.Classifier");
		    args[1] = classifier.getName();
		    JOptionPane.showMessageDialog(Gatherer.g_man, Dictionary.get("CDM.Move.At_Top", args), Dictionary.get("CDM.Move.Title"), JOptionPane.ERROR_MESSAGE);
		    return;
		}
		remove(classifier);
		add(index, classifier);
	    }
	    else {
		index++;
		if(index >= getSize()) {
		    String args[] = new String[2];
		    args[0] = Dictionary.get("CDM.ClassifierManager.Classifier_Str");
		    args[1] = classifier.getName();
		    JOptionPane.showMessageDialog(Gatherer.g_man, Dictionary.get("CDM.Move.At_Bottom", args), Dictionary.get("CDM.Move.Title"), JOptionPane.ERROR_MESSAGE);
		    return;
		}
		remove(classifier);
		add(index, classifier);
	    }
	}
	
	// tell the format manager to update the names of its format statements
	Gatherer.c_man.getCollection().cdm.format_manager.refresh();
    }
    
    /** This method removes an assigned classifier. I was tempted to call it unassign, but remove is more consistant. Note that there is no way to remove a classifier from the library.
     * @param classifier The Classifier to remove
     * @see org.greenstone.gatherer.cdm.DynamicListModel
     */
    private void removeClassifier(Classifier classifier) {
	remove(classifier);
    }


    /** A class which provides controls for assigned and editing classifiers. */
    private class ClassifierControl
	extends JPanel
	implements Control {
	/** A combobox containing all of the known classifiers, including those that may have already been assigned. */
	private JComboBox classifier_combobox = null;
	/** Button for adding classifiers. */
	private JButton add = null;
	/** Button for configuring the selected classifier. */
	private JButton configure = null;
	private JButton move_down_button;
	private JButton move_up_button;

	/** Button to remove the selected classifier. */
	private JButton remove = null;

	/** A list of assigned classifiers. */
	private JList classifier_list = null;

	/** Constructor.
	 * @see org.greenstone.gatherer.cdm.ClassifierManager.ClassifierControl.AddListener
	 * @see org.greenstone.gatherer.cdm.ClassifierManager.ClassifierControl.ConfigureListener
	 * @see org.greenstone.gatherer.cdm.ClassifierManager.ClassifierControl.RemoveListener
	 */
	public ClassifierControl()
	{
	    // Create
	    add = new GLIButton(Dictionary.get("CDM.ClassifierManager.Add"), Dictionary.get("CDM.ClassifierManager.Add_Tooltip"));
	    
	    JPanel button_pane = new JPanel();
	    JPanel central_pane = new JPanel();
	    
configure = new GLIButton(Dictionary.get("CDM.ClassifierManager.Configure"), Dictionary.get("CDM.ClassifierManager.Configure_Tooltip"));
	    configure.setEnabled(false);
	    
	    JPanel header_pane = new DesignPaneHeader("CDM.GUI.Classifiers", "classifiers");

	    ClassifierComboboxListener ccl = new ClassifierComboboxListener();
	    classifier_combobox = new JComboBox(getAvailableClassifiers());
	    classifier_combobox.setOpaque(!Utility.isMac());
	    classifier_combobox.setEditable(false);
	    if(classifier_combobox.getItemCount() > 0) {
		classifier_combobox.setSelectedIndex(0);
		ccl.itemStateChanged(new ItemEvent(classifier_combobox, 0, null, ItemEvent.SELECTED));
	    }

	    JLabel classifier_label = new JLabel(Dictionary.get("CDM.ClassifierManager.Classifier"));
	    
	    classifier_list = new JList(model);
	    classifier_list.setOpaque(true);
	    classifier_list.setSelectionMode(ListSelectionModel.SINGLE_SELECTION);
	    JLabel classifier_list_label = new JLabel(Dictionary.get("CDM.ClassifierManager.Assigned"));
	    
	    classifier_list_label.setOpaque(true);
	    
	    JPanel classifier_list_pane = new JPanel();
	    JPanel classifier_pane = new JPanel();
	    remove = new GLIButton(Dictionary.get("CDM.ClassifierManager.Remove"), Dictionary.get("CDM.ClassifierManager.Remove_Tooltip"));
	    remove.setEnabled(false);
	    
	    JPanel temp = new JPanel(new BorderLayout());

	    JPanel move_button_pane = new JPanel();

	    move_up_button = new GLIButton(Dictionary.get("CDM.Move.Move_Up"), JarTools.getImage("arrow-up.gif"), Dictionary.get("CDM.Move.Move_Up_Tooltip"));
	    move_up_button.setEnabled(false);
	    
	    move_down_button = new GLIButton(Dictionary.get("CDM.Move.Move_Down"), JarTools.getImage("arrow-down.gif"), Dictionary.get("CDM.Move.Move_Down_Tooltip"));
	    move_down_button.setEnabled(false);
	    
	    // Listeners
	    add.addActionListener(new AddListener());
	    classifier_combobox.addItemListener(ccl);
	    configure.addActionListener(new ConfigureListener());
	    remove.addActionListener(new RemoveListener());
	    remove.addActionListener(CollectionDesignManager.buildcol_change_listener);
	    classifier_list.addMouseListener(new ClickListener());
	    classifier_list.addListSelectionListener(new ListListener());
	    ccl = null;

	    MoveListener ml = new MoveListener();
	    move_down_button.addActionListener(ml);
	    move_down_button.addActionListener(CollectionDesignManager.buildcol_change_listener);
	    move_up_button.addActionListener(ml);
	    move_up_button.addActionListener(CollectionDesignManager.buildcol_change_listener);

	    // Layout
	    move_button_pane.setLayout(new GridLayout(4,1));
	    move_button_pane.add(move_up_button);
	    move_button_pane.add(new JPanel());
	    move_button_pane.add(new JPanel());
	    move_button_pane.add(move_down_button);

	    classifier_list_label.setBorder(BorderFactory.createEmptyBorder(0,2,0,2));

	    classifier_list_pane.setLayout(new BorderLayout());
	    classifier_list_pane.add(classifier_list_label, BorderLayout.NORTH);
	    classifier_list_pane.add(new JScrollPane(classifier_list), BorderLayout.CENTER);
	    classifier_list_pane.add(move_button_pane, BorderLayout.EAST);

	    classifier_label.setBorder(BorderFactory.createEmptyBorder(0,0,5,0));

	    classifier_pane.setBorder(BorderFactory.createEmptyBorder(5,0,5,0));
	    classifier_pane.setLayout(new BorderLayout(5,0));
	    classifier_pane.add(classifier_label, BorderLayout.WEST);
	    classifier_pane.add(classifier_combobox, BorderLayout.CENTER);

	    button_pane.setLayout(new GridLayout(1, 3));
	    button_pane.add(add);
	    button_pane.add(configure);
	    button_pane.add(remove);

	    temp.add(classifier_pane, BorderLayout.NORTH);
	    temp.add(button_pane, BorderLayout.SOUTH);

	    central_pane.setBorder(BorderFactory.createEmptyBorder(5,0,0,0));
	    central_pane.setLayout(new BorderLayout());
	    central_pane.add(classifier_list_pane, BorderLayout.CENTER);
	    central_pane.add(temp, BorderLayout.SOUTH);

	    setBorder(BorderFactory.createEmptyBorder(0,5,0,0));
	    setLayout(new BorderLayout());
	    add(header_pane, BorderLayout.NORTH);
	    add(central_pane, BorderLayout.CENTER);
	}

	/** Method which acts like a destructor, tidying up references to persistant objects.
	 */
	public void destroy() {
	    add = null;
	    classifier_combobox = null;
	    classifier_list = null;
	    configure = null;
	    //instructions = null;
	    remove = null;
	}

	public void gainFocus() {
	}

	public void loseFocus() {
	}


	private class AddListener
	    implements ActionListener
	{
	    public void actionPerformed(ActionEvent event)
	    {
		if (classifier_combobox.getSelectedItem() != null) {
		    // This must be done on a new thread for the remote building code
		    new AddClassifierTask(classifier_combobox.getSelectedItem().toString()).start();
		}
	    }
	}


	private class AddClassifierTask
	    extends Thread
	{
	    private String classifier_name;

	    public AddClassifierTask(String classifier_name)
	    {
		this.classifier_name = classifier_name;
	    }

	    public void run()
	    {
		// Retrieve the classifier
		Classifier classifier = Classifiers.getClassifier(classifier_name, true);
		if (classifier == null) {
		    System.err.println("Error: getClassifier() returned null.");
		    return;
		}

		// Create a new element in the DOM
		Element new_classifier_element = CollectionConfiguration.createElement(StaticStrings.CLASSIFY_ELEMENT);
		new_classifier_element.setAttribute(StaticStrings.TYPE_ATTRIBUTE, classifier.getName());
		Classifier new_classifier = new Classifier(new_classifier_element, classifier);

		ArgumentConfiguration ac = new ArgumentConfiguration(new_classifier);
		ac.addOKButtonActionListener(CollectionDesignManager.buildcol_change_listener);
		if (ac.display()) {
		    assignClassifier(new_classifier);
		    classifier_list.setSelectedValue(new_classifier, true);
		}
	    }
	}


	/** This listener reacts to changes in the current selection of the classifier combobox. */
	private class ClassifierComboboxListener
	    implements ItemListener {
	    /** When a user selects a certain classifier, update the tooltip to show the classifier description. */
	    public void itemStateChanged(ItemEvent event) {
		if(event.getStateChange() == ItemEvent.SELECTED) {
		    // Retrieve the selected classifier
		    Classifier current_selection = (Classifier) classifier_combobox.getSelectedItem();
		    // And reset the tooltip. 
		    classifier_combobox.setToolTipText(Utility.formatHTMLWidth(current_selection.getDescription(), 40));
		    current_selection = null;
		}
	    }
	}


	/** Listens for double clicks apon the list and react as if the configure button was pushed. */
	private class ClickListener
	    extends MouseAdapter {
	    /** Called whenever the mouse is clicked over a registered component, we use this to chain through to the configure prompt.
	     * @param event A <strong>MouseEvent</strong> containing information about the mouse click.
	     */
	    public void mouseClicked(MouseEvent event) {
		if(event.getClickCount() == 2 ) {
		    if(!classifier_list.isSelectionEmpty()) {
			Classifier classifier = (Classifier) classifier_list.getSelectedValue();
			ArgumentConfiguration ac = new ArgumentConfiguration(classifier);
			ac.addOKButtonActionListener(CollectionDesignManager.buildcol_change_listener);
			if (ac.display()) {
			    refresh(classifier);
			}
			ac.destroy();
			ac = null;
		    }
		}
	    }
	}

	/** This class listens for actions upon the configure button in the controls, and if detected creates a new ArgumentConfiguration dialog box to allow for configuration.
	 */
	private class ConfigureListener
	    implements ActionListener {
	    /** Any implementation of <i>ActionListener</i> must include this method so that we can be informed when an action has occured on one of our target controls.
	     * @param event An <strong>ActionEvent</strong> containing information garnered from the control action.
	     * @see org.greenstone.gatherer.cdm.ArgumentConfiguration
	     * @see org.greenstone.gatherer.cdm.Classifier
	     */
	    public void actionPerformed(ActionEvent event) {
		if(!classifier_list.isSelectionEmpty()) {
		   Classifier classifier = (Classifier) classifier_list.getSelectedValue();
		   ArgumentConfiguration ac = new ArgumentConfiguration(classifier);
		   ac.addOKButtonActionListener(CollectionDesignManager.buildcol_change_listener);
		   if (ac.display()) {
		       refresh(classifier);
		   }
		   ac.destroy();
		   ac = null;
		}
	    }
	}

	/** listens for changes in the list selection and enables the configure and remove buttons if there is a selection, disables them if there is no selection */
	private class ListListener
	    implements ListSelectionListener {

	    public void valueChanged(ListSelectionEvent e) {
		if (!e.getValueIsAdjusting()) { // we get two events for one change in list selection - use the false one ( the second one)
		    if (classifier_list.isSelectionEmpty()) {
			move_up_button.setEnabled(false);
			move_down_button.setEnabled(false);
			configure.setEnabled(false);
			remove.setEnabled(false);
		    }
		    else {
			configure.setEnabled(true);
			remove.setEnabled(true);
			int selected_index = classifier_list.getSelectedIndex();
			move_up_button.setEnabled(selected_index !=0);
			move_down_button.setEnabled(selected_index != model.getSize()-1);
		    }
		}
	    }
	}

	/** Listens for actions apon the move buttons in the manager controls, and if detected calls the <i>moveClassifier()</i> method of the manager with the appropriate details. */
	private class MoveListener
	    implements ActionListener {
	    /** Any implementation of <i>ActionListener</i> must include this method so that we can be informed when an action has occured on one of our target controls.
	     * @param event An <strong>ActionEvent</strong> containing information garnered from the control action.
	     */
	    public void actionPerformed(ActionEvent event) {
		if(!classifier_list.isSelectionEmpty()) {
		    Object object = classifier_list.getSelectedValue();
		    if(object instanceof Classifier) {
			Classifier classifier = (Classifier) object;
			if(event.getSource() == move_up_button) {
			    moveClassifier(classifier, true, false);
			}
			else if(event.getSource() == move_down_button) {
			    moveClassifier(classifier, false, false);
			}
			classifier_list.setSelectedValue(classifier, true);
		    }
		}
	    }
	}

	/** This class listens for actions upon the remove button in the controls, and if detected calls the <i>removeClassifier()</i> method.
	 */
	private class RemoveListener
	    implements ActionListener {
	    /** Any implementation of <i>ActionListener</i> must include this method so that we can be informed when an action has occured on one of our target controls.
	     * @param event An <strong>ActionEvent</strong> containing information garnered from the control action.
	     */
	    public void actionPerformed(ActionEvent event) {
		if(classifier_list.isSelectionEmpty()) {
		    remove.setEnabled(false);
		    return;
		}
		int selected_index = classifier_list.getSelectedIndex();
		Object selected_classifier = classifier_list.getSelectedValue();
		if (!(selected_classifier instanceof Classifier)) {
		    return; // what else could we have here???
		}
		removeClassifier((Classifier)selected_classifier);

		if (selected_index >= classifier_list.getModel().getSize()) {
		    selected_index--;
		}
		if (selected_index >=0) {
		    classifier_list.setSelectedIndex(selected_index);
		} else {
		    // no more classifiers in the list
		    remove.setEnabled(false);
		}
	    }
	}
    }
}
