/*
 * 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.impl.java;

import static org.jboss.errai.codegen.meta.MetaClassFactory.parameterizedAs;
import static org.jboss.errai.codegen.meta.MetaClassFactory.typeParametersOf;

import java.lang.annotation.Annotation;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.lang.reflect.ParameterizedType;
import java.lang.reflect.Type;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;

import javax.enterprise.util.TypeLiteral;

import org.jboss.errai.codegen.meta.MetaClass;
import org.jboss.errai.codegen.meta.MetaClassFactory;
import org.jboss.errai.codegen.meta.MetaConstructor;
import org.jboss.errai.codegen.meta.MetaField;
import org.jboss.errai.codegen.meta.MetaMethod;
import org.jboss.errai.codegen.meta.MetaTypeVariable;
import org.jboss.errai.codegen.meta.impl.AbstractMetaClass;
import org.jboss.errai.codegen.util.GenUtil;

public class JavaReflectionClass extends AbstractMetaClass<Class> {
  private Annotation[] _annotationsCache;

  protected JavaReflectionClass(final Class clazz, final boolean erased) {
    this(clazz, null, erased);
  }

  private JavaReflectionClass(final Class clazz, final Type type, final boolean erased) {
    super(clazz);
    _asClassCache = clazz;
    if (!erased) {
      if (type instanceof ParameterizedType) {
        super.parameterizedType = new JavaReflectionParameterizedType((ParameterizedType) type);
      }
      if (clazz.getGenericSuperclass() instanceof ParameterizedType) {
        super.genericSuperClass = new JavaReflectionParameterizedType((ParameterizedType) clazz.getGenericSuperclass());
      }
    }
  }

  private JavaReflectionClass(final TypeLiteral typeLiteral) {
    super(typeLiteral.getRawType());
    if (typeLiteral.getType() instanceof ParameterizedType) {
      super.parameterizedType = new JavaReflectionParameterizedType((ParameterizedType) typeLiteral.getType());
    }
  }

  public static MetaClass newInstance(final Class type) {
    if (type == null) return null;

    if (!MetaClassFactory.isCached(type.getName())) {
      final MetaClass clazz = newUncachedInstance(type);
      MetaClassFactory.getMetaClassCache().pushCache(clazz);

      return clazz;
    }
    else {
      return MetaClassFactory.get(type);
    }
  }

  public static MetaClass newUncachedInstance(final Class type) {
    return new JavaReflectionClass(type, false);
  }

  public static MetaClass newUncachedInstance(final Class clazz, final boolean erased) {
    return new JavaReflectionClass(clazz, erased);
  }

  public static MetaClass newUncachedInstance(final Class clazz, final Type type) {
    return new JavaReflectionClass(clazz, type, false);
  }

  public static MetaClass newInstance(final TypeLiteral type) {
    return MetaClassFactory.get(type);
  }

  public static MetaClass newUncachedInstance(final TypeLiteral type) {
    return new JavaReflectionClass(type);
  }

  @Override
  public MetaClass getErased() {
    if (parameterizedType == null) {
      return this;
    }
    return new JavaReflectionClass(getEnclosedMetaObject(), true);
  }

  @Override
  public String getName() {
    return getEnclosedMetaObject().getSimpleName();
  }

  @Override
  public String getFullyQualifiedName() {
    return getEnclosedMetaObject().getName();
  }

  @Override
  public String getCanonicalName() {
    return getEnclosedMetaObject().getCanonicalName();
  }

  private String _packageName = null;
  @Override
  public String getPackageName() {
    if (_packageName != null) {
      return _packageName;
    }

    final Package pack = getEnclosedMetaObject().getPackage();
    if (pack != null) {
      _packageName = pack.getName();
    }
    return _packageName;
  }

  private MetaMethod[] fromMethodArray(final Method[] methods) {
    final List<MetaMethod> methodList = new ArrayList<>();

    for (final Method m : methods) {
      // hack to exclude jacoco instrumented methods.
      if (!m.isBridge() && !m.getName().startsWith("$jacoco")) {
        methodList.add(new JavaReflectionMethod(this, m));
      }
    }

    return methodList.toArray(new MetaMethod[methodList.size()]);
  }

  private MetaMethod[] _methodCache = null;

  @Override
  public MetaMethod[] getMethods() {
    if (_methodCache != null) {
      return _methodCache;
    }

    final Set<MetaMethod> meths = new LinkedHashSet<>();

    Class<?> type = getEnclosedMetaObject();
    if (type == null) {
      return null;
    }

    final Set<String> processedMethods = new HashSet<>();
    do {
      for (final Method method : type.getDeclaredMethods()) {
        final JavaReflectionMethod metaMethod = new JavaReflectionMethod(this, method);
        final String readableMethodDecl = GenUtil.getMethodString(metaMethod);
        if (!metaMethod.isPrivate() && !method.isBridge() && !processedMethods.contains(readableMethodDecl)) {
            meths.add(metaMethod);
            processedMethods.add(readableMethodDecl);
        }
      }

      // we don't need to recurse on interfaces in this case because we already get the list of
      // all inherited methods from Class.getMethods() -- interface methods are public
      for (final Class<?> interfaceType : type.getInterfaces()) {
        for (final MetaMethod ifaceMethod : Arrays.asList(JavaReflectionClass.newInstance(interfaceType).getMethods())) {
          final String readableMethodDecl = GenUtil.getMethodString(ifaceMethod);
          if (!processedMethods.contains(readableMethodDecl)) {
            meths.add(ifaceMethod);
            processedMethods.add(readableMethodDecl);
          }
        }
      }
    }
    while ((type = type.getSuperclass()) != null);

    _methodCache = meths.toArray(new MetaMethod[meths.size()]);
    return _methodCache;
  }

  private transient volatile MetaMethod[] _declaredMethodCache;

  @Override
  public MetaMethod[] getDeclaredMethods() {
    return _declaredMethodCache != null ? _declaredMethodCache
            : (_declaredMethodCache = fromMethodArray(getEnclosedMetaObject().getDeclaredMethods()));
  }


  private static MetaField[] fromFieldArray(final Field[] methods) {
    return Arrays.stream(methods).map(f -> new JavaReflectionField(f)).toArray(s -> new MetaField[s]);
  }

  private transient volatile MetaField[] _fieldCache;

  @Override
  public MetaField[] getFields() {
    return _fieldCache != null ? _fieldCache : (_fieldCache = fromFieldArray(getEnclosedMetaObject().getFields()));
  }

  private transient volatile MetaField[] _declaredFieldCache;

  @Override
  public MetaField[] getDeclaredFields() {
    return _declaredFieldCache != null ? _declaredFieldCache
            : (_declaredFieldCache = fromFieldArray(getEnclosedMetaObject().getDeclaredFields()));
  }

  @Override
  public MetaField getField(final String name) {
    try {
      final MetaField mFld;
      if ("length".equals(name) && getEnclosedMetaObject().isArray()) {
        mFld = new MetaField.ArrayLengthMetaField(this);
      }
      else {
        mFld = new JavaReflectionField(getEnclosedMetaObject().getField(name));
      }
      return mFld;
    }
    catch (final Exception e) {
      return null;
    }
  }

  @Override
  public MetaField getDeclaredField(final String name) {
    try {
      final MetaField mFld;

      if ("length".equals(name) && getEnclosedMetaObject().isArray()) {
        mFld = new MetaField.ArrayLengthMetaField(this);
      }
      else {
        mFld = new JavaReflectionField(getEnclosedMetaObject().getDeclaredField(name));
      }
      return mFld;
    }
    catch (final Exception e) {
      return null;
    }
  }


  private transient volatile MetaConstructor[] constructorCache;

  @Override
  public MetaConstructor[] getConstructors() {
    if (constructorCache != null) {
      return constructorCache;
    }

    return constructorCache = Arrays.stream(getEnclosedMetaObject().getConstructors())
            .map(c -> new JavaReflectionConstructor(c)).toArray(s -> new MetaConstructor[s]);
  }

  private transient volatile MetaConstructor[] declConstructorCache;

  @Override
  public MetaConstructor[] getDeclaredConstructors() {
    if (getEnclosedMetaObject().isEnum()) {
      // Enum constructors have strange metadata, so we avoid trouble by saying enums have no constructors
      // (getParameterTypes().length != getGenericParameterTypes().length)
      return new MetaConstructor[0];
    }

    if (declConstructorCache != null) {
      return declConstructorCache;
    }
    return declConstructorCache = Arrays.stream(getEnclosedMetaObject().getDeclaredConstructors())
            .map(c -> new JavaReflectionConstructor(c)).toArray(s -> new MetaConstructor[s]);
  }

  @Override
  public MetaConstructor getConstructor(final Class... parameters) {
    try {
      return new JavaReflectionConstructor(getEnclosedMetaObject().getConstructor(parameters));
    }
    catch (final Exception e) {
      return null;
    }
  }

  @Override
  public MetaConstructor getDeclaredConstructor(final Class... parameters) {
    try {
      return new JavaReflectionConstructor(getEnclosedMetaObject().getDeclaredConstructor(parameters));
    }
    catch (final Exception e) {
      return null;
    }
  }

  @Override
  public MetaClass[] getDeclaredClasses() {
    final Class[] declaredClasses = getEnclosedMetaObject().getDeclaredClasses();
    final MetaClass[] declaredClassesMC = new MetaClass[declaredClasses.length];
    int i = 0;
    for (final Class c : declaredClasses) {
      declaredClassesMC[i++] = MetaClassFactory.get(c);
    }
    return declaredClassesMC;
  }

  private MetaClass[] _interfacesCache;

  @Override
  public MetaClass[] getInterfaces() {
    if (_interfacesCache != null) {
      return _interfacesCache;
    }

    final List<MetaClass> metaClassList = new ArrayList<>();
    final Type[] genIface = getEnclosedMetaObject().getGenericInterfaces();

    int i = 0;
    for (final Class<?> type : getEnclosedMetaObject().getInterfaces()) {
      if (genIface != null) {
        metaClassList.add(new JavaReflectionClass(type, genIface[i], false));
      }
      else {
        metaClassList.add(new JavaReflectionClass(type, false));
      }
      i++;
    }

    return _interfacesCache = metaClassList.toArray(new MetaClass[metaClassList.size()]);
  }

  private MetaClass _superClass;

  @Override
  public MetaClass getSuperClass() {
    if (_superClass != null)
      return _superClass;

    if (getGenericSuperClass() != null) {
      _superClass = parameterizedAs(getEnclosedMetaObject().getSuperclass(), typeParametersOf(getGenericSuperClass()
              .getTypeParameters()));
    }
    else {
      _superClass = newInstance(getEnclosedMetaObject().getSuperclass());
    }
    return _superClass;
  }

  @Override
  public MetaClass getComponentType() {
    return MetaClassFactory.get(getEnclosedMetaObject().getComponentType());
  }

  @Override
  public synchronized Annotation[] getAnnotations() {
    if (_annotationsCache == null) {
      _annotationsCache = getEnclosedMetaObject().getAnnotations();
    }
    return _annotationsCache;
  }

  @Override
  public MetaTypeVariable[] getTypeParameters() {
    return JavaReflectionUtil.fromTypeVariable(getEnclosedMetaObject().getTypeParameters());
  }


  @Override
  public boolean isPrimitive() {
    return getEnclosedMetaObject().isPrimitive();
  }

  @Override
  public boolean isVoid() {
    return getEnclosedMetaObject().equals(Void.TYPE);
  }

  @Override
  public boolean isInterface() {
    return getEnclosedMetaObject().isInterface();
  }

  @Override
  public boolean isAbstract() {
    return (getEnclosedMetaObject().getModifiers() & Modifier.ABSTRACT) != 0;
  }

  @Override
  public boolean isArray() {
    return getEnclosedMetaObject().isArray();
  }

  @Override
  public boolean isEnum() {
    return getEnclosedMetaObject().isEnum();
  }

  @Override
  public boolean isAnnotation() {
    return getEnclosedMetaObject().isAnnotation();
  }

  @Override
  public boolean isPublic() {
    return (getEnclosedMetaObject().getModifiers() & Modifier.PUBLIC) != 0;
  }

  @Override
  public boolean isPrivate() {
    return (getEnclosedMetaObject().getModifiers() & Modifier.PRIVATE) != 0;
  }

  @Override
  public boolean isProtected() {
    return (getEnclosedMetaObject().getModifiers() & Modifier.PROTECTED) != 0;
  }

  @Override
  public boolean isFinal() {
    return (getEnclosedMetaObject().getModifiers() & Modifier.FINAL) != 0;
  }

  @Override
  public boolean isStatic() {
    return (getEnclosedMetaObject().getModifiers() & Modifier.STATIC) != 0;
  }

  @Override
  public boolean isSynthetic() {
    return getEnclosedMetaObject().isSynthetic();
  }

  @Override
  public boolean isAnonymousClass() {
    return getEnclosedMetaObject().isAnonymousClass();
  }

  private final Map<Integer, MetaClass> _arrayTypeCache = new HashMap<>();

  @Override
  public MetaClass asArrayOf(final int dimensions) {
    MetaClass arrayType = _arrayTypeCache.get(dimensions);
    if (arrayType == null) {
      _arrayTypeCache.put(dimensions, arrayType = MetaClassFactory.getArrayOf(getEnclosedMetaObject(), dimensions));
    }
    return arrayType;
  }
}
