/*
 * Copyright 2000-2015 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.consts;

import org.jetbrains.java.decompiler.code.CodeConstants;
import org.jetbrains.java.decompiler.main.DecompilerContext;
import org.jetbrains.java.decompiler.modules.renamer.PoolInterceptor;
import org.jetbrains.java.decompiler.struct.gen.FieldDescriptor;
import org.jetbrains.java.decompiler.struct.gen.MethodDescriptor;
import org.jetbrains.java.decompiler.struct.gen.NewClassNameBuilder;
import org.jetbrains.java.decompiler.struct.gen.VarType;
import org.jetbrains.java.decompiler.util.DataInputFullStream;

import java.io.DataInputStream;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;

public class ConstantPool implements NewClassNameBuilder {

  public static final int FIELD = 1;
  public static final int METHOD = 2;

  private final List<PooledConstant> pool = new ArrayList<PooledConstant>();
  private final PoolInterceptor interceptor;


  public ConstantPool(DataInputStream in) throws IOException {
    int size = in.readUnsignedShort();
    int[] pass = new int[size];

    // first dummy constant
    pool.add(null);

    // first pass: read the elements
    for (int i = 1; i < size; i++) {
      byte tag = (byte)in.readUnsignedByte();

      switch (tag) {
        case CodeConstants.CONSTANT_Utf8:
          pool.add(new PrimitiveConstant(CodeConstants.CONSTANT_Utf8, in.readUTF()));
          break;
        case CodeConstants.CONSTANT_Integer:
          pool.add(new PrimitiveConstant(CodeConstants.CONSTANT_Integer, new Integer(in.readInt())));
          break;
        case CodeConstants.CONSTANT_Float:
          pool.add(new PrimitiveConstant(CodeConstants.CONSTANT_Float, new Float(in.readFloat())));
          break;
        case CodeConstants.CONSTANT_Long:
          pool.add(new PrimitiveConstant(CodeConstants.CONSTANT_Long, new Long(in.readLong())));
          pool.add(null);
          i++;
          break;
        case CodeConstants.CONSTANT_Double:
          pool.add(new PrimitiveConstant(CodeConstants.CONSTANT_Double, new Double(in.readDouble())));
          pool.add(null);
          i++;
          break;
        case CodeConstants.CONSTANT_Class:
        case CodeConstants.CONSTANT_String:
        case CodeConstants.CONSTANT_MethodType:
          pool.add(new PrimitiveConstant(tag, in.readUnsignedShort()));
          pass[i] = 1;
          break;
        case CodeConstants.CONSTANT_Fieldref:
        case CodeConstants.CONSTANT_Methodref:
        case CodeConstants.CONSTANT_InterfaceMethodref:
        case CodeConstants.CONSTANT_NameAndType:
        case CodeConstants.CONSTANT_InvokeDynamic:
          pool.add(new LinkConstant(tag, in.readUnsignedShort(), in.readUnsignedShort()));
          if (tag == CodeConstants.CONSTANT_NameAndType) {
            pass[i] = 1;
          }
          else {
            pass[i] = 2;
          }
          break;
        case CodeConstants.CONSTANT_MethodHandle:
          pool.add(new LinkConstant(tag, in.readUnsignedByte(), in.readUnsignedShort()));
          pass[i] = 3;
          break;
      }
    }

    // resolving complex pool elements
    for (int passIndex = 1; passIndex <= 3; passIndex++) {
      for (int i = 1; i < size; i++) {
        if (pass[i] == passIndex) {
          pool.get(i).resolveConstant(this);
        }
      }
    }

    // get global constant pool interceptor instance, if any available
    interceptor = DecompilerContext.getPoolInterceptor();
  }

  public static void skipPool(DataInputFullStream in) throws IOException {
    int size = in.readUnsignedShort();

    for (int i = 1; i < size; i++) {
      switch (in.readUnsignedByte()) {
        case CodeConstants.CONSTANT_Utf8:
          in.readUTF();
          break;
        case CodeConstants.CONSTANT_Integer:
        case CodeConstants.CONSTANT_Float:
        case CodeConstants.CONSTANT_Fieldref:
        case CodeConstants.CONSTANT_Methodref:
        case CodeConstants.CONSTANT_InterfaceMethodref:
        case CodeConstants.CONSTANT_NameAndType:
        case CodeConstants.CONSTANT_InvokeDynamic:
          in.discard(4);
          break;
        case CodeConstants.CONSTANT_Long:
        case CodeConstants.CONSTANT_Double:
          in.discard(8);
          i++;
          break;
        case CodeConstants.CONSTANT_Class:
        case CodeConstants.CONSTANT_String:
        case CodeConstants.CONSTANT_MethodType:
          in.discard(2);
          break;
        case CodeConstants.CONSTANT_MethodHandle:
          in.discard(3);
      }
    }
  }

  public int size() {
    return pool.size();
  }

  public String[] getClassElement(int elementType, String className, int nameIndex, int descriptorIndex) {
    String elementName = ((PrimitiveConstant)getConstant(nameIndex)).getString();
    String descriptor = ((PrimitiveConstant)getConstant(descriptorIndex)).getString();

    if (interceptor != null) {
      String newElement = interceptor.getName(className + " " + elementName + " " + descriptor);
      if (newElement != null) {
        elementName = newElement.split(" ")[1];
      }

      String newDescriptor = buildNewDescriptor(elementType == FIELD, descriptor);
      if (newDescriptor != null) {
        descriptor = newDescriptor;
      }
    }

    return new String[]{elementName, descriptor};
  }

  public PooledConstant getConstant(int index) {
    return pool.get(index);
  }

  public PrimitiveConstant getPrimitiveConstant(int index) {
    PrimitiveConstant cn = (PrimitiveConstant)getConstant(index);

    if (cn != null && interceptor != null) {
      if (cn.type == CodeConstants.CONSTANT_Class) {
        String newName = buildNewClassname(cn.getString());
        if (newName != null) {
          cn = new PrimitiveConstant(CodeConstants.CONSTANT_Class, newName);
        }
      }
    }

    return cn;
  }

  public LinkConstant getLinkConstant(int index) {
    LinkConstant ln = (LinkConstant)getConstant(index);

    if (ln != null && interceptor != null &&
        (ln.type == CodeConstants.CONSTANT_Fieldref ||
         ln.type == CodeConstants.CONSTANT_Methodref ||
         ln.type == CodeConstants.CONSTANT_InterfaceMethodref)) {
      String newClassName = buildNewClassname(ln.classname);
      String newElement = interceptor.getName(ln.classname + " " + ln.elementname + " " + ln.descriptor);
      String newDescriptor = buildNewDescriptor(ln.type == CodeConstants.CONSTANT_Fieldref, ln.descriptor);

      if (newClassName != null || newElement != null || newDescriptor != null) {
        String className = newClassName == null ? ln.classname : newClassName;
        String elementName = newElement == null ? ln.elementname : newElement.split(" ")[1];
        String descriptor = newDescriptor == null ? ln.descriptor : newDescriptor;
        ln = new LinkConstant(ln.type, className, elementName, descriptor);
      }
    }

    return ln;
  }

  @Override
  public String buildNewClassname(String className) {
    VarType vt = new VarType(className, true);

    String newName = interceptor.getName(vt.value);
    if (newName != null) {
      StringBuilder buffer = new StringBuilder();

      if (vt.arrayDim > 0) {
        for (int i = 0; i < vt.arrayDim; i++) {
          buffer.append("[");
        }

        buffer.append("L").append(newName).append(";");
      }
      else {
        buffer.append(newName);
      }

      return buffer.toString();
    }

    return null;
  }

  private String buildNewDescriptor(boolean isField, String descriptor) {
    if (isField) {
      return FieldDescriptor.parseDescriptor(descriptor).buildNewDescriptor(this);
    }
    else {
      return MethodDescriptor.parseDescriptor(descriptor).buildNewDescriptor(this);
    }
  }
}
