/*
 * @(#)XColor.java    2.4.0   9 May 2005
 *
 * Copyright 2005
 * College of Computer and Information Science
 * Northeastern University
 * Boston, MA  02115
 *
 * The Java Power Tools software may be used for educational
 * purposes as long as this copyright notice is retained intact
 * at the top of all source files.
 *
 * To discuss possible commercial use of this software, 
 * contact Richard Rasala at Northeastern University, 
 * College of Computer and Information Science,
 * 617-373-2462 or rasala@ccs.neu.edu.
 *
 * The Java Power Tools software has been designed and built
 * in collaboration with Viera Proulx and Jeff Raab.
 *
 * Should this software be modified, the words "Modified from 
 * Original" must be included as a comment below this notice.
 *
 * All publication rights are retained.  This software or its 
 * documentation may not be published in any media either
 * in whole or in part without explicit permission.
 *
 * This software was created with support from Northeastern 
 * University and from NSF grant DUE-9950829.
 */

package edu.neu.ccs;

import edu.neu.ccs.codec.*;
import edu.neu.ccs.console.*;
import edu.neu.ccs.parser.*;
import edu.neu.ccs.filter.*;
import edu.neu.ccs.util.*;
import java.awt.*;
import java.beans.*;
import java.text.ParseException;
import java.util.*;

/**
 * <p>Object wrapper for the <code>Color</code> class
 * that also provides
 * <code>{@link Stringable Stringable}</code> capabilities.
 *
 * The default value for this class is
 * the <code>Color</code> representation of black,
 * <code>{@link Color#black Color.black}</code>.</p>
 * 
 * @author  Richard Rasala
 * @author  Jeff Raab
 * @version 2.4.0
 * @since   1.0
 */
public final class XColor extends XObject {

    /** The wrapped color for this object. */
    private Color color = Color.black;
    
    /**
     * Constructs a wrapper for the default <code>Color</code> value.
     *
     * @see #XColor(Color)
     * @see #XColor(int, int, int)
     * @see #XColor(int, int, int, int)
     * @see #XColor(float, float, float)
     * @see #XColor(float, float, float, float)
     * @see #XColor(String)
     */
    public XColor() {}
    
    /**
     * Constructs a wrapper for the given <code>Color</code> value.
     * 
     * @param c the value to be wrapped
     * @see #XColor()
     * @see #XColor(int, int, int)
     * @see #XColor(int, int, int, int)
     * @see #XColor(float, float, float)
     * @see #XColor(float, float, float, float)
     * @see #XColor(String)
     */
    public XColor(Color c) {
        setValue(c);
    }
    
    /**
     * Constructs a wrapper for the <code>Color</code> constructed
     * from the specified red, green, and blue values in the range
     * [0, 255].
     *
     * @param r the red   value in the range [0, 255]
     * @param g the green value in the range [0, 255]
     * @param b the blue  value in the range [0, 255]
     * @see #XColor()
     * @see #XColor(Color)
     * @see #XColor(int, int, int, int)
     * @see #XColor(float, float, float)
     * @see #XColor(float, float, float, float)
     * @see #XColor(String)
     */
    public XColor(int r, int g, int b) {
        this(new Color(r, g, b));
    }
    
    /**
     * Constructs a wrapper for the <code>Color</code> constructed
     * from the specified red, green, blue, and alpha values in the
     * range [0, 255].
     *
     * @param r the red   value in the range [0, 255]
     * @param g the green value in the range [0, 255]
     * @param b the blue  value in the range [0, 255]
     * @param a the alpha value in the range [0, 255]
     * @see #XColor()
     * @see #XColor(Color)
     * @see #XColor(int, int, int)
     * @see #XColor(float, float, float)
     * @see #XColor(float, float, float, float)
     * @see #XColor(String)
     */
    public XColor(int r, int g, int b, int a) {
        this(new Color(r, g, b, a));
    }
    
    /**
     * Constructs a wrapper for the <code>Color</code> constructed
     * from the specified red, green, and blue values in the range
     * [0.0, 1.0].
     *
     * @param r the red   value in the range [0.0, 1.0]
     * @param g the green value in the range [0.0, 1.0]
     * @param b the blue  value in the range [0.0, 1.0]
     * @see #XColor()
     * @see #XColor(Color)
     * @see #XColor(int, int, int)
     * @see #XColor(int, int, int, int)
     * @see #XColor(float, float, float, float)
     * @see #XColor(String)
     */
    public XColor(float r, float g, float b) {
        this(new Color(r, g, b));
    }
    
    /**
     * Constructs a wrapper for the <code>Color</code> constructed
     * from the specified red, green, blue, and alpha values in the
     * range [0.0, 1.0].
     *
     * @param r the red   value in the range [0.0, 1.0]
     * @param g the green value in the range [0.0, 1.0]
     * @param b the blue  value in the range [0.0, 1.0]
     * @param a the alpha value in the range [0.0, 1.0]
     * @see #XColor()
     * @see #XColor(Color)
     * @see #XColor(int, int, int)
     * @see #XColor(int, int, int, int)
     * @see #XColor(float, float, float)
     * @see #XColor(String)
     */
    public XColor(float r, float g, float b, float a) {
        this(new Color(r, g, b, a));
    }
    
    /**
     * Constructs a wrapper for the <code>Color</code> value
     * whose state information is encapsulated in the given
     * <code>String</code> data.
     * 
     * @param s a <code>String</code> representation 
     *      of the desired value
     * @throws ParseException if the data is malformed
     * @see #XColor()
     * @see #XColor(Color)
     * @see #XColor(int, int, int)
     * @see #XColor(int, int, int, int)
     * @see #XColor(float, float, float)
     * @see #XColor(float, float, float, float)
     */
    public XColor(String s) throws ParseException {
        fromStringData(s);
    }
    
    ////////////////
    // Stringable //
    ////////////////

    /**
     * <p>Sets the state of the <code>XColor</code> object using
     * information contained in the given <code>String</code>.</p>
     *
     * <p>The following formats are supported for input of a color
     * using a text string.</p>
     *
     * <p><I>Data format 1</I>: A comma and/or blank separated text 
     * string with 3 <code>int</code> components: 
     * <I>red</I>, <I>green</I>, <I>blue</I>
     * or 4 <code>int</code> components: 
     * <I>red</I>, <I>green</I>, <I>blue</I>, <I>alpha</I>.</p>
     *
     * <p>The <code>int</code> components should be in the range
     * [0, 255].</p>
     *
     * <p>If the <I>alpha</I> component is omitted then it is
     * assumed to be 255.</p>
     *
     * <p><I>Data format 2</I>: A <code>String</code> of the form
     * <code>#rRgGbB</code> or <code>#rRgGbBaA</code>
     * where <code>r, R, g, G, b, B, a, A</code> are hex digits with
     * red   = <code>rR</code>,
     * green = <code>gG</code>,
     * blue  = <code>bB</code>,
     * alpha = <code>aA</code>.</p>
     * 
     * <p><I>Data format 3</I>: A <code>String</code> of the form
     * <code>#rgb</code> or <code>#rgba</code> that is interpreted as
     * <code>#rrggbb</code> or <code>#rrggbbaa</code> respectively.</p>
     *
     * <p><I>Data format 4</I>: A color name that is contained in
     * the internal <code>static</code> structure of the class
     * <code>Colors</code>.  An array with the currently valid
     * installed color names can be obtained via the call:</p>
     *
     * <p><code>Colors.getColorNamesAsArray()</code></p>
     *
     * @param  data the <code>String</code> containing color information
     * @throws ParseException if the data is malformed
     * @see #toStringData()
     */
    public void fromStringData(String data) throws ParseException {
        if (data == null)
            throw new ParseException
                ("Color string data is null" , -1);
        
        if (data.length() == 0)
            throw new ParseException
                ("Color string data has zero length", -1);
        
        data = data.trim();
        
        if (data.length() == 0)
            throw new ParseException
                ("Color string data contains only white space", -1);
        
        char first = data.charAt(0);
        
        if (Character.isLetter(first)) {
            Color color = Colors.getColorFromName(data);
            
            if (color != null) {
                setValue(color);
                return;
            }
            
            throw new ParseException
                ("Unrecognized color name in color string data", -1);
        }
        
        // check for hexidecimal color data
        if (first == '#') {
           setValue(getColorFromHexidecimal(data));
           return;
        }
        
        
        // check for decimal color data
        if (Character.isDigit(first)) {
            setValue(getColorFromDecimal(data));
            return;
        }
        
        throw new ParseException
            ("Color string data must be decimal r,g,b,alpha data, or hex data, or a name", -1);
    }
    
    
    /**
     * Returns the Color expressed in decimal data or throws a
     * ParseException.
     *
     * @param data the <code>String</code> that may contain decimal color information
     */
    private Color getColorFromDecimal(String data) throws ParseException
    {
        // Common error message
        String message =
            "Decimal color data must have the form r,g,b or r,g,b,alpha with values between 0 and 255";
        
        // check for the color as r, g, b or r, g, b, a
        StringTokenizer tokenizer =
            new StringTokenizer(data, ", ");
      
        int arraysize = tokenizer.countTokens();
      
        // throw a parse exception if we do not have
        //   3 components (red, green, blue)
        // or
        //   4 components (red, green, blue, alpha)
        if ((arraysize < 3) || (arraysize > 4)) {
            throw new ParseException(message, -1);
        }
        
        // if the number of components is correct then gather them
        String[] component = new String[arraysize];
        for (int i = 0; i < arraysize; i++)
            component[i] = tokenizer.nextToken();
        
        // prepare to extract the color component values
        int r = 0;
        int g = 0;
        int b = 0;
        int a = 255;    // alpha default is opaque
        
        // parse color components as int's between 0 and 255
        try {
            RangeFilter.Long filter = new RangeFilter.Long(0, 255);
                
            XLong R = (XLong) 
                filter.filterStringable(new XLong(component[0]));
            r = (int)R.getValue();
            
            XLong G = (XLong) 
                filter.filterStringable(new XLong(component[1]));
            g = (int)G.getValue();
            
            XLong B = (XLong) 
                filter.filterStringable(new XLong(component[2]));
            b = (int)B.getValue();
            
            if (arraysize == 4) {
                XLong A = (XLong) 
                    filter.filterStringable(new XLong(component[3]));
                a = (int)A.getValue();
            }
        }
        // translate exception to a parse exception
        catch (NumberFormatException ex) {
            throw new ParseException(message, -1);
        }
        catch (FilterException ex) {
            throw new ParseException(message, -1);
        }
        
        return new Color(r, g, b, a);
    }
    
    
    /**
     * Returns the Color expressed in hexidecimal data or throws a
     * ParseException.  Assumes the initial char is '#'.
     *
     * @param data the <code>String</code> that may contain hexidecimal color information
     */
    private Color getColorFromHexidecimal(String data) throws ParseException
    {
        // Common error message
        String message =
            "Hexidecimal color data must have the form #... with 3, 4, 6, or 8 hex digits";
        
        // remove the leading '#'
        data = data.substring(1);
        int length = data.length();
        
        // length must be 3, 4, 6, 8.
        boolean ok = (length == 3) || (length == 4) || (length == 6) || (length == 8);
        
        // characters must be hex digits
        if (ok)
            for (int i = 0; i < length; i++)
                if (! Hex.isHexDigit(data.charAt(i))) {
                    ok = false;
                    break;
                }
        
        if (! ok)
            throw new ParseException(message, -1);
        
        StringBuffer buffer = new StringBuffer(8);
        
        if (length <= 4) {
            char r = data.charAt(0);
            char g = data.charAt(1);
            char b = data.charAt(2);
            
            buffer.append(r);
            buffer.append(r);
            buffer.append(g);
            buffer.append(g);
            buffer.append(b);
            buffer.append(b);
            
            if (length == 4) {
                char a = data.charAt(3);
                
                buffer.append(a);
                buffer.append(a);
            }
            
            data = buffer.toString();
            length *= 2;
        }
        
        String rR = data.substring(0, 2);
        String gG = data.substring(2, 4);
        String bB = data.substring(4, 6);
        String aA =
            (length == 8)
                ? data.substring(6, 8)
                : "ff";
        
        return new Color(
            Hex.hexToInt(rR),
            Hex.hexToInt(gG),
            Hex.hexToInt(bB),
            Hex.hexToInt(aA));
    }
    
    
    /**
     * <p>Returns a <code>String</code> encapsulation of the
     * <code>XColor</code> object that contains the
     * int values of red, green, blue, (and possibly alpha)
     * each in the range [0, 255].</p>
     *
     * <p>The alpha component is omitted if it is equal to 255.</p>
     *
     * @see #fromStringData(String)
     */
    public String toStringData() {
        int r = color.getRed();
        int g = color.getGreen();
        int b = color.getBlue();
        int a = color.getAlpha();
        
        if (a == 255)
            return r + ", " + g + ", " + b;
        else
            return r + ", " + g + ", " + b + ", " + a;
    }
    
    ////////////////
    // Public API //
    ////////////////

    /**
     * Returns a <code>String</code> representation 
     * of the wrapped value.
     */
    public String toString() {
        return toStringData();
    }
    
    /**
     * Returns <code>true</code> if <code>other</code> is of class
     * <code>XColor</code> and if the wrapped color of this object
     * and the wrapped color of <code>other</code> are equal.
     *
     * @param the object to be compared with the wrapped object
     */
    public boolean equals(Object other) {
        if (other instanceof XColor)
            return getValue().equals(((XColor)other).getValue());
        
        return false;
    }
    
    /**
     * Returns an <code>int</code> hash code 
     * appropriate for the wrapped color.
     */
    public int hashCode() {
        return getValue().hashCode();
    }

    /**
     * Sets the value wrapped by this object to the given
     * <code>Color</code> value.
     *
     * If <code>null</code>, the value is set to
     * <code>Color.black</code>.
     *
     * @param c the value to be wrapped
     * @see #setValue(int, int, int)
     * @see #setValue(int, int, int, int)
     * @see #setValue(float, float, float)
     * @see #setValue(float, float, float, float)
     * @see #getValue()
     */
    public void setValue(Color c) {
        Color oldValue = color;
        
        // check for null value
        if (c == null)
            c = Color.black;

        color = c;

        // if the value has changed
        if (!getValue().equals(oldValue)) {

            // notify listeners of property change
            changeAdapter.firePropertyChange(
                VALUE, 
                oldValue, 
                getValue());
        }
    }

    /**
     * Sets the wrapped value to the <code>Color</code> constructed
     * from the specified red, green, and blue values in the range
     * [0, 255].
     *
     * @param r the red   value in the range [0, 255]
     * @param g the green value in the range [0, 255]
     * @param b the blue  value in the range [0, 255]
     * @see #setValue(Color)
     * @see #setValue(int, int, int, int)
     * @see #setValue(float, float, float)
     * @see #setValue(float, float, float, float)
     * @see #getValue()
     */
    public void setValue(int r, int g, int b) {
        setValue(new Color(r, g, b));
    }
    
    /**
     * Sets the wrapped value to the <code>Color</code> constructed
     * from the specified red, green, blue, and alpha values in the
     * range [0, 255].
     *
     * @param r the red   value in the range [0, 255]
     * @param g the green value in the range [0, 255]
     * @param b the blue  value in the range [0, 255]
     * @param a the alpha value in the range [0, 255]
     * @see #setValue(Color)
     * @see #setValue(int, int, int)
     * @see #setValue(float, float, float)
     * @see #setValue(float, float, float, float)
     * @see #getValue()
     */
    public void setValue(int r, int g, int b, int a) {
        setValue(new Color(r, g, b, a));
    }
    
    /**
     * Sets the wrapped value to the <code>Color</code> constructed
     * from the specified red, green, and blue values in the range
     * [0.0, 1.0].
     * 
     * @param r the red   value in the range [0.0, 1.0]
     * @param g the green value in the range [0.0, 1.0]
     * @param b the blue  value in the range [0.0, 1.0]
     * @see #setValue(Color)
     * @see #setValue(int, int, int)
     * @see #setValue(int, int, int, int)
     * @see #setValue(float, float, float, float)
     * @see #getValue()
     */
    public void setValue(float r, float g, float b) {
        setValue(new Color(r, g, b));
    }
    
    /**
     * Sets the wrapped value to the <code>Color</code> constructed
     * from the specified red, green, blue, and alpha values in the
     * range [0.0, 1.0].
     *
     * @see #setValue(Color)
     * @see #setValue(int, int, int)
     * @see #setValue(int, int, int, int)
     * @see #setValue(float, float, float)
     * @see #getValue()
     */
    public void setValue(float r, float g, float b, float a) {
        setValue(new Color(r, g, b, a));
    }
    
    /**
     * Returns the value wrapped by this object.
     * 
     * @param r the red   value in the range [0.0, 1.0]
     * @param g the green value in the range [0.0, 1.0]
     * @param b the blue  value in the range [0.0, 1.0]
     * @param a the alpha value in the range [0.0, 1.0]
     * @see #setValue(Color)
     * @see #setValue(int, int, int)
     * @see #setValue(int, int, int, int)
     * @see #setValue(float, float, float)
     * @see #setValue(float, float, float, float)
     */
    public Color getValue() {
        return color;
    }

    ////////////////////
    // Static methods //
    ////////////////////
    
    /**
     * <p>Gets a <code>Color</code> value from a data String
     * if the data is valid or returns <code>null</code>.</p>
     *
     * @param  data the String to parse
     * @return the extracted Color value
     */
    public static Color getColor(String data) {
        try {
            return parseColor(data);
        }
        catch (ParseException ex) {
            return null;
        }
    }
    
    /**
     * <p>Gets a <code>Color</code> value from a data String
     * if the data is valid or throws a
     * <code>ParseException</code>.</p>
     *
     * @param  data the String to parse
     * @return the extracted Color value
     * @throws ParseException if the data is invalid
     */
    public static Color parseColor(String data) 
        throws ParseException 
    {
        return (new XColor(data)).color;
    }
    
    /**
     * <p>Returns a <code>String</code> encapsulation of the
     * <code>Color</code> parameter that contains the
     * int values of red, green, blue, (and possibly alpha)
     * each in the range [0, 255].</p>
     *
     * <p>The alpha component is omitted if it is equal to 255.</p>
     *
     * @param color the color to represent as a string
     */
    public static String colorToString(Color color) {
        return new XColor(color).toStringData();
    }
    
    /**
     * Returns an array of <code>Color</code> objects 
     * copied from the given array 
     * of <code>XColor</code> objects.</p>
     * 
     * @param x an array of <code>XColor</code>s
     * @return the resulting array 
     *      of <code>Color</code> objects
     * @see #toXArray(Color[])
     */
    public static Color[] toPrimitiveArray(XColor[] x) {
        
        // return null array if appropriate
        if (x == null)
            return null;
        
        // otherwise perform the type translation
        Color[] temp = new Color[x.length];
        for (int i = 0; i < temp.length; i++)
            if (x[i] != null)
                temp[i] = x[i].getValue();
        return temp;
    }

    /**
     * Returns an array of <code>XColor</code> objects
     * initialized from the given array 
     * of <code>Color</code> objects.
     * 
     * @param a an array of <code>Color</code>s
     * @return the resulting array 
     *      of <code>XColor</code> objects
     * @see #toPrimitiveArray(XColor[])
     */
    public static XColor[] toXArray(Color[] a) {
        
        // return null array if appropriate
        if (a == null)
            return null;
        
        // otherwise perform the type translation
        XColor[] temp = new XColor[a.length];
        for (int i = 0; i < temp.length; i++)
            if (a[i] != null)
                temp[i] = new XColor(a[i]);
        return temp;
    }
}
