/*
 * Copyright 2000-2014 JetBrains s.r.o.
 *
 * 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 org.jetbrains.java.decompiler.struct.gen;

import org.jetbrains.java.decompiler.code.CodeConstants;
import org.jetbrains.java.decompiler.util.InterpreterUtil;

public class VarType {  // TODO: optimize switch

  public static final VarType[] EMPTY_ARRAY = {};

  public static final VarType VARTYPE_UNKNOWN = new VarType(CodeConstants.TYPE_UNKNOWN);
  public static final VarType VARTYPE_INT = new VarType(CodeConstants.TYPE_INT);
  public static final VarType VARTYPE_FLOAT = new VarType(CodeConstants.TYPE_FLOAT);
  public static final VarType VARTYPE_LONG = new VarType(CodeConstants.TYPE_LONG);
  public static final VarType VARTYPE_DOUBLE = new VarType(CodeConstants.TYPE_DOUBLE);
  public static final VarType VARTYPE_BYTE = new VarType(CodeConstants.TYPE_BYTE);
  public static final VarType VARTYPE_CHAR = new VarType(CodeConstants.TYPE_CHAR);
  public static final VarType VARTYPE_SHORT = new VarType(CodeConstants.TYPE_SHORT);
  public static final VarType VARTYPE_BOOLEAN = new VarType(CodeConstants.TYPE_BOOLEAN);
  public static final VarType VARTYPE_BYTECHAR = new VarType(CodeConstants.TYPE_BYTECHAR);
  public static final VarType VARTYPE_SHORTCHAR = new VarType(CodeConstants.TYPE_SHORTCHAR);

  public static final VarType VARTYPE_NULL = new VarType(CodeConstants.TYPE_NULL, 0, null);
  public static final VarType VARTYPE_STRING = new VarType(CodeConstants.TYPE_OBJECT, 0, "java/lang/String");
  public static final VarType VARTYPE_CLASS = new VarType(CodeConstants.TYPE_OBJECT, 0, "java/lang/Class");
  public static final VarType VARTYPE_OBJECT = new VarType(CodeConstants.TYPE_OBJECT, 0, "java/lang/Object");
  public static final VarType VARTYPE_VOID = new VarType(CodeConstants.TYPE_VOID);

  public final int type;
  public final int arrayDim;
  public final String value;
  public final int typeFamily;
  public final int stackSize;
  public final boolean falseBoolean;

  public VarType(int type) {
    this(type, 0);
  }

  public VarType(int type, int arrayDim) {
    this(type, arrayDim, getChar(type));
  }

  public VarType(int type, int arrayDim, String value) {
    this(type, arrayDim, value, getFamily(type, arrayDim), getStackSize(type, arrayDim), false);
  }

  private VarType(int type, int arrayDim, String value, int typeFamily, int stackSize, boolean falseBoolean) {
    this.type = type;
    this.arrayDim = arrayDim;
    this.value = value;
    this.typeFamily = typeFamily;
    this.stackSize = stackSize;
    this.falseBoolean = falseBoolean;
  }

  public VarType(String signature) {
    this(signature, false);
  }

  public VarType(String signature, boolean clType) {
    int type = 0;
    int arrayDim = 0;
    String value = null;

    loop:
    for (int i = 0; i < signature.length(); i++) {
      switch (signature.charAt(i)) {
        case '[':
          arrayDim++;
          break;

        case 'L':
          if (signature.charAt(signature.length() - 1) == ';') {
            type = CodeConstants.TYPE_OBJECT;
            value = signature.substring(i + 1, signature.length() - 1);
            break loop;
          }

        default:
          value = signature.substring(i, signature.length());
          if ((clType && i == 0) || value.length() > 1) {
            type = CodeConstants.TYPE_OBJECT;
          }
          else {
            type = getType(value.charAt(0));
          }
          break loop;
      }
    }

    this.type = type;
    this.arrayDim = arrayDim;
    this.value = value;
    this.typeFamily = getFamily(type, arrayDim);
    this.stackSize = getStackSize(type, arrayDim);
    this.falseBoolean = false;
  }

  private static String getChar(int type) {
    switch (type) {
      case CodeConstants.TYPE_BYTE:
        return "B";
      case CodeConstants.TYPE_CHAR:
        return "C";
      case CodeConstants.TYPE_DOUBLE:
        return "D";
      case CodeConstants.TYPE_FLOAT:
        return "F";
      case CodeConstants.TYPE_INT:
        return "I";
      case CodeConstants.TYPE_LONG:
        return "J";
      case CodeConstants.TYPE_SHORT:
        return "S";
      case CodeConstants.TYPE_BOOLEAN:
        return "Z";
      case CodeConstants.TYPE_VOID:
        return "V";
      case CodeConstants.TYPE_GROUP2EMPTY:
        return "G";
      case CodeConstants.TYPE_NOTINITIALIZED:
        return "N";
      case CodeConstants.TYPE_ADDRESS:
        return "A";
      case CodeConstants.TYPE_BYTECHAR:
        return "X";
      case CodeConstants.TYPE_SHORTCHAR:
        return "Y";
      case CodeConstants.TYPE_UNKNOWN:
        return "U";
      case CodeConstants.TYPE_NULL:
      case CodeConstants.TYPE_OBJECT:
        return null;
      default:
        throw new RuntimeException("Invalid type");
    }
  }

  private static int getStackSize(int type, int arrayDim) {
    if (arrayDim > 0) {
      return 1;
    }

    switch (type) {
      case CodeConstants.TYPE_DOUBLE:
      case CodeConstants.TYPE_LONG:
        return 2;
      case CodeConstants.TYPE_VOID:
      case CodeConstants.TYPE_GROUP2EMPTY:
        return 0;
      default:
        return 1;
    }
  }

  private static int getFamily(int type, int arrayDim) {
    if (arrayDim > 0) {
      return CodeConstants.TYPE_FAMILY_OBJECT;
    }

    switch (type) {
      case CodeConstants.TYPE_BYTE:
      case CodeConstants.TYPE_BYTECHAR:
      case CodeConstants.TYPE_SHORTCHAR:
      case CodeConstants.TYPE_CHAR:
      case CodeConstants.TYPE_SHORT:
      case CodeConstants.TYPE_INT:
        return CodeConstants.TYPE_FAMILY_INTEGER;
      case CodeConstants.TYPE_DOUBLE:
        return CodeConstants.TYPE_FAMILY_DOUBLE;
      case CodeConstants.TYPE_FLOAT:
        return CodeConstants.TYPE_FAMILY_FLOAT;
      case CodeConstants.TYPE_LONG:
        return CodeConstants.TYPE_FAMILY_LONG;
      case CodeConstants.TYPE_BOOLEAN:
        return CodeConstants.TYPE_FAMILY_BOOLEAN;
      case CodeConstants.TYPE_NULL:
      case CodeConstants.TYPE_OBJECT:
        return CodeConstants.TYPE_FAMILY_OBJECT;
      default:
        return CodeConstants.TYPE_FAMILY_UNKNOWN;
    }
  }

  public VarType decreaseArrayDim() {
    if (arrayDim > 0) {
      return new VarType(type, arrayDim - 1, value);
    }
    else {
      //throw new RuntimeException("array dimension equals 0!"); FIXME: investigate this case
      return this;
    }
  }

  public VarType resizeArrayDim(int newArrayDim) {
    return new VarType(type, newArrayDim, value, typeFamily, stackSize, falseBoolean);
  }

  public VarType copy() {
    return copy(false);
  }

  public VarType copy(boolean forceFalseBoolean) {
    return new VarType(type, arrayDim, value, typeFamily, stackSize, falseBoolean || forceFalseBoolean);
  }

  public boolean isFalseBoolean() {
    return falseBoolean;
  }

  public boolean isSuperset(VarType val) {
    return this.equals(val) || this.isStrictSuperset(val);
  }

  public boolean isStrictSuperset(VarType val) {
    int valType = val.type;

    if (valType == CodeConstants.TYPE_UNKNOWN && type != CodeConstants.TYPE_UNKNOWN) {
      return true;
    }

    if (val.arrayDim > 0) {
      return this.equals(VARTYPE_OBJECT);
    }
    else if (arrayDim > 0) {
      return (valType == CodeConstants.TYPE_NULL);
    }

    boolean res = false;

    switch (type) {
      case CodeConstants.TYPE_INT:
        res = (valType == CodeConstants.TYPE_SHORT || valType == CodeConstants.TYPE_CHAR);
      case CodeConstants.TYPE_SHORT:
        res |= (valType == CodeConstants.TYPE_BYTE);
      case CodeConstants.TYPE_CHAR:
        res |= (valType == CodeConstants.TYPE_SHORTCHAR);
      case CodeConstants.TYPE_BYTE:
      case CodeConstants.TYPE_SHORTCHAR:
        res |= (valType == CodeConstants.TYPE_BYTECHAR);
      case CodeConstants.TYPE_BYTECHAR:
        res |= (valType == CodeConstants.TYPE_BOOLEAN);
        break;

      case CodeConstants.TYPE_OBJECT:
        if (valType == CodeConstants.TYPE_NULL) {
          return true;
        }
        else if (this.equals(VARTYPE_OBJECT)) {
          return valType == CodeConstants.TYPE_OBJECT && !val.equals(VARTYPE_OBJECT);
        }
    }

    return res;
  }

  @Override
  public boolean equals(Object o) {
    if (o == this) {
      return true;
    }

    if (o == null || !(o instanceof VarType)) {
      return false;
    }

    VarType vt = (VarType)o;
    return type == vt.type && arrayDim == vt.arrayDim && InterpreterUtil.equalObjects(value, vt.value);
  }

  @Override
  public String toString() {
    StringBuilder res = new StringBuilder();
    for (int i = 0; i < arrayDim; i++) {
      res.append('[');
    }
    if (type == CodeConstants.TYPE_OBJECT) {
      res.append('L').append(value).append(';');
    }
    else {
      res.append(value);
    }
    return res.toString();
  }

  // type1 and type2 must not be null
  public static VarType getCommonMinType(VarType type1, VarType type2) {
    if (type1.type == CodeConstants.TYPE_BOOLEAN && type2.type == CodeConstants.TYPE_BOOLEAN) { // special case booleans
      return type1.isFalseBoolean() ? type2 : type1;
    }

    if (type1.isSuperset(type2)) {
      return type2;
    }
    else if (type2.isSuperset(type1)) {
      return type1;
    }
    else if (type1.typeFamily == type2.typeFamily) {
      switch (type1.typeFamily) {
        case CodeConstants.TYPE_FAMILY_INTEGER:
          if ((type1.type == CodeConstants.TYPE_CHAR && type2.type == CodeConstants.TYPE_SHORT)
              || (type1.type == CodeConstants.TYPE_SHORT && type2.type == CodeConstants.TYPE_CHAR)) {
            return VARTYPE_SHORTCHAR;
          }
          else {
            return VARTYPE_BYTECHAR;
          }
        case CodeConstants.TYPE_FAMILY_OBJECT:
          return VARTYPE_NULL;
      }
    }

    return null;
  }

  // type1 and type2 must not be null
  public static VarType getCommonSupertype(VarType type1, VarType type2) {
    if (type1.type == CodeConstants.TYPE_BOOLEAN && type2.type == CodeConstants.TYPE_BOOLEAN) { // special case booleans
      return type1.isFalseBoolean() ? type1 : type2;
    }

    if (type1.isSuperset(type2)) {
      return type1;
    }
    else if (type2.isSuperset(type1)) {
      return type2;
    }
    else if (type1.typeFamily == type2.typeFamily) {
      switch (type1.typeFamily) {
        case CodeConstants.TYPE_FAMILY_INTEGER:
          if ((type1.type == CodeConstants.TYPE_SHORTCHAR && type2.type == CodeConstants.TYPE_BYTE)
              || (type1.type == CodeConstants.TYPE_BYTE && type2.type == CodeConstants.TYPE_SHORTCHAR)) {
            return VARTYPE_SHORT;
          }
          else {
            return VARTYPE_INT;
          }
        case CodeConstants.TYPE_FAMILY_OBJECT:
          return VARTYPE_OBJECT;
      }
    }

    return null;
  }

  public static VarType getMinTypeInFamily(int family) {
    switch (family) {
      case CodeConstants.TYPE_FAMILY_BOOLEAN:
        return VARTYPE_BOOLEAN;
      case CodeConstants.TYPE_FAMILY_INTEGER:
        return VARTYPE_BYTECHAR;
      case CodeConstants.TYPE_FAMILY_OBJECT:
        return VARTYPE_NULL;
      case CodeConstants.TYPE_FAMILY_FLOAT:
        return VARTYPE_FLOAT;
      case CodeConstants.TYPE_FAMILY_LONG:
        return VARTYPE_LONG;
      case CodeConstants.TYPE_FAMILY_DOUBLE:
        return VARTYPE_DOUBLE;
      case CodeConstants.TYPE_FAMILY_UNKNOWN:
        return VARTYPE_UNKNOWN;
      default:
        throw new IllegalArgumentException("Invalid type family: " + family);
    }
  }

  public static int getType(char c) {
    switch (c) {
      case 'B':
        return CodeConstants.TYPE_BYTE;
      case 'C':
        return CodeConstants.TYPE_CHAR;
      case 'D':
        return CodeConstants.TYPE_DOUBLE;
      case 'F':
        return CodeConstants.TYPE_FLOAT;
      case 'I':
        return CodeConstants.TYPE_INT;
      case 'J':
        return CodeConstants.TYPE_LONG;
      case 'S':
        return CodeConstants.TYPE_SHORT;
      case 'Z':
        return CodeConstants.TYPE_BOOLEAN;
      case 'V':
        return CodeConstants.TYPE_VOID;
      case 'G':
        return CodeConstants.TYPE_GROUP2EMPTY;
      case 'N':
        return CodeConstants.TYPE_NOTINITIALIZED;
      case 'A':
        return CodeConstants.TYPE_ADDRESS;
      case 'X':
        return CodeConstants.TYPE_BYTECHAR;
      case 'Y':
        return CodeConstants.TYPE_SHORTCHAR;
      case 'U':
        return CodeConstants.TYPE_UNKNOWN;
      default:
        throw new IllegalArgumentException("Invalid type: " + c);
    }
  }
}
