/*
 * Copyright 2016 Miroslav Janíček
 *
 * 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.classdump.luna.compiler.gen.asm.helpers;

import static org.objectweb.asm.Opcodes.BIPUSH;
import static org.objectweb.asm.Opcodes.CHECKCAST;
import static org.objectweb.asm.Opcodes.DCONST_0;
import static org.objectweb.asm.Opcodes.DCONST_1;
import static org.objectweb.asm.Opcodes.F_SAME;
import static org.objectweb.asm.Opcodes.F_SAME1;
import static org.objectweb.asm.Opcodes.ICONST_0;
import static org.objectweb.asm.Opcodes.ICONST_1;
import static org.objectweb.asm.Opcodes.ICONST_2;
import static org.objectweb.asm.Opcodes.ICONST_3;
import static org.objectweb.asm.Opcodes.ICONST_4;
import static org.objectweb.asm.Opcodes.ICONST_5;
import static org.objectweb.asm.Opcodes.ICONST_M1;
import static org.objectweb.asm.Opcodes.INVOKESPECIAL;
import static org.objectweb.asm.Opcodes.LCONST_0;
import static org.objectweb.asm.Opcodes.LCONST_1;
import static org.objectweb.asm.Opcodes.SIPUSH;

import java.util.Arrays;
import java.util.Objects;
import org.objectweb.asm.Type;
import org.objectweb.asm.tree.AbstractInsnNode;
import org.objectweb.asm.tree.FrameNode;
import org.objectweb.asm.tree.InsnNode;
import org.objectweb.asm.tree.IntInsnNode;
import org.objectweb.asm.tree.LdcInsnNode;
import org.objectweb.asm.tree.MethodInsnNode;
import org.objectweb.asm.tree.TypeInsnNode;

public abstract class ASMUtils {

  private ASMUtils() {
    // not to be instantiated
  }

  public static Type arrayTypeFor(Class<?> clazz) {
    return arrayTypeFor(clazz, 1);
  }

  public static Type arrayTypeFor(Class<?> clazz, int dimensions) {
    Objects.requireNonNull(clazz);
    if (dimensions < 1) {
      throw new IllegalArgumentException("dimensions must be at least 1");
    }

    String prefix = "[";
    for (int i = 1; i < dimensions; i++) {
      prefix = prefix + "[";
    }

    return Type.getType(prefix + Type.getType(clazz).getDescriptor());
  }

  public static Type typeForClassName(String className) {
    Objects.requireNonNull(className);
    return Type.getType("L" + className.replace(".", "/") + ";");
  }

  public static Type[] fillTypes(Type t, int n) {
    Type[] result = new Type[n];
    Arrays.fill(result, t);
    return result;
  }

  public static FrameNode frameSame() {
    return new FrameNode(F_SAME, 0, null, 0, null);
  }

  public static FrameNode frameSame1(Class clazz) {
    return new FrameNode(F_SAME1, 0, null, 1, new Object[]{Type.getInternalName(clazz)});
  }

  public static AbstractInsnNode checkCast(Class clazz) {
    return new TypeInsnNode(CHECKCAST, Type.getInternalName(clazz));
  }

  public static AbstractInsnNode loadInt(int i) {
    switch (i) {
      case -1:
        return new InsnNode(ICONST_M1);
      case 0:
        return new InsnNode(ICONST_0);
      case 1:
        return new InsnNode(ICONST_1);
      case 2:
        return new InsnNode(ICONST_2);
      case 3:
        return new InsnNode(ICONST_3);
      case 4:
        return new InsnNode(ICONST_4);
      case 5:
        return new InsnNode(ICONST_5);
      default: {
        if (i >= Byte.MIN_VALUE && i <= Byte.MAX_VALUE) {
          return new IntInsnNode(BIPUSH, i);
        } else if (i >= Short.MIN_VALUE && i <= Short.MAX_VALUE) {
          return new IntInsnNode(SIPUSH, i);
        } else {
          return new LdcInsnNode(i);
        }
      }
    }
  }

  public static AbstractInsnNode loadLong(long l) {
    if (l == 0L) {
      return new InsnNode(LCONST_0);
    } else if (l == 1L) {
      return new InsnNode(LCONST_1);
    } else {
      return new LdcInsnNode(l);
    }
  }

  public static AbstractInsnNode loadDouble(double d) {
    // We want to distinguish -0.0 from 0.0, but -0.0 == 0.0;
    // luckily, Double.equals() distinguishes these two cases.
    if (Double.valueOf(d).equals(0.0)) {
      return new InsnNode(DCONST_0);
    } else if (d == 1.0) {
      return new InsnNode(DCONST_1);
    } else {
      return new LdcInsnNode(d);
    }
  }

  public static MethodInsnNode ctor(Type of, Type... args) {
    return new MethodInsnNode(
        INVOKESPECIAL,
        of.getInternalName(),
        "<init>",
        Type.getMethodDescriptor(
            Type.VOID_TYPE,
            args),
        false);
  }

  public static MethodInsnNode ctor(Class clazz, Class... args) {
    Type[] argTypes = new Type[args.length];
    for (int i = 0; i < args.length; i++) {
      argTypes[i] = Type.getType(args[i]);
    }
    return ctor(Type.getType(clazz), argTypes);
  }

}
