/*
 * Copyright (c) 1998-2006 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.es;

import com.caucho.util.CharBuffer;

class Printf {
  private final static int ALT = 0x01;
  private final static int ZERO_FILL = 0x02;
  private final static int POS_PLUS = 0x04;
  private final static int POS_SPACE = 0x08;
  private final static int LALIGN = 0x10;
  private final static int BIG = 0x20;
  private final static int NO_TRAIL_ZERO = 0x40;

  private static char []digits = {'0', '1', '2', '3', '4', '5', '6', '7', '8', '9',
			  'a', 'b', 'c', 'd', 'e', 'f'};
  private static char []bigDigits = {'0', '1', '2', '3', '4', '5', '6', '7', '8', '9',
			     'A', 'B', 'C', 'D', 'E', 'F'};

  private Printf()
  {
  }

  public static String sprintf(Call eval, int length)
    throws Throwable
  {
    if (length == 0)
      return "";

    CharBuffer buf = new CharBuffer();

    printf(buf, eval.getArg(0).toStr(), eval, length);

    return buf.toString();
  }

  public static CharBuffer printf(CharBuffer result, ESString format, 
				  Call eval, int length)
				  throws Throwable
  {
    int arg = 1;
    int len = format.length();

    for (int i = 0; i < len; i++) {
      int ch;
      int start = i;

      if ((ch = format.charAt(i)) != '%') {
	result.append((char) ch);
	continue;
      }

      int flags = 0;
    loop:
      while (++i < len) {
	switch ((ch = format.charAt(i))) {
	case '0': flags |= ZERO_FILL; break;
	case '+': flags |= POS_PLUS; break;
	case ' ': flags |= POS_SPACE; break;
	case '#': flags |= ALT; break;
	case '-': flags |= LALIGN; break;

	default: break loop;
	}
      }

      int width = 0;
      for (; i < len && (ch = format.charAt(i)) >= '0' && ch <= '9'; i++) {
	width = 10 * width + ch - '0';
      }

      if (i >= len) {
	fixBits(result, format, start, i);
	break;
      }

      int prec = 0;
      if (ch == '.') {
	while (++i < len && (ch = format.charAt(i)) >= '0' && ch <= '9') {
	  prec = 10 * prec + ch - '0';
	}
      } else
	prec = -1;

      if (i >= len) {
	fixBits(result, format, start, i);
	break;
      }

      switch (ch) {
      case '%':
	result.append((char) '%');
	break;

      case 'd':
	if (arg >= length)
	  throw new ESException("missing printf argument");
	formatInteger(result, eval.getArg(arg++).toNum(),
		      width, prec, flags, 10);
	break;

      case 'o':
	if (arg >= length)
	  throw new ESException("missing printf argument");
	formatInteger(result, eval.getArg(arg++).toNum(),
		      width, prec, flags, 8);
	break;

      case 'X':
	flags |= BIG;
      case 'x':
	if (arg >= length)
	  throw new ESException("missing printf argument");
	formatInteger(result, eval.getArg(arg++).toNum(), 
		      width, prec, flags, 16);
	break;

      case 'E':
      case 'G':
	flags |= BIG;
      case 'f':
      case 'e':
      case 'g':
	if (arg >= length)
	  throw new ESException("missing printf argument");
	formatDouble(result, eval.getArg(arg++).toNum(), 
		      width, prec, flags, ch);
	break;

      case 'c':
	if (arg >= length)
	  throw new ESException("missing printf argument");
	formatChar(result, (int) eval.getArg(arg++).toNum(), width, flags);
	break;

      case 's':
	if (arg >= length)
	  throw new ESException("missing printf argument");
	formatString(result, eval.getArg(arg++).toStr(), 
		     prec, width, flags);
	break;

      default:
	fixBits(result, format, start, i + 1);
	break;
      }
    }

    return result;
  }

  private static void formatDouble(CharBuffer cb, double value,
				  int prec, int flags, int type)
  {
    String raw = Double.toString(value);
    int expt = 0;
    int i = 0;
    CharBuffer digits = new CharBuffer();
    int ch = raw.charAt(i);
    boolean seenDigit = false;

    // XXX: locale screws us?
    for (; i < raw.length(); i++) {
      if ((ch = raw.charAt(i)) == '.' || ch == 'e' || ch == 'E')
	break;
      else if (! seenDigit && ch == '0') {
      }
      else {
	seenDigit = true;
	digits.append((char) ch);
	expt++;
      }
    }

    if (ch == '.')
      i++;

    for (; i < raw.length(); i++) {
      ch = raw.charAt(i);

      if (! seenDigit && ch == '0') {
	expt--;
      } else if (ch >= '0' && ch <= '9') {
	digits.append((char) ch);
	seenDigit = true;
      }
      else {
	int sign = 1;
	i++;
	if ((ch = raw.charAt(i)) == '+') {
	  i++;
	}
	else if (ch == '-') {
	  i++;
	  sign = -1;
	}
	  
	int e = 0;
	for (; i < raw.length() && (ch = raw.charAt(i)) >= '0' && ch <= '9';
	     i++) {
	  e = 10 * e + ch - '0';
	}
	
	expt += sign * e;
	break;
      }
    }

    if (! seenDigit)
      expt = 1;

    while (digits.length() > 0 && digits.charAt(digits.length() - 1) == '0')
      digits.setLength(digits.length() - 1);

    if (type == 'f') {
      if (roundDigits(digits, expt + prec)) {
	expt++;
      }

      formatFixed(cb, digits, expt, prec, flags);
    }
    else if (type == 'e' || type == 'E') {
      if (roundDigits(digits, prec + 1))
	expt++;

      formatExpt(cb, digits, expt, prec, flags);
    }
    else {
      if (roundDigits(digits, prec))
	expt++;

      if (expt < -3 || expt > prec)
	formatExpt(cb, digits, expt, prec - 1, flags|NO_TRAIL_ZERO);
      else
	formatFixed(cb, digits, expt, prec - expt, flags|NO_TRAIL_ZERO);
    }
  }

  private static void formatDouble(CharBuffer cb, double value,
				  int width, int prec, int flags, 
				  int type)
  {
    if (prec < 0)
      prec = 6;

    int offset = cb.length();

    if ((flags & ZERO_FILL) != 0 &&
	(value < 0 || (flags & (POS_PLUS|POS_SPACE)) != 0)) {
	offset++;
	width--;
    }

    if (value < 0) {
      cb.append((char) '-');
      value = -value;
    } else if ((flags & POS_PLUS) != 0) {
      cb.append((char) '+');
    } else if ((flags & POS_SPACE) != 0) {
      cb.append((char) ' ');
    }

    formatDouble(cb, value, prec, flags, type);

    width -= cb.length() - offset;

    for (int i = 0; i < width; i++) {
      if ((flags & LALIGN) != 0)
	cb.append(' ');
      else
	cb.insert(offset, (flags & ZERO_FILL) == 0 ? ' ' : '0');
    }
  }

  private static boolean roundDigits(CharBuffer digits, int len)
  {
    if (len < 0 || digits.length() <= len)
      return false;

    int value = digits.charAt(len);
    if (value < '5')
      return false;

    for (int i = len - 1; i >= 0; i--) {
      int ch = digits.charAt(i);

      if (ch != '9') {
	digits.setCharAt(i, (char) (ch + 1));
	return false;
      }
      digits.setCharAt(i, '0');
    }

    digits.insert(0, '1');

    return true;
  }

  private static void formatFixed(CharBuffer cb, CharBuffer digits,
				 int expt, int prec, int flags)
  {
    int i = 0;
    int origExpt = expt;

    for (; expt > 0; expt--) {
      if (i < digits.length())
	cb.append((char) digits.charAt(i++));
      else
	cb.append('0');
    }
    
    if (origExpt <= 0) // || digits.length() == 0)
      cb.append('0');

    if (prec > 0 || (flags & ALT) != 0)
      cb.append('.');

    for (; expt < 0 && prec > 0; expt++) {
      cb.append('0');
      prec--;
    }

    for (; prec > 0 && i < digits.length(); i++) {
      cb.append(digits.charAt(i));
      prec--;
    }

    for (; prec > 0 && (flags & (NO_TRAIL_ZERO|ALT)) != NO_TRAIL_ZERO; prec--)
      cb.append('0');
  }

  private static void formatExpt(CharBuffer cb, CharBuffer digits,
				 int expt, int prec, int flags)
  {
    if (digits.length() == 0)
      cb.append('0');
    else
      cb.append((char) digits.charAt(0));

    if (prec > 0 || (flags & ALT) != 0)
      cb.append('.');

    for (int i = 1; i < digits.length(); i++) {
      if (prec > 0)
	cb.append((char) digits.charAt(i));
      prec--;
    }

    for (; prec > 0 && (flags & (NO_TRAIL_ZERO|ALT)) != NO_TRAIL_ZERO; prec--)
      cb.append('0');

    if ((flags & BIG) != 0)
      cb.append('E');
    else
      cb.append('e');

    formatInteger(cb, expt - 1, 0, 2, POS_PLUS, 10);
  }

  private static void formatInteger(CharBuffer cb, double dvalue,
				   int width, int prec, int flags, int radix)
  {
    boolean isBig = (flags & BIG) != 0;
    int begin = cb.length();

    long value;
    if (dvalue > 0)
      value = (long) (dvalue + 0.5);
    else
      value = (long) (dvalue - 0.5);

    if (value < 0 && radix == 10) {
      cb.append((char) '-');
      value = -value;
    } else if (value >= 0 && radix == 10 && (flags & POS_PLUS) != 0)
      cb.append((char) '+');
    else if (value >= 0 && radix == 10 && (flags & POS_SPACE) != 0)
      cb.append((char) ' ');
    else if (value < 0)
      value &= 0xffffffffL;
    else if (radix == 8 && (flags & ALT) != 0 && value != 0)
      cb.append('0');
    else if (radix == 16 && (flags & ALT) != 0)
      cb.append((flags & BIG) == 0 ? "0x" : "0X");

    if ((flags & ZERO_FILL) != 0) {
      width -= cb.length() - begin;
      begin = cb.length();
    }

    int offset = cb.length();
    int len = 0;
    while (value != 0) {
      len++;
      cb.insert(offset, (isBig ? bigDigits : digits)[(int) (value % radix)]);
      value /= radix;
    }

    for (int i = 0; i < prec - len; i++)
      cb.insert(offset, '0');
    if (len == 0 && prec == 0)
      cb.insert(offset, '0');

    width -= cb.length() - begin;
    for (; width > 0; width--) {
      if ((flags & LALIGN) != 0)
	cb.append(' ');
      else if ((flags & ZERO_FILL) != 0 && prec < 0)
	cb.insert(begin, '0');
      else
	cb.insert(begin, ' ');
    }

    if (cb.length() == begin)
      cb.append('0');
  }

  private static void formatChar(CharBuffer cb, int ch, int width, int flags)
  {
    int offset = cb.length();

    cb.append((char) ch);

    if ((flags & LALIGN) == 0) {
      for (int i = 0; i < width - 1; i++)
	cb.insert(offset, (char) ' ');
    } else {
      for (int i = 0; i < width - 1; i++)
	cb.append((char) ' ');
    }
  }

  private static void formatString(CharBuffer cb, ESString string,
				  int prec, int width, int flags)
  {
    int offset = cb.length();

    if (prec < 0)
      prec = Integer.MAX_VALUE;

    for (int i = 0; i < string.length() && i < prec; i++) {
      width--;
      cb.append(string.charAt(i));
    }

    if ((flags & LALIGN) == 0) {
      for (int i = 0; i < width; i++)
	cb.insert(offset, (char) ' ');
    } else {
      for (int i = 0; i < width; i++)
	cb.append((char) ' ');
    }
  }

  private static void fixBits(CharBuffer cb, ESString format, int s, int i)
  {
    for (; s < i; s++)
      cb.append((char) format.charAt(s));
  }
}
