//
// RomanNumeralFormat.java
//
// by Bill Seymour, 2014-12-30
//
// This file is in the public domain.
//

package seminumerical.antiquities;

import java.text.Format;
import java.text.FieldPosition;
import java.text.ParsePosition;
import java.text.ParseException;

//
// Because Format implements Serializable (*headdesk*):
//
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.io.IOException;
import java.io.ObjectStreamException;

public class RomanNumeralFormat extends Format
{
    public static final int ROMAN_NUMERAL_FIELD = 0;

    public static final int NULLUS = 0; // extra added attraction
    public static final int MMMMCMXCIX = 4999;

    public static final int MIN_ROMAN_NUMERAL = NULLUS; // You're welcome.
    public static final int MAX_ROMAN_NUMERAL = MMMMCMXCIX;

    public static final int QUIET_NaRN = -1;

    private int formatCase;
    public static final int FORMAT_UPPER = 0;
    public static final int FORMAT_LOWER = 1;

    private static int checkFormatCase(int fc)
    {
        if (fc == FORMAT_UPPER || fc == FORMAT_LOWER)
            return fc;
        throw new IllegalArgumentException("Invalid format case " + fc);
    }

    public RomanNumeralFormat(int fc)
    {
        formatCase = checkFormatCase(fc);
    }
    public static RomanNumeralFormat getInstance(int fc)
    {
        return new RomanNumeralFormat(fc);
    }

    public RomanNumeralFormat()
    {
        formatCase = FORMAT_UPPER;
    }
    public static RomanNumeralFormat getInstance()
    {
        return new RomanNumeralFormat();
    }

    public RomanNumeralFormat(RomanNumeralFormat rnf)
    {
        formatCase = rnf.formatCase;
    }
    @Override public Object clone() // Format implements Cloneable
    {
        return new RomanNumeralFormat(this);
    }

    public static boolean isGroupingUsed() { return false; }
    public static boolean isParseIntegerOnly() { return true; }

    public int getFormatCase()
    {
        return formatCase;
    }
    public void setFormatCase(int fc)
    {
        formatCase = checkFormatCase(fc);
    }

    private static final class Fmt
    {
        final int    ival;
        final String sval;
        Fmt(int i, String s) { ival = i; sval = s; }
    }
    private static final Fmt[][] formats =
    {
        {
            new Fmt(3000, "MMM"), new Fmt(2000, "MM"), new Fmt(1000, "M"),
            new Fmt( 900, "CM"),  new Fmt( 500, "D"),  new Fmt( 400, "CD"),
            new Fmt( 300, "CCC"), new Fmt( 200, "CC"), new Fmt( 100, "C"),
            new Fmt(  90, "XC"),  new Fmt(  50, "L"),  new Fmt(  40, "XL"),
            new Fmt(  30, "XXX"), new Fmt(  20, "XX"), new Fmt(  10, "X"),
            new Fmt(   9, "IX"),  new Fmt(   5, "V"),  new Fmt(   4, "IV"),
            new Fmt(   3, "III"), new Fmt(   2, "II"), new Fmt(   1, "I")
        },
        {
            new Fmt(3000, "mmm"), new Fmt(2000, "mm"), new Fmt(1000, "m"),
            new Fmt( 900, "cm"),  new Fmt( 500, "d"),  new Fmt( 400, "cd"),
            new Fmt( 300, "ccc"), new Fmt( 200, "cc"), new Fmt( 100, "c"),
            new Fmt(  90, "xc"),  new Fmt(  50, "l"),  new Fmt(  40, "xl"),
            new Fmt(  30, "xxx"), new Fmt(  20, "xx"), new Fmt(  10, "x"),
            new Fmt(   9, "ix"),  new Fmt(   5, "v"),  new Fmt(   4, "iv"),
            new Fmt(   3, "iii"), new Fmt(   2, "ii"), new Fmt(   1, "i")
        }
    };

    private static final char[] nullusFormat = { 'N', 'n' };

    public StringBuffer format(int val, StringBuffer dest, FieldPosition pos)
    {
        if (val < MIN_ROMAN_NUMERAL || val > MAX_ROMAN_NUMERAL)
            throw new IllegalArgumentException("Not a Roman numeral:  " + val);

        if (dest == null)
            dest = new StringBuffer();

        if (pos != null)
            pos.setBeginIndex(dest.length());

        if (val == NULLUS)
        {
            dest.append(nullusFormat[formatCase]);
        }
        else
        {
            Fmt[] fmt = formats[formatCase];

            for (int i = 0; val != NULLUS; ++i)
            {
                Fmt f = fmt[i];
                if (val >= f.ival)
                {
                    dest.append(f.sval);
                    val -= f.ival;
                }
            }
        }

        if (pos != null)
            pos.setEndIndex(dest.length());

        return dest;
    }

    public String format(int val)
    {
        return format(val, null, null).toString();
    }

    //
    // The tokens returned by the lexer:
    //
    private static final int M = 0;
    private static final int D = 1;
    private static final int C = 2;
    private static final int L = 3;
    private static final int X = 4;
    private static final int V = 5;
    private static final int I = 6;
    private static final int N = 7;
    private static final int WHITE = 8;
    private static final int PUNCT = 9;
    private static final int OTHER = 10;

    //
    // We want to allow punctuation to successfully terminate a parse;
    // but since there's no Character.isPunctuation(char), and since a
    // token table with an entry for every possible Unicode code unit
    // would be humongous, we'll define punctuation to be only graphic
    // ASCII characters that aren't digits or letters.
    //
    private static final int[] token =
    {
      /*00*/ OTHER, OTHER, OTHER, OTHER, OTHER, OTHER, OTHER, OTHER,
      /*08*/ OTHER, OTHER, OTHER, OTHER, OTHER, OTHER, OTHER, OTHER,
      /*10*/ OTHER, OTHER, OTHER, OTHER, OTHER, OTHER, OTHER, OTHER,
      /*18*/ OTHER, OTHER, OTHER, OTHER, OTHER, OTHER, OTHER, OTHER,

      /*20*/ WHITE, PUNCT, PUNCT, PUNCT, PUNCT, PUNCT, PUNCT, PUNCT, // !"#$%&'
      /*28*/ PUNCT, PUNCT, PUNCT, PUNCT, PUNCT, PUNCT, PUNCT, PUNCT, //()*+,-./
      /*30*/ OTHER, OTHER, OTHER, OTHER, OTHER, OTHER, OTHER, OTHER, //01234567
      /*38*/ OTHER, OTHER, PUNCT, PUNCT, PUNCT, PUNCT, PUNCT, PUNCT, //89:;<=>?

      /*40*/ PUNCT, OTHER, OTHER, C,     D,     OTHER, OTHER, OTHER, //@ABCDEFG
      /*48*/ OTHER, I,     OTHER, OTHER, L,     M,     N,     OTHER, //HIJKLMNO
      /*50*/ OTHER, OTHER, OTHER, OTHER, OTHER, OTHER, V,     OTHER, //PQRSTUVW
      /*58*/ X,     OTHER, OTHER, PUNCT, PUNCT, PUNCT, PUNCT, PUNCT, //XYZ[\]^_

      /*60*/ PUNCT, OTHER, OTHER, C,     D,     OTHER, OTHER, OTHER, //`abcdefg
      /*68*/ OTHER, I,     OTHER, OTHER, L,     M,     N,     OTHER, //hijklmno
      /*70*/ OTHER, OTHER, OTHER, OTHER, OTHER, OTHER, V,     OTHER, //pqrstuvw
      /*78*/ X,     OTHER, OTHER, PUNCT, PUNCT, PUNCT, PUNCT, OTHER  //xyz{|}~
    };

    private static final char TERMCHAR = ','; // successfully terminates parse
    private static final char NONASCII = '\u0080';

    private static int lex(char c)
    {
        if (Character.isWhitespace(c))
            return WHITE;  // Any Unicode whitespace character is OK;
        if (c >= NONASCII) // but any other non-ASCII character
            return OTHER;  // is just bogus.
        return token[c];
    }

    //
    // The parser's starting and ending states:
    //
    private static final int START =  0;
    private static final int OK    = -1;
    private static final int ERR   = -2;

    //
    // The parser is a finite state machine implemented as a three-dimensional
    // array of ints.
    //
    // The parse table's outer dimension is the current state of the machine;
    // the middle dimension is the token returned by the lexer; [][][0] is
    // the next state of the machine; [][][1] is the value that the token
    // represents in the current state.
    //
    private static final int[][][] parseTable =
    {
// state     M/X/WHITE      D/V/PUNCT      C/I/OTHER        L/N
  /* 0*/ { {  1, 1000  }, {  5, 500   }, {  6, 100   }, {  9, 50    },
           { 10, 10    }, { 13, 5     }, { 14, 1     }, { 16, NULLUS},
           {  0, NULLUS}, {ERR, NULLUS}, {ERR, NULLUS} },
  /* 1*/ { {  2, 1000  }, {  5, 500   }, {  6, 100   }, {  9, 50    },
           { 10, 10    }, { 13, 5     }, { 14, 1     }, {ERR, NULLUS},
           { OK, NULLUS}, { OK, NULLUS}, {ERR, NULLUS} },
  /* 2*/ { {  3, 1000  }, {  5, 500   }, {  6, 100   }, {  9, 50    },
           { 10, 10    }, { 13, 5     }, { 14, 1     }, {ERR, NULLUS},
           { OK, NULLUS}, { OK, NULLUS}, {ERR, NULLUS} },
  /* 3*/ { {  4, 1000  }, {  5, 500   }, {  6, 100   }, {  9, 50    },
           { 10, 10    }, { 13, 5     }, { 14, 1     }, {ERR, NULLUS},
           { OK, NULLUS}, { OK, NULLUS}, {ERR, NULLUS} },
  /* 4*/ { {ERR, NULLUS}, {  5, 500   }, {  6, 100   }, {  9, 50    },
           { 10, 10    }, { 13, 5     }, { 14, 1     }, {ERR, NULLUS},
           { OK, NULLUS}, { OK, NULLUS}, {ERR, NULLUS} },
  /* 5*/ { {ERR, NULLUS}, {ERR, NULLUS}, { 17, 100   }, {  9, 50    },
           { 10, 10    }, { 13, 5     }, { 14, 1     }, {ERR, NULLUS},
           { OK, NULLUS}, { OK, NULLUS}, {ERR, NULLUS} },
  /* 6*/ { {  8, 800   }, {  8, 300   }, {  7, 100   }, {  9, 50    },
           { 10, 10    }, { 13, 5     }, { 14, 1     }, {ERR, NULLUS},
           { OK, NULLUS}, { OK, NULLUS}, {ERR, NULLUS} },
  /* 7*/ { {ERR, NULLUS}, {ERR, NULLUS}, {  8, 100   }, {  9, 50    },
           { 10, 10    }, { 13, 5     }, { 14, 1     }, {ERR, NULLUS},
           { OK, NULLUS}, { OK, NULLUS}, {ERR, NULLUS} },
  /* 8*/ { {ERR, NULLUS}, {ERR, NULLUS}, {ERR, NULLUS}, {  9, 50    },
           { 10, 10    }, { 13, 5     }, { 14, 1     }, {ERR, NULLUS},
           { OK, NULLUS}, { OK, NULLUS}, {ERR, NULLUS} },
  /* 9*/ { {ERR, NULLUS}, {ERR, NULLUS}, {ERR, NULLUS}, {ERR, NULLUS},
           { 18, 10    }, { 13, 5     }, { 14, 1     }, {ERR, NULLUS},
           { OK, NULLUS}, { OK, NULLUS}, {ERR, NULLUS} },
// state     M/X/WHITE      D/V/PUNCT      C/I/OTHER        L/N
  /*10*/ { {ERR, NULLUS}, {ERR, NULLUS}, { 12, 80    }, { 12, 30    },
           { 11, 10    }, { 13, 5     }, { 14, 1     }, {ERR, NULLUS},
           { OK, NULLUS}, { OK, NULLUS}, {ERR, NULLUS} },
  /*11*/ { {ERR, NULLUS}, {ERR, NULLUS}, {ERR, NULLUS}, {ERR, NULLUS},
           { 12, 10    }, { 13, 5     }, { 14, 1     }, {ERR, NULLUS},
           { OK, NULLUS}, { OK, NULLUS}, {ERR, NULLUS} },
  /*12*/ { {ERR, NULLUS}, {ERR, NULLUS}, {ERR, NULLUS}, {ERR, NULLUS},
           {ERR, NULLUS}, { 13, 5     }, { 14, 1     }, {ERR, NULLUS},
           { OK, NULLUS}, { OK, NULLUS}, {ERR, NULLUS} },
  /*13*/ { {ERR, NULLUS}, {ERR, NULLUS}, {ERR, NULLUS}, {ERR, NULLUS},
           {ERR, NULLUS}, {ERR, NULLUS}, { 19, 1     }, {ERR, NULLUS},
           { OK, NULLUS}, { OK, NULLUS}, {ERR, NULLUS} },
  /*14*/ { {ERR, NULLUS}, {ERR, NULLUS}, {ERR, NULLUS}, {ERR, NULLUS},
           { 16, 8     }, { 16, 3     }, { 15, 1     }, {ERR, NULLUS},
           { OK, NULLUS}, { OK, NULLUS}, {ERR, NULLUS} },
  /*15*/ { {ERR, NULLUS}, {ERR, NULLUS}, {ERR, NULLUS}, {ERR, NULLUS},
           {ERR, NULLUS}, {ERR, NULLUS}, { 16, 1     }, {ERR, NULLUS},
           { OK, NULLUS}, { OK, NULLUS}, {ERR, NULLUS} },
  /*16*/ { {ERR, NULLUS}, {ERR, NULLUS}, {ERR, NULLUS}, {ERR, NULLUS},
           {ERR, NULLUS}, {ERR, NULLUS}, {ERR, NULLUS}, {ERR, NULLUS},
           { OK, NULLUS}, { OK, NULLUS}, {ERR, NULLUS} },
  /*17*/ { {ERR, NULLUS}, {ERR, NULLUS}, {  7, 100   }, {  9, 50    },
           { 10, 10    }, { 13, 5     }, { 14, 1     }, {ERR, NULLUS},
           { OK, NULLUS}, { OK, NULLUS}, {ERR, NULLUS} },
  /*18*/ { {ERR, NULLUS}, {ERR, NULLUS}, {ERR, NULLUS}, {ERR, NULLUS},
           { 11, 10    }, { 13, 5     }, { 14, 1     }, {ERR, NULLUS},
           { OK, NULLUS}, { OK, NULLUS}, {ERR, NULLUS} },
  /*19*/ { {ERR, NULLUS}, {ERR, NULLUS}, {ERR, NULLUS}, {ERR, NULLUS},
           {ERR, NULLUS}, {ERR, NULLUS}, { 15, 1     }, {ERR, NULLUS},
           { OK, NULLUS}, { OK, NULLUS}, {ERR, NULLUS} }
    };

    //
    // The least significant dimension:
    //
    private static final int NEXT_STATE = 0;
    private static final int VALUE = 1;

  //
  // The parser is indifferent to whether its input is in upper or lower case,
  // so the following two methods can just be static.
  //

    public static int parse(String s, ParsePosition pos)
    {
        int idx = pos.getIndex();
        int max = s.length();

        int result = NULLUS;
        int state  = START;

        for (;;)
        {
            char c = idx < max ? s.charAt(idx) : TERMCHAR;

            int[] parseNode = parseTable[state][lex(c)];

            if ((state = parseNode[NEXT_STATE]) < START)
                break;

            result += parseNode[VALUE];
            ++idx;
        }

        if (state == OK)
        {
            pos.setIndex(idx);
        }
        else
        {
            pos.setErrorIndex(idx);
            result = QUIET_NaRN;
        }

        return result;
    }

    public static int parse(String s) throws ParseException
    {
        ParsePosition pos = new ParsePosition(0);
        int result = parse(s, pos);
        if (result != QUIET_NaRN)
            return result;
        throw new ParseException("Not a Roman numeral:  " + s,
                                 pos.getErrorIndex());
    }

  //
  // extends Format:
  //
    @Override
    public StringBuffer format(Object obj, StringBuffer buf, FieldPosition pos)
    {
        if (obj instanceof Number)
            return format(((Number)obj).intValue(), buf, pos);
        throw new IllegalArgumentException(
            "Not a Roman numeral; indeed, not a number at all");
    }
    @Override public Object parseObject(String s, ParsePosition pos)
    {
        return new Integer(parse(s, pos));
    }
    @Override public Object parseObject(String s) throws ParseException
    {
        return new Integer(parse(s));
    }

  //
  // Format implements Serializable (*ugh*):
  //
    private void writeObject(ObjectOutputStream out) throws IOException
    {
        out.writeInt(formatCase);
    }
    private void readObject(ObjectInputStream in)
      throws IOException, ClassNotFoundException
    {
        formatCase = in.readInt();
    }
    private void readObjectNoData() throws ObjectStreamException
    {
        formatCase = FORMAT_UPPER;
    }
    private static final long serialVersionUID = 1L;
}

// End of RomanNumeralFormat.java