package com.github.kklisura.cdt.services.invocation;

/*-
 * #%L
 * cdt-java-client
 * %%
 * Copyright (C) 2018 Kenan Klisura
 * %%
 * 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.
 * #L%
 */

import com.github.kklisura.cdt.protocol.support.annotations.EventName;
import com.github.kklisura.cdt.protocol.support.annotations.ParamName;
import com.github.kklisura.cdt.protocol.support.annotations.ReturnTypeParameter;
import com.github.kklisura.cdt.protocol.support.annotations.Returns;
import com.github.kklisura.cdt.protocol.support.types.EventHandler;
import com.github.kklisura.cdt.protocol.support.types.EventListener;
import com.github.kklisura.cdt.services.ChromeDevToolsService;
import com.github.kklisura.cdt.services.types.MethodInvocation;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Parameter;
import java.lang.reflect.ParameterizedType;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.atomic.AtomicLong;

/**
 * Command invocation handler intercepts command requests and invokes the command using
 * DevToolsService.
 *
 * @author Kenan Klisura
 */
public class CommandInvocationHandler implements InvocationHandler {
  private static final String EVENT_LISTENER_PREFIX = "on";

  private static final AtomicLong ID_SUPPLIER = new AtomicLong(1L);

  private ChromeDevToolsService chromeDevToolsService;

  /**
   * Sets dev tools service.
   *
   * @param chromeDevToolsService Chrome dev tools service.
   */
  public void setChromeDevToolsService(ChromeDevToolsService chromeDevToolsService) {
    this.chromeDevToolsService = chromeDevToolsService;
  }

  @Override
  public Object invoke(Object unused, Method method, Object[] args) throws Throwable {
    if (isEventSubscription(method)) {
      String domainName = method.getDeclaringClass().getSimpleName();
      String eventName = getEventName(method);

      Class<?> eventHandlerType = getEventHandlerType(method);
      return chromeDevToolsService.addEventListener(
          domainName, eventName, (EventHandler) args[0], eventHandlerType);
    }

    Class<?> returnType = method.getReturnType();

    Class<?>[] returnTypeClasses = null;
    ReturnTypeParameter returnTypeParameter = method.getAnnotation(ReturnTypeParameter.class);
    if (returnTypeParameter != null) {
      returnTypeClasses = returnTypeParameter.value();
    }

    String returnProperty = null;
    Returns returnsAnnotation = method.getAnnotation(Returns.class);
    if (returnsAnnotation != null) {
      returnProperty = returnsAnnotation.value();
    }

    MethodInvocation methodInvocation = createMethodInvocation(method, args);
    return chromeDevToolsService.invoke(
        returnProperty, returnType, returnTypeClasses, methodInvocation);
  }

  /**
   * Creates the method invocation object given a method and its args.
   *
   * @param method Method.
   * @param args Method args.
   * @return Chrome method invocation object.
   */
  private MethodInvocation createMethodInvocation(Method method, Object[] args) {
    String domainName = method.getDeclaringClass().getSimpleName();
    String methodName = method.getName();

    MethodInvocation methodInvocation = new MethodInvocation();
    methodInvocation.setId(ID_SUPPLIER.getAndIncrement());
    methodInvocation.setMethod(domainName + "." + methodName);
    methodInvocation.setParams(buildMethodParams(method, args));

    return methodInvocation;
  }

  /**
   * Builds method params given a method and its args.
   *
   * @param method Method.
   * @param args Method args.
   * @return Map of params.
   */
  private Map<String, Object> buildMethodParams(Method method, Object[] args) {
    Map<String, Object> params = new HashMap<>();
    Parameter[] parameters = method.getParameters();

    if (args != null) {
      for (int i = 0; i < args.length; i++) {
        params.put(parameters[i].getAnnotation(ParamName.class).value(), args[i]);
      }
    }

    return params;
  }

  /**
   * Returns event name given an event subscription method.
   *
   * @param method Method.
   * @return Event name.
   */
  private static String getEventName(Method method) {
    return method.getAnnotation(EventName.class).value();
  }

  private static Class<?> getEventHandlerType(Method method) {
    return (Class<?>)
        ((ParameterizedType) method.getGenericParameterTypes()[0]).getActualTypeArguments()[0];
  }

  /**
   * Checks if given method has signature of event subscription.
   *
   * @param method Method to check.
   * @return True if this is event subscription method that is: EventListener on*(EventHandler)
   */
  public static boolean isEventSubscription(Method method) {
    String name = method.getName();
    Parameter[] parameters = method.getParameters();

    return name.startsWith(EVENT_LISTENER_PREFIX)
        && EventListener.class.equals(method.getReturnType())
        && (parameters != null
            && parameters.length == 1
            && EventHandler.class.isAssignableFrom(parameters[0].getType()));
  }
}