/*
 * Copyright (c) 2009, Takeyuki Nagao
 * All rights reserved.
 * 
 * Redistribution and use in source and binary forms, with or
 * without modification, are permitted provided that the
 * following conditions are met:
 * 
 *  * Redistributions of source code must retain the above
 *    copyright notice, this list of conditions and the
 *    following disclaimer.
 *  * Redistributions in binary form must reproduce the above
 *    copyright notice, this list of conditions and the
 *    following disclaimer in the documentation and/or other
 *    materials provided with the distribution.
 *    
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND
 * CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES,
 * INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
 * MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
 * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR
 * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
 * BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
 * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
 * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
 * WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
 * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE
 * USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY
 * OF SUCH DAMAGE.
 */

package dvi;

import java.awt.Color;
import java.util.HashMap;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

// immutable.

public final class DviColor
implements java.io.Serializable
{
  private static final long serialVersionUID = 5615669030601265281L;

  public static final DviColor INVALID = new DviColor();

  private final int r, g, b;
  private final int intRGB;
  private final boolean valid;
  
  public int getRed() { return r;}
  public int getGreen() { return g;}
  public int getBlue() { return b;}
  
  public Color toColor()
  {
    return new Color(r, g, b);
  }
  
  public DviColor() {
    r = g = b = intRGB = 0;
    valid = false;
  }

  public DviColor(int rgb)
  {
    this(
      (rgb >>> 16) & 0xff,
      (rgb >>> 8) & 0xff,
      rgb & 0xff
    );
  }

  public DviColor(int r, int g, int b)
  {
    if (0 <= r && r < 256 && 0 <= g && g < 256 && 0 <= b && b < 256) {
      this.r = r;
      this.g = g;
      this.b = b;
      intRGB = (r << 16) | (g << 8) | b;
      valid = true;
    } else {
      throw new IllegalArgumentException
        ("r=" + r + " g=" + g + " b=" + b);
    }
  }

  public boolean isValid()
  {
    return valid;
  }

  public static DviColor fromIntRGB(int v)
  {
    final int r, g, b;

    b = v & 0xff;   v >>>= 8;
    g = v & 0xff;   v >>>= 8;
    r = v & 0xff;

    return RGB(r, g, b);
  }

  public static DviColor RGB(int r, int g, int b)
  {
    return new DviColor(r, g, b);
  }

  public static DviColor RGB(double r, double g, double b)
  {
    return RGB(
      (int)(r * 255 + 0.5),
      (int)(g * 255 + 0.5),
      (int)(b * 255 + 0.5)
    );
  }

  public static DviColor HSV(int h, int s, int v)
  {
    if (s == 0) {
      return RGB(v, v, v);
    }
    
    h = ((h % 360) + 360) % 360; // h = h mod 360
    final double f = h % 60;
    
    final int p = (int)((v*(255.0 - s) + 255.0/2.0) / 255.0);
    final int q = (int)((v*(60.0*255.0 - f * s) + 60.0*255.0 / 2.0) / (60.0 * 255.0));
    final int t = (int)((v*(60.0*255.0 - (60.0 - f) * s) + 60.0*255.0 / 2.0) / (60.0 * 255.0));
    
    int r, g, b;
    
    switch (h / 60) {
    case 0: r=v; g=t; b=p; break;
    case 1: r=q; g=v; b=p; break;
    case 2: r=p; g=v; b=t; break;
    case 3: r=p; g=q; b=v; break;
    case 4: r=t; g=p; b=v; break;
    case 5: r=v; g=p; b=q; break;
    default: throw new InternalError();
    }
    return RGB(r, g, b);
  }

  public static DviColor CMYK(double c, double m, double y, double k)
  {
    return RGB(
      Math.max(0.0, 1.0 - c - k),
      Math.max(0.0, 1.0 - m - k),
      Math.max(0.0, 1.0 - y - k)
    );
  }

  public int toIntRGB()
  {
    return intRGB;
  }

  private static final Pattern rgbPat
    = Pattern.compile(
        "rgb\\s+([.0-9]+)\\s+([.0-9]+)\\s+([.0-9]+)",
        Pattern.CASE_INSENSITIVE
      );
  private static final Pattern hsbPat
    = Pattern.compile(
        "hsb\\s+([.0-9]+)\\s+([.0-9]+)\\s+([.0-9]+)",
        Pattern.CASE_INSENSITIVE
      );
  private static final Pattern cmykPat
    = Pattern.compile(
        "cmyk\\s+([.0-9]+)\\s+([.0-9]+)\\s+([.0-9]+)\\s+([.0-9]+)",
        Pattern.CASE_INSENSITIVE
      );
  private static final Pattern grayPat
    = Pattern.compile(
        "gray\\s+([.0-9]+)",
        Pattern.CASE_INSENSITIVE
      );
  
  private static final HashMap<String, DviColor> aliases
    = new HashMap<String, DviColor>();

  static {
    aliases.put(  "black", fromIntRGB(0x00000000));
    aliases.put(   "blue", fromIntRGB(0x000000ff));
    aliases.put(  "green", fromIntRGB(0x0000ff00));
    aliases.put(   "cyan", fromIntRGB(0x0000ffff));
    aliases.put(    "red", fromIntRGB(0x00ff0000));
    aliases.put("magenta", fromIntRGB(0x00ff00ff));
    aliases.put( "yellow", fromIntRGB(0x00ffff00));
    aliases.put(  "white", fromIntRGB(0x00ffffff));
    // TODO: support colors used by dvips.
  }

  public static DviColor parseColor(String str)
  throws DviException
  {
    str = str.trim();
    Matcher mat;

    try {
      if ((mat = rgbPat.matcher(str)).matches()) {
        double r = Double.parseDouble(mat.group(1));
        double g = Double.parseDouble(mat.group(2));
        double b = Double.parseDouble(mat.group(3));
        return RGB(r, g, b);
      } else if ((mat = hsbPat.matcher(str)).matches()) {
        double h = Double.parseDouble(mat.group(1));
        double s = Double.parseDouble(mat.group(2));
        double b = Double.parseDouble(mat.group(3));
        return HSV(
          (int)(h*359 + 0.5),
          (int)(s*255 + 0.5),
          (int)(b*255 + 0.5)
        );
      } else if ((mat = cmykPat.matcher(str)).matches()) {
        double c = Double.parseDouble(mat.group(1));
        double m = Double.parseDouble(mat.group(2));
        double y = Double.parseDouble(mat.group(3));
        double k = Double.parseDouble(mat.group(3));
        return CMYK(c, m, y, k);
      } else if ((mat = grayPat.matcher(str)).matches()) {
        double v = Double.parseDouble(mat.group(1));
        return RGB(v, v, v);
      } else {
        DviColor c = aliases.get(str.toLowerCase());
        if (c != null) return c;
        throw new DviException
          ("unrecognized color specification: " + str);
      }
    } catch(NumberFormatException ex) {
      throw new DviException(ex);
    }
  }

  public int hashCode()
  {
    return r + 33*(g + 33*b);
  }

  public boolean equals(Object obj)
  {
    if (obj instanceof DviColor) {
      DviColor c = (DviColor) obj;
      return (r == c.r && g == c.g && b == c.b);
    }
    return false;
  }

  public String toString()
  {
    return getClass().getName() + "[hex=#"
           + toHex(r) + toHex(g) + toHex(b) + "]";
  }

  private static String toHex(int a)
  {
    return (a < 0x10) ? "0" + Integer.toHexString(a)
                      : Integer.toHexString(a);
  }
}
