/*
 * Copyright (c) 2011 Hidenori Sugiyama Java's String.format() like string formatting library. Licensed under the MIT
 * license. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT
 * LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
 * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF
 * CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
 * THE SOFTWARE.
 */

( function( )
{

  /**
   * This is a Java String.format() like string formatting library for JavaScript.
   * 
   * @param format
   * @param arg1...
   * @returns
   */
  String.format = function( )
  {
    var args = Array.prototype.slice.apply(arguments), formatter = new Formatter( );
    return formatter.format(args.shift( ).toString( ), args);

  };

  if (typeof jQuery != 'undefined') { // jQuery plugin
    jQuery.extend({
      format : function(fmt)
      {
        return String.format.apply(this, arguments);
      }
    });
  }

  /*
   * Utilities
   */
  function multiplePush(v, c, len)
  {
    for ( var i = 0; i < len; i++) {
      v.push(c);
    }
    return v;
  }

  function paddingLeft(str, pad, len)
  {
    return ( multiplePush(Array( ), pad, len).join('') + str ).slice(-len);
  }

  function paddingRight(str, pad, len)
  {
    return ( str + multiplePush(Array( ), pad, len).join('') ).slice(0, len);
  }

  var AbstractConverter = function(isUpperCase)
  {
    this.isUpperCase = isUpperCase;
  };

  AbstractConverter.prototype.format = function(str, flags, width)
  {
    assert(!flags.alternateForm, "FormatFlagsConversionMismatch");

    if (width && ( str.length < width )) {
      if (flags.leftAlign) {
        str = paddingRight(str, ' ', width);
      } else {
        str = paddingLeft(str, ' ', width);
      }
    }

    if (this.isUpperCase) {
      str = str.toUpperCase( );
    }

    return str;
  };

  AbstractConverter.prototype.convert = function(argument, precision, flags, width, conversion)
  {
    if (argument === undefined) {
      return conversion;
    }

    return this._convert(argument, precision, flags, width, conversion);
  };

  AbstractConverter.prototype._convert = function(argument, precision, flags, width, conversion)
  {
    return "";
  };

  /*
   * class BooleanConverter
   */
  var BooleanConverter = function(isUpperCase)
  {
    AbstractConverter.call(this, isUpperCase);
  };
  BooleanConverter.prototype = new AbstractConverter( );

  BooleanConverter.prototype._convert = function(argument, precision, flags, width, conversion)
  {
    var str = '';

    if (argument == null) {
      str = 'false';
    } else if (typeof argument == 'boolean') {
      str = ( argument ) ? 'true' : 'false';
    } else {
      str = 'true';
    }

    return this.format(str, flags, width);
  };

  /*
   * class JSONConverter
   */
  var JSONConverter = function(isUpperCase)
  {
    AbstractConverter.call(this, isUpperCase);
  };
  JSONConverter.prototype = new AbstractConverter( );

  JSONConverter.prototype._convert = function(argument, precision, flags, width, conversion)
  {
    return this.format(JSON.stringify(argument), flags, width);
  };

  /*
   * class AbstractCharacterConverter
   */
  var AbstractCharacterConverter = function(isUpperCase)
  {
    AbstractConverter.call(this, isUpperCase);
  };
  AbstractCharacterConverter.prototype = new AbstractConverter( );

  /*
   * class StringConverter
   */
  var StringConverter = function(isUpperCase)
  {
    AbstractCharacterConverter.call(this, isUpperCase);
  };

  StringConverter.prototype = new AbstractCharacterConverter( );

  StringConverter.prototype._convert = function(argument, precision, flags, width, conversion)
  {
    var str = argument.toString( );
    return this.format(str, flags, width);
  };

  /*
   * class CharacterConverter
   */
  var CharacterConverter = function(isUpperCase)
  {
    AbstractCharacterConverter.call(this, isUpperCase);
  };

  CharacterConverter.prototype = new AbstractCharacterConverter( );

  CharacterConverter.prototype._convert = function(argument, precision, flags, width, conversion)
  {
    var str = argument.toString( );
    assert(str.length == 1, "Argument is not a character.");

    return this.format(str, flags, width);
  };

  /*
   * class IntegerConverter
   */
  var IntegerConverter = function(isUpperCase)
  {
    AbstractConverter.call(this, isUpperCase);
  };
  IntegerConverter.prototype = new AbstractConverter( );

  IntegerConverter.prototype.format = function(num, str, flags, width)
  {
    if (flags.alternateForm) {
      str = this.formatAlternateForm(str);
    }
    if (flags.signPlus) {
      str = this.formatSignPlus(str, num);
    }
    if (flags.signSpace) {
      str = this.formatSignSpace(str, num);
    }
    if (flags.groupingSeparator) {
      str = this.formatGroupingSepartor(str);
    }
    if (flags.surroundNegative) {
      str = this.formatSurroundNegative(str, num);
    }

    if (width && ( str.length < width )) {
      str = this.formatPadding(str, flags, width);
    }

    if (this.isUpperCase) {
      str = str.toUpperCase( );
    }

    return str;
  };

  IntegerConverter.prototype.formatPadding = function(str, flags, width)
  {
    if (flags.leftAlign) {
      str = paddingRight(str, ' ', width);
    } else {
      var pad = ( flags.zeroPadding ) ? '0' : ' ';
      str = paddingLeft(str, pad, width);
    }
    return str;
  };

  IntegerConverter.prototype.formatSignPlus = function(str, num)
  {
    assert(false, "FormatFlagsConversionMismatch");
    return "";
  };

  IntegerConverter.prototype.formatSignSpace = function(str, num)
  {
    assert(false, "FormatFlagsConversionMismatch");
    return "";
  };

  IntegerConverter.prototype.formatGroupingSepartor = function(str)
  {
    assert(false, "FormatFlagsConversionMismatch");
    return "";
  };

  IntegerConverter.prototype.formatSurroundNegative = function(str, num)
  {
    assert(false, "FormatFlagsConversionMismatch");
    return "";
  };

  IntegerConverter.prototype.formatAlternateForm = function(str)
  {
    assert(false, "FormatFlagsConversionMismatch");
    return "";
  };

  /*
   * class DecimalConverter
   */
  var DecimalConverter = function( )
  {
    IntegerConverter.call(this, false);
  };

  DecimalConverter.prototype = new IntegerConverter( );

  DecimalConverter.prototype._convert = function(argument, precision, flags, width, conversion)
  {
    var num = Math.floor(argument);
    var str = num.toString( );

    str = this.format(num, str, flags, width);

    return str;
  };

  DecimalConverter.prototype.formatSignPlus = function(str, num)
  {
    if (num > 0) {
      return '+' + str;
    } else {
      return str;
    }
  };

  DecimalConverter.prototype.formatSignSpace = function(str, num)
  {
    if (num > 0) {
      return ' ' + str;
    } else {
      return str;
    }
  };

  DecimalConverter.prototype.formatGroupingSepartor = function(str)
  {
    var s = '';
    for ( var i = str.length; i > 3; i -= 3) {
      s = ',' + str.substring(i - 3, i) + s;
    }
    return str.substring(0, i) + s;
  };

  DecimalConverter.prototype.formatSurroundNegative = function(str, num)
  {
    if (num < 0) {
      return '(' + str.substring(1, str.length) + ')';
    } else {
      return str;
    }
  };

  /*
   * class OctalConverter
   */
  var OctalConverter = function(isUpperCase)
  {
    IntegerConverter.call(this, isUpperCase);
  };

  OctalConverter.prototype = new IntegerConverter( );

  OctalConverter.prototype._convert = function(argument, precision, flags, width, conversion)
  {
    var num = Math.floor(argument);
    var str = num.toString(8);

    return this.format(num, str, flags, width);
  };

  OctalConverter.prototype.formatAlternateForm = function(str)
  {
    return '0' + str;
  };

  /*
   * class HexadecimalConverter
   */
  var HexadecimalConverter = function(isUpperCase)
  {
    IntegerConverter.call(this, isUpperCase);
  };

  HexadecimalConverter.prototype = new IntegerConverter( );

  HexadecimalConverter.prototype._convert = function(argument, precision, flags, width, conversion)
  {
    var num = Math.floor(argument);
    var str = num.toString(16);

    return this.format(num, str, flags, width);
  };

  HexadecimalConverter.prototype.formatAlternateForm = function(str)
  {
    return '0x' + str;
  };

  /*
   * class AbstractFloatConverter
   */
  var AbstractFloatConverter = function(isUpperCase)
  {
    AbstractConverter.call(this, isUpperCase);
  };
  AbstractFloatConverter.prototype = new AbstractConverter( );

  AbstractFloatConverter.prototype.format = function(num, str, flags, width)
  {
    if (flags.alternateForm) {
      str = this.formatAlternateForm(str);
    }
    if (flags.signPlus) {
      str = this.formatSignPlus(str, num);
    }
    if (flags.signSpace) {
      str = this.formatSignSpace(str, num);
    }
    if (flags.groupingSeparator) {
      str = this.formatGroupingSepartor(str);
    }
    if (flags.surroundNegative) {
      str = this.formatSurroundNegative(str, num);
    }

    if (width && ( str.length < width )) {
      str = this.formatPadding(str, flags, width);
    }

    if (this.isUpperCase) {
      str = str.toUpperCase( );
    }

    return str;
  };

  AbstractFloatConverter.prototype.formatPadding = function(str, flags, width)
  {
    if (flags.leftAlign) {
      str = paddingRight(str, ' ', width);
    } else {
      var pad = ( flags.zeroPadding ) ? '0' : ' ';
      str = paddingLeft(str, pad, width);
    }
    return str;
  };

  AbstractFloatConverter.prototype.formatAlternateForm = function(str)
  {
    return str;
  };

  AbstractFloatConverter.prototype.formatSignPlus = function(str, num)
  {
    if (num > 0) {
      return '+' + str;
    } else {
      return str;
    }
  };

  AbstractFloatConverter.prototype.formatSignSpace = function(str, num)
  {
    if (num > 0) {
      return ' ' + str;
    } else {
      return str;
    }
  };

  AbstractFloatConverter.prototype.formatGroupingSepartor = function(str)
  {
    var integerPart = str.substring(0, str.indexOf('.'));
    var floatingPart = str.substring(str.indexOf('.') + 1, str.length);

    var s = '', i = integerPart.length;
    for (; i > 3; i -= 3) {
      s = ',' + integerPart.substring(i - 3, i) + s;
    }

    if (floatingPart) {
      return str.substring(0, i) + s + '.' + floatingPart;
    } else {
      return str.substring(0, i) + s;
    }
  };

  AbstractFloatConverter.prototype.formatSurroundNegative = function(str, num)
  {
    if (num < 0) {
      return '(' + str.substring(1, str.length) + ')';
    } else {
      return str;
    }
  };

  AbstractFloatConverter.prototype.normalize = function(num)
  {
    var exp = {};

    var vals = num.toExponential( ).split(/e/);
    exp.real = vals[ 0 ];
    exp.sign = vals[ 1 ].charAt(0);
    exp.base = vals[ 1 ].substring(1, vals[ 1 ].length);

    return exp;
  };

  /*
   * class ExponentialConverter
   */
  var ExponentialConverter = function(isUpperCase)
  {
    AbstractFloatConverter.call(this, isUpperCase);
  };

  ExponentialConverter.prototype = new AbstractFloatConverter;

  ExponentialConverter.prototype._convert = function(argument, precision, flags, width, conversion)
  {
    if (precision == -1) {
      precision = 6;
    }

    var num = parseFloat(argument);
    var exp = this.normalize(num);
    var str = this.toExponential(exp, precision);

    return this.format(num, str, flags, width);
  };

  ExponentialConverter.prototype.formatGroupingSepartor = function(str, num)
  {
    assert(false, "FormatFlagsConversionMismatch");
  };

  ExponentialConverter.prototype.toExponential = function(exp, precision)
  {
    var offset = 2;

    var real = exp.real;
    if (!real.match(/\./)) {
      real = real + '.';
    }
    if (real < 0) {
      offset += 1;
    }

    real = real.substring(0, precision + offset);
    real = paddingRight(real, '0', precision + offset);

    return real + 'e' + exp.sign + paddingLeft(exp.base, '0', 2);
  };

  /*
   * class FloatConverter
   */
  var FloatConverter = function( )
  {
    AbstractFloatConverter.call(this, false);
  };

  FloatConverter.prototype = new AbstractFloatConverter;

  FloatConverter.prototype._convert = function(argument, precision, flags, width, conversion)
  {
    if (precision == -1) {
      precision = 6;
    }

    var num = parseFloat(argument);
    var converted = this.toFloat(num, precision);

    return this.format(num, converted, flags, width);
  };

  FloatConverter.prototype.toFloat = function(num, precision)
  {
    var str = num.toString( );
    var point = str.indexOf('.');

    var integerPart = ( point != -1 ) ? str.substring(0, point) : str;
    var floatingPart = ( point != -1 ) ? str.substring(point + 1, str.length) : '';

    var len = floatingPart.length;
    if (len > precision) {
      floatingPart = floatingPart.substring(0, precision) + '.' + floatingPart.substring(precision, precision + 1);
      floatingPart = Math.round(floatingPart).toString( );
    } else if (len < precision) {
      floatingPart = paddingRight(floatingPart, '0', precision);
    }

    var result = '';
    if (floatingPart.length > 0) {
      result = integerPart + '.' + floatingPart;
    } else {
      result = integerPart;
    }

    return result;
  };

  /*
   * class ComputerizedConverter
   */
  var ComputerizedConverter = function( )
  {
    AbstractFloatConverter.call(this, false);
  };

  ComputerizedConverter.prototype = new AbstractFloatConverter;

  ComputerizedConverter.prototype._convert = function(argument, precision, flags, width, conversion)
  {
    if (precision == -1) {
      precision = 6;
    } else if (precision == 0) {
      precision = 1;
    }

    var num = parseFloat(argument);
    var exp = this.normalize(num);

    var converted = '';
    if (exp.base > precision) {
      converted = this.toExponential(exp, precision);
    } else {
      converted = this.toFloat(num, precision);
    }

    return this.format(num, converted, flags, width);
  };

  ComputerizedConverter.prototype.formatAlternateForm = function(str)
  {
    assert(false, "FormatFlagsConversionMismatch");
  };

  ComputerizedConverter.prototype.toExponential = function(exp, precision)
  {
    var offset = 0;
    var real = exp.real;
    if (real.match(/\./)) {
      offset++;
    }
    real = real.substring(0, precision + offset);

    if (real.length < precision) {
      if (!real.match(/\./)) {
        real = real + '.';
      }
      real = paddingRight(real, '0', precision + 2);
    }

    return real + 'e' + exp.sign + paddingLeft(exp.base, '0', 2);
  };

  ComputerizedConverter.prototype.toFloat = function(num, precision)
  {
    var str = num.toString( );
    var sig = str.replace(/\./, '');

    var offset = ( sig.match(/^-/) ) ? 1 : 0;
    var len = sig.length - offset;
    ;
    if (len > precision) {
      sig = sig.substring(0, precision + offset) + '.' + sig.substring(precision + offset, sig.length);
      sig = Math.round(parseFloat(sig)).toString( );
    } else if (len < precision) {
      sig = paddingRight(sig, '0', precision + offset);
    }

    var point = str.indexOf('.');
    var integerPart = sig.substring(0, point);
    var floatingPart = sig.substring(point, sig.length);

    var result = '';
    if (floatingPart.length > 0) {
      result = integerPart + '.' + floatingPart;
    } else {
      result = integerPart;
    }
    return result;
  };

  /*
   * class HexadecimalFloatConverter
   */
  var HexadecimalFloatConverter = function(isUpperCase)
  {
    AbstractFloatConverter.call(this, isUpperCase);
  };

  HexadecimalFloatConverter.prototype = new AbstractFloatConverter;

  HexadecimalFloatConverter.prototype._convert = function(argument, precision, flags, width, conversion)
  {
    var num = parseFloat(argument);

    // IEEE 754
    var exp = argument.toExponential( );
    var m = exp.match(/([0-9.]+)e([+-][0-9]+)/);
    var a = m[ 1 ] - 2;
    var bin = a.toString(2).replace('.', '');
    if (( bin.length % 4 ) != 0) {
      var len = 4 - bin.length % 4;
      bin = paddingRight(bin, '0', bin.length + len);
    }
    var converted = '0x1.' + parseInt(bin, 2).toString(16) + 'p' + ( parseInt(m[ 2 ]) + 1 );

    return this.format(num, converted, flags, width);
  };

  HexadecimalFloatConverter.prototype.formatPadding = function(str, flags, width)
  {
    if (flags.zeroPadding) {
      var temp = str.substring(2, str.length);
      temp = AbstractFloatConverter.prototype.formatPadding.call(this, temp, flags, width - 2);
      return '0x' + temp;
    } else {
      return AbstractFloatConverter.prototype.formatPadding.call(this, str, flags, width);
    }
  };

  HexadecimalFloatConverter.prototype.formatGroupingSepartor = function(str)
  {
    assert(false, "Format flags conversion mismatch");
    return "";
  };

  HexadecimalFloatConverter.prototype.formatSurroundNegative = function(str)
  {
    assert(false, "Format flags conversion mismatch");
  };

  /*
   * class LiteralConverter
   */
  var LiteralConverter = function(isUpperCase)
  {
    AbstractConverter.call(this, isUpperCase);
  };

  LiteralConverter.prototype = new AbstractConverter( );

  LiteralConverter.prototype.format = function(str, flags, width)
  {
    return str;
  };

  LiteralConverter.prototype._convert = function(argument, precision, flags, width, conversion)
  {
    return this.format('%', flags, width);
  };

  /*
   * class LineSeparatorConverter
   */
  var LineSeparatorConverter = function(isUpperCase)
  {
    AbstractConverter.call(this, isUpperCase);
  };

  LineSeparatorConverter.prototype = new AbstractConverter( );

  LineSeparatorConverter.prototype.format = function(str, flags, width)
  {
    assert(width < 0, "IllegalFormatWidth");

    return str;
  };

  LineSeparatorConverter.prototype._convert = function(argument, precision, flags, width, conversion)
  {
    return this.format('\n', flags, width);
  };

  /*
   * class Formatter
   */
  function Formatter( )
  {
    this.initialize( );
  }
  ;

  Formatter.prototype = {
    /*
     * converters
     */
    converters : {
      'b' : new BooleanConverter(false),
      'B' : new BooleanConverter(true),
      'j' : new JSONConverter(true),
      's' : new StringConverter(false),
      'S' : new StringConverter(true),
      'c' : new CharacterConverter(false),
      'C' : new CharacterConverter(true),
      'd' : new DecimalConverter( ),
      'o' : new OctalConverter(false),
      'O' : new OctalConverter(true),
      'x' : new HexadecimalConverter(false),
      'X' : new HexadecimalConverter(true),
      'e' : new ExponentialConverter(false),
      'E' : new ExponentialConverter(true),
      'f' : new FloatConverter( ),
      'g' : new ComputerizedConverter(false),
      'G' : new ComputerizedConverter(true),
      'a' : new HexadecimalFloatConverter(false),
      'A' : new HexadecimalFloatConverter(true),
      '%' : new LiteralConverter( ),
      'n' : new LineSeparatorConverter( )
    },

    initialize : function( )
    {

    },

    /**
     * 
     * @param fmt
     * @param args
     * @returns
     */
    format : function(fmt, args)
    {
      var exp = /%(([1-9][0-9]*)\$|\<)?([-#+0,(]+)?(\d+)?(\.\d+)?([%bBjsScCdoOxXeEf]|[tT][a-zA-Z])/g;

      this.args = args;
      this.count = 0;
      this.previous = -1;

      var self = this;
      return fmt.replace(exp, function(whole, g1, g2, flags, width, precision, conversion)
      {
        return self.replace(whole, g1, g2, flags, width, precision, conversion);
      });
    },

    /**
     * 
     * @param whole
     * @param g1
     * @param g2
     * @param flags
     * @param width
     * @param precision
     * @param conversion
     * @returns {String}
     */
    replace : function(whole, g1, g2, flags, width, precision, conversion)
    {
      var index = this.count, replaced = '', converter = undefined, argument = null;
      if (g1 && ( g1 == '<' )) {
        index = this.previous;
      } else if (g2 && ( g2.length != 0 )) {
        index = g2 - 1;
      } else if (( conversion != '%' ) && ( conversion != '\n' )) {
        this.count++;
      }
      this.previous = index;
      flags = this.parseFlags(flags);
      width = ( width && ( width.length != 0 ) ) ? width : -1;
      precision = ( precision && ( precision.length != 0 ) ) ? parseInt(precision.substring(1, precision.length)) : -1;

      assert(!( ( conversion != '%' ) && ( conversion != 'n' ) && ( ( index >= this.args.length ) || ( index < 0 ) ) ), "Missing argument.");

      argument = this.args[ index ];

      converter = this.converters[ conversion.charAt(0) ];
      assert(converter !== undefined, "Unknown conversion: " + conversion);
      replaced = converter.convert(argument, precision, flags, width, conversion);
      return replaced;
    },

    /**
     * 
     * @param a
     * @returns {Boolean}
     */
    hasDuplication : function(a)
    {
      for ( var i = 0; i < a.length - 1; i++) {
        for ( var j = i + 1; j < a.length; j++) {
          if (a.charAt(i) == a.charAt(j)) {
            return true;
          }
        }
      }
      return false;
    },

    /**
     * 
     * @param str
     * @returns
     */
    parseFlags : function(str)
    {
      if (str === undefined || str === null || ( str.length <= 0 )) {
        return {};
      }

      // duplication check
      assert(!this.hasDuplication(str), "Duplicated flag.");

      var flags = {};
      flags.leftAlign = false;
      flags.signPlus = false;
      flags.signSpace = false;
      flags.zeroPadding = false;
      flags.surroundNegative = false;
      flags.alternateForm = false;
      flags.groupingSeparator = false;

      for ( var i = 0; i < str.length; i++) {
        var c = str.charAt(i);
        switch (c)
        {
          case '-':
            flags.leftAlign = true;
            break;
          case '#':
            flags.alternateForm = true;
            break;
          case '+':
            flags.signPlus = true;
            break;
          case '0':
            flags.zeroPadding = true;
            break;
          case ',':
            flags.groupingSeparator = true;
            break;
          case '(':
            flags.surroundNegative = true;
            break;
          default:
            throw new TypeError('Unknown format flag: ' + c);
        }
      }

      if (( flags.signPlus && flags.signSpace ) || ( flags.leftAlign && flags.zeroPadding )) {
        assert(!( ( flags.signPlus && flags.signSpace ) || ( flags.leftAlign && flags.zeroPadding ) ), "Illegal format flags: '+' & ' '");
      }

      return flags;
    }
  };

} )( );
