package com.googlecode.totallylazy;

import com.googlecode.totallylazy.functions.Block;
import com.googlecode.totallylazy.functions.Function1;
import com.googlecode.totallylazy.predicates.Predicate;

import java.io.PrintWriter;

import static com.googlecode.totallylazy.Left.left;
import static com.googlecode.totallylazy.Option.none;
import static com.googlecode.totallylazy.predicates.Predicates.instanceOf;
import static com.googlecode.totallylazy.predicates.Predicates.notNullValue;
import static com.googlecode.totallylazy.Right.right;
import static com.googlecode.totallylazy.Sequences.iterate;
import static com.googlecode.totallylazy.Sequences.sequence;

public class Exceptions {

    public static Exception toException(Throwable throwable) throws Exception {
        if (throwable instanceof Error) {
            throw (Error) throwable;
        }
        return (Exception) throwable;
    }

    public static <T extends Throwable> Option<T> find(final Throwable throwable, final Class<T> aClass) {
        return causes(throwable).
                safeCast(aClass).
                headOption();
    }

    public static Sequence<Throwable> causes(Throwable throwable) {
        return iterate(getCause(), throwable).
                takeWhile(notNullValue());
    }

    public static Function1<Throwable, Throwable> getCause() {
        return throwable -> throwable.getCause();
    }

    public static final Function1<Exception,String> message = e -> e.getMessage();

    public static Function1<Exception, String> message() {
        return message;
    }

    public static <T, S> Function1<T, Option<S>> ignoringException(final Function1<? super T, ? extends S> callable) {
        return optional(callable);
    }

    @SafeVarargs
    public static <T, S> Function1<T, Option<S>> handleException(final Function1<? super T, ? extends S> callable, final Class<? extends Exception>... exceptionClasses) {
        return handleException(callable, sequence(exceptionClasses).map(asInstanceOf()));
    }

    private static <T> Function1<Class<? extends T>, Predicate<? super T>> asInstanceOf() {
        return aClass -> instanceOf(aClass);
    }

    public static <T, S> Function1<T, Option<S>> handleException(final Function1<? super T, ? extends S> callable, final Predicate<? super Exception> first) {
        return handleException(callable, sequence(first));
    }

    @SafeVarargs
    public static <T, S> Function1<T, Option<S>> handleException(final Function1<? super T, ? extends S> callable, final Predicate<? super Exception>... exceptionClasses) {
        return handleException(callable, sequence(exceptionClasses));
    }

    public static <T, S> Function1<T, Option<S>> handleException(final Function1<? super T, ? extends S> callable, final Iterable<? extends Predicate<? super Exception>> predicates) {
        return t -> {
            try {
                return Option.some(callable.call(t));
            } catch (Exception e) {
                if (sequence(predicates).exists(matches(e))) {
                    return none();
                }
                throw e;
            }
        };
    }

    public static String asString(Exception e) {
        StringPrintStream stream = new StringPrintStream();
        e.printStackTrace(stream);
        return stream.toString();
    }

    private static <T> Predicate<? super Predicate<? super T>> matches(final T instance) {
        return other -> other.matches(instance);
    }

    public static <T, S> Function1<T, Either<S, Throwable>> captureException(final Function1<? super T, ? extends S> callable) {
        return input -> {
            try {
                return left(callable.call(input));
            } catch (Throwable e) {
                return right(e);
            }
        };
    }

    public static Block<PrintWriter> printStackTrace(final Throwable e) {
        return e::printStackTrace;
    }

    public static <A, B> Function1<A, Either<Exception, B>> either(final Function1<? super A, ? extends B> callable) {
        return a -> Either.either(() -> callable.call(a));
    }

    public static <A, B> Function1<A, B> orElse(final Function1<? super A, ? extends B> callable, final B result) {
        return a -> {
            try {
                return callable.call(a);
            } catch (Exception e) {
                return result;
            }
        };
    }

    public static <T, S> Function1<T, Option<S>> optional(final Function1<? super T, ? extends S> callable) {
        return t -> Option.option(() -> callable.call(t));
    }
}
