/* FunctionChain v.2 */
package com.google.common.base;

import static com.google.common.collect.Iterables.addAll;
import static com.google.common.collect.ObjectArrays.newArray;

import java.lang.annotation.Annotation;
import java.lang.reflect.*;
import static java.util.Arrays.asList;
import java.util.*;

/**
 * @author ymeymann
 * @since Nov 14, 2007 11:22:00 AM
 */
public class FunctionChain<A,B> implements Function<A,B> {
  final Function<A,B> function;
  final boolean isNullSupported;

  private FunctionChain(Function<A,B> f){
    function = f;
    isNullSupported = isNullSupported(function);
  }

  public <C> FunctionChain<A,C> function(final Function<? super B,C> f) {
    if (isNullSupported(f)) {
      return new FunctionChain<A,C>(new Function<A,C>(){
        public C apply(@Nullable A from) {
          return f.apply(FunctionChain.this.apply(from));
        }
      });
    } else {
      return new FunctionChain<A,C>(new Function<A,C>(){
        public C apply(A from) {
          return f.apply(FunctionChain.this.apply(from));
        }
      });
    }
  }

  public Predicate<A> condition(final Predicate<? super B> p) {
    return new Predicate<A>() {
      public boolean apply(A a) {
        return p.apply(FunctionChain.this.apply(a));
      }
    };
  }

  public B apply(A from) {
    return function.apply(from);
  }

  /**
   * With this method, instead of FunctionChain.&lt;Foo&gt;self() one can write self(Foo.class)
   */
  public static <S> FunctionChain<S,S> self(Class<S> c) {
    return self(true);
  }

  public static <S> FunctionChain<S,S> self(Class<S> c, boolean nullSafe) {
    return self(nullSafe);
  }

  public static <S> FunctionChain<S,S> self() {
    return self(true);
  }

  public static <A,B> FunctionChain<A,B> self(Function<A,B> f) {
    return self(f, true);
  }

  public static <A,B> FunctionChain<A,B> self(Function<A,B> f, boolean nullSafe) {
    return nullSafe? new NullSafe<A,B>(f) : new FunctionChain<A,B>(f);
  }

  public static <S> FunctionChain<S,S> self(boolean nullSafe) {
    return nullSafe ?
            new NullSafe<S,S>(Functions.<S>identity()) :
            new FunctionChain<S,S>(Functions.<S>identity());
  }

  public static boolean isNullSupported(Function<?,?> function) {

    //TODO cache this per function class

    Type functionInt = findGenericFunctionAncestor(function);
    Class[] genericParams = getGenericParameterClasses(functionInt, 1);
    if (genericParams.length == 0) genericParams = new Class[] {Object.class};
    Method applyMethod = null;
    try {
      applyMethod = function.getClass().getMethod("apply", genericParams[0]);
    } catch (NoSuchMethodException e) {
      e.printStackTrace();
    }

    List<Annotation> l = asList(applyMethod.getParameterAnnotations()[0]);
    for (Annotation a: l) {
      if (a.annotationType().equals(Nullable.class)) return true;
    }
    return false;
  }

  public static Type findGenericFunctionAncestor(Function<?, ?> function) {
    Class<?> curClass = function.getClass();
    Set<Type> allInt = new LinkedHashSet<Type>();
    while (curClass != Object.class) {
      Type[] ints = curClass.getGenericInterfaces();
      if (ints != null) {
        addAll(allInt,  asList(ints));
      }
      curClass = curClass.getSuperclass();
    }
    Type functionInt = null;
    for (Type t: allInt) {
      if ((t instanceof Class && t == Function.class) ||
          (t instanceof ParameterizedType && ((ParameterizedType)t).getRawType() == Function.class)) {
        functionInt = t;
        break;
      }
    }
    return functionInt == null ? Function.class : functionInt;
  }

  public static Class[] getGenericParameterClasses(Type superType) {
    return getGenericParameterClasses(superType, Integer.MAX_VALUE);
  }

  public static Class[] getGenericParameterClasses(Type superType, int parameterCount) {
    List<Class<?>> res = new LinkedList<Class<?>>();
    if (!(superType instanceof Class)) {
      ParameterizedType paramSuperclass = (ParameterizedType) superType;
      Type[] args = paramSuperclass.getActualTypeArguments();
      int i = 0;
      for (Type arg: args) {
        if (i >= parameterCount) break;
        if (arg instanceof Class<?>) {
          res.add((Class<?>) arg);
        } else if (arg instanceof ParameterizedType) {
          res.add((Class<?>) ((ParameterizedType) arg).getRawType());
        } else if (arg instanceof GenericArrayType) {
          Type elemType = ((GenericArrayType) arg).getGenericComponentType();
          //TODO support for multi-dimensional arrays
          Class<?> elemClass = Object.class;
          if (elemType instanceof Class<?>)
            elemClass = (Class<?>) elemType;
          else if (elemType instanceof ParameterizedType)
            elemClass = (Class<?>)((ParameterizedType)elemType).getRawType();
          res.add(Array.newInstance(elemClass ,0).getClass());
        } else {
          res.add(Object.class);
        }
        i++;
      } //for
    }
    return res.toArray(newArray(Class.class, res.size()));
  }

 
  public static class NullSafe<A,B> extends FunctionChain<A,B> {
    private NullSafe(Function<A,B> f){
      super(f);
    }

    @Override
    public <C> NullSafe<A,C> function(final Function<? super B,C> f) {
      if (isNullSupported(f)) {
        return new NullSafe<A,C>(new Function<A,C>(){
          public C apply(@Nullable A from) {
            B res = NullSafe.this.apply(from);
            return f.apply(res);
          }
        });
      } else {
        return new NullSafe<A,C>(new Function<A,C>(){
          public C apply(A from) {
            B res = NullSafe.this.apply(from);
            return res == null ? null : f.apply(res);
          }
        });
      }
    }

    @Override
    public Predicate<A> condition(final Predicate<? super B> p) {
      return new Predicate<A>() {
        public boolean apply(A a) {
          B res = NullSafe.this.apply(a);
          return res!= null && p.apply(res);
        }
      };
    }

    @Override
    public B apply(@Nullable A from) {
      if (from == null && !isNullSupported) return null;
      return function.apply(from);
    }

  }

}