/**
***   sihcnd.java, Rich Franzen, 2013-02-08
*** mailto:rich@r0k.us
***
*** (c) 2013, Rich Franzen
*** May be used without restriction, but I would appreciate
*** credit if you implement this technique in your own code.
***
***   Build one of three large color name dictionaries (CNDs)
*** to span most of the SIH (fc16) quantized colorspace.
***
*** rev 1.0, 2013-02-09:  initial
*** rev 1.1, 2013-02-11:  added css 3-digit hex CND
*** rev 1.2, 2013-02-20:  tweaked extended greyscale table
*** rev 1.3, 2013-02-22:  renamed "extended pseudogrey" to "pseudogrey plus"
**/

import java.awt.*;
import java.io.*;
import java.text.DecimalFormat;

class sihcnd
{

final static String grey_CND = new String("pseudogreyPlus.txt");
final static String greySIH_CND = new String("SIH_pseudogreyPlus.txt");
final static String augmentedSIH_CND = new String("SIH_augmentedColor.txt");
final static String css3digit_CND = new String("css3digitHex.txt");
static int greyInts[] = new int[4096];

static PrintWriter openPW(String filename)
{
    PrintWriter pw = null;

    if (filename.equals(null))
    {
        System.out.println("openPW() received null filename");
    }
      else if (filename.length() < 1)
      {
        System.out.println("openPW() received empty filename");
      }
      else
      {
        try { pw = new PrintWriter(filename); }
          catch(FileNotFoundException e)
          {
            pw = null;
            System.out.println("Unable to open '" + filename + "'");
          }
      }

    return pw;
}

// build a 6-digit hex color string of form rrggbb
// input c is already coded with blue as low byte, grn as middle, and red as third.
static String sixDigitHex(int c)
{
    String          hexColor;

    // leave an extra character at high end
    // both forces leading zeroes (if necessary) and leaves room for #
    hexColor = Integer.toHexString(0x1000000 + c);
    return(hexColor.substring(1, 7));
}

/**
*** The first part of all 3 dictionaries will be the grey values, 4096 for options
*** 0 and 1, and 316 for option 2.
**/
static boolean writeGrey(PrintWriter pw, int option)
{
    int line = 0;
    int grey;
    char filler = '0';
    float sixteenths = 0;

    if (pw.equals(null))
    {
        System.out.println("writeGrey() received null object");
        return false;
    }

    pw.println(sixDigitHex(greyInts[line]) + " = black");
    line += 1;
    while (line < 10)
    {   // 1-digit preceded by 3 spaces
        if (option == 2)
            pw.println(sixDigitHex(greyInts[line]) + " = deepGrey " + line/16f);
          else
            pw.println(sixDigitHex(greyInts[line]) + " = grey" + filler + filler + filler + line);
        line += 1;
    }
    while (line < 100)
    {   // 2-digit preceded by 2 spaces
        if (option == 2)
        {
            if (line == 64)
            {   // rest is normal 8-bit grey
                for (line = 4; line < 256; line++)
                {
                    grey = (line << 16) + (line << 8) + line;
                    if (line < 10)
                        pw.println(sixDigitHex(grey) + " = grey" + filler + filler + line);
                      else if (line < 100)
                        pw.println(sixDigitHex(grey) + " = grey" + filler + line);
                      else if (line == 255)
                        pw.println(sixDigitHex(grey) + " = white");
                      else
                        pw.println(sixDigitHex(grey) + " = grey" + line);
                }
                return true;  // done with option 2
            }
            pw.println(sixDigitHex(greyInts[line]) + " = deepGrey " + line/16f);
        }
          else
            pw.println(sixDigitHex(greyInts[line]) + " = grey" + filler + filler + line);
        line += 1;
    }
    while (line < 1000)
    {   // 3-digit preceded by 1 space
        pw.println(sixDigitHex(greyInts[line]) + " = grey" + filler + line);
        line += 1;
    }
    while (line < 4095)
    {   // 4 digits
        pw.println(sixDigitHex(greyInts[line]) + " = grey" + line);
        line += 1;
    }
    pw.println(sixDigitHex(greyInts[line]) + " = white");

    return true;
}

/**
*** Write the 4096-entry CND of CSS 3-hex-digit colors.
*** Use a consistent format, except identify w3c colors and
*** "web-safe" colors.
**/
static boolean write3digithex(PrintWriter pw)
{
    int red, red2, grn, grn2, blu, blu2;
    int hex3;
    String c = "";
    float hsb[] = { 0f, 0f, 0f };

    if (pw.equals(null))
    {
        System.out.println("write3digithex() received null object");
        return false;
    }

    hex3 = 0;  // hex3 is the 3-digit hex value and increments linearly in this algorithm
    for (red = 0; red < 16; red++)
    {
        red2 = (red + (red << 4)) << 16;      // rr0000
        for (grn = 0; grn < 16; grn++)
        {
            grn2 = (grn + (grn << 4)) << 8;   // 00gg00
            for (blu = 0; blu < 16; blu++)
            {
                blu2 = blu + (blu << 4);     // 0000bb

                // check for pures
                if (red == grn)
                {
                    if (red == blu)
                    {   // grey family
                        if (red == 0)  c = "black";
                          else if (red == 15)  c = "white";
                          else  c = "grey";
                    }
                      else if (red > blu)  c = "yellow";
                      else  c = "blue";
                }
                  else if (red == blu)
                  { // greys already handled
                    if (red > grn)  c = "magenta";
                      else  c = "green";
                  }
                  else if (grn == blu)
                  { // greys already handled
                    if (grn > red)  c = "cyan";
                      else  c = "red";
                  }
                  else
                  { // give the color a pri, sec, or tertiary name
                    // tertiary names from http://en.wikipedia.org/wiki/Tertiary_color
                    hsb = Color.RGBtoHSB(red2 >> 16, grn2 >> 8, blu2, null);
                    if (hsb[0] > 23/24f)         c = "red-";
                      else if (hsb[0] <  1/24f)  c = "red+";
                      else if (hsb[0] <  3/24f)  c = "orangeħ";
                      else if (hsb[0] <  4/24f)  c = "yellow-";
                      else if (hsb[0] <  5/24f)  c = "yellow+";
                      else if (hsb[0] <  7/24f)  c = "chartreuseħ";
                      else if (hsb[0] <  8/24f)  c = "green-";
                      else if (hsb[0] <  9/24f)  c = "green+";
                      else if (hsb[0] < 11/24f)  c = "spring greenħ";
                      else if (hsb[0] < 12/24f)  c = "cyan-";
                      else if (hsb[0] < 13/24f)  c = "cyan+";
                      else if (hsb[0] < 15/24f)  c = "azureħ";
                      else if (hsb[0] < 16/24f)  c = "blue-";
                      else if (hsb[0] < 17/24f)  c = "blue+";
                      else if (hsb[0] < 19/24f)  c = "violetħ";
                      else if (hsb[0] < 20/24f)  c = "magenta-";
                      else if (hsb[0] < 21/24f)  c = "magenta+";
                      else                       c = "roseħ";
                  }

                c = c.concat(", " + "#" + sixDigitHex(hex3).substring(3, 6));

                // check for "websafe"
                if ((red % 3) + (grn % 3) + (blu % 3) == 0)
                { // red, green, and blue are all divisible by 3
                    c = c.concat(":ws");
                }

                pw.println(sixDigitHex(red2 + grn2 + blu2) + " = " + c);
                hex3 += 1;                  // rgb
            }
        }
    }

    return true;
}

/**
*** Write a single entry to the CND.
**/
static void writeColor(PrintWriter pw, float hue, float saturation, float brightness)
{
    int sextant, cInt, bri255;
    Color c = Color.black;
    float sextOffset, sextDegrees;
    DecimalFormat df = new DecimalFormat("0.#");
    final String sextNames[] = { "red", "yel", "grn", "cyn", "blu", "mag" };

    sextDegrees = hue * 360f + .001f;
    sextant = (int)((sextDegrees + .001f) / 60f);
    sextOffset = sextDegrees - (sextant * 60f);
    c = Color.getHSBColor(hue, saturation, brightness);
    cInt = c.getRGB() & 0xffffff;  // mask alpha byte
    bri255 = (int)(brightness * 255f);
    pw.println(sixDigitHex(cInt) + " = " +
               sextNames[sextant] + "+" + df.format(sextOffset) + "°, " +
               df.format(saturation * 100f) + "%, " +
               bri255);
}

/**
*** Unaugmented segments have 10 hues per sextant, augmented have 12.
*** Option 1 has 2 augmented sextants (cyan and red), giving it 64 hues.
*** Option 2 has 4 augmented sextants (adding blue and magenta), giving it 68 hues.
**/
static boolean writeColors(PrintWriter pw, int option)
{
    int coneSeg, SIH_i, ringCount, ring;
    float hue, saturation, brightness;
    Color c = Color.black;
    final float hu8 = 7.5f;  // stop compiler from bitching on use of 7.5 instead of 7.5f
    // establish hue augmentation by pre-fixing degrees per hue cone segment
    final int innermost[] = { 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30 };
    final float second64[] = { 6, 6, 6, 6, 6, 6, 6, 6, 6, 6,            // augmented red
                               hu8, hu8, hu8, hu8, hu8, hu8, hu8, hu8,  // unaugmented yellow
                               hu8, hu8, hu8, hu8, hu8, hu8, hu8, hu8,  // unaugmented green
                               6, 6, 6, 6, 6, 6, 6, 6, 6, 6,            // augmented cyan
                               hu8, hu8, hu8, hu8, hu8, hu8, hu8, hu8,  // unaugmented blue
                               hu8, hu8, hu8, hu8, hu8, hu8, hu8, hu8,  // unaugmented magenta
    };
    final float second68[] = { 6, 6, 6, 6, 6, 6, 6, 6, 6, 6,            // augmented red
                               hu8, hu8, hu8, hu8, hu8, hu8, hu8, hu8,  // unaugmented yellow
                               hu8, hu8, hu8, hu8, hu8, hu8, hu8, hu8,  // unaugmented green
                               6, 6, 6, 6, 6, 6, 6, 6, 6, 6,            // augmented cyan
                               6, 6, 6, 6, 6, 6, 6, 6, 6, 6,            // augmented blue
                               6, 6, 6, 6, 6, 6, 6, 6, 6, 6,            // augmented magenta
    };
    final int outer64[] = { 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5,         // augmented red
                            6, 6, 6, 6, 6, 6, 6, 6, 6, 6,               // unaugmented yellow
                            6, 6, 6, 6, 6, 6, 6, 6, 6, 6,               // unaugmented green
                            5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5,         // augmented cyan
                            6, 6, 6, 6, 6, 6, 6, 6, 6, 6,               // unaugmented blue
                            6, 6, 6, 6, 6, 6, 6, 6, 6, 6,               // unaugmented magenta
    };
    final int outer68[] = { 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5,         // augmented red
                            6, 6, 6, 6, 6, 6, 6, 6, 6, 6,               // unaugmented yellow
                            6, 6, 6, 6, 6, 6, 6, 6, 6, 6,               // unaugmented green
                            5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5,         // augmented cyan
                            5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5,         // augmented blue
                            5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5,         // augmented magenta
    };
    final int satRings[] = { 1, 1, 2, 2, 3, 3, 4, 4, 5, 6, 7, 8, 9, 10,
                             11, 12, 13, 13, 14, 14, 15, 15, 15, 16     // remainder all have 16
    };

    if (pw.equals(null))
    {
        System.out.println("writeGrey() received null object");
        return false;
    }

    // First, handle SIH intensities ½ and 1, corresponding to 2 and 4 in traditional
    // 8-bit terminology.  They are the darkest colors, so both hue and saturation levels
    // are both reduced.  The ½ ring gets 12 hues (2/sextant).  The 1 ring gets either 52
    // or 56, which is the augmentation count minus the 12.
    SIH_i = -1;  // signifies ½
    hue = 0f;
    saturation = 1f;
    brightness = 2 / 255f;
    for (coneSeg = 0; coneSeg < innermost.length; coneSeg++)
    {
        writeColor(pw, hue, saturation, brightness);
        hue += innermost[coneSeg] / 360f;
    }

    SIH_i = 1;
    hue = 0f;
    saturation = 1f;
    brightness = 4 / 255f;
    if (option == 1)
    {
        for (coneSeg = 0; coneSeg < second64.length; coneSeg++)
        {
            writeColor(pw, hue, saturation, brightness);
            hue += second64[coneSeg] / 360f;
        }
    }
      else
      {
        for (coneSeg = 0; coneSeg < second68.length; coneSeg++)
        {
            writeColor(pw, hue, saturation, brightness);
            hue += second68[coneSeg] / 360f;
        }
      }

    while (SIH_i < 74)
    {
        SIH_i += 1;
        brightness = SIH_i / 74f;
        if (SIH_i < satRings.length)
            ringCount = satRings[SIH_i];
          else
            ringCount = 16;
        for (ring = 1; ring <= ringCount; ring++)
        {
            // ring 1 and 2, the innermost saturation rings, are "hand-tuned".
            // The innermost ring steals 12 hues from the next ring.  I call this technique
            // the "Pale Parables", as at very low saturations, distinguishing between hues
            // is difficult.
            hue = 0f;
            saturation = ring / (float)ringCount;
            if (ring == 1)
            {
                for (coneSeg = 0; coneSeg < innermost.length; coneSeg++)
                {
                    writeColor(pw, hue, saturation, brightness);
                    hue += innermost[coneSeg] / 360f;
                }
            }
              else if (ring == 2)
              {
                if (option == 1)
                {
                    for (coneSeg = 0; coneSeg < second64.length; coneSeg++)
                    {
                        writeColor(pw, hue, saturation, brightness);
                        hue += second64[coneSeg] / 360f;
                    }
                }
                  else
                  {
                    for (coneSeg = 0; coneSeg < second68.length; coneSeg++)
                    {
                        writeColor(pw, hue, saturation, brightness);
                        hue += second68[coneSeg] / 360f;
                    }
                  }
              }
              else
              {
                if (option == 1)
                {
                    for (coneSeg = 0; coneSeg < outer64.length; coneSeg++)
                    {
                        writeColor(pw, hue, saturation, brightness);
                        hue += outer64[coneSeg] / 360f;
                    }
                }
                  else
                  {
                    for (coneSeg = 0; coneSeg < outer68.length; coneSeg++)
                    {
                        writeColor(pw, hue, saturation, brightness);
                        hue += outer68[coneSeg] / 360f;
                    }
                  }
               }
        }
    }

    return true;
}

static void build_pseudogrey_plus()
{
    /*  pseudogrey plus offsets (14 unique values)  */
    final int redxD[] = { 0, 0, 0, 1, 1, 1, 1, 2, 2, 2, 2, 0, 0, 0, 0, 1 };
    final int grnxD[] = { 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1 };
    final int bluxD[] = { 0, 1, 2, 0, 1, 1, 2, 0, 1, 2, 2, 0, 1, 2, 2, 0 };
    /*  normal pseudogrey offsets (7 unique values)  */
    final int redD[] = { 0, 0, 0, 0, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 1, 1 };
    final int grnD[] = { 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1 };
    final int bluD[] = { 0, 0, 1, 1, 0, 0, 0, 1, 1, 0, 0, 1, 1, 1, 0, 0 };

    int index4bit = 0;
    int index8bit = 0;
    int index12bit = 0;
    int current = 0;

    for (index8bit = 0; index8bit < 254; index8bit++)
    {
        for (index4bit = 0; index4bit < 16; index4bit++)
        {
            greyInts[index12bit] = ((index8bit + redxD[index4bit]) << 16)
                                 + ((index8bit + grnxD[index4bit]) << 8)
                                 +  (index8bit + bluxD[index4bit]);
            index12bit += 1;
        }
    }
    for (index4bit = 0; index4bit < 15; index4bit++)
    {   /*  index8bit equals 254  */
        greyInts[index12bit] = ((index8bit + redD[index4bit]) << 16)
                             + ((index8bit + grnD[index4bit]) << 8)
                             +  (index8bit + bluD[index4bit]);
        // need to stretch the bright end because pushing against white (infinity)
        greyInts[index12bit + 1] = greyInts[index12bit];
        index12bit += 2;
    }
    /*  index12bit equals 4094, almost white  */
    greyInts[4094] = greyInts[4093];
    greyInts[4095] = (255 << 16) + (255 << 8) + 255;  // only one "white infinity"
}

public static void main(String[] args)
{
    Integer option = 2;
    boolean success = true;
    String filename;
    PrintWriter pw = null;

    if (args.length < 1)
        option = -1;
      else
      {
        try { option = Integer.valueOf(args[0]); }
          catch(NumberFormatException e) { option = -1; }
      }

    if (option.equals(-1))
    {
        System.out.println(
            "Which Color Name Dictionary (CND) would you like to produce?");
        System.out.println("  java sihcnd 0 : build CND with only 12-bit greyscale");
        System.out.println("  java sihcnd 1 : build CND including 12-bit greyscale");
        System.out.println("  java sihcnd 2 : build CND with maximum colors and 316 greys");
        System.out.println("  java sihcnd 3 : build CND with 4096 colors, of form #34a");
        System.out.println("    (option 2 is the default)");
        System.exit(0);
    }

    /**
    *** Most options need at least the first 64 values of 12-bit greyscale,
    *** so always build the table.
    **/
    build_pseudogrey_plus();

    switch (option)
    {
      case 0:  // 4096 entries, extended pseudogrey only
        filename = grey_CND;
        pw = openPW(filename);
        if (pw.equals(null))  break;
        success = writeGrey(pw, option);
        break;

      case 1:  // 64,576 entries, including 4096 greys
        filename = greySIH_CND;
        pw = openPW(filename);
        if (pw.equals(null))  break;
        success = writeGrey(pw, option);
        if (!success)  break;
        success = writeColors(pw, option);
        break;

      case 2:  // 64,580 entries, augmented color,  including 316 greys
      default:
        filename = augmentedSIH_CND;
        pw = openPW(filename);
        if (pw.equals(null))  break;
        success = writeGrey(pw, option);
        if (!success)  break;
        success = writeColors(pw, option);
        break;

      case 3:  // 4,096 entries in CSS 3-digit hex form
        filename = css3digit_CND;
        pw = openPW(filename);
        if (pw.equals(null))  break;
        success = write3digithex(pw);
        break;
   }

    if (!pw.equals(null))  pw.close();
    if (success)
        System.out.println("output written to '" + filename + "'");
}

}
