/*
 * Copyright (C) 2011 Red Hat, Inc. and/or its affiliates.
 *
 * 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.jboss.errai.codegen.meta;

import java.io.File;
import java.lang.reflect.Array;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.lang.reflect.Type;
import java.net.URL;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.Set;
import java.util.stream.Collectors;

import javax.enterprise.util.TypeLiteral;

import org.jboss.errai.codegen.BlockStatement;
import org.jboss.errai.codegen.Context;
import org.jboss.errai.codegen.DefParameters;
import org.jboss.errai.codegen.Parameter;
import org.jboss.errai.codegen.Statement;
import org.jboss.errai.codegen.ThrowsDeclaration;
import org.jboss.errai.codegen.builder.callstack.LoadClassReference;
import org.jboss.errai.codegen.meta.impl.build.BuildMetaClass;
import org.jboss.errai.codegen.meta.impl.build.BuildMetaConstructor;
import org.jboss.errai.codegen.meta.impl.build.BuildMetaField;
import org.jboss.errai.codegen.meta.impl.build.BuildMetaMethod;
import org.jboss.errai.codegen.meta.impl.build.BuildMetaParameterizedType;
import org.jboss.errai.codegen.meta.impl.build.ShadowBuildMetaField;
import org.jboss.errai.codegen.meta.impl.build.ShadowBuildMetaMethod;
import org.jboss.errai.codegen.meta.impl.java.JavaReflectionClass;
import org.jboss.errai.codegen.util.ClassChangeUtil;
import org.jboss.errai.codegen.util.EmptyStatement;
import org.jboss.errai.codegen.util.GenUtil;
import org.jboss.errai.common.metadata.RebindUtils;
import org.jboss.errai.common.rebind.CacheUtil;
import org.mvel2.ConversionHandler;
import org.mvel2.DataConversion;

/**
 * @author Mike Brock <cbrock@redhat.com>
 */
public final class MetaClassFactory {
  static {
    DataConversion.addConversionHandler(Class.class, new ConversionHandler() {
      @Override
      public Object convertFrom(final Object in) {
        if (MetaClass.class.isAssignableFrom(in.getClass())) {
          return ((MetaClass) in).asClass();
        }
        else {
          throw new RuntimeException("cannot convert from " + in.getClass() + "; to " + Class.class.getName());
        }
      }

      @Override
      public boolean canConvertFrom(final Class cls) {
        return MetaType.class.isAssignableFrom(cls);
      }
    });

    DataConversion.addConversionHandler(Class[].class, new ConversionHandler() {
      @Override
      public Object convertFrom(final Object in) {
        final int length = Array.getLength(in);
        final Class[] cls = new Class[length];

        for (int i = 0; i < length; i++) {
          final Object el = Array.get(in, i);

          if (el instanceof MetaClass) {
            cls[i] = ((MetaClass) el).asClass();
          }
          else if (el != null && el.getClass().isArray()) {
            cls[i] = (Class) convertFrom(el);
          }
          else {
            cls[i] = null;
          }
        }
        return cls;
      }

      @Override
      public boolean canConvertFrom(Class cls) {
        while (cls.isArray()) {
          cls = cls.getComponentType();
        }
        return MetaClass.class.isAssignableFrom(cls) || MetaType.class.isAssignableFrom(cls);
      }
    });
  }

  private static MetaClassCache cache;

  public static MetaClassCache getMetaClassCache() {
    if (cache == null) {
      cache = CacheUtil.getCache(MetaClassCache.class);
    }
    return cache;
  }

  public static MetaClass get(final String fullyQualifiedClassName, final boolean erased) {
    return createOrGet(fullyQualifiedClassName, erased);
  }

  public static MetaClass get(final String fullyQualifiedClassName) {
    return createOrGet(fullyQualifiedClassName);
  }

  public static MetaClass get(final Class<?> clazz) {
    if (clazz == null)
      return null;
    return createOrGet(clazz, null);
  }

  public static MetaClass getArrayOf(final Class<?> clazz, final int dims) {
    final int[] da = new int[dims];
    for (int i = 0; i < da.length; i++) {
      da[i] = 0;
    }

    return getArrayOf(clazz, da);
  }

  public static MetaClass getArrayOf(final Class<?> clazz, int... dims) {
    if (dims.length == 0) {
      dims = new int[1];
    }
    return JavaReflectionClass.newInstance(Array.newInstance(clazz, dims).getClass());
  }

  public static MetaClass get(final Class<?> clazz, final Type type) {
    return createOrGet(clazz, type);
  }

  public static MetaClass get(final TypeLiteral<?> literal) {
    return createOrGet(literal);
  }

  public static MetaMethod get(final Method method) {
    return get(method.getDeclaringClass()).getDeclaredMethod(method.getName(), method.getParameterTypes());
  }

  public static MetaField get(final Field field) {
    return get(field.getDeclaringClass()).getDeclaredField(field.getName());
  }

  public static Statement getAsStatement(final Class<?> clazz) {
    return getAsStatement(createOrGet(clazz.getName(), false));
  }

  public static Statement getAsStatement(final MetaClass metaClass) {
    return new Statement() {
      @Override
      public String generate(final Context context) {
        return LoadClassReference.getClassReference(metaClass, context);
      }

      @Override
      public MetaClass getType() {
        return MetaClassFactory.get(Class.class);
      }
    };
  }

  public static boolean isCached(final String name) {
    return getMetaClassCache().isKnownErasedType(name);
  }

  private static MetaClass createOrGet(final String fullyQualifiedClassName) {
    if (!getMetaClassCache().isKnownErasedType(fullyQualifiedClassName)) {
      return createOrGet(fullyQualifiedClassName, false);
    }

    return getMetaClassCache().getErased(fullyQualifiedClassName);
  }

  private static MetaClass createOrGet(final TypeLiteral type) {
    if (type == null)
      return null;

    if (!getMetaClassCache().isKnownErasedType(type.toString())) {
      final MetaClass gwtClass = JavaReflectionClass.newUncachedInstance(type);

      addLookups(type, gwtClass);
      return gwtClass;
    }

    return getMetaClassCache().getErased(type.toString());
  }

  private static MetaClass createOrGet(final String clsName, final boolean erased) {
    if (clsName == null)
      return null;

    MetaClass mCls;
    if (erased) {
      mCls = getMetaClassCache().getErased(clsName);
      if (mCls == null) {
        getMetaClassCache().pushErasedCache(clsName, mCls = JavaReflectionClass.newUncachedInstance(loadClass(clsName), erased));
      }
    }
    else {
      mCls = getMetaClassCache().get(clsName);
      if (mCls == null) {
        getMetaClassCache().pushCache(clsName, mCls = JavaReflectionClass.newUncachedInstance(loadClass(clsName), erased));
      }
    }
    return mCls;
  }

  private static MetaClass createOrGet(final Class cls, final Type type) {
    if (cls == null)
      return null;

    if (type != null) {
      if (cls.getTypeParameters() != null) {
        return JavaReflectionClass.newUncachedInstance(cls, type);
      }

      if (!getMetaClassCache().isKnownErasedType(cls.getName())) {
        final MetaClass javaReflectionClass = JavaReflectionClass.newUncachedInstance(cls, type);
        addLookups(cls, javaReflectionClass);
        return javaReflectionClass;
      }

      return getMetaClassCache().getErased(cls.getName());
    }
    else {
      MetaClass mCls;
      mCls = getMetaClassCache().get(cls.getName());
      if (mCls == null) {
        getMetaClassCache().pushCache(cls.getName(), mCls = JavaReflectionClass.newUncachedInstance(cls));
      }
      return mCls;
    }
  }

  public static MetaClass parameterizedAs(final Class clazz, final MetaParameterizedType parameterizedType) {
    return parameterizedAs(MetaClassFactory.get(clazz), parameterizedType);
  }

  public static MetaClass parameterizedAs(final MetaClass clazz, final MetaParameterizedType parameterizedType) {
    return parameterizedAs(clazz, parameterizedType, true);
  }

  public static MetaClass parameterizedAs(final MetaClass clazz,
                                          final MetaParameterizedType parameterizedType,
                                          final boolean reifyRecursively) {
    return cloneToBuildMetaClass(clazz, parameterizedType, reifyRecursively);
  }

  private static BuildMetaClass cloneToBuildMetaClass(final MetaClass clazz) {
    return cloneToBuildMetaClass(clazz, null, false);
  }

  private static BuildMetaClass cloneToBuildMetaClass(final MetaClass clazz,
                                                      final MetaParameterizedType parameterizedType,
                                                      final boolean reifyRecursively) {
    final BuildMetaClass buildMetaClass = new BuildMetaClass(null, clazz.getFullyQualifiedName());

    buildMetaClass.setReifiedFormOf(clazz);
    buildMetaClass.setAbstract(clazz.isAbstract());
    buildMetaClass.setFinal(clazz.isFinal());
    buildMetaClass.setStatic(clazz.isStatic());
    buildMetaClass.setInterface(clazz.isInterface());
    addInterfaces(clazz, buildMetaClass, parameterizedType);
    buildMetaClass.setScope(GenUtil.scopeOf(clazz));
    buildMetaClass.setSuperClass(clazz.getSuperClass());

    for (final MetaTypeVariable typeVariable : clazz.getTypeParameters()) {
      buildMetaClass.addTypeVariable(typeVariable);
    }

    if (parameterizedType != null) {
      buildMetaClass.setParameterizedType(parameterizedType);
    }
    else {
      buildMetaClass.setParameterizedType(clazz.getParameterizedType());
    }

    for (final MetaField field : clazz.getDeclaredFields()) {
      final BuildMetaField bmf = new ShadowBuildMetaField(buildMetaClass, EmptyStatement.INSTANCE,
          GenUtil.scopeOf(field), field.getType(), field.getName(), field);

      bmf.setFinal(field.isFinal());
      bmf.setStatic(field.isStatic());
      bmf.setVolatile(field.isVolatile());
      bmf.setTransient(field.isTransient());

      buildMetaClass.addField(bmf);
    }

    for (final MetaConstructor c : clazz.getDeclaredConstructors()) {
      final BuildMetaConstructor newConstructor = new BuildMetaConstructor(buildMetaClass, EmptyStatement.INSTANCE,
          GenUtil.scopeOf(c),
          DefParameters.from(c));
      newConstructor.setReifiedFormOf(c);

      buildMetaClass.addConstructor(newConstructor);
    }

    for (final MetaMethod method : clazz.getDeclaredMethods()) {

      MetaClass returnType = method.getReturnType();
      if (method.getGenericReturnType() instanceof MetaTypeVariable) {
        final MetaTypeVariable typeVariable = (MetaTypeVariable) method.getGenericReturnType();
        final MetaClass tVarVal = getTypeVariableValue(typeVariable, buildMetaClass);
        if (tVarVal != null) {
          returnType = tVarVal;
        }
      }
      else if (method.getGenericReturnType() instanceof MetaParameterizedType) {
        final MetaParameterizedType metaParameterizedType
            = (MetaParameterizedType) method.getGenericReturnType();

        final List<MetaType> typeVarValues = new ArrayList<MetaType>();
        boolean defaultOnly = true;
        for (final MetaType metaType : metaParameterizedType.getTypeParameters()) {
          if (metaType instanceof MetaTypeVariable) {
            final MetaTypeVariable typeVariable = (MetaTypeVariable) metaType;
            final MetaClass tVarVar = getTypeVariableValue(typeVariable, buildMetaClass);
            if (tVarVar != null) {
              defaultOnly = false;
              typeVarValues.add(tVarVar);
            }
            else {
              typeVarValues.add(MetaClassFactory.get(Object.class));
            }
          }
          else {
            typeVarValues.add(metaType);
          }
        }

        if (reifyRecursively && !defaultOnly) {
          returnType = parameterizedAs(returnType, typeParametersOf(typeVarValues.toArray(new MetaType[typeVarValues.size()])), false);
        }
      }

      final List<Parameter> parameters = new ArrayList<Parameter>();
      int i = 0;
      for (final MetaParameter parm : method.getParameters()) {
        MetaClass parmType = null;
        if (method.getGenericParameterTypes() != null) {
          if (method.getGenericParameterTypes()[i] instanceof MetaTypeVariable) {
            final MetaTypeVariable typeVariable = (MetaTypeVariable) method.getGenericParameterTypes()[i];

            final MetaClass tVarVal = getTypeVariableValue(typeVariable, buildMetaClass);
            if (tVarVal != null) {
              parmType = tVarVal;
            }
          }
        }

        if (parmType == null) {
          parmType = parm.getType();
        }

        parameters.add(Parameter.of(parmType, parm.getName()));
        i++;
      }

      final BuildMetaMethod newMethod = new ShadowBuildMetaMethod(buildMetaClass, BlockStatement.EMPTY_BLOCK,
          GenUtil.scopeOf(method), GenUtil.modifiersOf(method), method.getName(), returnType,
          DefParameters.fromParameters(parameters), ThrowsDeclaration.of(method.getCheckedExceptions()), method);

      newMethod.setReifiedFormOf(method);

      buildMetaClass.addMethod(newMethod);
    }

    return buildMetaClass;
  }

  private static void addInterfaces(final MetaClass clazz, final BuildMetaClass buildMetaClass, final MetaParameterizedType parameterizedType) {
    final MetaType[] typeParams = (clazz.getParameterizedType() != null ? clazz.getParameterizedType().getTypeParameters() : clazz.getTypeParameters());
    final MetaType[] typeArgs = parameterizedType.getTypeParameters();
    validateTypeArgumentsAgainstParameters(clazz, typeParams, typeArgs);

    final Map<String, MetaType> typeArgsByTypeParam = mapTypeArgsByTypeParamName(typeParams, typeArgs);
    final List<MetaClass> ifaces = Arrays
            .stream(clazz.getInterfaces())
            .map(iface -> getParameterizedInterface(typeArgsByTypeParam, iface))
            .collect(Collectors.toList());
    buildMetaClass.setInterfaces(ifaces);
  }

  private static BuildMetaClass getParameterizedInterface(final Map<String, MetaType> typeArgsByTypeParam, final MetaClass iface) {
    final MetaType[] ifaceTypeArgs = getTypeArgumentsForInterface(typeArgsByTypeParam, iface);
    return cloneToBuildMetaClass(iface, typeParametersOf(ifaceTypeArgs), true);
  }

  private static MetaType[] getTypeArgumentsForInterface(final Map<String, MetaType> typeArgsByTypeParam, final MetaClass iface) {
    return Arrays
      .stream(Optional.ofNullable(iface.getParameterizedType())
                      .map(i -> i.getTypeParameters())
                      .orElseGet(() -> iface.getTypeParameters()))
      .map(mt -> {
        if (typeArgsByTypeParam.containsKey(mt.getName())) {
          return typeArgsByTypeParam.get(mt.getName());
        }
        else {
          return mt;
        }
    }).toArray(size -> new MetaType[size]);
  }

  private static Map<String, MetaType> mapTypeArgsByTypeParamName(final MetaType[] typeParams, final MetaType[] typeArgs) {
    final Map<String, MetaType> typeArgsByTypeParam = new HashMap<>();
    for (int i = 0; i < typeParams.length; i++) {
      typeArgsByTypeParam.put(typeParams[i].getName(), typeArgs[i]);
    }
    return typeArgsByTypeParam;
  }

  private static void validateTypeArgumentsAgainstParameters(final MetaClass clazz, final MetaType[] typeParams, final MetaType[] assignedTypes) {
    if (typeParams.length != assignedTypes.length) {
      final String message = "Number of provided types does not match the number of type parameters for " + clazz.getFullyQualifiedName()
      + ".\nType parameters: " + Arrays.toString(typeParams)
      + "\nProvided types: " + Arrays.toString(assignedTypes);
      throw new IllegalArgumentException(message);
    }
  }

  private static MetaClass getTypeVariableValue(final MetaTypeVariable typeVariable, final MetaClass clazz) {
    int idx = -1;
    final MetaTypeVariable[] typeVariables = clazz.getTypeParameters();
    for (int i = 0; i < typeVariables.length; i++) {
      if (typeVariables[i].getName().equals(typeVariable.getName())) {
        idx = i;
        break;
      }
    }

    if (idx != -1) {
      final MetaType type = clazz.getParameterizedType().getTypeParameters()[idx];
      if (type instanceof MetaClass) {
        return (MetaClass) type;
      }
    }
    return null;
  }

  public static MetaParameterizedType typeParametersOf(final Object... classes) {
    final MetaType[] types = new MetaType[classes.length];
    int i = 0;
    for (final Object o : classes) {
      if (o instanceof Class) {
        types[i++] = MetaClassFactory.get((Class) o);
      }
      else if (o instanceof TypeLiteral) {
        types[i++] = MetaClassFactory.get((TypeLiteral) o);
      }
      else if (o instanceof MetaType) {
        types[i++] = (MetaType) o;
      }
      else {
        throw new RuntimeException("not a recognized type reference: " + o.getClass().getName());
      }
    }

    return typeParametersOf(types);
  }

  public static MetaParameterizedType typeParametersOf(final Class<?>... classes) {
    return typeParametersOf(fromClassArray(classes));
  }

  public static MetaParameterizedType typeParametersOf(final MetaType... classes) {
    return new BuildMetaParameterizedType(classes, null, null);
  }

  private static void addLookups(final TypeLiteral literal, final MetaClass metaClass) {
    getMetaClassCache().pushErasedCache(literal.toString(), metaClass);
  }

  private static void addLookups(final Class cls, final MetaClass metaClass) {
    getMetaClassCache().pushErasedCache(cls.getName(), metaClass);
  }

  public static final Map<String, Class<?>> PRIMITIVE_LOOKUP = Collections.unmodifiableMap(new HashMap<String, Class<?>>() {
    {
      put("void", void.class);
      put("boolean", boolean.class);
      put("int", int.class);
      put("long", long.class);
      put("float", float.class);
      put("double", double.class);
      put("short", short.class);
      put("byte", byte.class);
      put("char", char.class);
    }
  });

  public static Class<?> loadClass(final String fullyQualifiedName) {
    try {
      Class<?> cls = PRIMITIVE_LOOKUP.get(fullyQualifiedName);
      if (cls == null) {
        cls = Class.forName(fullyQualifiedName, false, Thread.currentThread().getContextClassLoader());
      }
      return cls;
    }
    catch (final ClassNotFoundException e) {
      final URL url = MetaClassFactory.class.getClassLoader()
          .getResource(fullyQualifiedName.replace('.', '/') + ".java");

      if (url != null) {
        final File sourceFile = new File(url.getFile()).getAbsoluteFile();
        if (sourceFile.exists()) {

          final File directory = sourceFile.getParentFile();
          final String packageName = fullyQualifiedName.substring(0, fullyQualifiedName.lastIndexOf('.'));
          final String className = fullyQualifiedName.substring(fullyQualifiedName.lastIndexOf('.') + 1);

          final String location = ClassChangeUtil.compileClass(directory.getAbsolutePath(),
              packageName,
              className,
              RebindUtils.getTempDirectory() + "/hotload/");

          try {
            return ClassChangeUtil.loadClassDefinition(location,
                packageName, className);
          }
          catch (final Exception e2) {
            throw new RuntimeException("Could not load class: " + fullyQualifiedName, e2);
          }
        }
      }

      throw new RuntimeException("Could not load class: " + fullyQualifiedName);
    }
  }

  public static boolean canLoadClass(final String fullyQualifiedName) {
    try {
      final Class cls = loadClass(fullyQualifiedName);
      return cls != null;
    }
    catch (final Throwable t) {
      return false;
    }
  }

  public static MetaClass[] fromClassArray(final Class<?>[] classes) {
    final MetaClass[] newClasses = new MetaClass[classes.length];
    for (int i = 0; i < classes.length; i++) {
      newClasses[i] = createOrGet(classes[i].getName(), false);
    }
    return newClasses;
  }

  public static MetaClass[] asMetaClassArray(final MetaParameter[] parms) {
    final MetaClass[] type = new MetaClass[parms.length];
    for (int i = 0; i < parms.length; i++) {
      type[i] = parms[i].getType();
    }
    return type;
  }

  public static Class<?>[] asClassArray(final MetaParameter[] parms) {
    final MetaType[] type = new MetaType[parms.length];
    for (int i = 0; i < parms.length; i++) {
      type[i] = parms[i].getType();
    }
    return asClassArray(type);
  }

  public static Class<?>[] asClassArray(final MetaType[] cls) {
    final Class<?>[] newClasses = new Class<?>[cls.length];
    for (int i = 0; i < cls.length; i++) {
      if (cls[i] instanceof MetaParameterizedType) {
        newClasses[i] = ((MetaClass) ((MetaParameterizedType) cls[i]).getRawType()).asClass();
      }
      else {
        newClasses[i] = ((MetaClass) cls[i]).asClass();
      }
    }
    return newClasses;
  }

  public static Collection<MetaClass> getAllNewOrUpdatedClasses() {
    return getMetaClassCache().getAllNewOrUpdated();
  }

  public static Collection<MetaClass> getNewClasses() {
    return getMetaClassCache().getAllNewClasses();
  }

  public static boolean isChangedOrDeleted(final String fqcn) {
    return getMetaClassCache().getAllDeletedClasses().contains(fqcn) || getMetaClassCache().isNewOrUpdated(fqcn);
  }

  public static Set<String> getAllDeletedClasses() {
    return getMetaClassCache().getAllDeletedClasses();
  }

  public static Collection<MetaClass> getAllCachedClasses() {
    return getMetaClassCache().getAllCached();
  }

  public static boolean isKnownType(final String fqcn) {
    return getMetaClassCache().isKnownType(fqcn);
  }

  public static boolean hasAnyChanges() {
    return !getAllNewOrUpdatedClasses().isEmpty() || !getAllDeletedClasses().isEmpty();
  }
}
