/*
 * Copyright (c) 1998-2008 Caucho Technology -- all rights reserved
 *
 * This file is part of Resin(R) Open Source
 *
 * Each copy or derived work must preserve the copyright notice and this
 * notice unmodified.
 *
 * Resin Open Source is free software; you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation; either version 2 of the License, or
 * (at your option) any later version.
 *
 * Resin Open Source is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE, or any warranty
 * of NON-INFRINGEMENT.  See the GNU General Public License for more
 * details.
 *
 * You should have received a copy of the GNU General Public License
 * along with Resin Open Source; if not, write to the
 *   Free SoftwareFoundation, Inc.
 *   59 Temple Place, Suite 330
 *   Boston, MA 02111-1307  USA
 *
 * @author Scott Ferguson
 */

package com.caucho.xsl;

import com.caucho.util.CharBuffer;
import com.caucho.util.IntArray;

import java.util.ArrayList;

/**
 * Formatting for the xsl:number action.
 */
public class XslNumberFormat {
  private String head;
  private String tail;

  private String []separators;
  private int []formats;
  private int []zeroSize;

  private String format;
  private String lang;
  private boolean isAlphabetic;
  private String groupSeparator;
  private int groupSize;

  /**
   * Create a new number formatting object.
   *
   * @param format format string as specified by the XSLT draft
   * @param lang language for alphanumeric numbering
   * @param isAlphabetic resolves ambiguity for alphanumeric numbering
   * @param groupSeparator separator between number grouping
   * @param groupSize digits between group separator
   */
  public XslNumberFormat(String format, String lang, boolean isAlphabetic,
                         String groupSeparator, int groupSize)
  {
    this.format = format;
    this.lang = lang;
    this.isAlphabetic = isAlphabetic;
    if (groupSize <= 0) {
      groupSize = 3;
      groupSeparator = "";
    }
    if (groupSeparator == null)
      groupSeparator = "";
    this.groupSeparator = groupSeparator;
    this.groupSize = groupSize;

    if (format == null)
      format = "1";

    int headIndex = format.length();

    ArrayList separators = new ArrayList();
    IntArray zeroSizes = new IntArray();
    IntArray formats = new IntArray();

    CharBuffer cb = new CharBuffer();
    int i = 0;
    while (i < format.length()) {
      char ch;

      // scan the separator
      cb.clear();
      for (; i < format.length(); i++) {
	ch = format.charAt(i);
	if (Character.isLetterOrDigit(ch))
	  break;
	cb.append(ch);
      }

      // head and tail separators are just sticked on the ends
      if (head == null)
	head = cb.toString();
      else if (i >= format.length())
	tail = cb.toString();
      else
	separators.add(cb.toString());

      if (i >= format.length())
	break;

      // scan the format code
      int zeroSize = 1;
      int code = '0';
      for (; i < format.length(); i++) {
	ch = format.charAt(i);
	if (! Character.isLetterOrDigit(ch))
	  break;

	if (! Character.isDigit(ch)) {
	  if (code != '0' || zeroSize != 1)
	    code = 0;
	  else
	    code = ch;
	}
	else if (Character.digit(ch, 10) == 0 && zeroSize >= 0)
	  zeroSize++;
	else if (Character.digit(ch, 10) == 1)
	  code = ch - 1;
	else
	  code = 0;
      }
      if (code == 0)
	code = '0';

      zeroSizes.add(zeroSize);
      formats.add(code);
    }

    // default format is '1'
    if (formats.size() == 0) {
      tail = head;
      head = "";
      formats.add('0');
      zeroSizes.add(0);
    }

    // default separator is '.'
    if (separators.size() == 0)
      separators.add(".");
    if (separators.size() < formats.size())
      separators.add(separators.get(separators.size() - 1));

    this.separators = (String []) separators.toArray(new String[separators.size()]);
    this.zeroSize = zeroSizes.toArray();
    this.formats = formats.toArray();

    if (head == null)
      head = "";
    if (tail == null)
      tail = "";
  }
  
  public String getFormat()
  {
    return format;
  }
  
  public String getLang()
  {
    return lang;
  }
  
  public boolean isAlphabetic()
  {
    return isAlphabetic;
  }
  
  public String getGroupSeparator()
  {
    return groupSeparator;
  }
  
  public int getGroupSize()
  {
    return groupSize;
  }

  /**
   * Converts the array of numbers into a formatted number
   *
   * @param numbers array of numbers
   */
  void format(XslWriter out, IntArray numbers) 
  {
    CharBuffer buf = new CharBuffer();
    
    buf.append(head);

    int i;
    for (i = numbers.size() - 1; i >= 0; i--) {
      int index = numbers.size() - i - 1;
      if (index >= formats.length)
	index = formats.length - 1;

      char code = (char) formats[index];
      int zeroCount = zeroSize[index];

      int count = numbers.get(i);

      switch (code) {
      case 'i':
	romanize(buf, "mdclxvi", count);
	break;

      case 'I':
	romanize(buf, "MDCLXVI", count);
	break;
      
      case 'a':
	formatAlpha(buf, 'a', count);
	break;
      
      case 'A':
	formatAlpha(buf, 'A', count);
	break;
      
      default:
	formatDecimal(buf, code, zeroCount, count);
	break;
      }

      if (i > 0)
	buf.append(separators[index]);
    }

    buf.append(tail);

    out.print(buf.toString());
  }

  /**
   * Formats a roman numeral.  Numbers bigger than 5000 are formatted as
   * a decimal.
   *
   * @param cb buffer to accumulate the result
   * @param xvi roman characters, e.g. "mdclxvi" and "MDCLXVI" 
   * @param count the number to convert,
   */
  private void romanize(CharBuffer cb, String xvi, int count)
  {
    if (count <= 0)
      throw new RuntimeException();

    if (count > 5000) {
      cb.append(count);
      return;
    }

    for (; count > 1000; count -= 1000)
      cb.append(xvi.charAt(0));

    romanize(cb, xvi.charAt(0), xvi.charAt(1), xvi.charAt(2), count / 100);
    count %= 100;
    romanize(cb, xvi.charAt(2), xvi.charAt(3), xvi.charAt(4), count / 10);
    count %= 10;
    romanize(cb, xvi.charAt(4), xvi.charAt(5), xvi.charAt(6), count);
  }

  /**
   * Convert a single decimal digit to a roman number
   *
   * @param cb buffer to accumulate the result.
   * @param x character for the tens number
   * @param v character for the fives number
   * @param i character for the ones number
   * @param count digit to convert
   */
  private void romanize(CharBuffer cb, char x, char v, char i, int count)
  {
    switch (count) {
    case 0:
      break;
    case 1:
      cb.append(i);
      break;
    case 2:
      cb.append(i);
      cb.append(i);
      break;
    case 3:
      cb.append(i);
      cb.append(i);
      cb.append(i);
      break;
    case 4:
      cb.append(i);
      cb.append(v);
      break;
    case 5:
      cb.append(v);
      break;
    case 6:
      cb.append(v);
      cb.append(i);
      break;
    case 7:
      cb.append(v);
      cb.append(i);
      cb.append(i);
      break;
    case 8:
      cb.append(v);
      cb.append(i);
      cb.append(i);
      cb.append(i);
      break;
    case 9:
      cb.append(i);
      cb.append(x);
      break;

    default:
      throw new RuntimeException();
    }
  }

  /**
   * Format an alphabetic number.  Only English encodings are supported. 
   *
   * @param cb buffer to accumulate results
   * @param a starting character
   * @param count number to convert
   */
  private void formatAlpha(CharBuffer cb, char a, int count)
  {
    if (count <= 0)
      throw new RuntimeException();
    
    int index = cb.length();
    while (count > 0) {
      count--;
      cb.insert(index, (char) (a + count % 26));

      count /= 26;
    }
  }

  /**
   * Format a decimal number.
   *
   * @param cb buffer to accumulate results
   * @param code code for the zero digit
   * @param zeroCount minimum digits
   * @param count number to convert
   */
  private void formatDecimal(CharBuffer cb, int code,
			     int zeroCount, int count)
  {
    int digits = 0;
    int index = cb.length();
    while (count > 0) {
      if (digits > 0 && digits % groupSize == 0)
	cb.insert(index, groupSeparator);
      cb.insert(index, (char) (code + count % 10));
      count /= 10;
      digits++;
    }
    while (cb.length() - index < zeroCount) {
      cb.insert(index, (char) code);
    }
  }
}
