/* @(#)KeyLabelData.java   14 September 2006 */

/* Useful imports */

import edu.neu.ccs.*;
import edu.neu.ccs.gui.*;
import edu.neu.ccs.codec.*;
import edu.neu.ccs.console.*;
import edu.neu.ccs.filter.*;
import edu.neu.ccs.jpf.*;
import edu.neu.ccs.parser.*;
import edu.neu.ccs.pedagogy.*;
import edu.neu.ccs.quick.*;
import edu.neu.ccs.util.*;

import java.awt.*;
import java.awt.event.*;
import java.awt.geom.*;
import java.awt.font.*;
import java.awt.image.*;
import javax.swing.*;
import javax.swing.border.*;
import java.io.*;
import java.util.*;
import java.math.*;
import java.beans.*;
import java.lang.reflect.*;
import java.net.URL;
import java.util.regex.*;
import java.text.ParseException;

/**
 * <p>A <code>KeyLabelData</code> consists of a search key string
 * and zero or more lines of text that constitute the label contents.
 * The maximum number of lines of text is set at construction and
 * defaults to 6 if not provided.</p>
 * 
 * <p>When storing string data, this class converts all non-trivial
 * whitespace to blanks and trims leading and trailing whitespace.</p>
 * 
 * @author Richard Rasala
 */
public class KeyLabelData
    implements Stringable, Comparable
{
    /** The default number of label lines to store = 6. */
    public static final int CAPACITY = 6;
    
    /** The maximum number of label lines to store. */
    private int capacity = CAPACITY;
    
    /** The search key string. */
    private String key = null;
    
    /** The item array of label strings. */
    private String[] label = null;
    
    /**
     * The array of sub-keys extracted from the key and
     * converted to lower case.
     */
    private String[] keys = null;
    
    
    /**
     * <p>The default constructor that
     * leaves all fields empty.</p>
     */
    public KeyLabelData() {
        this(CAPACITY, null, null);
    }
    
    
    /**
     * <p>The constructor that
     * sets the search key and labels.</p>
     * 
     * @param key the search key
     * @param label the varargs list of labels
     */
    public KeyLabelData(String key, String... label) {
        this(CAPACITY, key, label);
    }
    
    
    /**
     * <p>The default constructor that sets the capacity and
     * leaves all fields empty.</p>
     * 
     * <p>The capacity will be set to at least 1.</p>
     * 
     * @param capacity the maximum number of labels
     */
    public KeyLabelData(int capacity) {
        this(capacity, null, null);
    }
    
    
    /**
     * <p>The constructor that sets the capacity and
     * sets the search key and labels.</p>
     * 
     * <p>The capacity will be set to at least 1.</p>
     * 
     * @param capacity the maximum number of labels
     * @param key the search key
     * @param label the varargs list of labels
     */
    public KeyLabelData(int capacity, String key, String... label) {
        if (capacity < 1)
            capacity = 1;
        
        this.capacity = capacity;
        
        this.label = new String[capacity];
        
        set(key, label);
    }
    
    
    /**
     * <p>Set the key label data to the given search key and the
     * given label strings.</p>
     * 
     * <p>In each string, convert all non-trivial whitespace to
     * blanks and trim leading and trailing whitespace.</p>
     * 
     * <p>Store at most the number of label strings limited by
     * the capacity.</p>
     * 
     * <p>If not enough label strings are supplied, the missing
     * labels are set to empty strings.</p>
     * 
     * @param key the search key
     * @param label the varargs list of labels
     */
    public void set(String key, String... label) {
        setKey(key);
        setLabels(label);
    }
    
    
    /**
     * <p>Set the search key to the given key.</p>
     * 
     * <p>Convert all non-trivial whitespace to blanks
     * and trim leading and trailing whitespace.</p>
     * 
     * <p>For internal search purposes, this method
     * constructs a list of search subkeys from the
     * original key as follows.</p>
     * 
     * <p>The subkeys are those extracted by converting
     * the original key to lowercase and treating that
     * string as a blank-comma separated list that will
     * be split by a string tokenizer.</p>
     * 
     * <p>By searching against subkeys, we are likely
     * to find most matches desired by an end user.</p>
     * 
     * @param key the search key
     */
    public void setKey(String key) {
        this.key = flattenAndTrim(key);
        
        String lowercase = this.key.toLowerCase();
        
        keys = Strings.tokenize(lowercase, " ,", false);
    }
    
    
    /**
     * <p>Set the labels to the given label strings.</p>
     * 
     * <p>In each string, convert all non-trivial whitespace to
     * blanks and trim leading and trailing whitespace.</p>
     * 
     * <p>Store at most the number of label strings limited by
     * the capacity.</p>
     * 
     * <p>If not enough label strings are supplied, the missing
     * labels are set to empty strings.</p>
     * 
     * @param label the varargs list of labels
     */
    public void setLabels(String... label) {
        int L = (label == null) ? 0 : label.length;
        
        int M = capacity;
        
        if (L > M)
            L = M;
        
        for (int i = 0; i < L; i++)
            this.label[i] = flattenAndTrim(label[i]);
        
        for (int i = L; i < M; i++)
            this.label[i] = "";
    }
    
    
    /**
     * <p>Sets the i-th label to the given label if the index i
     * is in bounds.</p>
     * 
     * <p>In the string, convert all non-trivial whitespace to
     * blanks and trim leading and trailing whitespace.</p>
     * 
     * @param i the label index
     * @param string the label to set
     */
    public void setLabel(int i, String string) {
        if ((i >= 0) && (i < capacity))
            label[i] = flattenAndTrim(string);
    }
    
    
    /**
     * <p>Appends the given label at the index returned by
     * <code>freeSlotIndex()</code>.</p>
     * 
     * <p>Does nothing if the index equals the capacity.</p>
     * 
     * @param string the label to append
     */
    public void appendLabel(String string) {
        setLabel(freeSlotIndex(), string);
    }
    
    
    /**
     * <p>Find the index of the first label slot that occurs
     * after all the slots for non-trivial labels have been
     * passed.</p>
     * 
     * <p>The index returned is between 0 and the capacity.
     * If the index equals the capacity then that means that
     * the last slot has a non-trivial label and therefore
     * no additional labels may be appended.</p>
     */
    public int freeSlotIndex() {
        int i = capacity - 1;
        
        while ((i >= 0) && (label[i].length() == 0))
            i--;
        
        i++;
        
        return i;
    }
    
    
    /**
     * Returns the search key.
     */
    public String getKey() {
        return key;
    }
    
    
    /**
     * Returns the i-th label or an empty string
     * if i is out of bounds.
     */
    public String getLabel(int i) {
        if ((0 <= i) && (i < capacity))
            return label[i];
        else
            return "";
    }
    
    
    /**
     * Returns the capacity.
     */
    public int getCapacity() {
        return capacity;
    }
    
    
    /**
     * <p>Returns true if the key or one of its subkeys
     * starts with the given string.</p>
     * 
     * <p>The subkeys are those extracted by converting
     * the original key to lowercase and treating that
     * string as a blank-comma separated list that will
     * be split by a string tokenizer.</p>
     * 
     * <p>This test ignores case.</p>
     * 
     * @param string the string to test
     */
    public boolean keyStartsWith(String string) {
        if (string == null)
            return false;
        
        string = string.toLowerCase();
        
        int L = keys.length;
        
        for (int i = 0; i < L; i++)
            if (keys[i].startsWith(string))
                return true;
        
        return false;
    }
    
    
    /**
     * <p>Returns a string that concatenates all labels
     * terminated by a newline
     * starting from index zero up to but not including
     * the index <code>freeSlotIndex()</code>.</p>
     * 
     * <p>Does not include the search key in any way.</p>
     */
    public String toString() {
        StringBuilder builder = new StringBuilder();
        
        int limit = freeSlotIndex();
        
        for (int i = 0; i < limit; i++) {
            builder.append(label[i]);
            builder.append('\n');
        }
        
        return builder.toString();
    }
    
    
    /**
     * <p>Returns a string that concatenates the search key
     * terminated by a newline together with all labels
     * terminated by a newline
     * starting from index zero up to but not including
     * the index <code>freeSlotIndex()</code>.</p>
     */
    public String toStringData() {
        StringBuilder builder = new StringBuilder();
        
        builder.append(key);
        builder.append('\n');
        
        builder.append(toString());
        
        return builder.toString();
    }
    
    
    /**
     * <p>Splits the given data into an array of strings using
     * the newline character as a separator;
     * uses the first string in the array to set the key and
     * the remaining strings in the array to set the labels.</p>
     * 
     * <p>If key or label data is missing then such data will be
     * set to an empty string.</p>
     * 
     * @param data the data for this key label data as a string
     */
    public void fromStringData(String data) {
        if (data == null) {
            set(null, null);
            return;
        }
        
        fromStringArrayData(Strings.exactSplitNewlineList(data));
    }
    
    
    /**
     * <p>Uses the first string in the array to set the key and
     * the remaining strings in the array to set the labels.</p>
     * 
     * <p>If key or label data is missing then such data will be
     * set to an empty string.</p>
     * 
     * @param list the data for this key label data as a string array
     */
    public void fromStringArrayData(String[] list) {
        if (list == null) {
            set(null, null);
            return;
        }
        
        int K = list.length;
        
        if (K == 0) {
            set(null, null);
            return;
        }
        
        setKey(list[0]);
        
        int L = K - 1;
        
        int M = capacity;
        
        if (L > M)
            L = M;
        
        for (int i = 0; i < L; i++)
            label[i] = flattenAndTrim(list[i+1]);
        
        for (int i = L; i < M; i++)
            label[i] = "";
       
    }
    
    
    /**
     * <p>Returns true if the given object is a key data label,
     * the capacity of the object is the same as this capacity,
     * and all of the object's fields are equal to the corresponding
     * fields of this key data label; returns false otherwise.</p>
     */
    public boolean equals(Object object) {
        if (! (object instanceof KeyLabelData))
            return false;
        
        KeyLabelData a = (KeyLabelData) object;
        
        if (! (capacity == a.capacity))
            return false;
        
        if (! key.equals(a.key))
            return false;
        
        for (int i = 0; i < capacity; i++)
            if (! label[i].equals(a.label[i]))
                return false;
        
        return true;
    }
    
    
    /**
     * <p>Returns the sum of the hash codes of its constituent
     * string data fields.</p>
     */
    public int hashCode() {
        int sum = key.hashCode();
        
        for (int i = 0; i < capacity; i++)
            sum += label[i].hashCode();
        
        return sum;
    }
    
    
    /**
     * <p>Compares each of the corresponding string data fields
     * and returns the first comparison that is non-zero; if
     * all corresponding fields are equal then returns 0.</p>
     * 
     * <p>In this implementation, the comparison looks beyond
     * the search key to the label strings if two keys are
     * equal.  This implementation means that compareTo is
     * compatible with equals.</p>
     * 
     * <p>Accounts for the fact that the two objects may have
     * different capacity and provides a sensible ordering.</p>
     * 
     * <p>Throws <code>ClassCastException</code> if the given
     * object is not an address label.</p>
     * 
     * @param object the address label to compare
     */
    public int compareTo(Object object) {
        KeyLabelData a = (KeyLabelData) object;
        
        int test = key.compareTo(a.key);
        
        if (test != 0)
            return test;
        
        int minimum = Math.min(capacity, a.capacity);
        
        for (int i = 0; i < minimum; i++) {
            test = label[i].compareTo(a.label[i]);
            
            
            if (test != 0)
                return test;
        }
        
        if (capacity == a.capacity)
            return 0;
        else if (minimum < capacity)
            return +1;
        else
            return -1;
    }
    
    
    /**
     * <p>Return the given string with all non-trivial whitespace
     * converted to blanks and leading and trailing whitespace
     * trimmed.</p>
     * 
     * <p>If the given string data is <code>null</code>,
     * return an empty string.</p>
     * 
     * @param string the string to flatten and trim
     */
    public static final String flattenAndTrim(String string) {
        if (string == null)
            return "";
        
        return Strings.flatten(string).trim();
    }
    
}

