// Copyright (c) 2011 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

package org.chromium.sdk.internal.protocolparser.dynamicimpl;

import java.lang.reflect.InvocationHandler;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
import java.lang.reflect.Type;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;

import org.chromium.sdk.internal.protocolparser.JsonParseMethod;
import org.chromium.sdk.internal.protocolparser.JsonParserRoot;
import org.chromium.sdk.internal.protocolparser.JsonProtocolModelParseException;
import org.chromium.sdk.internal.protocolparser.JsonProtocolParseException;
import org.chromium.sdk.internal.protocolparser.dynamicimpl.JavaCodeGenerator.ClassScope;

import static org.chromium.sdk.util.BasicUtil.*;
import org.json.simple.JSONObject;

/**
 * Dynamic implementation of user 'root' interface to parser.
 * @param <R> 'root' interface type
 * @see JsonParserRoot
 */
class ParserRootImpl<R> {
  private final Class<R> rootClass;
  private final InvocationHandlerImpl invocationHandler;
  private final R instance;

  ParserRootImpl(Class<R> rootClass, Map<Class<?>, TypeHandler<?>> type2TypeHandler)
      throws JsonProtocolModelParseException {
    this.rootClass = rootClass;
    ParseInterfaceSession session = new ParseInterfaceSession(type2TypeHandler);
    session.run(rootClass);
    this.invocationHandler = session.createInvocationHandler();
    Object result = Proxy.newProxyInstance(rootClass.getClassLoader(),
        new Class<?>[] { rootClass }, invocationHandler);
    this.instance = (R) result;
  }

  R getInstance() {
    return instance;
  }

  private static class ParseInterfaceSession {
    private final Map<Class<?>, TypeHandler<?>> type2TypeHandler;
    private final Set<Class<?>> visitedInterfaces = new HashSet<Class<?>>(1);
    private final Map<Method, MethodDelegate> methodMap = new HashMap<Method, MethodDelegate>();

    ParseInterfaceSession(Map<Class<?>, TypeHandler<?>> type2TypeHandler) {
      this.type2TypeHandler = type2TypeHandler;
    }

    void run(Class<?> clazz) throws JsonProtocolModelParseException {
      parseInterfaceRecursive(clazz);
      for (Method method : BaseHandlersLibrary.OBJECT_METHODS) {
        methodMap.put(method, new SelfCallDelegate(method));
      }
    }

    private void parseInterfaceRecursive(Class<?> clazz) throws JsonProtocolModelParseException {
      if (containsSafe(visitedInterfaces, clazz)) {
        return;
      }
      visitedInterfaces.add(clazz);
      if (!clazz.isInterface()) {
        throw new JsonProtocolModelParseException(
            "Parser root type must be an interface: " + clazz);
      }
      JsonParserRoot jsonParserRoot = clazz.getAnnotation(JsonParserRoot.class);
      if (jsonParserRoot == null) {
        throw new JsonProtocolModelParseException(
            JsonParserRoot.class.getCanonicalName() + " annotation is expected in " + clazz);
      }
      for (Method m : clazz.getMethods()) {
        JsonParseMethod jsonParseMethod = m.getAnnotation(JsonParseMethod.class);
        if (jsonParseMethod == null) {
          throw new JsonProtocolModelParseException(
              JsonParseMethod.class.getCanonicalName() + " annotation is expected in " + clazz);
        }

        Class<?>[] exceptionTypes = m.getExceptionTypes();
        if (exceptionTypes.length > 1) {
          throw new JsonProtocolModelParseException("Too many exception declared in " + m);
        }
        if (exceptionTypes.length < 1 || exceptionTypes[0] != JsonProtocolParseException.class) {
          throw new JsonProtocolModelParseException(
              JsonProtocolParseException.class.getCanonicalName() +
              " exception must be declared in " + m);
        }

        Type returnType = m.getGenericReturnType();
        TypeHandler<?> typeHandler = type2TypeHandler.get(returnType);
        if (typeHandler == null) {
          throw new JsonProtocolModelParseException("Unknown return type in " + m);
        }

        Type[] arguments = m.getGenericParameterTypes();
        if (arguments.length != 1) {
          throw new JsonProtocolModelParseException("Exactly one argument is expected in " + m);
        }
        Type argument = arguments[0];
        MethodDelegate delegate;
        if (argument == JSONObject.class) {
          delegate = new ParseDelegate(typeHandler);
        } else if (argument == Object.class) {
          delegate = new ParseDelegate(typeHandler);
        } else {
          throw new JsonProtocolModelParseException("Unrecognized argument type in " + m);
        }
        methodMap.put(m, delegate);
      }

      for (Type baseType : clazz.getGenericInterfaces()) {
        if (baseType instanceof Class == false) {
          throw new JsonProtocolModelParseException("Base interface must be class in " + clazz);
        }
        Class<?> baseClass = (Class<?>) baseType;
        parseInterfaceRecursive(baseClass);
      }
    }

    InvocationHandlerImpl createInvocationHandler() {
      return new InvocationHandlerImpl(methodMap);
    }
  }

  private static class InvocationHandlerImpl implements InvocationHandler {
    private final Map<Method, MethodDelegate> map;

    InvocationHandlerImpl(Map<Method, MethodDelegate> map) {
      this.map = map;
    }

    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
      return getSafe(map, method).invoke(proxy, this, args);
    }

    public void writeStaticMethodJava(ClassScope scope) {
      for (Map.Entry<Method, MethodDelegate> en : map.entrySet()) {
        en.getValue().writeStaticMethodJava(scope, en.getKey());
      }
    }
  }

  private static abstract class MethodDelegate {
    abstract Object invoke(Object proxy, InvocationHandlerImpl invocationHandlerImpl,
        Object[] args) throws Throwable;

    abstract void writeStaticMethodJava(ClassScope scope, Method key);
  }

  private static class ParseDelegate extends MethodDelegate {
    private final TypeHandler<?> typeHandler;

    ParseDelegate(TypeHandler<?> typeHandler) {
      this.typeHandler = typeHandler;
    }

    @Override
    Object invoke(Object proxy, InvocationHandlerImpl invocationHandlerImpl, Object[] args)
        throws JsonProtocolParseException {
      Object obj = args[0];
      return typeHandler.parseRoot(obj);
    }

    @Override
    void writeStaticMethodJava(ClassScope scope, Method method) {
      MethodHandler.writeMethodDeclarationJava(scope, method, STATIC_METHOD_PARAM_NAME_LIST);
      scope.append(JavaCodeGenerator.Util.THROWS_CLAUSE + " {\n");
      scope.indentRight();

      scope.startLine("return " + scope.getTypeImplReference(typeHandler) + ".parse(" +
          STATIC_METHOD_PARAM_NAME + ");\n");
      scope.indentLeft();
      scope.startLine("}\n");
      scope.append("\n");
    }

    private static final String STATIC_METHOD_PARAM_NAME = "obj";

    private static final List<String> STATIC_METHOD_PARAM_NAME_LIST =
        Collections.singletonList(STATIC_METHOD_PARAM_NAME);
  }

  private static class SelfCallDelegate extends MethodDelegate {
    private final Method method;

    SelfCallDelegate(Method method) {
      this.method = method;
    }

    @Override
    Object invoke(Object proxy, InvocationHandlerImpl invocationHandlerImpl, Object[] args)
        throws Throwable {
      try {
        return method.invoke(invocationHandlerImpl, args);
      } catch (IllegalAccessException e) {
        throw new RuntimeException(e);
      } catch (InvocationTargetException e) {
        throw new RuntimeException(e);
      }
    }

    @Override
    void writeStaticMethodJava(ClassScope scope, Method method) {
    }
  }

  public Class<R> getType() {
    return rootClass;
  }

  public void writeStaticMethodJava(ClassScope rootClassScope) {
    invocationHandler.writeStaticMethodJava(rootClassScope);
  }
}
