/**
 * Copyright (c) 2002-2006 IBM Corporation and others.
 * All rights reserved.   This program and the accompanying materials
 * are made available under the terms of the Eclipse Public License v2.0
 * which accompanies this distribution, and is available at
 * http://www.eclipse.org/legal/epl-v20.html
 *
 * Contributors:
 *   IBM - Initial API and implementation
 */
package org.eclipse.emf.codegen.ecore.genmodel.impl;


import java.math.BigDecimal;
import java.math.BigInteger;
import java.util.Date;

import org.eclipse.emf.codegen.ecore.genmodel.GenJDKLevel;
import org.eclipse.emf.codegen.ecore.genmodel.GenModel;


/**
 * Utility class for converting primitive values, strings, and classes to
 * literals that could appear in code.
 */
public class Literals
{
  // Suppress default constructor for non-instantiability.
  private Literals()
  {
    super();
  }

  /**
   * Convenience dispatch method.  If the <code>object</code> is an instance
   * of <code>Boolean</code>, <code>Byte</code>, <code>Short</code>,
   * <code>Integer</code>, <code>Long</code>, <code>Float</code>,
   * <code>Double</code>, <code>Character</code>, <code>String</code>, 
   * <code>BigDecimal</code>, <code>BigInteger</code>, <code>Date</code>,
   * <code>Class</code>, or <code>byte[]</code>, the appropriate conversion
   * method is called, with the unboxed primitive, or the typed object as an
   * argument. Class names are never imported; the qualified name is used.
   */
  public static String toLiteral(Object o)
  {
    return toLiteral(o, null);
  }

  /**
   * Convenience dispatch method.  If the <code>object</code> is an instance
   * of <code>Boolean</code>, <code>Byte</code>, <code>Short</code>,
   * <code>Integer</code>, <code>Long</code>, <code>Float</code>,
   * <code>Double</code>, <code>Character</code>, <code>String</code>, 
   * <code>BigDecimal</code>, <code>BigInteger</code>, <code>Date</code>,
   * <code>Class</code>, or <code>byte[]</code>, the appropriate conversion
   * method is called, with the unboxed primitive, or the typed object as an
   * argument.
   * <p>The specified {@link org.eclipse.emf.codegen.ecore.genmodel.GenModel},
   * if non-null, is used when necessary to import class names.
   */
  public static String toLiteral(Object o, GenModel genModel)
  {
    return toLiteral(o, false, genModel);
  }

  /**
   * Convenience dispatch method.  If the <code>object</code> is an instance
   * of <code>Boolean</code>, <code>Byte</code>, <code>Short</code>,
   * <code>Integer</code>, <code>Long</code>, <code>Float</code>,
   * <code>Double</code>, <code>Character</code>, <code>String</code>, 
   * <code>BigDecimal</code>, <code>BigInteger</code>, <code>Date</code>,
   * <code>Class</code>, or <code>byte[]</code>, the appropriate conversion
   * method is called, with the unwrapped primitive, or the typed object as an
   * argument.
   * <p>The specified {@link org.eclipse.emf.codegen.ecore.genmodel.GenModel},
   * if non-null, is used when necessary to import class names.
   * <p>If the object is a primitive type, <boolean>boxPrimitive</boolean>
   * indicates whether to include boxing code in the literal expression.
   * Otherwise, this argument is ignored. 
   */
  public static String toLiteral(Object o, boolean boxPrimitive, GenModel genModel)
  {
    if (o instanceof Boolean)
    {
      return toBooleanLiteral((Boolean)o, boxPrimitive, genModel);
    }
    if (o instanceof Byte)
    {
      return toByteLiteral((Byte)o, boxPrimitive, genModel);
    }
    if (o instanceof Short)
    {
      String result = toShortLiteral((Short)o, genModel);
      return boxPrimitive ? box(result, "Short", "short", genModel) : result;
    }
    if (o instanceof Integer)
    {
      String result =  toIntLiteral((Integer)o, genModel);
      return boxPrimitive ? box(result, "Integer", null, genModel) : result;
    }
    if (o instanceof Long)
    {
      String result = toLongLiteral((Long)o, genModel);
      return boxPrimitive ? box(result, "Long", null, genModel) : result;
    }
    if (o instanceof Float)
    {
      String result = toFloatLiteral((Float)o, genModel);
      return boxPrimitive ? box(result, "Float", null, genModel) : result;
    }
    if (o instanceof Double)
    {
      String result = toDoubleLiteral((Double)o, genModel);
      return boxPrimitive ? box(result, "Double", null, genModel) : result;
    }
    if (o instanceof Character)
    {
      String result = toCharLiteral((Character)o, genModel);
      return boxPrimitive ? box(result, "Character", null, genModel) : result;
    }
    if (o instanceof String)
    {
      return toStringLiteral((String)o, genModel);
    }
    if (o instanceof BigDecimal)
    {
      return toBigDecimalLiteral((BigDecimal)o, genModel);
    }
    if (o instanceof BigInteger)
    {
      return toBigIntegerLiteral((BigInteger)o, genModel);
    }
    if (o instanceof Date)
    {
      return toDateLiteral((Date)o, genModel);
    }
    if (o instanceof Class<?>)
    {
      return toClassLiteral((Class<?>)o, genModel);
    }
    if (o instanceof byte[])
    {
      return toByteArrayLiteral((byte[])o, genModel);
    }
    return null;
  }

  /**
   * Returns the literal expression for the given <code>boolean</code> value,
   * with optional boxing.
   */
  private static String toBooleanLiteral(boolean b, boolean box, GenModel genModel)
  {
    if (box)
    {
      return importName("java.lang.Boolean", genModel) + (b ? ".TRUE" : ".FALSE");
    }
    return b ? "true" : "false";
  }

  /**
   * Returns the literal expression for the given <code>boolean</code> value.
   */
  public static String toBooleanLiteral(boolean b, GenModel genModel)
  {
    return toBooleanLiteral(b, false, genModel);
  }

  /**
   * Returns the hexadecimal literal expression for the given <code>byte</code>
   * value, with optional boxing.
   */
  private static String toByteLiteral(byte b, boolean box, GenModel genModel)
  {
    String result = toByteLiteral(b, genModel);
    if (box)
    {
      String wrapperClass = importName("java.lang.Byte", genModel);
      StringBuilder boxed = new StringBuilder(16 + wrapperClass.length());
      appendBoxPrefix(boxed, wrapperClass, genModel);
      if (b >= 0)
      {
        boxed.append("(byte)");
      }
      boxed.append(result);
      boxed.append(')');
      result = boxed.toString();
    }
    return result;
  }

  /**
   * Returns the hexadecimal literal expression for the given <code>byte</code>
   * value.
   */
  public static String toByteLiteral(byte b, GenModel genModel)
  {
    String hex = Integer.toHexString(b);
    switch (hex.length())
    {
      case 1: return "0x0" + hex;
      case 2: return "0x" + hex;
      case 8: return "(byte)0x" + hex.substring(hex.length() - 2);
    }
    throw new IllegalArgumentException("A byte cannot convert to a " + hex.length() +  " digit hex string (Java platform error in Integer.toHexString())");
  }

  /**
   * Returns the boxed form of the literal 
   * new wrapperClass((castType)literal) 
   * or wrapperClass.valueOf((castType>literal)),
   * if the genModel is not null and the compliance level is Java 9 or higher.
   * The wrapperClass is imported, and, if the castType is null, no cast is
   * included.
   */
  private static String box(String literal, String wrapperClass, String castType, GenModel genModel)
  {
    wrapperClass = importName(wrapperClass, genModel);
    StringBuilder boxed = new StringBuilder(6 + literal.length() + wrapperClass.length() + (castType != null ? castType.length() + 2 : 0));
    appendBoxPrefix(boxed, wrapperClass, genModel);
    if (castType != null)
    {
      boxed.append('(');
      boxed.append(castType);
      boxed.append(')');
    }
    boxed.append(literal);
    boxed.append(')');
    return boxed.toString();
  }

  private static void appendBoxPrefix(StringBuilder boxed, String wrapperClass, GenModel genModel)
  {
    if (genModel != null && genModel.getComplianceLevel().getValue() >= GenJDKLevel.JDK90)
    {
      boxed.append(wrapperClass);
      boxed.append(".valueOf(");
    }
    else
    {
      boxed.append("new ");
      boxed.append(wrapperClass);
      boxed.append('(');
    }
  }

  /**
   * Returns the decimal literal expression for the given <code>short</code>
   * value.
   */
  public static String toShortLiteral(short s, GenModel genModel)
  {
    return Short.toString(s);
  }

  /**
   * Returns the decimal literal expression for the given <code>int</code>
   * value.
   */
  public static String toIntLiteral(int i, GenModel genModel)
  {
    return Integer.toString(i);
  }

  /**
   * Returns the decimal literal expression for the given <code>long</code>
   * value.
   */
  public static String toLongLiteral(long l, GenModel genModel)
  {
    return Long.toString(l) + "L";
  }

  /**
   * Returns a literal expression for the given <code>float</code> value.
   * This literal may be in simple form or exponential notation, or it may
   * be one of the special values <code>java.lang.Float.NaN</code>,
   * <code>java.lang.Float.POSITIVE_INFINITY</code>, or
   * <code>java.lang.Float.NEGATIVE_INFINITY</code>.
   */
  public static String toFloatLiteral(float f, GenModel genModel)
  {
    if (Float.isNaN(f)) return importName("java.lang.Float", genModel) + ".NaN";
    if (Float.isInfinite(f)) return f > 0 ?
      importName("java.lang.Float", genModel) + ".POSITIVE_INFINITY" :
      importName("java.lang.Float", genModel) + ".NEGATIVE_INFINITY";
    return Float.toString(f) + "F";
  }

  /**
   * Returns a literal expression for the given <code>double</code> value.
   * This literal may be in simple form or exponential notation, or it may
   * be one of the special values <code>java.lang.Double.NaN</code>,
   * <code>java.lang.Double.POSITIVE_INFINITY</code>, or
   * <code>java.lang.Double.NEGATIVE_INFINITY</code>.
   */
  public static String toDoubleLiteral(double d, GenModel genModel)
  {
    if (Double.isNaN(d)) return importName("java.lang.Double", genModel) + ".NaN";
    if (Double.isInfinite(d)) return d > 0 ?
      importName("java.lang.Double", genModel) + ".POSITIVE_INFINITY" :
      importName("java.lang.Double", genModel) + ".NEGATIVE_INFINITY";
    return Double.toString(d);
  }

  private static String importName(String name, GenModel genModel)
  {
    return genModel != null && genModel.getImportManager() != null ? genModel.getImportedName(name) : name;
  }

  /**
   * Returns a literal expression for the given <code>char</code> value.
   * This literal will be in its escaped form if it is backspace,
   * horizontal tab, newline, form feed, carriage return, double quote,
   * single quote, or backslash.  If it is within the common printable
   * range of space (32) to <code>~</code> (126), it will simply be the
   * character literal.  Otherwise, it will be in the escaped Unicode
   * encoding form.
   */
  public static String toCharLiteral(char c, GenModel genModel)
  {
    StringBuilder result = new StringBuilder(8);
    result.append('\'');
    result.append(escapeChar(c, false));
    result.append('\'');
    return result.toString();
  }

  /**
   * Returns a literal expression for the given <code>char</code> value.
   * This literal will be in its escaped form if it is backspace,
   * horizontal tab, newline, form feed, carriage return, double quote,
   * single quote, or backslash.  Otherwise, it will simply be the character
   * literal. This result may not fall within the common printable range.
   * @since 2.5
   */
  public static String toUnsafeCharLiteral(char c, GenModel genModel)
  {
    StringBuilder result = new StringBuilder(8);
    result.append('\'');
    result.append(escapeChar(c, true));
    result.append('\'');
    return result.toString();    
  }

  /**
   * Returns a literal expression for the given <code>String</code>.  Each
   * of its characters will appear in the same form as if it was the
   * argument to {@link #toCharLiteral}.
   */
  public static String toStringLiteral(String s, GenModel genModel)
  {
    return toStringLiteral(s, genModel, false);
  }

  /**
   * Returns a literal expression for the given <code>String</code>.  Each
   * of its characters will appear in the same form as if it was the
   * argument to {@link #toUnsafeCharLiteral}.
   * @since 2.5
   */
  public static String toUnsafeStringLiteral(String s, GenModel genModel)
  {
    return toStringLiteral(s, genModel, true);
  }

  private static String toStringLiteral(String s, GenModel genModel, boolean unsafe)
  {
    if (s == null) return "null";
    int len = s.length();
    StringBuilder result = new StringBuilder(len + 16);
    result.append('\"');
    for (int i = 0; i < len; i++)
    {
      result.append(escapeChar(s.charAt(i), unsafe));
    }
    result.append('\"');
    return result.toString();
  }

  private static String escapeChar(char c, boolean unsafe)
  {
    if (c == '\b') return "\\b";
    if (c == '\t') return "\\t";
    if (c == '\n') return "\\n";
    if (c == '\f') return "\\f";
    if (c == '\r') return "\\r";
    if (c == '\"') return "\\\"";
    if (c == '\'') return "\\\'";
    if (c == '\\') return "\\\\";
    if (unsafe || (c >= 32 && c < 127)) return String.valueOf(c);

    // escaped unicode form
    String num = Integer.toHexString(c);
    switch(num.length()) {
      case 1: return "\\u000" + num;
      case 2: return "\\u00" + num;
      case 3: return "\\u0" + num;
    }
    return "\\u" + num;
  }

  /**
   * Returns a literal expression for the given <code>BigDecimal</code> value.  
   */
  public static String toBigDecimalLiteral(BigDecimal bigDecimal, GenModel genModel)
  {
    if (bigDecimal == null) return "null";
    return "new " + importName("java.math.BigDecimal", genModel) + "(\"" + bigDecimal.toString() + "\")";
  }

  /**
   * Returns a literal expression for the given <code>BigInteger</code> value.  
   */
  public static String toBigIntegerLiteral(BigInteger bigInteger, GenModel genModel)
  {
    if (bigInteger == null) return "null";
    return "new " + importName("java.math.BigInteger", genModel) + "(\"" + bigInteger.toString() + "\")";
  }

  /**
   * Returns a literal expression for the given <code>Date</code> value.
   */
  public static String toDateLiteral(Date date, GenModel genModel)
  {
    String timeLiteral = toLongLiteral(date.getTime(), genModel);
    return "new " + importName("java.util.Date", genModel) + "(" + timeLiteral + ")";
  }

  /**
   * Returns a literal expression for the given <code>Class</code> value.
   */
  public static String toClassLiteral(Class<?> c, GenModel genModel)
  {
    if (c == null) return "null";
    String name = c.getName(); 

    // See java.lang.Class.getName() javadoc for explanation of array encoding.
    int arrayDepth = 0;
    while (name.charAt(arrayDepth) == '[')
    {
      ++arrayDepth;
    }

    if (arrayDepth > 0)
    {
      if      (name.charAt(arrayDepth) == 'B') name = "byte";
      else if (name.charAt(arrayDepth) == 'C') name = "char";
      else if (name.charAt(arrayDepth) == 'D') name = "double";
      else if (name.charAt(arrayDepth) == 'F') name = "float";
      else if (name.charAt(arrayDepth) == 'I') name = "int";
      else if (name.charAt(arrayDepth) == 'J') name = "long";
      else if (name.charAt(arrayDepth) == 'S') name = "short";
      else if (name.charAt(arrayDepth) == 'Z') name = "boolean";
      else if (name.charAt(arrayDepth) == 'L') name = importName(name.substring(arrayDepth + 1, name.length() - 1), genModel);
      else throw new IllegalArgumentException("Invalid class name: " + name);
    }
    else if (!c.isPrimitive())
    {
      name = importName(name, genModel);
    }
    
    StringBuilder result = new StringBuilder(name.length() + 2 * arrayDepth + 8);
    result.append(name);
    for (int i = 0; i < arrayDepth; i++)
    {
      result.append('[');
      result.append(']');
    }
    result.append(".class");
    return result.toString();
  }

  /**
   * Returns a literal expression for the given <code>byte[]</code> value.
   */
  public static String toByteArrayLiteral(byte[] bytes, GenModel genModel)
  {
    if (bytes == null) return "null";
    if (bytes.length == 0) return "{}";

    StringBuilder result = new StringBuilder(2 + bytes.length * 7);
    result.append("{ ");
    for (int i = 0, last = bytes.length - 1; i <= last; i++)
    {
      result.append(toByteLiteral(bytes[i], genModel));
      if (i < last)
      {
        result.append(", ");
      }
    }
    result.append(" }");
    return result.toString();
  }
}
