/*
 * File:     ControlledVocabulary.java
 * Project:  MPI Linguistic Application
 * Date:     03 April 2006
 *
 * Copyright (C) 2001-2006  Max Planck Institute for Psycholinguistics
 *
 * 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., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
 */

package mpi.eudico.server.corpora.clomimpl.type;

import mpi.eudico.server.corpora.util.ACMEditEvent;
import mpi.eudico.server.corpora.util.ACMEditableObject;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.Comparator;
import java.util.Iterator;
import java.util.List;


/**
 * A ControlledVocabulary holds a restricted list of entries.<br>
 * The entries should be unique in the sence that the value of the  entries
 * must be unique.  Pending: we are using a List now and take care ourselves
 * that all elements are unique. Could use some kind of Set when we would
 * decide to let CVEntry  override the equals() method. Pending: should the
 * entries always be sorted (alphabetically)?  <b>Note:</b> this class is not
 * thread-safe.
 *
 * @author Han Sloetjes
 * @version jun 04
 * @version apr 05, added equals() method
 */
public class ControlledVocabulary {
    /** constant for the move-to-top edit type */
    public static final int MOVE_TO_TOP = 0;

    /** constant for the move-up edit type */
    public static final int MOVE_UP = 1;

    /** constant for the move-down edit type */
    public static final int MOVE_DOWN = 2;

    /** constant for the move-to-bottom edit type */
    public static final int MOVE_TO_BOTTOM = 3;
    private String name;
    private String description;
    private List entries;
    private EntryComparator entryComparator;
    private ACMEditableObject acmEditable;

    /**
     * Creates a CV with the specified name as identifier.
     *
     * @param name the name of the CV
     *
     * @see ControlledVocabulary(String, String, ACMEditableObject)
     */
    public ControlledVocabulary(String name) {
        this(name, null, null);
    }

    /**
     * Creates a CV with the specified name as identifier.
     *
     * @param name the name of the CV
     * @param acmEditable the ACMEditableObject to be notified of changes
     *
     * @see ControlledVocabulary(String, String, ACMEditableObject)
     */
    public ControlledVocabulary(String name, ACMEditableObject acmEditable) {
        this(name, null, acmEditable);
    }

    /**
     * Creates a CV with the specified name and description.
     *
     * @param name the name of the CV
     * @param description the description of the CV
     *
     * @exception IllegalArgumentException when the name is <code>null</code>
     *            or of length 0
     */
    public ControlledVocabulary(String name, String description) {
        this(name, description, null);
    }

    /**
     * Creates a CV with the specified name and description and the specified
     * ACMEditableObject to be notified of changes in the CV.
     *
     * @param name the name of the CV
     * @param description the description of the CV
     * @param acmEditable the ACMEditableObject to be notified of changes
     *
     * @exception IllegalArgumentException when the name is <code>null</code>
     *            or of length 0
     */
    public ControlledVocabulary(String name, String description,
        ACMEditableObject acmEditable) {
        if ((name == null) || (name.length() == 0)) {
            throw new IllegalArgumentException(
                "The name can not be null or empty.");
        }

        this.name = name;
        this.description = description;
        this.acmEditable = acmEditable;

        entries = new ArrayList();
        entryComparator = new EntryComparator();
    }

    /**
     * Sets the name of this CV.
     *
     * @param name the new name of the CV
     *
     * @exception IllegalArgumentException when the name is <code>null</code>
     *            or of length 0
     */
    public void setName(String name) {
        if ((name == null) || (name.length() == 0)) {
            throw new IllegalArgumentException(
                "The name can not be null or empty.");
        }

        this.name = name;

        handleModified();
    }

    /**
     * Returns the name of the CV.
     *
     * @return the name of this CV
     */
    public String getName() {
        return name;
    }

    /**
     * Sets the description of this CV.
     *
     * @param description the new description of the CV
     */
    public void setDescription(String description) {
        this.description = description;

        handleModified();
    }

    /**
     * Returns the description of the CV.
     *
     * @return the description of the CV, can be <code>null</code>
     */
    public String getDescription() {
        return description;
    }

    /**
     * Returns the connected ACMEditableObject.
     *
     * @return the connected ACMEditableObject
     */
    public ACMEditableObject getACMEditableObject() {
        return acmEditable;
    }

    /**
     * Sets the connected ACMEditableObject.
     *
     * @param acmEditable the connected ACMEditableObject
     */
    public void setACMEditableObject(ACMEditableObject acmEditable) {
        this.acmEditable = acmEditable;
    }

    /**
     * Adds a new CVEntry to the List.
     *
     * @param entry the new entry
     *
     * @return true if the entry was succesfully added, false otherwise
     */
    public boolean addEntry(CVEntry entry) {
        if (entry == null) {
            return false;
        }

        Iterator it = entries.iterator();

        while (it.hasNext()) {
            if (((CVEntry) it.next()).getValue().equals(entry.getValue())) {
                return false;
            }
        }

        entries.add(entry);

        handleModified();

        return true;
    }

    /**
     * A shorthand for adding more than one CVEntry at a time.
     *
     * @param entries an array of entries
     */
    public void addAll(CVEntry[] entries) {
        if (entries != null) {
            for (int i = 0; i < entries.length; i++) {
                addEntry(entries[i]);
            }
        }

        handleModified();
    }

    /**
     * Returns the CVEntry with the specified value, if present.
     *
     * @param value the value of the entry
     *
     * @return the CVEntry with the specified value, or null
     */
    public CVEntry getEntryWithValue(String value) {
        CVEntry entry = null;

        if (value == null) {
            return entry;
        }

        for (int i = 0; i < entries.size(); i++) {
            if (((CVEntry) entries.get(i)).getValue().equals(value)) { //ignore case ??
                entry = (CVEntry) entries.get(i);

                break;
            }
        }

        return entry;
    }

    /**
     * Removes an entry from the Vocabulary.
     *
     * @param entry the entry to remove
     */
    public void removeEntry(CVEntry entry) {
        entries.remove(entry);

        handleModified();
    }

    /**
     * Removes a set of entries from the Vocabulary.
     *
     * @param entryArray the entries to remove
     */
    public void removeEntries(CVEntry[] entryArray) {
        if (entryArray == null) {
            return;
        }

        boolean removed = false;

        for (int i = 0; i < entryArray.length; i++) {
            boolean b = entries.remove(entryArray[i]);

            if (b) {
                removed = true;
            }
        }

        if (removed) {
            handleModified();
        }
    }

    /**
     * Removes the CVEntry with the specified value from the CV, if present.
     *
     * @param value the value to remove
     *
     * @return true if the value was removed from the CV, false otherwise
     */
    public void removeValue(String value) {
        if (value == null) {
            return;
        }

        CVEntry en;
        boolean removed = false;

        for (int i = 0; i < entries.size(); i++) {
            en = (CVEntry) entries.get(i);

            if (en.getValue().equals(value)) { //ignore case ??
                removed = entries.remove(en);

                break;
            }
        }

        if (removed) {
            handleModified();
        }
    }

    /**
     * Removes all existing CVEntries and adds the specified new entries.
     *
     * @param newEntries the new entries for the CV
     */
    public void replaceAll(CVEntry[] newEntries) {
        if (newEntries == null) {
            return;
        }

        entries.clear();

        addAll(newEntries);

        handleModified();
    }

    /**
     * This is a checked way to change the value of an existing CVEntry.<br>
     * This method (silently) does nothing when the specified entry is not  in
     * this ControlledVocabulary, or when the value already exists in this
     * CV.
     *
     * @param entry the CVEntry
     * @param value the new value for the entry
     */
    public void modifyEntryValue(CVEntry entry, String value) {
        if ((entry == null) || (value == null)) {
            return;
        }

        if (!entries.contains(entry)) {
            return;
        }

        if (containsValue(value)) {
            return;
        }

        // the entry is in the list and the new value is not, 
        // so it is allowed to change the value
        entry.setValue(value);

        handleModified();
    }

    /**
     * Moves a set of entries up or down in the list of entries of the Vocabulary.
     *
     * @param entryArray the entries to move
     * @param moveType the type of move action, one of MOVE_TO_TOP, MOVE_UP,
     *        MOVE_DOWN or MOVE_TO_BOTTOM
     */
    public void moveEntries(CVEntry[] entryArray, int moveType) {
        switch (moveType) {
        case MOVE_TO_TOP:
            moveToTop(entryArray);

            break;

        case MOVE_UP:
            moveUp(entryArray);

            break;

        case MOVE_DOWN:
            moveDown(entryArray);

            break;

        case MOVE_TO_BOTTOM:
            moveToBottom(entryArray);

            break;

        default:
            break;
        }
    }

    /**
     * Moves the CVEntries in the array to the top of the list.<br>
     * It is assumed that the entries come in ascending order!
     *
     * @param entryArray the array of CVEntry objects
     */
    private void moveToTop(CVEntry[] entryArray) {
        if ((entryArray == null) || (entryArray.length == 0)) {
            return;
        }

        CVEntry entry = null;
        boolean moved = false;

        for (int i = 0; i < entryArray.length; i++) {
            entry = entryArray[i];

            boolean removed = entries.remove(entry);

            if (removed) {
                moved = true;
                entries.add(i, entry);
            }
        }

        if (moved) {
            handleModified();
        }
    }

    /**
     * Moves the CVEntries in the array up one position in the list.<br>
     * It is assumed that the entries come in ascending order!
     *
     * @param entryArray the array of CVEntry objects
     */
    private void moveUp(CVEntry[] entryArray) {
        if ((entryArray == null) || (entryArray.length == 0)) {
            return;
        }

        CVEntry entry = null;
        boolean moved = false;
        int curIndex;

        for (int i = 0; i < entryArray.length; i++) {
            entry = entryArray[i];
            curIndex = entries.indexOf(entry);

            if (curIndex > 0) {
                boolean removed = entries.remove(entry);

                if (removed) {
                    moved = true;
                    entries.add(curIndex - 1, entry);
                }
            }
        }

        if (moved) {
            handleModified();
        }
    }

    /**
     * Moves the CVEntries in the array down one position in the list.<br>
     * It is assumed that the entries come in ascending order!
     *
     * @param entryArray the array of CVEntry objects
     */
    private void moveDown(CVEntry[] entryArray) {
        if ((entryArray == null) || (entryArray.length == 0)) {
            return;
        }

        CVEntry entry = null;
        boolean moved = false;
        int curIndex;

        for (int i = entryArray.length - 1; i >= 0; i--) {
            entry = entryArray[i];
            curIndex = entries.indexOf(entry);

            if ((curIndex >= 0) && (curIndex < (entries.size() - 1))) {
                boolean removed = entries.remove(entry);

                if (removed) {
                    moved = true;
                    entries.add(curIndex + 1, entry);
                }
            }
        }

        if (moved) {
            handleModified();
        }
    }

    /**
     * Moves the CVEntries in the array to the bottom of the list.<br>
     * It is assumed that the entries come in ascending order!
     *
     * @param entryArray the array of CVEntry objects
     */
    private void moveToBottom(CVEntry[] entryArray) {
        if ((entryArray == null) || (entryArray.length == 0)) {
            return;
        }

        CVEntry entry = null;
        boolean moved = false;

        for (int i = 0; i < entryArray.length; i++) {
            entry = entryArray[i];

            boolean removed = entries.remove(entry);

            if (removed) {
                moved = true;
                entries.add(entry);
            }
        }

        if (moved) {
            handleModified();
        }
    }

    /**
     * Removes all entries from this ControlledVocabulary.
     */
    public void clear() {
        entries.clear();

        handleModified();
    }

    /**
     * Checks whether the specified CVEntry is in this CV.<br>
     * <b>Note:</b> This only checks for object equality.
     *
     * @param entry the CVEntry
     *
     * @return true if entry is in the CV, false otherwise
     *
     * @see #containsValue(String)
     */
    public boolean contains(CVEntry entry) {
        if (entry == null) {
            return false;
        } else {
            return entries.contains(entry);
        }
    }

    /**
     * Checks whether there is a CVEntry with the specified value in this
     * CV.<br>
     *
     * @param value the value
     *
     * @return true if there is an entry with this value in the CV, false
     *         otherwise
     *
     * @see #contains(CVEntry)
     */
    public boolean containsValue(String value) {
        if (value == null) {
            return false;
        }

        for (int i = 0; i < entries.size(); i++) {
            if (((CVEntry) entries.get(i)).getValue().equals(value)) { //ignore case??

                return true;
            }
        }

        return false;
    }

    /**
     * Returns an array containing all entries in this Vocabulary.
     *
     * @return an array of entries
     */
    public CVEntry[] getEntries() {
        return (CVEntry[]) entries.toArray(new CVEntry[] {  });
    }

    /**
     * Returns a sorted array of entries. The values are sorted  using the
     * String.compareTo(String) method.
     *
     * @return a sorted array of CVEntry objects
     */
    public CVEntry[] getEntriesSortedByAlphabet() {
        CVEntry[] allEntries = getEntries();
        Arrays.sort(allEntries, entryComparator);

        return allEntries;
    }

    /**
     * Returns an array containing the values (Strings) of the entries. This is
     * convenience method to get a view on the entry values in the CV.
     *
     * @return an array of Strings containing the vallues in this CV
     */
    public String[] getEntryValues() {
        String[] values = new String[entries.size()];

        for (int i = 0; i < entries.size(); i++) {
            values[i] = ((CVEntry) entries.get(i)).getValue();
        }

        return values;
    }

    /**
     * Returns an array containing the values (Strings) of the entries, ordered alphabetically.<br>
     * This is convenience method to get an ordered view on the entry values
     * in the CV.
     *
     * @return an sorted array of Strings containing the vallues in this CV
     */
    public String[] getValuesSortedByAlphabet() {
        String[] values = getEntryValues();
        Arrays.sort(values);

        return values;
    }

    /**
     * Sends a general notification to an interested ACMEditableObject, such as
     * a Transcription, that this CV has been changed.<br>
     * This method does not specify the kind of modification.
     */
    protected void handleModified() {
        if (acmEditable != null) {
            acmEditable.modified(ACMEditEvent.CHANGE_CONTROLLED_VOCABULARY, this);
        }
    }

    /**
     * Override Object's toString method to return the name of the CV.
     *
     * @return the name of the CV
     */
    public String toString() {
        return name;
    }

    /**
     * Overrides <code>Object</code>'s equals method by checking all  fields of
     * the other object to be equal to all fields in this  object.
     *
     * @param obj the reference object with which to compare
     *
     * @return true if this object is the same as the obj argument; false
     *         otherwise
     */
    public boolean equals(Object obj) {
        if (obj == null) {
            // null is never equal
            return false;
        }

        if (obj == this) {
            // same object reference 
            return true;
        }

        if (!(obj instanceof ControlledVocabulary)) {
            // it should be a MediaDescriptor object
            return false;
        }

        // check the fields
        ControlledVocabulary other = (ControlledVocabulary) obj;

        if (!name.equals(other.getName())) {
            return false;
        } else {
            if ((description != null) &&
                    !description.equals(other.getDescription())) {
                return false;
            } else if ((other.getDescription() != null) &&
                    !other.getDescription().equals(description)) {
                return false;
            }
        }

        // compare cventries, ignoring the order in the list
        boolean entriesEqual = true;

        CVEntry entry;
        CVEntry[] otherEntries = other.getEntries();
loop: 
        for (int i = 0; i < entries.size(); i++) {
            entry = (CVEntry) entries.get(i);

            for (int j = 0; j < otherEntries.length; j++) {
                if (entry.equals(otherEntries[j])) {
                    continue loop;
                }
            }

            // if we get here the cv entries are unequal
            entriesEqual = false;

            break;
        }

        return entriesEqual;
    }

    ////////////////////////////////
    // inner class:: EntryComparator
    ////////////////////////////////

    /**
     * A Comparator that compares CVEntry objects by simply comparing their
     * values.  <b>Note:</b> this comparator imposes orderings that are
     * inconsistent with equals.
     *
     * @author Han Sloetjes
     */
    private class EntryComparator implements Comparator {
        /**
         * Compares two CVEntry objects.<br>
         * It compares the <i>value</i> fields of the entries. Since the
         * values in  a CV are supposed to be unique this method should never
         * return 0. This  Comparator does not check on this however. When any
         * of the objects is not an instance of CVEntry a ClassCastException
         * is thrown.
         *
         * @param entry1 the first entry to compare
         * @param entry2 the second entry to compare
         *
         * @return -1 if entry1 is less than entry2, 0 when they are equal or 1
         *         when entry2 is less then entry 1
         *
         * @exception ClassCastException when any of the objects is not an
         *            CVEntry
         */
        public int compare(Object entry1, Object entry2) {
            if (!(entry1 instanceof CVEntry) || !(entry2 instanceof CVEntry)) {
                throw new ClassCastException(
                    "Both objects should be CVEntry objects.");
            }

            return ((CVEntry) entry1).getValue().compareTo(((CVEntry) entry2).getValue());
        }
    }
}
