package org.pitest.kotlin;

import static org.pitest.bytecode.analysis.InstructionMatchers.anIntegerConstant;
import static org.pitest.bytecode.analysis.InstructionMatchers.anyInstruction;
import static org.pitest.bytecode.analysis.InstructionMatchers.jumpsTo;
import static org.pitest.bytecode.analysis.InstructionMatchers.labelNode;
import static org.pitest.bytecode.analysis.InstructionMatchers.methodCallTo;
import static org.pitest.bytecode.analysis.InstructionMatchers.notAnInstruction;
import static org.pitest.bytecode.analysis.InstructionMatchers.opCode;
import static org.pitest.bytecode.analysis.InstructionMatchers.recordTarget;

import java.util.Collection;
import java.util.function.Predicate;
import java.util.regex.Pattern;

import org.objectweb.asm.Opcodes;
import org.objectweb.asm.tree.AbstractInsnNode;
import org.objectweb.asm.tree.LabelNode;
import org.objectweb.asm.tree.MethodInsnNode;
import org.pitest.bytecode.analysis.ClassTree;
import org.pitest.bytecode.analysis.MethodMatchers;
import org.pitest.bytecode.analysis.MethodTree;
import org.pitest.classinfo.ClassName;
import org.pitest.functional.FCollection;
import org.pitest.mutationtest.build.InterceptorType;
import org.pitest.mutationtest.build.MutationInterceptor;
import org.pitest.mutationtest.engine.Mutater;
import org.pitest.mutationtest.engine.MutationDetails;
import org.pitest.sequence.Context;
import org.pitest.sequence.Match;
import org.pitest.sequence.QueryParams;
import org.pitest.sequence.QueryStart;
import org.pitest.sequence.SequenceMatcher;
import org.pitest.sequence.SequenceQuery;
import org.pitest.sequence.Slot;

public class KotlinInterceptor implements MutationInterceptor {

  private ClassTree currentClass;
  private boolean isKotlinClass;


  private static final boolean DEBUG = false;
  
  private static final Slot<AbstractInsnNode> MUTATED_INSTRUCTION = Slot.create(AbstractInsnNode.class);
  private static final Slot<Boolean> FOUND = Slot.create(Boolean.class);

  static final SequenceMatcher<AbstractInsnNode> KOTLIN_JUNK = QueryStart
    .match(Match.<AbstractInsnNode>never())
    .zeroOrMore(QueryStart.match(anyInstruction()))
    .or(destructuringCall())
    .or(nullCast())
    .or(safeNullCallOrElvis())
    .or(safeCast())
    .then(containMutation(FOUND))
    .zeroOrMore(QueryStart.match(anyInstruction()))
    .compile(QueryParams.params(AbstractInsnNode.class)
      .withIgnores(notAnInstruction())
      .withDebug(DEBUG)
    );

  private static SequenceQuery<AbstractInsnNode> nullCast() {
    return QueryStart
      .any(AbstractInsnNode.class)
      .then(opCode(Opcodes.IFNONNULL).and(mutationPoint()))
      .then(methodCallTo(ClassName.fromString("kotlin/jvm/internal/Intrinsics"), "throwNpe").and(mutationPoint()));
  }

  private static SequenceQuery<AbstractInsnNode> safeCast() {
    Slot<LabelNode> nullJump = Slot.create(LabelNode.class);
    return QueryStart
      .any(AbstractInsnNode.class)
      .then(opCode(Opcodes.INSTANCEOF).and(mutationPoint()))
      .then(opCode(Opcodes.IFNE).and(jumpsTo(nullJump.write()).and(mutationPoint())))
      .then(opCode(Opcodes.POP))
      .then(opCode(Opcodes.ACONST_NULL))
      .then(labelNode(nullJump.read()));
  }


  private static SequenceQuery<AbstractInsnNode> destructuringCall() {
    return QueryStart
      .any(AbstractInsnNode.class)
      .then(aComponentNCall().and(mutationPoint()));
  }

  private static SequenceQuery<AbstractInsnNode> safeNullCallOrElvis() {
    Slot<LabelNode> nullJump = Slot.create(LabelNode.class);
    return QueryStart
      .any(AbstractInsnNode.class)
      .then(opCode(Opcodes.IFNULL).and(jumpsTo(nullJump.write())).and(mutationPoint()))
      .oneOrMore(QueryStart.match(anyInstruction()))
      .then(opCode(Opcodes.GOTO))
      .then(labelNode(nullJump.read()))
      .then(opCode(Opcodes.POP))
      .then(aConstant().and(mutationPoint()));
  }

  private static Match<AbstractInsnNode> aConstant() {
    return opCode(Opcodes.ACONST_NULL).or(anIntegerConstant().or(opCode(Opcodes.SIPUSH)).or(opCode(Opcodes.LDC)));
  }

  private static Match<AbstractInsnNode> aComponentNCall() {
    final Pattern componentPattern = Pattern.compile("component\\d");
    return new Match<AbstractInsnNode>() {
      @Override
      public boolean test(Context<AbstractInsnNode> c, AbstractInsnNode abstractInsnNode) {
        if (abstractInsnNode instanceof MethodInsnNode) {
          MethodInsnNode call = (MethodInsnNode) abstractInsnNode;
          return isDestructuringCall(call) && takesNoArgs(call);
        }
        return false;
      }

      private boolean isDestructuringCall(MethodInsnNode call) {
        return takesNoArgs(call) && isComponentNCall(call);
      }

      private boolean isComponentNCall(MethodInsnNode call) {
        return componentPattern.matcher(call.name).matches();
      }

      private boolean takesNoArgs(MethodInsnNode call) {
        return call.desc.startsWith("()");
      }
    };
  }

  @Override
  public Collection<MutationDetails> intercept(
      Collection<MutationDetails> mutations, Mutater m) {
    if(!isKotlinClass) {
      return mutations;
    }
    return FCollection.filter(mutations, isKotlinJunkMutation(currentClass).negate());
  }
  
  @Override
  public InterceptorType type() {
    return InterceptorType.FILTER;
  }

  @Override
  public void begin(ClassTree clazz) {
    currentClass = clazz;
    isKotlinClass = clazz.annotations().stream()
        .filter(annotationNode -> annotationNode.desc.equals("Lkotlin/Metadata;"))
        .findFirst()
        .isPresent();
  }

  @Override
  public void end() {
    currentClass = null;
  }
  
  private static Predicate<MutationDetails> isKotlinJunkMutation(final ClassTree currentClass) {
    return a -> {
        int instruction = a.getInstructionIndex();
        MethodTree method = currentClass.methods().stream()
            .filter(MethodMatchers.forLocation(a.getId().getLocation()))
            .findFirst()
            .get();
        AbstractInsnNode mutatedInstruction = method.instruction(instruction);
        Context<AbstractInsnNode> context = Context.start(method.instructions(), DEBUG);
        context.store(MUTATED_INSTRUCTION.write(), mutatedInstruction);
        return KOTLIN_JUNK.matches(method.instructions(), context);
    };
  }

  private static Match<AbstractInsnNode> mutationPoint() {
    return recordTarget(MUTATED_INSTRUCTION.read(), FOUND.write());
  }

  private static Match<AbstractInsnNode> containMutation(final Slot<Boolean> found) {
    return (context, node) ->  context.retrieve(found.read()).isPresent();
  }
}