/*
 * Copyright 2008 Google Inc.
 *
 * Licensed under the Apache License, Version 2.0 (the "License"); you may not
 * use this file except in compliance with the License. You may obtain a copy of
 * the License at
 *
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
 * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
 * License for the specific language governing permissions and limitations under
 * the License.
 */
package com.google.gwt.dev.jjs.ast;

import com.google.gwt.dev.jjs.SourceOrigin;
import com.google.gwt.dev.util.StringInterner;
import com.google.gwt.thirdparty.guava.common.collect.ImmutableCollection;
import com.google.gwt.thirdparty.guava.common.collect.ImmutableList;
import com.google.gwt.thirdparty.guava.common.collect.Maps;

import java.util.Map;

/**
 * Base class for all Java primitive types.
 */
public class JPrimitiveType extends JType {
  private static final Map<String, JPrimitiveType> primitiveTypeByName = Maps.newHashMap();

  /*
   * Primitive types are static singletons. Serialization via readResolve().
   */
  public static final JPrimitiveType BOOLEAN = new JPrimitiveType(
      "boolean", "Z", "java.lang.Boolean", JBooleanLiteral.FALSE, Coercion.TO_BOOLEAN);
  public static final JPrimitiveType BYTE =
      new JPrimitiveType("byte", "B", "java.lang.Byte", JIntLiteral.ZERO, Coercion.TO_BYTE);
  public static final JPrimitiveType CHAR =
      new JPrimitiveType("char", "C", "java.lang.Character", JCharLiteral.NULL, Coercion.TO_CHAR);
  public static final JPrimitiveType DOUBLE = new JPrimitiveType(
      "double", "D", "java.lang.Double", JDoubleLiteral.ZERO, Coercion.TO_DOUBLE);
  public static final JPrimitiveType FLOAT =
      new JPrimitiveType("float", "F", "java.lang.Float", JFloatLiteral.ZERO, Coercion.TO_FLOAT);
  public static final JPrimitiveType INT =
      new JPrimitiveType("int", "I", "java.lang.Integer", JIntLiteral.ZERO, Coercion.TO_INT);
  public static final JPrimitiveType LONG =
      new JPrimitiveType("long", "J", "java.lang.Long", JLongLiteral.ZERO, Coercion.TO_LONG);
  public static final JPrimitiveType SHORT =
      new JPrimitiveType("short", "S", "java.lang.Short", JIntLiteral.ZERO, Coercion.TO_SHORT);
  public static final JPrimitiveType VOID =
      new JPrimitiveType("void", "V", "java.lang.VOID", null, Coercion.TO_VOID);

  public static final ImmutableCollection<JPrimitiveType> types = ImmutableList.of(BOOLEAN, BYTE,
      CHAR, DOUBLE, FLOAT, INT, LONG, SHORT, VOID);

  private final transient JValueLiteral defaultValue;
  private final transient String signatureName;
  private final transient String wrapperTypeName;
  private final transient Coercion coercion;

  private JPrimitiveType(String name, String signatureName, String wrapperTypeName,
      JValueLiteral defaultValue, Coercion coercion) {
    super(SourceOrigin.UNKNOWN, name);
    this.defaultValue = defaultValue;
    this.signatureName = StringInterner.get().intern(signatureName);
    this.wrapperTypeName = StringInterner.get().intern(wrapperTypeName);
    this.coercion = coercion;
    primitiveTypeByName.put(this.name, this);
  }

  @Override
  public boolean canBeNull() {
    return false;
  }

  @Override
  public boolean isArrayType() {
    return false;
  }

  @Override
  public boolean isPrimitiveType() {
    return true;
  }

  @Override
  public boolean isJsType() {
    return false;
  }

  @Override
  public boolean isJsFunction() {
    return false;
  }

  @Override
  public boolean isJsFunctionImplementation() {
    return false;
  }

  @Override
  public boolean isJsNative() {
    return false;
  }

  @Override
  public boolean canBeImplementedExternally() {
    return false;
  }

  @Override
  public boolean canBeSubclass() {
    return false;
  }

  public JValueLiteral coerce(JValueLiteral literal) {
    return this.coercion.coerce(literal);
  }

  @Override
  public final JLiteral getDefaultValue() {
    return defaultValue;
  }

  @Override
  public String getJavahSignatureName() {
    return signatureName;
  }

  @Override
  public String getJsniSignatureName() {
    return signatureName;
  }

  @Override
  public JEnumType isEnumOrSubclass() {
    return null;
  }

  @Override
  public JPrimitiveType strengthenToNonNull() {
    return this;
  }

  public String getWrapperTypeName() {
    return wrapperTypeName;
  }

  @Override
  public boolean isFinal() {
    return true;
  }

  @Override
  public boolean isJsoType() {
    return false;
  }

  @Override
  public boolean canBeReferencedExternally() {
    return this != JPrimitiveType.LONG;
  }

  @Override
  public boolean isJavaLangObject() {
    return false;
  }

  @Override
  public void traverse(JVisitor visitor, Context ctx) {
    if (visitor.visit(this, ctx)) {
    }
    visitor.endVisit(this, ctx);
  }

  /**
   * Returns the JPrimitiveType instance corresponding to {@code typeName} or {@code null} if
   * typeName is not the name of a primitive type.
   */
  public static JPrimitiveType getType(String typeName) {
    return primitiveTypeByName.get(typeName);
  }

  /**
   * Canonicalize to singleton; uses {@link JType#name}.
   */
  private Object readResolve() {
    return primitiveTypeByName.get(name);
  }

  private enum Coercion {
    TO_CHAR() {
      @Override
      JValueLiteral coerce(JValueLiteral literal) {
        if (literal instanceof JCharLiteral) {
          return literal;
        }

        Object valueObject = literal.getValueObj();
        if (valueObject instanceof Number) {
          return new JCharLiteral(
              literal.getSourceInfo(), (char) ((Number) valueObject).intValue());
        }
        return null;
      }
    },
    TO_INT() {
      @Override
      JValueLiteral coerce(JValueLiteral literal) {
        Object valueObject = literal.getValueObj();
        if (!(valueObject instanceof Number) && !(valueObject instanceof Character)) {
          return null;
        }
        int value = 0;
        if (valueObject instanceof Number) {
          value = ((Number) valueObject).intValue();
        } else if (valueObject instanceof Character) {
          value = ((Character) valueObject).charValue();
        }
        return new JIntLiteral(literal.getSourceInfo(), value);
      }
    },
    TO_BYTE() {
      @Override
      JValueLiteral coerce(JValueLiteral literal) {
        Object valueObject = literal.getValueObj();
        if (!(valueObject instanceof Number) && !(valueObject instanceof Character)) {
          return null;
        }
        byte value = 0;
        if (valueObject instanceof Number) {
          value = ((Number) valueObject).byteValue();
        } else if (valueObject instanceof Character) {
          value = (byte) ((Character) valueObject).charValue();
        }
        return new JIntLiteral(literal.getSourceInfo(), value);
      }
    },
    TO_SHORT() {
      @Override
      JValueLiteral coerce(JValueLiteral literal) {
        Object valueObject = literal.getValueObj();
        if (!(valueObject instanceof Number) && !(valueObject instanceof Character)) {
          return null;
        }
        short value = 0;
        if (valueObject instanceof Number) {
          value = ((Number) valueObject).shortValue();
        } else if (valueObject instanceof Character) {
          value = (short) ((Character) valueObject).charValue();
        }
        return new JIntLiteral(literal.getSourceInfo(), value);
      }
    },
    TO_LONG() {
      @Override
      JValueLiteral coerce(JValueLiteral literal) {
        Object valueObject = literal.getValueObj();
        if (!(valueObject instanceof Number) && !(valueObject instanceof Character)) {
          return null;
        }
        long value = 0;
        if (valueObject instanceof Number) {
          value = ((Number) valueObject).longValue();
        } else if (valueObject instanceof Character) {
          value = (long) ((Character) valueObject).charValue();
        }
        return new JLongLiteral(literal.getSourceInfo(), value);
      }
    },
    TO_FLOAT() {
      @Override
      JValueLiteral coerce(JValueLiteral literal) {
        Object valueObject = literal.getValueObj();
        if (!(valueObject instanceof Number) && !(valueObject instanceof Character)) {
          return null;
        }
        float value = 0;
        if (valueObject instanceof Number) {
          value = ((Number) valueObject).floatValue();
        } else if (valueObject instanceof Character) {
          value = (float) ((Character) valueObject).charValue();
        }
        return new JFloatLiteral(literal.getSourceInfo(), value);
      }
    },
    TO_DOUBLE() {
      @Override
      JValueLiteral coerce(JValueLiteral literal) {
        Object valueObject = literal.getValueObj();
        if (!(valueObject instanceof Number) && !(valueObject instanceof Character)) {
          return null;
        }
        double value = 0;
        if (valueObject instanceof Number) {
          value = ((Number) valueObject).doubleValue();
        } else if (valueObject instanceof Character) {
          value = (double) ((Character) valueObject).charValue();
        }
        return new JDoubleLiteral(literal.getSourceInfo(), value);
      }
    },
    TO_BOOLEAN() {
      @Override
      public JValueLiteral coerce(JValueLiteral literal) {
        if (literal instanceof JBooleanLiteral) {
          return literal;
        }
        return null;
      }
    },
    TO_VOID() {
      @Override
      public JValueLiteral coerce(JValueLiteral literal) {
        return null;
      }
    };

    /**
     * Coerces a literal into a literal of (possibly) a different type; returns {@code null} if
     * coercion is not valid.
     */
    abstract JValueLiteral coerce(JValueLiteral literal);
  }
}
