package com.beust.jcommander;

import com.beust.jcommander.internal.Lists;
import com.beust.jcommander.internal.Sets;

import java.lang.annotation.Annotation;
import java.lang.reflect.*;
import java.util.*;
import java.util.stream.Collectors;

/**
 * Encapsulate a field or a method annotated with @Parameter or @DynamicParameter
 */
public class Parameterized {

  // Either a method or a field
  private Field field;
  private Method method;
  private Method getter;

  // Either of these two
  private WrappedParameter wrappedParameter;
  private ParametersDelegate parametersDelegate;

  public Parameterized(WrappedParameter wp, ParametersDelegate pd,
      Field field, Method method) {
    wrappedParameter = wp;
    this.method = method;
    this.field = field;
    if (this.field != null) {
      if(pd == null) {
        setFieldAccessible(this.field);
      } else {
        setFieldAccessibleWithoutFinalCheck(this.field);
      }
    }
    parametersDelegate = pd;
  }

  /**
   * Recursive handler for describing the set of classes while
   * using the setOfClasses parameter as a collector
   *
   * @param inputClass the class to analyze
   * @param setOfClasses the set collector to collect the results
     */
  private static void describeClassTree(Class<?> inputClass, Set<Class<?>> setOfClasses) {
    // can't map null class
    if(inputClass == null) {
      return;
    }

    // don't further analyze a class that has been analyzed already
    if(Object.class.equals(inputClass) || setOfClasses.contains(inputClass)) {
      return;
    }

    // add to analysis set
    setOfClasses.add(inputClass);

    // perform super class analysis
    describeClassTree(inputClass.getSuperclass(), setOfClasses);

    // perform analysis on interfaces
    for(Class<?> hasInterface : inputClass.getInterfaces()) {
      describeClassTree(hasInterface, setOfClasses);
    }
  }

  /**
   * Given an object return the set of classes that it extends
   * or implements.
   *
   * @param inputClass object to describe
   * @return set of classes that are implemented or extended by that object
   */
  private static Set<Class<?>> describeClassTree(Class<?> inputClass) {
    if(inputClass == null) {
      return Collections.emptySet();
    }

    // create result collector
    Set<Class<?>> classes = Sets.newLinkedHashSet();

    // describe tree
    describeClassTree(inputClass, classes);

    return classes;
  }

  public static List<Parameterized> parseArg(Object arg) {
    List<Parameterized> result = Lists.newArrayList();

    Class<?> rootClass = arg.getClass();

    // get the list of types that are extended or implemented by the root class
    // and all of its parent types
    Set<Class<?>> types = describeClassTree(rootClass);

    Set<Method> bridgeOrSyntheticMethods = new HashSet<>();
    Map<String, Parameterized> methods = new HashMap<>();

    // analyze each type
    for(Class<?> cls : types) {

      // check fields
      for (Field f : cls.getDeclaredFields()) {
        Annotation annotation = f.getAnnotation(Parameter.class);
        Annotation delegateAnnotation = f.getAnnotation(ParametersDelegate.class);
        Annotation dynamicParameter = f.getAnnotation(DynamicParameter.class);
        if (annotation != null) {
          result.add(new Parameterized(new WrappedParameter((Parameter) annotation), null,
                  f, null));
        } else if (dynamicParameter != null) {
          result.add(new Parameterized(new WrappedParameter((DynamicParameter) dynamicParameter), null,
                  f, null));
        } else if (delegateAnnotation != null) {
          result.add(new Parameterized(null, (ParametersDelegate) delegateAnnotation,
                  f, null));
        }
      }

      // check methods
      for (Method m : cls.getDeclaredMethods()) {
        // Ignore bridge and synthetic methods for now
        if (m.isBridge() || m.isSynthetic()) {
          continue;
        }

        Parameterized parameterized = createParameterizedFromMethod(m);
        if (parameterized != null) {
          methods.put(m.getName(), parameterized);
        }
      }

      // Accumulate the bridge and synthetic methods to check later
      bridgeOrSyntheticMethods.addAll(Arrays.stream(cls.getDeclaredMethods())
        .filter(method -> method.isBridge() || method.isSynthetic())
        .collect(Collectors.toList()));
    }

    // If there are any bridge or synthetic methods that do not have a name which is already present, add them to the
    // methods map. Otherwise, the non-bridge or non-synthetic method of the same name will take precedence
    bridgeOrSyntheticMethods.stream()
      .map(Parameterized::createParameterizedFromMethod)
      .filter(Objects::nonNull)
      .forEach(parameterized -> methods.putIfAbsent(parameterized.method.getName(), parameterized));

    result.addAll(methods.values());

    return result;
  }

  private static Parameterized createParameterizedFromMethod(Method m) {
    m.setAccessible(true);
    Annotation annotation = m.getAnnotation(Parameter.class);
    Annotation delegateAnnotation = m.getAnnotation(ParametersDelegate.class);
    Annotation dynamicParameter = m.getAnnotation(DynamicParameter.class);
    if (annotation != null) {
      return new Parameterized(new WrappedParameter((Parameter) annotation), null,
              null, m);
    } else if (dynamicParameter != null) {
      return new Parameterized(new WrappedParameter((DynamicParameter) dynamicParameter), null,
              null, m);
    } else if (delegateAnnotation != null) {
      return new Parameterized(null, (ParametersDelegate) delegateAnnotation,
              null, m);
    }

    return null;
  }

  public WrappedParameter getWrappedParameter() {
    return wrappedParameter;
  }

  public Class<?> getType() {
    if (method != null) {
      return method.getParameterTypes()[0];
    } else {
      return field.getType();
    }
  }

  public String getName() {
    if (method != null) {
      return method.getName();
    } else {
      return field.getName();
    }
  }

  public Object get(Object object) {
    try {
      if (method != null) {
        if (getter == null) {
          setGetter(object);
        }
        return getter.invoke(object);
      } else {
        return field.get(object);
      }
    } catch (SecurityException | IllegalArgumentException | InvocationTargetException | IllegalAccessException e) {
      throw new ParameterException(e);
    } catch (NoSuchMethodException e) {
      // Try to find a field
      String name = method.getName();
      String fieldName = Character.toLowerCase(name.charAt(3)) + name.substring(4);
      Object result = null;
      try {
        Field field = method.getDeclaringClass().getDeclaredField(fieldName);
        if (field != null) {
          setFieldAccessible(field);
          result = field.get(object);
        }
      } catch(NoSuchFieldException | IllegalAccessException ex) {
        // ignore
      }
      return result;
    }
  }

  private void setGetter(Object object) throws IllegalAccessException, InvocationTargetException, NoSuchMethodException {
    if(Boolean.class.getSimpleName().toLowerCase().equals(getType().getName())){
      // try is<fieldname> notation
      try {
        getter = object.getClass()
                .getMethod("is" + method.getName().substring(3));
        // we have found a is<fieldname> getter we can return
        return;
      } catch (NoSuchMethodException n){
        // if not found ignore exception and try with default get<fieldname> below
      }
    }
    getter = object.getClass()
            .getMethod("g" + method.getName().substring(1));
  }

  @Override
  public int hashCode() {
    final int prime = 31;
    int result = 1;
    result = prime * result + ((field == null) ? 0 : field.hashCode());
    result = prime * result + ((method == null) ? 0 : method.hashCode());
    return result;
  }

  @Override
  public boolean equals(Object obj) {
    if (this == obj)
      return true;
    if (obj == null)
      return false;
    if (getClass() != obj.getClass())
      return false;
    Parameterized other = (Parameterized) obj;
    if (field == null) {
      if (other.field != null)
        return false;
    } else if (!field.equals(other.field))
      return false;
    if (method == null) {
      if (other.method != null)
        return false;
    } else if (!method.equals(other.method))
      return false;
    return true;
  }

  public boolean isDynamicParameter(Field field) {
    if (method != null) {
      return method.getAnnotation(DynamicParameter.class) != null;
    } else {
      return this.field.getAnnotation(DynamicParameter.class) != null;
    }
  }

  private static void setFieldAccessible(Field f) {
    if (Modifier.isFinal(f.getModifiers())) {
      throw new ParameterException(
        "Cannot use final field " + f.getDeclaringClass().getName() + "#" + f.getName() + " as a parameter;"
        + " compile-time constant inlining may hide new values written to it.");
    }
    f.setAccessible(true);
  }

  private static void setFieldAccessibleWithoutFinalCheck(Field f) {
    f.setAccessible(true);
  }

  private static String errorMessage(Method m, Exception ex) {
    return "Could not invoke " + m + "\n    Reason: " + ex.getMessage();
  }

  public void set(Object object, Object value) {
    try {
      if (method != null) {
        method.invoke(object, value);
      } else {
          field.set(object, value);
      }
    } catch (IllegalAccessException | IllegalArgumentException ex) {
      throw new ParameterException(errorMessage(method, ex));
    } catch (InvocationTargetException ex) {
      // If a ParameterException was thrown, don't wrap it into another one
      if (ex.getTargetException() instanceof ParameterException) {
        throw (ParameterException) ex.getTargetException();
      } else {
        throw new ParameterException(errorMessage(method, ex), ex.getTargetException());
      }
    }
  }

  public ParametersDelegate getDelegateAnnotation() {
    return parametersDelegate;
  }

  public Type getGenericType() {
    if (method != null) {
      return method.getGenericParameterTypes()[0];
    } else {
      return field.getGenericType();
    }
  }

  public Parameter getParameter() {
    return wrappedParameter.getParameter();
  }

  /**
   * @return the generic type of the collection for this field, or null if not applicable.
   */
  public Type findFieldGenericType() {
    if (method != null) {
      return null;
    } else {
      if (field.getGenericType() instanceof ParameterizedType) {
        ParameterizedType p = (ParameterizedType) field.getGenericType();
        Type cls = p.getActualTypeArguments()[0];
        if (cls instanceof Class) {
          return cls;
        } else if ( cls instanceof WildcardType) {
          WildcardType wildcardType = (WildcardType)cls;
          if (wildcardType.getLowerBounds().length > 0) {
            return wildcardType.getLowerBounds()[0];
          }
          if (wildcardType.getUpperBounds().length > 0) {
            return wildcardType.getUpperBounds()[0];
          }
        }
      }
    }

    return null;
  }

  public boolean isDynamicParameter() {
    return wrappedParameter.getDynamicParameter() != null;
  }

}
