package com.google.common.base;

import java.util.Arrays;
import java.util.List;
import java.lang.annotation.Annotation;

/**
 * @author ymeymann@gmail.com
 */
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 <S> FunctionChain<S,S> self(boolean nullSafe) {
    return nullSafe ?
            new NullSafeFunctionChain<S,S>(Functions.<S>identity()) :
            new FunctionChain<S,S>(Functions.<S>identity());
  }

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

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

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

  }

  public static boolean isNullSupported(Function<?,?> function) {
    try {
      List<Annotation> l = Arrays.asList(function.getClass().getMethod("apply", Object.class).getParameterAnnotations()[0]);
      for (Annotation a: l) {
        if (a.annotationType().equals(Nullable.class)) return true;
      }
    } catch (Exception e) {
      e.printStackTrace(); //it should never happen
    }
    return false;
  }
}