package haskell.prelude;

import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;

import haskell.lang.AbstractData;
import haskell.lang.AbstractExp;
import haskell.lang.Data;
import haskell.lang.Exception;
import haskell.lang.Exp;

/**
 * data a -> b
 */
public class Function<A, B>
        extends AbstractData<Function<A, B>>
        implements Data<Function<A, B>> {

    protected static class Internal
            extends AbstractData.Internal {

        final Object target;
        final Class<?> returnType;
        final Method method;
        final Object[] bounds;

        protected Internal(
                final Object target,
                final Class<?> returnType,
                final Method method,
                final Object... bounds) {
            this.target = target;
            this.returnType = returnType;
            this.method = method;
            this.bounds = bounds;
        }

        /**
         * f x y ...
         */
        @SuppressWarnings("unchecked")
        public <C> C apply(final Function<?, ?> f, final Object... args) {
            final Object[] newArgs = new Object[bounds.length + args.length];
            System.arraycopy(bounds, 0, newArgs, 0, bounds.length);
            System.arraycopy(args, 0, newArgs, bounds.length, args.length);
            final Class<?>[] paramTypes = method.getParameterTypes();
            if (newArgs.length < paramTypes.length) {
                return (C) new Function(
                        f.$name(), target, returnType, method, newArgs);
            }
            try {
                return (C) newExpression(returnType, f, newArgs);
            } catch (Exception e) {
                return evaluate(newArgs);
            }
        }

        protected <C> C newExpression(
                final Class<C> clazz,
                final Function<?, ?> f,
                final Object... args) {
            final Constructor<C> c = getConstructor(clazz, Exp.class);
            final Exp<C> expression = new AbstractExp<C>(f.toString(args)) {
                public C eval() {
                    return evaluate(args);
                }
            };
            return newInstance(c, expression);
        }

        protected <C> C evaluate(final Object... args) {
            return invoke(target, method, args);
        }

    }

    protected Function() {
        super("(\\a -> b)", null);
    }

    protected Function(final java.lang.String name) {
        super(name, null);
    }

    public Function(
            final java.lang.String name,
            final Object target,
            final Class<?> returnType,
            final Method method,
            final Object... bounds) {
        super(name, new Internal(target, returnType, method, bounds));
    }

    /**
     * public static B name(A x);
     */ 
    public Function(
            final Class<?> clazz,
            final Class<?> returnType,
            final java.lang.String name) {
        this(name, clazz, returnType, name, 1);
    }

    /**
     * public B name(A x);
     */
    public Function(
            final Object target,
            final Class<?> returnType,
            final java.lang.String name) {
        this(name, target, returnType, name, 1);
    }

    /**
     * public static B methodName(A x);
     */
    public Function(
            final java.lang.String name,
            final Class<?> clazz,
            final Class<?> returnType,
            final java.lang.String methodName) {
        this(name, clazz, returnType, methodName, 1);
    }

    /**
     * public B methodName(A x);
     */
    public Function(
            final java.lang.String name,
            final Object target,
            final Class<?> returnType,
            final java.lang.String methodName) {
        this(name, target, returnType, methodName, 1);
    }

    /**
     * public static T name(A x, ...);
     */
    public Function(
            final Class<?> clazz,
            final Class<?> returnType,
            final java.lang.String name,
            final int nParams) {
        this(name, clazz, returnType, name, nParams);
    }

    /**
     * public T name(A x, ...);
     */
    public Function(
            final Object target,
            final Class<?> returnType,
            final java.lang.String name,
            final int nParams) {
        this(name, target, returnType, name, nParams);
    }

    /**
     * public static T methodName(A x, ...);
     */
    public Function(
            final java.lang.String name,
            final Class<?> clazz,
            final Class<?> returnType,
            final java.lang.String methodName,
            final int nParams) {
        this(name, null, returnType,
                getMethod(clazz, methodName, nParams));
    }

    /**
     * public T methodName(A x, ...);
     */
    public Function(
            final java.lang.String name,
            final Object target,
            final Class<?> returnType,
            final java.lang.String methodName,
            final int nParams) {
        this(name, target, returnType,
                getMethod(target.getClass(), methodName, nParams));
    }

    /**
     * public static T name(A x, ...);
     */
    public Function(
            final Class<?> clazz,
            final Class<?> returnType,
            final java.lang.String name,
            final Class<?>... paramTypes) {
        this(name, clazz, returnType, name, paramTypes);
    }

    /**
     * public T name(A x, ...);
     */
    public Function(
            final Object target,
            final Class<?> returnType,
            final java.lang.String name,
            final Class<?>... paramTypes) {
        this(name, target, returnType, name, paramTypes);
    }

    /**
     * public static T methodName(A x, ...);
     */
    public Function(
            final java.lang.String name,
            final Class<?> clazz,
            final Class<?> returnType,
            final java.lang.String methodName,
            final Class<?>... paramTypes) {
        this(name, null, returnType,
                getMethod(clazz, methodName, paramTypes));
    }

    /**
     * public T methodName(A x, ...);
     */
    public Function(
            final java.lang.String name,
            final Object target,
            final Class<?> returnType,
            final java.lang.String methodName,
            final Class<?>... paramTypes) {
        this(name, target, returnType,
                getMethod(target.getClass(), methodName, paramTypes));
    }

    @Override
    protected Internal $internal() {
        return (Internal) super.$internal();
    }

    /**
     * f x
     */
    public B apply(final A x) {
        return $internal().apply(this, x);
    }

    /**
     * f x y ...
     */
    @SuppressWarnings("unchecked")
    public <C> C apply(final Object... args) {
        final Internal internal = eval().$internal();
        if (internal != null) {
            return internal.apply(this, args);
        }
        final B result = apply((A) args[0]);
        if (args.length == 1) {
            return (C) result;
        }
        final Object[] newArgs = new Object[args.length - 1];
        System.arraycopy(args, 1, newArgs, 0, newArgs.length);
        return (C) ((Function<?, ?>) result).apply(newArgs);
    }

    /**
     * (f x y ...)
     */
    public java.lang.String toString(final Object... xs) {
        return expression(toString(), xs);
    }

    /**
     * (f x y ...)
     */
    public static java.lang.String expression(
            final java.lang.String f, final Object... xs) {
        final StringBuilder sb = new StringBuilder();
        sb.append('(');
        sb.append(f);
        for (Object x : xs) {
            sb.append(' ').append(x);
        }
        sb.append(')');
        return sb.toString();
    }

    // ---- reflection utililties

    public static Method getMethod(
            final Class<?> clazz,
            final java.lang.String name,
            final int nParams) {
        for (Method method : clazz.getMethods()) {
            if (name.equals(method.getName())
                    && method.getParameterTypes().length == nParams) {
                return method;
            }
        }
        throw new Exception("no such method: " + name);
    }

    public static Method getMethod(
            final Class<?> clazz,
            final java.lang.String name,
            final Class<?>... paramTypes) {
        try {
            return clazz.getMethod(name, paramTypes);
        } catch (final NoSuchMethodException e) {
            throw new Exception(e);
        }
    }

    @SuppressWarnings("unchecked")
    public static <A> A invoke(
            final Object target,
            final Method method,
            final Object... args) {
        try {
            return (A) method.invoke(target, args);
        } catch (final IllegalAccessException e) {
            throw new Exception(e);
        } catch (final InvocationTargetException e) {
            throw new Exception(e.getCause());
        }
    }

    public static <A> Constructor<A> getConstructor(
            final Class<A> clazz,
            final Class<?>... paramTypes) {
        try {
            final Constructor<A> constructor
                    = clazz.getDeclaredConstructor(paramTypes);
            constructor.setAccessible(true);
            return constructor;
        } catch (final NoSuchMethodException e) {
            throw new Exception(e);
        }
    }

    public static <A> A newInstance(
            final Constructor<A> constructor,
            final Object... args) {
        try {
            return constructor.newInstance(args);
        } catch (final InstantiationException e) {
            throw new Exception(e);
        } catch (final IllegalAccessException e) {
            throw new Exception(e);
        } catch (final InvocationTargetException e) {
            throw new Exception(e.getCause());
        }
    }

    // ---- data a -> b

    /**
     * id :: a -> a
     */
    public static <A> Function<A, A> id() {
        return new Function<A, A>("id") {
            @Override
            public A apply(final A x) {
                return id(x);
            }
        };
    }

    /**
     * id x = x
     */
    public static <A> A id(final A x) {
        return x;
    }

    /**
     * const :: a -> b -> a
     */
    public static <A, B> Function2<A, B, A> const_() {
        return new Function2<A, B, A>("const") {
            @Override
            public A apply(final A x, final B y) {
                return const_(x, y);
            }
        };
    }

    /**
     * const x _ = x
     */
    public static <A, B> A const_(final A x, final B y) {
        return x;
    }

    /**
     * (.) :: (b -> c) -> (a -> b) -> a -> c
     */
    public static <A, B, C> Operator
            <Function<B, C>, Function<A, B>, Function<A, C>> dot() {
        return new Operator<Function<B, C>, Function<A, B>, Function<A, C>>(
                ".") {
            @Override
            public Function<A, C> apply(
                    final Function<B, C> f, final Function<A, B> g) {
                return dot(f, g);
            }
        };
    }

    /**
     * f . g = \x -> f (g x)
     */
    public static <A, B, C> Function<A, C> dot(
            final Function<B, C> f, final Function<A, B> g) {
        return new Function<A, C>() {
            @Override
            public C apply(final A x) {
                return f.apply(g.apply(x));
            }
        };
    }

    /**
     * this . f
     */
    public <C> Function<C, B> _dot_(final Function<C, A> f) {
        return dot(this, f);
    }

    /**
     * flip :: (a -> b -> c) -> b -> a -> c
     */
    public static <A, B, C>
    Function<Function<A, Function<B, C>>, Function2<B, A, C>> flip() {
        return new Function<Function<A, Function<B, C>>, Function2<B, A, C>>(
                "flip") {
            @Override
            public Function2<B, A, C> apply(
                    final Function<A, Function<B, C>> f) {
                return flip(f);
            }
        };
    }

    /**
     * flip f x y = f y x
     */
    public static <A, B, C> Function2<B, A, C> flip(
            final Function<A, Function<B, C>> f) {
        return new Function2<B, A, C>() {
            @Override
            public C apply(final B x, final A y) {
                return f.apply(y, x);
            }
        };
    }

    /**
     * flip f x
     */
    public static <A, B, C> Function<A, C> flip(
            final Function<A, Function<B, C>> f, final B x) {
        return new Function<A, C>() {
            @Override
            public C apply(final A y) {
                return f.apply(y, x);
            }
        };
    }

    /**
     * error :: String -> a
     */
    public static <A> A error(final List<Char> s) {
        return error(s.eval().toString());
    }

    public static <A> A error(final java.lang.String s) {
        throw new Exception(s);
    }

    /**
     * undefined :: a
     */
    public static <A> A undefined() {
        return error("Prelude.undefined");
    }

}
