package io.vertx.codegen;

import io.vertx.codegen.type.ClassKind;
import io.vertx.codegen.type.DataObjectInfo;
import io.vertx.codegen.type.ParameterizedTypeInfo;
import io.vertx.codegen.type.TypeInfo;
import io.vertx.codegen.type.TypeVariableInfo;

import javax.lang.model.element.Element;
import javax.lang.model.element.ExecutableElement;
import javax.lang.model.element.TypeParameterElement;
import javax.lang.model.element.VariableElement;
import javax.lang.model.type.TypeMirror;
import java.util.List;
import java.util.Map;
import java.util.Set;

/**
 * Validates codegen types.
 */
class TypeValidator {

  static void validateParamType(ExecutableElement elem, TypeInfo typeInfo, boolean allowAnyJavaType) {
    if (isValidNonCallableType(elem, typeInfo, true, false, true, allowAnyJavaType)) {
      return;
    }
    if (isValidClassTypeParam(elem, typeInfo)) {
      return;
    }
    if (isValidHandlerType(elem, typeInfo, allowAnyJavaType)) {
      return;
    }
    if (isValidFunctionType(elem, typeInfo, allowAnyJavaType)) {
      return;
    }
    throw new GenException(elem, "type " + typeInfo + " is not legal for use for a parameter in code generation");
  }

  static void validateReturnType(ExecutableElement elem, TypeInfo type, boolean allowAnyJavaType) {
    if (type.isVoid()) {
      return;
    }
    if (isValidNonCallableType(elem, type, false, true, true, allowAnyJavaType)) {
      return;
    }
    if (isValidHandlerType(elem, type, allowAnyJavaType)) {
      return;
    }
    throw new GenException(elem, "type " + type + " is not legal for use for a return type in code generation");
  }

  static void validateConstantType(VariableElement elem, TypeInfo type, TypeMirror typeMirror, boolean allowAnyJavaType) {
    if (isValidNonCallableType(elem, type, false, true, true, allowAnyJavaType)) {
      return;
    }
    throw new GenException(elem, "type " + type + " is not legal for use for a constant type in code generation");
  }

  private static boolean isValidNonCallableType(Element elem, TypeInfo type, boolean isParam, boolean isReturn, boolean allowParameterized, boolean allowAnyJavaType) {
    if (type.isDataObjectHolder()) {
      DataObjectInfo dataObject = type.getDataObject();
      if (isParam && !dataObject.isDeserializable()) {
        if (dataObject.isAnnotated()) {
          throw new GenException(elem, "annotated @DataObject " + type + " requires a JSON constructor");
        } else {
          throw new GenException(elem, "type " + type + " requires a JSON deserializer method");
        }
      }
      if (isReturn && !dataObject.isSerializable()) {
        if (dataObject.isAnnotated()) {
          throw new GenException(elem, "annotated @DataObject " + type + " requires a toJson() method");
        } else {
          throw new GenException(elem, "type " + type + " requires a JSON serializer method");
        }
      }
      return true;
    }
    if (type.getKind() == ClassKind.VOID) {
      return true;
    }
    if (type.getKind().basic) {
      return true;
    }
    if (type.getKind().json) {
      return true;
    }
    if (isValidEnum(type)) {
      return true;
    }
    if (type.getKind() == ClassKind.THROWABLE) {
      return true;
    }
    if (type.isVariable()) {
      return true;
    }
    if (type.getKind() == ClassKind.OBJECT) {
      return true;
    }
    if (isValidFutureType(elem, type, allowAnyJavaType)) {
      return true;
    }
    if (isValidVertxGenInterface(elem, type, allowParameterized, allowAnyJavaType)) {
      return true;
    }
    if (isValidOtherType(type, allowAnyJavaType)) {
      return true;
    }
    if (allowParameterized && isValidContainer(elem, type, allowAnyJavaType)) {
      return true;
    }
    return false;
  }

  private static boolean isValidEnum(TypeInfo info) {
    return info.getKind() == ClassKind.ENUM;
  }

  private static boolean isValidClassTypeParam(ExecutableElement elem, TypeInfo type) {
    if (type.getKind() == ClassKind.CLASS_TYPE && type.isParameterized()) {
      ParameterizedTypeInfo parameterized = (ParameterizedTypeInfo) type;
      TypeInfo arg = parameterized.getArg(0);
      if (arg.isVariable()) {
        TypeVariableInfo variable = (TypeVariableInfo) arg;
        for (TypeParameterElement typeParamElt : elem.getTypeParameters()) {
          if (typeParamElt.getSimpleName().toString().equals(variable.getName())) {
            return true;
          }
        }
      }
    }
    return false;
  }

  private static boolean isValidContainer(Element elem, TypeInfo type, boolean allowAnyJavaType) {
    TypeInfo argument = null;
    if (rawTypeIs(type, List.class, Set.class, Map.class)) {
      ParameterizedTypeInfo parameterizedType = (ParameterizedTypeInfo) type;
      if (type.getKind() != ClassKind.MAP) {
        argument = parameterizedType.getArgs().get(0);
      } else if (parameterizedType.getArgs().get(0).getKind() == ClassKind.STRING) { // Only allow Map's with String's for keys
        argument= parameterizedType.getArgs().get(1);
      }
    }
    return argument != null && isValidContainerComponent(elem, argument, allowAnyJavaType);
  }

  private static boolean isValidContainerComponent(Element elem, TypeInfo arg, boolean allowAnyJavaType) {
    return isValidNonCallableType(elem, arg, true, true, false, allowAnyJavaType);
  }

  private static boolean isValidVertxGenTypeArgument(Element elem, TypeInfo arg, boolean allowAnyJavaType) {
    return isValidNonCallableType(elem, arg, false, false, true, allowAnyJavaType);
  }

  private static boolean isValidOtherType(TypeInfo type, boolean allowAnyJavaType) {
    return allowAnyJavaType && type.getKind() == ClassKind.OTHER;
  }

  private static boolean isValidVertxGenInterface(Element elem, TypeInfo type, boolean allowParameterized, boolean allowAnyJavaType) {
    if (type.getKind() == ClassKind.API) {
      if (type.isParameterized()) {
        ParameterizedTypeInfo parameterized = (ParameterizedTypeInfo) type;
        return allowParameterized &&
          parameterized
            .getArgs()
            .stream()
            .noneMatch(arg -> !isValidVertxGenTypeArgument(elem, arg, allowAnyJavaType) || arg.isNullable());
      } else {
        return true;
      }
    }
    return false;
  }

  private static boolean isValidFutureType(Element elem, TypeInfo type, boolean allowAnyJavaType) {
    if (type.getKind() == ClassKind.FUTURE) {
      ParameterizedTypeInfo parameterized = (ParameterizedTypeInfo) type;
      return parameterized
          .getArgs()
          .stream()
          .allMatch(arg -> isValidCallbackValueType(elem, arg, allowAnyJavaType));
    }
    return false;
  }

  private static boolean isValidFunctionType(Element elem, TypeInfo typeInfo, boolean allowAnyJavaType) {
    if (typeInfo.getErased().getKind() == ClassKind.FUNCTION) {
      TypeInfo paramType = ((ParameterizedTypeInfo) typeInfo).getArgs().get(0);
      if (isValidCallbackValueType(elem, paramType, allowAnyJavaType)) {
        TypeInfo returnType = ((ParameterizedTypeInfo) typeInfo).getArgs().get(1);
        return isValidNonCallableType(elem, returnType, true, false, true, allowAnyJavaType);
      }
    }
    return false;
  }

  private static boolean isValidHandlerType(Element elem, TypeInfo type, boolean allowAnyJavaType) {
    if (type.getErased().getKind() == ClassKind.HANDLER) {
      TypeInfo eventType = ((ParameterizedTypeInfo) type).getArgs().get(0);
      if (isValidCallbackValueType(elem, eventType, allowAnyJavaType)) {
        return true;
      } else if (eventType.getErased().getKind() == ClassKind.ASYNC_RESULT && !eventType.isNullable()) {
        TypeInfo resultType = ((ParameterizedTypeInfo) eventType).getArgs().get(0);
        if (isValidCallbackValueType(elem, resultType, allowAnyJavaType)) {
          return true;
        }
      }
    }
    return false;
  }

  private static boolean isValidCallbackValueType(Element elem, TypeInfo type, boolean allowAnyJavaType) {
    return isValidNonCallableType(elem, type, false, true, true, allowAnyJavaType);
  }

  private static boolean rawTypeIs(TypeInfo type, Class<?>... classes) {
    if (type instanceof ParameterizedTypeInfo) {
      String rawClassName = type.getRaw().getName();
      for (Class<?> c : classes) {
        if (rawClassName.equals(c.getName())) {
          return true;
        }
      }
    }
    return false;
  }
}
