/**
 * Apache License
 * Version 2.0, January 2004
 * http://www.apache.org/licenses/
 *
 * TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
 *
 * 1. Definitions.
 *
 * "License" shall mean the terms and conditions for use, reproduction,
 * and distribution as defined by Sections 1 through 9 of this document.
 *
 * "Licensor" shall mean the copyright owner or entity authorized by
 * the copyright owner that is granting the License.
 *
 * "Legal Entity" shall mean the union of the acting entity and all
 * other entities that control, are controlled by, or are under common
 * control with that entity. For the purposes of this definition,
 * "control" means (i) the power, direct or indirect, to cause the
 * direction or management of such entity, whether by contract or
 * otherwise, or (ii) ownership of fifty percent (50%) or more of the
 * outstanding shares, or (iii) beneficial ownership of such entity.
 *
 * "You" (or "Your") shall mean an individual or Legal Entity
 * exercising permissions granted by this License.
 *
 * "Source" form shall mean the preferred form for making modifications,
 * including but not limited to software source code, documentation
 * source, and configuration files.
 *
 * "Object" form shall mean any form resulting from mechanical
 * transformation or translation of a Source form, including but
 * not limited to compiled object code, generated documentation,
 * and conversions to other media types.
 *
 * "Work" shall mean the work of authorship, whether in Source or
 * Object form, made available under the License, as indicated by a
 * copyright notice that is included in or attached to the work
 * (an example is provided in the Appendix below).
 *
 * "Derivative Works" shall mean any work, whether in Source or Object
 * form, that is based on (or derived from) the Work and for which the
 * editorial revisions, annotations, elaborations, or other modifications
 * represent, as a whole, an original work of authorship. For the purposes
 * of this License, Derivative Works shall not include works that remain
 * separable from, or merely link (or bind by name) to the interfaces of,
 * the Work and Derivative Works thereof.
 *
 * "Contribution" shall mean any work of authorship, including
 * the original version of the Work and any modifications or additions
 * to that Work or Derivative Works thereof, that is intentionally
 * submitted to Licensor for inclusion in the Work by the copyright owner
 * or by an individual or Legal Entity authorized to submit on behalf of
 * the copyright owner. For the purposes of this definition, "submitted"
 * means any form of electronic, verbal, or written communication sent
 * to the Licensor or its representatives, including but not limited to
 * communication on electronic mailing lists, source code control systems,
 * and issue tracking systems that are managed by, or on behalf of, the
 * Licensor for the purpose of discussing and improving the Work, but
 * excluding communication that is conspicuously marked or otherwise
 * designated in writing by the copyright owner as "Not a Contribution."
 *
 * "Contributor" shall mean Licensor and any individual or Legal Entity
 * on behalf of whom a Contribution has been received by Licensor and
 * subsequently incorporated within the Work.
 *
 * 2. Grant of Copyright License. Subject to the terms and conditions of
 * this License, each Contributor hereby grants to You a perpetual,
 * worldwide, non-exclusive, no-charge, royalty-free, irrevocable
 * copyright license to reproduce, prepare Derivative Works of,
 * publicly display, publicly perform, sublicense, and distribute the
 * Work and such Derivative Works in Source or Object form.
 *
 * 3. Grant of Patent License. Subject to the terms and conditions of
 * this License, each Contributor hereby grants to You a perpetual,
 * worldwide, non-exclusive, no-charge, royalty-free, irrevocable
 * (except as stated in this section) patent license to make, have made,
 * use, offer to sell, sell, import, and otherwise transfer the Work,
 * where such license applies only to those patent claims licensable
 * by such Contributor that are necessarily infringed by their
 * Contribution(s) alone or by combination of their Contribution(s)
 * with the Work to which such Contribution(s) was submitted. If You
 * institute patent litigation against any entity (including a
 * cross-claim or counterclaim in a lawsuit) alleging that the Work
 * or a Contribution incorporated within the Work constitutes direct
 * or contributory patent infringement, then any patent licenses
 * granted to You under this License for that Work shall terminate
 * as of the date such litigation is filed.
 *
 * 4. Redistribution. You may reproduce and distribute copies of the
 * Work or Derivative Works thereof in any medium, with or without
 * modifications, and in Source or Object form, provided that You
 * meet the following conditions:
 *
 * (a) You must give any other recipients of the Work or
 * Derivative Works a copy of this License; and
 *
 * (b) You must cause any modified files to carry prominent notices
 * stating that You changed the files; and
 *
 * (c) You must retain, in the Source form of any Derivative Works
 * that You distribute, all copyright, patent, trademark, and
 * attribution notices from the Source form of the Work,
 * excluding those notices that do not pertain to any part of
 * the Derivative Works; and
 *
 * (d) If the Work includes a "NOTICE" text file as part of its
 * distribution, then any Derivative Works that You distribute must
 * include a readable copy of the attribution notices contained
 * within such NOTICE file, excluding those notices that do not
 * pertain to any part of the Derivative Works, in at least one
 * of the following places: within a NOTICE text file distributed
 * as part of the Derivative Works; within the Source form or
 * documentation, if provided along with the Derivative Works; or,
 * within a display generated by the Derivative Works, if and
 * wherever such third-party notices normally appear. The contents
 * of the NOTICE file are for informational purposes only and
 * do not modify the License. You may add Your own attribution
 * notices within Derivative Works that You distribute, alongside
 * or as an addendum to the NOTICE text from the Work, provided
 * that such additional attribution notices cannot be construed
 * as modifying the License.
 *
 * You may add Your own copyright statement to Your modifications and
 * may provide additional or different license terms and conditions
 * for use, reproduction, or distribution of Your modifications, or
 * for any such Derivative Works as a whole, provided Your use,
 * reproduction, and distribution of the Work otherwise complies with
 * the conditions stated in this License.
 *
 * 5. Submission of Contributions. Unless You explicitly state otherwise,
 * any Contribution intentionally submitted for inclusion in the Work
 * by You to the Licensor shall be under the terms and conditions of
 * this License, without any additional terms or conditions.
 * Notwithstanding the above, nothing herein shall supersede or modify
 * the terms of any separate license agreement you may have executed
 * with Licensor regarding such Contributions.
 *
 * 6. Trademarks. This License does not grant permission to use the trade
 * names, trademarks, service marks, or product names of the Licensor,
 * except as required for reasonable and customary use in describing the
 * origin of the Work and reproducing the content of the NOTICE file.
 *
 * 7. Disclaimer of Warranty. Unless required by applicable law or
 * agreed to in writing, Licensor provides the Work (and each
 * Contributor provides its Contributions) on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
 * implied, including, without limitation, any warranties or conditions
 * of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
 * PARTICULAR PURPOSE. You are solely responsible for determining the
 * appropriateness of using or redistributing the Work and assume any
 * risks associated with Your exercise of permissions under this License.
 *
 * 8. Limitation of Liability. In no event and under no legal theory,
 * whether in tort (including negligence), contract, or otherwise,
 * unless required by applicable law (such as deliberate and grossly
 * negligent acts) or agreed to in writing, shall any Contributor be
 * liable to You for damages, including any direct, indirect, special,
 * incidental, or consequential damages of any character arising as a
 * result of this License or out of the use or inability to use the
 * Work (including but not limited to damages for loss of goodwill,
 * work stoppage, computer failure or malfunction, or any and all
 * other commercial damages or losses), even if such Contributor
 * has been advised of the possibility of such damages.
 *
 * 9. Accepting Warranty or Additional Liability. While redistributing
 * the Work or Derivative Works thereof, You may choose to offer,
 * and charge a fee for, acceptance of support, warranty, indemnity,
 * or other liability obligations and/or rights consistent with this
 * License. However, in accepting such obligations, You may act only
 * on Your own behalf and on Your sole responsibility, not on behalf
 * of any other Contributor, and only if You agree to indemnify,
 * defend, and hold each Contributor harmless for any liability
 * incurred by, or claims asserted against, such Contributor by reason
 * of your accepting any such warranty or additional liability.
 *
 * END OF TERMS AND CONDITIONS
 *
 * APPENDIX: How to apply the Apache License to your work.
 *
 * To apply the Apache License to your work, attach the following
 * boilerplate notice, with the fields enclosed by brackets "{}"
 * replaced with your own identifying information. (Don't include
 * the brackets!)  The text should be enclosed in the appropriate
 * comment syntax for the file format. We also recommend that a
 * file or class name and description of purpose be included on the
 * same "printed page" as the copyright notice for easier
 * identification within third-party archives.
 *
 * Copyright 2014 Edgar Espina
 *
 * 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.jooby.internal.apitool;

import com.fasterxml.jackson.annotation.JsonAutoDetect;
import com.fasterxml.jackson.annotation.JsonInclude;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.module.SimpleModule;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.ImmutableSet;
import com.google.common.io.ByteStreams;
import com.google.common.primitives.Primitives;
import com.google.inject.internal.MoreTypes;
import com.google.inject.util.Types;
import com.typesafe.config.ConfigFactory;
import com.typesafe.config.ConfigValueFactory;
import org.jooby.Env;
import org.jooby.Jooby;
import org.jooby.Request;
import org.jooby.Response;
import org.jooby.Result;
import org.jooby.Route;
import org.jooby.Session;
import org.jooby.Status;
import org.jooby.Upload;
import org.jooby.apitool.RouteMethod;
import org.jooby.apitool.RouteParameter;
import org.jooby.apitool.RouteResponse;
import org.jooby.funzy.Try;
import static org.jooby.funzy.When.when;
import org.jooby.internal.RouteMetadata;
import static org.jooby.internal.apitool.Filters.access;
import static org.jooby.internal.apitool.Filters.and;
import static org.jooby.internal.apitool.Filters.call;
import static org.jooby.internal.apitool.Filters.file;
import static org.jooby.internal.apitool.Filters.getOrCreateKotlinClass;
import static org.jooby.internal.apitool.Filters.is;
import static org.jooby.internal.apitool.Filters.joobyRun;
import static org.jooby.internal.apitool.Filters.kotlinRouteHandler;
import static org.jooby.internal.apitool.Filters.method;
import static org.jooby.internal.apitool.Filters.methodName;
import static org.jooby.internal.apitool.Filters.mount;
import static org.jooby.internal.apitool.Filters.mutantToSomething;
import static org.jooby.internal.apitool.Filters.mutantValue;
import static org.jooby.internal.apitool.Filters.opcode;
import static org.jooby.internal.apitool.Filters.param;
import static org.jooby.internal.apitool.Filters.path;
import static org.jooby.internal.apitool.Filters.scriptRoute;
import static org.jooby.internal.apitool.Filters.use;
import org.jooby.internal.mvc.MvcRoutes;
import org.jooby.mvc.Body;
import org.jooby.mvc.Flash;
import org.jooby.mvc.Header;
import org.jooby.mvc.Local;
import org.jooby.mvc.POST;
import org.objectweb.asm.ClassReader;
import org.objectweb.asm.Handle;
import org.objectweb.asm.Opcodes;
import static org.objectweb.asm.Opcodes.GETSTATIC;
import static org.objectweb.asm.Opcodes.INVOKESPECIAL;
import org.objectweb.asm.Type;
import org.objectweb.asm.tree.AbstractInsnNode;
import org.objectweb.asm.tree.ClassNode;
import org.objectweb.asm.tree.FieldInsnNode;
import org.objectweb.asm.tree.InnerClassNode;
import org.objectweb.asm.tree.InsnNode;
import org.objectweb.asm.tree.IntInsnNode;
import org.objectweb.asm.tree.InvokeDynamicInsnNode;
import org.objectweb.asm.tree.LdcInsnNode;
import org.objectweb.asm.tree.LocalVariableNode;
import org.objectweb.asm.tree.MethodInsnNode;
import org.objectweb.asm.tree.MethodNode;
import org.objectweb.asm.tree.TypeInsnNode;
import org.objectweb.asm.tree.VarInsnNode;
import org.objectweb.asm.util.ASMifier;
import org.objectweb.asm.util.TraceClassVisitor;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import javax.inject.Named;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
import java.io.PrintWriter;
import java.io.Writer;
import java.lang.annotation.Annotation;
import java.lang.reflect.Method;
import java.lang.reflect.Parameter;
import java.net.URL;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.Set;
import java.util.function.Consumer;
import java.util.function.Predicate;
import java.util.function.Supplier;
import java.util.stream.Collectors;
import java.util.stream.IntStream;
import java.util.stream.Stream;

public class BytecodeRouteParser {

  static final Set<Class<?>> SKIP_TYPES = ImmutableSet.of(
      Route.Chain.class, Response.class, Request.class, Session.class, Route.class, Flash.class,
      Local.class);
  static final Predicate<Parameter> TYPE_TO_SKIP = p -> !SKIP_TYPES.contains(p.getType());
  static final Predicate<Parameter> ANNOTATION_TO_SKIP = p ->
      !Arrays.asList(p.getAnnotations()).stream()
          .filter(a -> SKIP_TYPES.contains(a.annotationType()))
          .findFirst()
          .isPresent();
  static final Predicate<Parameter> SKIP = TYPE_TO_SKIP.and(ANNOTATION_TO_SKIP);

  private static final ObjectMapper mapper = new ObjectMapper();
  private static final String OBJECT = Type.getInternalName(Object.class);
  private static final String RETURN_OBJ = "L" + OBJECT + ";";

  static {
    mapper.setVisibility(mapper.getVisibilityChecker()
        .withFieldVisibility(JsonAutoDetect.Visibility.ANY)
        .withGetterVisibility(JsonAutoDetect.Visibility.NONE)
        .withSetterVisibility(JsonAutoDetect.Visibility.NONE));
    mapper.setSerializationInclusion(JsonInclude.Include.NON_EMPTY);
    SimpleModule module = new SimpleModule();
    module.addSerializer(java.lang.reflect.Type.class, new TypeJsonSerializer());
    module.addDeserializer(java.lang.reflect.Type.class, new TypeJsonDeserializer());
    mapper.registerModule(module);
  }

  /**
   * The logging system.
   */
  private final Logger log = LoggerFactory.getLogger(getClass());

  private final Map<String, ClassNode> cache = new HashMap<>();

  private final DocParser javadoc;

  private final Predicate<MethodInsnNode> scriptRoute;

  private final ClassLoader loader;

  public BytecodeRouteParser(ClassLoader loader, Path dir) {
    javadoc = new DocParser(dir);
    this.loader = loader;
    this.scriptRoute = scriptRoute(loader);
  }

  public List<RouteMethod> read(String classname) {
    String filename = "/" + classname.replace(".", "/") + ".json";
    URL resource = getClass().getResource(filename);
    if (resource != null) {
      try {
        return mapper.readValue(resource,
            mapper.getTypeFactory().constructCollectionType(ArrayList.class, RouteMethod.class));
      } catch (IOException e) {
        log.error("read of {} resulted in exception", filename, e);
      }
    }
    return null;
  }

  private Path write(Path output, List<RouteMethod> routes) throws IOException {
    Files.createDirectories(output.getParent());
    log.debug("writing {} with {}", output, routes);
    mapper.writer().withDefaultPrettyPrinter().writeValue(output.toFile(), routes);
    return output;
  }

  private Path toFile(Path dir, String classname) {
    Path path = Arrays.asList(classname.split("\\.")).stream()
        .reduce(dir, Path::resolve, Path::resolve);
    ClassNode classNode = loadClass(classname);
    return path.getParent().resolve(classNode.sourceFile
        .replaceAll("\\.java|\\.kt", "") + ".json");
  }

  public Path export(Path dir, String classname) throws Exception {
    Path output = toFile(dir, classname);
    Files.deleteIfExists(output);
    List<RouteMethod> routes = parse(classname);
    return write(output, routes);
  }

  public List<RouteMethod> parse(String classname) throws Exception {
    ClassNode owner = loadClass(classname);
    List<Object> lambdas = bindMethods(owner, lambdas(loader, owner));

    List<RouteMethod> methods = new ArrayList<>();
    for (Object it : lambdas) {
      log.debug("found: {}", it);
      if (it instanceof RouteMethod) {
        methods.add((RouteMethod) it);
      } else {
        Lambda lambda = (Lambda) it;
        if (lambda.method.isPresent()) {
          MethodNode method = lambda.method.get();

          java.lang.reflect.Type returnType;
          if (method.desc.endsWith("V")) {
            returnType = void.class;
          } else if (method.desc.endsWith(RETURN_OBJ)) {
            returnType = returnType(loader, method);
          } else {
            returnType = TypeDescriptorParser.parse(loader,
                Optional.ofNullable(method.signature).orElse(method.desc));
          }
          Integer status;
          if (returnType instanceof TypeWithStatus) {
            status = ((TypeWithStatus) returnType).status;
            returnType = ((TypeWithStatus) returnType).type();
          } else {
            status = null;
          }
          List<RouteParameter> parameters = params(loader, owner, lambda.pattern, method);
          RouteResponse routeResponse = new RouteResponse(simplifyType(returnType));
          if (status != null) {
            routeResponse.status(ImmutableMap.of(status,
                Try.apply(() -> Status.valueOf(status.intValue()).reason())
                    .orElse(status.toString())));
          }
          RouteMethod route = new RouteMethod(lambda.name, lambda.pattern,
              routeResponse).parameters(parameters);
          javadoc(route, javadoc.pop(lambda.declaringClass, lambda.name, lambda.pattern));
          methods.add(route);
        } else {
          log.debug("can't bind implementation for {} at {}", lambda, lambda.owner);
        }
      }
    }
    return methods;
  }

  private RouteMethod javadoc(final RouteMethod method, final Optional<DocItem> doc) {
    doc.ifPresent(it -> {
      method.description(it.text);
      method.summary(it.summary);
      method.response().description(it.returns);
      method.response().status(it.statusCodes);
      method.parameters().forEach(p -> {
        p.description(it.parameters.get(p.name()));
      });
    });
    return method;
  }

  @SuppressWarnings("unchecked")
  private List<Object> bindMethods(final ClassNode owner, final List<Object> lambdas) {
    // Find target bindMethods for each lambda expression
    List<MethodNode> methods = owner.methods;
    return lambdas.stream()
        .flatMap(e -> {
          if (e instanceof RouteMethod) {
            return Stream.of(e);
          }
          Lambda it = (Lambda) e;
          if (it.method.isPresent()) {
            return Stream.of(it);
          }
          return methods.stream()
              .filter(m -> owner.name.equals(it.owner) && it.implementationName.equals(m.name)
                  && it.desc.equals(m.desc))
              .findFirst()
              .map(m -> Stream.of((Object) it.method(m)))
              .orElseGet(() -> {
                ClassNode jump = loadClass(it.owner);
                List<Object> ext = bindMethods(jump, Arrays.asList(it));
                return ext.stream();
              });
        })
        .collect(Collectors.toList());
  }

  private List<Object> lambdas(final ClassLoader loader, final ClassNode owner, MethodNode method) {
    List<Object> result = new ArrayList<>();
    new Insns(method)
        // get(pattern, ); or post, put, etc...
        .on(InvokeDynamicInsnNode.class, it -> {
          log.debug("found candidate: {}", it);
          it.next()
              .filter(is(MethodInsnNode.class))
              .map(MethodInsnNode.class::cast)
              .filter(scriptRoute)
              .findFirst()
              .ifPresent(m -> {
                log.debug("found script route: {}.{}{}", m.owner, m.name, m.desc);
                Lambda.create(loader, scriptRoute, owner.name.replace("/", "."),
                    it.node, null)
                    .forEach(result::add);
              });
        })
        // path(String)
        .on(path(loader, owner.name), it -> {
          log.debug("found path: " + it);
          result.addAll(pathOperator(owner, owner.methods, it));
        })
        // use(Mvc class)
        .on(use(loader, owner.name), it -> mvc(it, result::add))
        // use(new App());
        .on(mount(loader, owner.name), it -> {
          log.debug("found mount: " + it);
          it.prev()
              .filter(and(is(MethodInsnNode.class), opcode(INVOKESPECIAL)))
              .findFirst()
              .map(MethodInsnNode.class::cast)
              .ifPresent(node -> {
                List<Lambda> lambdas = lambdas(loader, loadClass(node.owner)).stream()
                    .filter(Lambda.class::isInstance)
                    .map(Lambda.class::cast)
                    .collect(Collectors.toList());
                Insn.ldcFor(node).stream()
                    .map(e -> e.cst.toString())
                    .findFirst()
                    .ifPresent(prefix -> {
                      IntStream.range(0, lambdas.size())
                          .forEach(i -> lambdas.set(i, lambdas.get(i).prefix(prefix)));
                    });
                result.addAll(lambdas);
              });
        })
        .forEach();
    log.debug("results: {}", result);
    return result;
  }

  private List<Lambda> pathOperator(ClassNode owner, List<MethodNode> methods,
      Insn<MethodInsnNode> it) {
    List<Lambda> result = new ArrayList<>();
    Insn.ldcFor(it.node).stream()
        .map(e -> e.cst.toString())
        .findFirst()
        .ifPresent(path -> {
          it.prev()
              .filter(is(InvokeDynamicInsnNode.class))
              .findFirst()
              .map(InvokeDynamicInsnNode.class::cast)
              .ifPresent(n -> {
                Arrays.asList(n.bsmArgs).stream()
                    .filter(Handle.class::isInstance)
                    .findFirst()
                    .map(Handle.class::cast)
                    .ifPresent(handle -> {
                      methods.stream()
                          .filter(m -> m.name.equals(handle.getName()))
                          .findFirst()
                          .ifPresent(pathAction -> {
                            log.debug("pathAction {}", path);
                            lambdas(loader, owner, pathAction).stream()
                                .filter(Lambda.class::isInstance)
                                .map(Lambda.class::cast)
                                .forEach(lambda -> {
                                  result.add(lambda.prefix(path));
                                });
                          });

                    });
              });
        });
    log.debug("pathOperator: {}", result);
    return result;
  }

  @SuppressWarnings("unchecked")
  private List<Object> lambdas(final ClassLoader loader, final ClassNode owner) {
    List compiled = read(owner.name);
    if (compiled != null) {
      return compiled;
    }

    if (owner.sourceFile.endsWith(".kt")) {
      return kotlinSource(loader, owner);
    } else {
      List<MethodNode> methods = owner.methods;
      List<Object> handles = methods.stream()
          .filter(access(Opcodes.ACC_SYNTHETIC).negate())
          .flatMap(method -> lambdas(loader, owner, method).stream())
          .collect(Collectors.toList());
      return handles;
    }
  }

  private void mvcRoutes(String path, final Class type, Consumer<RouteMethod> callback) {
    Env env = Env.DEFAULT.build(ConfigFactory.empty()
        .withValue("application.env", ConfigValueFactory.fromAnyRef("dev")));
    MvcRoutes.routes(env, new RouteMetadata(env), "", true, type)
        .forEach(r -> {
          RouteMethod method = toRouteMethod(r);
          javadoc(method, javadoc.pop(type.getName(), r.method(), r.pattern()));
          if (path.length() > 0) {
            method.pattern(Route.normalize(path) + method.pattern());
          }
          callback.accept(method);
        });
  }

  @SuppressWarnings("unchecked")
  private List<Object> kotlinSource(final ClassLoader loader, final ClassNode owner) {
    List<Object> result = kotlinLambdas(loader, owner);

    if (result.size() == 0) {
      // Try main
      List<MethodNode> methods = owner.methods;
      methods.stream()
          .filter(method("main", String.class.getName() + "[]"))
          .findFirst()
          .ifPresent(main -> {
            log.debug("found main method: {}.main", owner.name);
            new Insns(main)
                .on(joobyRun(loader), n -> {
                  log.debug("found run(::Type, *args)");
                  n.prev()
                      .filter(and(is(FieldInsnNode.class), opcode(GETSTATIC)))
                      .findFirst()
                      .map(FieldInsnNode.class::cast)
                      .ifPresent(f -> {
                        ClassNode mainOwner = loadClass(f.owner);
                        log.debug("found ::{}", mainOwner.name);
                        mainOwner.methods.stream()
                            .filter(kotlinRouteHandler())
                            .findFirst()
                            .ifPresent(m -> {
                              log.debug("{}.invoke({})", mainOwner.name, ((MethodNode) m).desc);
                              new Insns((MethodNode) m)
                                  .on(is(TypeInsnNode.class).and(opcode(Opcodes.NEW)), it -> {
                                    ClassNode lambda = loadClass(it.node.desc);
                                    log.debug("source {}", lambda.name);
                                    result.addAll(kotlinLambdas(loader, lambda));
                                  })
                                  .forEach();
                            });
                      });
                })
                .forEach();
          });
    }
    return result;
  }

  @SuppressWarnings("unchecked")
  private List<Object> kotlinLambdas(final ClassLoader loader, final ClassNode owner) {
    List<Object> result = new ArrayList<>();
    List<InnerClassNode> innerClasses = owner.innerClasses;
    for (InnerClassNode innerClass : innerClasses) {
      ClassNode innerNode = loadClass(innerClass.name);
      result.addAll(kotlinLambda(loader, innerNode));
    }

    return result;
  }

  private List<Object> kotlinLambda(final ClassLoader loader, final ClassNode owner,
      MethodNode method) {
    List<Object> result = new ArrayList<>();
    new Insns(method)
        .on(scriptRoute, it -> {
          log.debug("  lambda candidate: {}", it.node);
          it.prev()
              .filter(and(is(FieldInsnNode.class), opcode(GETSTATIC)))
              .findFirst()
              .map(FieldInsnNode.class::cast)
              .ifPresent(field -> {
                ClassNode lambda = loadClass(field.owner);
                log.debug("  lambda: {}", field.owner);
                lambda.methods.stream()
                    .filter(kotlinRouteHandler())
                    .forEach(e -> {
                      MethodNode m = (MethodNode) e;
                      log.debug("    implementation: {}.{}()", lambda.name, m.name, m.desc);
                      Lambda.create(field.owner, Optional.empty(), it.node, m)
                          .forEach(result::add);
                    });
              });
        })
        // use(Mvc class)
        .on(use(loader, "org.jooby.Kooby"), it -> mvc(it, result::add))
        // route ("...") {...}
        .on(call(loader, "org.jooby.Kooby", "route", String.class,
            "kotlin.jvm.functions.Function1"), it -> {
          Optional<String> prefix = Insn.ldcFor(it.node).stream()
              .map(e -> e.cst.toString())
              .findFirst();
          it.prev()
              .filter(and(is(FieldInsnNode.class), opcode(GETSTATIC)))
              .findFirst()
              .map(FieldInsnNode.class::cast)
              .ifPresent(field -> {
                ClassNode lambda = loadClass(field.owner);
                lambda.methods.stream()
                    .filter(method("invoke", "org.jooby.KRouteGroup"))
                    .forEach(m -> {
                      new Insns((MethodNode) m)
                          .on(scriptRoute, n -> {
                            n.prev()
                                .filter(and(is(FieldInsnNode.class), opcode(GETSTATIC)))
                                .findFirst()
                                .map(FieldInsnNode.class::cast)
                                .ifPresent(f -> {
                                  ClassNode route = loadClass(f.owner);
                                  route.methods.stream()
                                      .filter(kotlinRouteHandler())
                                      .forEach(e -> Lambda
                                          .create(f.owner, prefix, n.node, (MethodNode) e)
                                          .forEach(result::add)
                                      );
                                });
                          }).forEach();
                    });
              });
        })
        // use(Jooby())
        .on(mount(loader, Jooby.class.getName()), it -> {
          it.prev()
              .filter(and(is(MethodInsnNode.class), opcode(INVOKESPECIAL)))
              .findFirst()
              .map(MethodInsnNode.class::cast)
              .ifPresent(n -> result.addAll(lambdas(loader, loadClass(n.owner))));
        })
        // path(String) {...}
        .on(path(loader, "org.jooby.Kooby"), it -> {
          result.addAll(kotlinPathOperator(owner, owner.methods, it));
        })
        .forEach();
    return result;
  }

  private void mvc(Insn<MethodInsnNode> it, Consumer<Object> consumer) {
    log.debug("found mvc {}", it);
    it.prev()
        .filter(is(LdcInsnNode.class))
        .findFirst()
        .map(LdcInsnNode.class::cast)
        .filter(ldc -> ldc.cst instanceof Type)
        .ifPresent(ldc -> {
          String arg0 = Type.getArgumentTypes(it.node.desc)[0].getClassName();
          String prefix = "";
          if (arg0.equals(String.class.getName())) {
            prefix = new Insn<>(it.method, ldc.getPrevious())
                .prev()
                .filter(is(LdcInsnNode.class))
                .findFirst()
                .map(LdcInsnNode.class::cast)
                .map(n -> n.cst.toString())
                .orElse("");
          }
          String mvcClass = ((Type) ldc.cst).getClassName();
          mvcRoutes(prefix, (Class) loadType(loader, mvcClass), consumer::accept);
        });
  }

  private List<Object> kotlinLambda(final ClassLoader loader, final ClassNode owner) {
    List<Object> result = new ArrayList<>();
    log.debug("visiting lambda class: {}", owner.name);
    List<MethodNode> methods = owner.methods;
    methods.stream()
        .filter(method("invoke", "org.jooby.Jooby")
            .or(method("invoke", "org.jooby.Kooby")))
        .findFirst()
        .ifPresent(method -> {
          log.debug("  invoke: {}", method.desc);
          result.addAll(kotlinLambda(loader, owner, method));
        });
    return result;
  }

  private List<Lambda> kotlinPathOperator(ClassNode owner, List<MethodNode> methods,
      Insn<MethodInsnNode> it) {
    List<Lambda> result = new ArrayList<>();
    it.prev()
        .filter(and(is(MethodInsnNode.class), opcode(INVOKESPECIAL)))
        .findFirst()
        .map(MethodInsnNode.class::cast)
        .ifPresent(node -> {
          Insn.ldcFor(node).stream()
              .map(e -> e.cst.toString())
              .findFirst()
              .ifPresent(path -> {
                it.prev()
                    .filter(and(is(MethodInsnNode.class), opcode(INVOKESPECIAL)))
                    .findFirst()
                    .map(MethodInsnNode.class::cast)
                    .ifPresent(n -> {
                      ClassNode classNode = loadClass(n.owner);
                      classNode.methods.stream()
                          .map(MethodNode.class::cast)
                          .filter(methodName("run"))
                          .findFirst()
                          .ifPresent(runMethod -> {
                            kotlinLambda(loader, owner, (MethodNode) runMethod)
                                .stream()
                                .filter(Lambda.class::isInstance)
                                .map(Lambda.class::cast)
                                .map(lambda -> lambda.prefix(path))
                                .forEach(result::add);
                          });
                    });
              });
        });
    return result;
  }

  private List<RouteParameter> params(final ClassLoader loader, final ClassNode owner,
      final String pattern, final MethodNode lambda) {
    List<RouteParameter> result = new ArrayList<>();

    new Insns(lambda)
        .on(param(loader), it -> {
          String name = parameterName(loader, it.method, it.node)
              .orElse("arg" + result.size());

          AbstractInsnNode next = it.node.getNext();
          Object value = paramValue(loader, owner, lambda, next);
          if (value != next) {
            // there is a default value, move next
            next = next.getNext();
          } else {
            value = null;
          }

          java.lang.reflect.Type parameterType = parameterType(loader, next);
          // boolean are ICONST_0 or ICONST_1
          if (boolean.class.equals(parameterType) && Integer.class.isInstance(value)) {
            value = (((Integer) value)).intValue() == 1;
          }
          result.add(new RouteParameter(name, kind(pattern, it.node.name, name),
              simplifyType(parameterType), value));
        }).forEach();

    return result;
  }

  private RouteParameter.Kind kind(final String pattern, final String method, final String name) {
    if (method.equals("header")) {
      return RouteParameter.Kind.HEADER;
    }
    if (method.equals("param") || method.equals("params")) {
      return isPathParam(pattern, name) ? RouteParameter.Kind.PATH : RouteParameter.Kind.QUERY;
    }
    if (method.equals("form")) {
      return RouteParameter.Kind.FORM;
    }
    if (method.equals("file") || method.equals("files")) {
      return RouteParameter.Kind.FILE;
    }
    return RouteParameter.Kind.BODY;
  }

  private static boolean isPathParam(final String pattern, final String name) {
    if (pattern.contains("{" + name + "}") || pattern.contains(":" + name)) {
      return true;
    }
    return false;
  }

  private Object paramValue(final ClassLoader loader, final ClassNode owner,
      final MethodNode method, final AbstractInsnNode n) {
    if (n instanceof LdcInsnNode) {

      Object cst = ((LdcInsnNode) n).cst;
      if (cst instanceof Type) {
        boolean typePresent = new Insn<>(method, n)
            .next()
            .filter(is(MethodInsnNode.class))
            .findFirst()
            .map(MethodInsnNode.class::cast)
            .filter(mutantToSomething().or(getOrCreateKotlinClass()))
            .isPresent();
        if (typePresent) {
          return null;
        }
        return loadType(loader, ((Type) cst).getClassName());
      }
      return cst;
    } else if (n instanceof InsnNode) {
      InsnNode insn = (InsnNode) n;
      switch (insn.getOpcode()) {
        case Opcodes.ICONST_0:
          return 0;
        case Opcodes.ICONST_1:
          return 1;
        case Opcodes.ICONST_2:
          return 2;
        case Opcodes.ICONST_3:
          return 3;
        case Opcodes.ICONST_4:
          return 4;
        case Opcodes.ICONST_5:
          return 5;
        case Opcodes.LCONST_0:
          return 0L;
        case Opcodes.LCONST_1:
          return 1L;
        case Opcodes.FCONST_0:
          return 0f;
        case Opcodes.FCONST_1:
          return 1f;
        case Opcodes.FCONST_2:
          return 2f;
        case Opcodes.DCONST_0:
          return 0d;
        case Opcodes.DCONST_1:
          return 1d;
        case Opcodes.ICONST_M1:
          return -1;
        case Opcodes.ACONST_NULL:
          return null;
      }
    } else if (n instanceof IntInsnNode) {
      IntInsnNode intinsn = (IntInsnNode) n;
      return intinsn.operand;
    } else if (n instanceof FieldInsnNode) {
      // toEnum
      FieldInsnNode finsn = (FieldInsnNode) n;
      if (finsn.getOpcode() == GETSTATIC) {
        java.lang.reflect.Type possibleEnum = loadType(loader, finsn.owner);
        if (MoreTypes.getRawType(possibleEnum).isEnum()) {
          return finsn.name;
        }
      }
    }
    return n;
  }

  private java.lang.reflect.Type parameterType(final ClassLoader loader, final AbstractInsnNode n) {
    if (n instanceof MethodInsnNode) {
      MethodInsnNode node = (MethodInsnNode) n;
      if (mutantValue().test(node)) {
        /** value(); intValue(); booleanValue(); */
        return TypeDescriptorParser.parse(loader, node.desc);
      } else if (mutantToSomething().test(node)
          || getOrCreateKotlinClass().test(node)) {
        /** to(String.class); toOptional; toList(); */
        String owner = Type.getReturnType(node.desc).getClassName();
        AbstractInsnNode prev = node.getPrevious();
        if (prev instanceof FieldInsnNode) {
          /** toEnum(Letter.A); */
          return loadType(loader, ((FieldInsnNode) prev).owner);
        }
        java.lang.reflect.Type toType = String.class;
        if (prev instanceof LdcInsnNode) {
          /** to(Foo.class); */
          Object cst = ((LdcInsnNode) prev).cst;
          if (cst instanceof Type) {
            toType = loadType(loader, ((Type) cst).getClassName());
          }
        }

        Set<String> skipOwners = ImmutableSet.of(
            Object.class.getName(),
            Enum.class.getName(),
            "kotlin.reflect.KClass");
        if (skipOwners.contains(owner)) {
          return toType;
        }

        /** toList(Foo.class); */
        return Types.newParameterizedType(loadType(loader, owner), toType);
      }
    } else if (n instanceof VarInsnNode) {
      return new Insn<>(null, n)
          .prev()
          .filter(is(MethodInsnNode.class))
          .findFirst()
          .map(MethodInsnNode.class::cast)
          .filter(file())
          .map(m -> {
            return m.name.equals("files")
                ? Types.newParameterizedType(List.class, File.class)
                : File.class;
          }).orElse(Object.class);
    } else if (n instanceof TypeInsnNode) {
      TypeInsnNode typeInsn = (TypeInsnNode) n;
      if (typeInsn.getOpcode() == Opcodes.CHECKCAST) {
        return loadType(loader, typeInsn.desc);
      }
    } else if (n != null && Opcodes.DUP == n.getOpcode()) {
      // Kotlin 1.2.x
      // mv.visitInsn(DUP);
      // mv.visitLdcInsn("req.param(\"p1\")");
      // mv.visitMethodInsn(INVOKESTATIC, "kotlin/jvm/internal/Intrinsics", "checkExpressionValueIsNotNull", "(Ljava/lang/Object;Ljava/lang/String;)V", false);
      // mv.visitMethodInsn(INVOKESTATIC, "org/jooby/JoobyKt", "getValue", "(Lorg/jooby/Mutant;)Ljava/lang/String;", false);
      AbstractInsnNode next = new Insn<>(null, n)
          .next()
          .filter(MethodInsnNode.class::isInstance)
          .skip(1)
          .findFirst()
          .orElse(null);
      java.lang.reflect.Type result = parameterType(loader, next);
      if (result == Object.class) {
        next = new Insn<>(null, n)
            .next()
            .filter(TypeInsnNode.class::isInstance)
            .findFirst()
            .orElse(null);
        result = parameterType(loader, next);
      }
      return result;
    }
    return Object.class;
  }

  private Optional<String> parameterName(final ClassLoader loader, final MethodNode method,
      final MethodInsnNode node) {
    if (node.name.equals("body") || node.name.equals("form")) {
      return Optional.of(node.name);
    }

    /** Collect all ldc between a previous method invocation and current invocation: */
    List<LdcInsnNode> ldc = Insn.ldcFor(node);
    return ldc.size() > 0 ? Optional.of((String) ldc.get(0).cst) : Optional.empty();
  }

  @SuppressWarnings("unchecked")
  private java.lang.reflect.Type returnType(final ClassLoader loader,
      final MethodNode m) throws Exception {
    return new Insns(m)
        .last()
        .prev()
        .filter(and(is(InsnNode.class), opcode(Opcodes.ARETURN)))
        .findFirst()
        .map(AbstractInsnNode::getPrevious)
        .map(previous -> {
          /** return 1; return true; return new Foo(); */
          if (previous instanceof MethodInsnNode) {
            MethodInsnNode minnsn = ((MethodInsnNode) previous);
            if (minnsn.name.equals("<init>")) {
              return loadType(loader, minnsn.owner);
            }
            String desc = minnsn.desc;
            java.lang.reflect.Type type = TypeDescriptorParser.parse(loader, desc);
            if (type.getTypeName().equals(Result.class.getName())) {
              return new TypeWithStatus(type, statusCodeFor(minnsn));
            }
            return type;
          }
          /** return "String" | int | double */
          if (previous instanceof LdcInsnNode) {
            Object cst = ((LdcInsnNode) previous).cst;
            if (cst instanceof Type) {
              return TypeDescriptorParser.parse(loader, ((Type) cst).getDescriptor());
            }
            return cst.getClass();
          }
          /** return variable */
          if (previous instanceof VarInsnNode) {
            VarInsnNode varInsn = (VarInsnNode) previous;
            return localVariable(loader, m, varInsn);
          }

          return Object.class;
        }).orElse(Object.class);
  }

  private Integer statusCodeFor(MethodInsnNode node) {
    if (node.name.equals("noContent")) {
      return 204;
    }
    if (node.name.equals("with")) {
      AbstractInsnNode previous = node.getPrevious();
      if (previous instanceof IntInsnNode) {
        return ((IntInsnNode) previous).operand;
      }
      if (previous instanceof FieldInsnNode) {
        String owner = ((FieldInsnNode) previous).owner.replace("/", ".");
        if (owner.equals(Status.class.getName())) {
          String statusName = ((FieldInsnNode) previous).name;
          return Try.apply(() -> Status.class.getDeclaredField(statusName).get(null))
              .map(it -> ((Status) it).value())
              .orElse(null);
        }
      }
    }
    return null;
  }

  @SuppressWarnings("unchecked")
  static java.lang.reflect.Type localVariable(final ClassLoader loader, final MethodNode m,
      final VarInsnNode varInsn) {
    if (varInsn.getOpcode() == Opcodes.ALOAD) {
      List<LocalVariableNode> vars = m.localVariables;
      LocalVariableNode var = vars.stream()
          .filter(v -> v.index == varInsn.var)
          .findFirst()
          .orElse(null);
      if (var != null) {
        String signature = "()" + Optional.ofNullable(var.signature).orElse(var.desc);
        return TypeDescriptorParser.parse(loader, signature);
      }
    }
    return Object.class;
  }

  static Class loadType(final ClassLoader loader, final String name) {
    return when(name)
        .<Class>is("boolean", boolean.class)
        .is("char", char.class)
        .is("byte", byte.class)
        .is("short", short.class)
        .is("int", int.class)
        .is("long", long.class)
        .is("float", float.class)
        .is("double", double.class)
        .orElseGet(() -> loader.loadClass(name.replace("/", ".")));
  }

  static java.lang.reflect.Type simplifyType(final java.lang.reflect.Type type) {
    Class<?> rawType = MoreTypes.getRawType(type);
    if (Primitives.isWrapperType(rawType)) {
      return Primitives.unwrap(rawType);
    }
    return type;
  }

  static Writer writer(final Logger log, final String owner) {
    return new Writer() {
      StringBuilder buff = new StringBuilder();

      @Override
      public void write(final char[] cbuf, final int off, final int len) throws IOException {
        buff.append(cbuf, off, len);
      }

      @Override
      public void flush() throws IOException {
        log.info("{}:\n{}", owner, buff);
      }

      @Override
      public void close() throws IOException {
      }
    };
  }

  private ClassNode loadClass(String name) {
    return Try.apply(() -> {
      String cname = name.replace("/", ".");
      ClassNode node = cache.get(cname);
      if (node == null) {
        String rname = cname.replace(".", "/") + ".class";
        try (InputStream in = loader.getResourceAsStream(rname)) {
          if (in == null) {
            throw new FileNotFoundException(rname + " using " + loader);
          }
          ClassReader reader = new ClassReader(ByteStreams.toByteArray(in));
          node = new ClassNode();
          reader.accept(node, 0);
          cache.put(cname, node);
          if (log.isDebugEnabled()) {
            log.info("Source: {}; Class: {}", node.sourceFile, node.name);
            reader.accept(
                new TraceClassVisitor(null, new ASMifier(), new PrintWriter(writer(log, name))),
                0);
          }
        }
      }
      return node;
    }).get();
  }

  private static RouteMethod toRouteMethod(final Route.Definition route) {
    Method handler = ((Route.MethodHandler) route.filter()).method();
    return new RouteMethod(route.method(), route.pattern(),
        new RouteResponse(handler.getGenericReturnType()))
        .name(route.name())
        .parameters(Arrays.asList(handler.getParameters()).stream()
            .filter(SKIP)
            .map(it -> BytecodeRouteParser.toRouteParameter(route.pattern(), it))
            .collect(Collectors.toList()));
  }

  private static RouteParameter toRouteParameter(String pattern, final Parameter p) {
    Annotation[] annotations = p.getAnnotations();
    Supplier<String> name = () -> {
      for (int i = 0; i < annotations.length; i++) {
        Class<? extends Annotation> annotationType = annotations[i].annotationType();
        if (annotationType == Named.class) {
          return ((Named) annotations[i]).value();
        }
        if (annotationType == Header.class) {
          return ((Header) annotations[i]).value();
        }
      }
      return p.getName();
    };

    String pname = name.get();
    Supplier<RouteParameter.Kind> kind = () -> {
      if (p.getType() == Upload.class) {
        return RouteParameter.Kind.FILE;
      }
      for (int i = 0; i < annotations.length; i++) {
        Class<? extends Annotation> annotationType = annotations[i].annotationType();
        if (annotationType == Header.class) {
          return RouteParameter.Kind.HEADER;
        } else if (annotationType == Body.class) {
          return RouteParameter.Kind.BODY;
        }
      }
      boolean hasBody = Arrays.asList(p.getDeclaringExecutable().getParameters()).stream()
          .filter(it -> Stream.of(it.getAnnotations())
              .filter(e -> e.annotationType() == Body.class)
              .findFirst()
              .isPresent())
          .findFirst()
          .isPresent();
      boolean formLike = !hasBody && p.getDeclaringExecutable().getAnnotation(POST.class) != null;
      if (formLike) {
        return RouteParameter.Kind.FORM;
      }
      return isPathParam(pattern, pname) ? RouteParameter.Kind.PATH : RouteParameter.Kind.QUERY;
    };
    return new RouteParameter(pname, kind.get(), p.getParameterizedType(), null);
  }

  private static class TypeWithStatus implements java.lang.reflect.Type {
    private final java.lang.reflect.Type forwarding;
    final Integer status;

    TypeWithStatus(java.lang.reflect.Type forwarding, Integer status) {
      this.forwarding = forwarding;
      this.status = status;
    }

    public java.lang.reflect.Type type() {
      if (status != null && status == 204 && forwarding.getTypeName()
          .equals(Result.class.getName())) {
        return void.class;
      }
      return forwarding;
    }

    @Override public String getTypeName() {
      return forwarding.getTypeName();
    }
  }

}
