/*
 * @author Ranides Atterwim {@literal <ranides@gmail.com>}
 * @copyright Ranides Atterwim
 * @license WTFPL
 * @url http://ranides.net/projects/assira
 */
package net.ranides.assira.trace;

import lombok.AccessLevel;
import lombok.RequiredArgsConstructor;
import lombok.Setter;
import lombok.experimental.Accessors;

import lombok.experimental.UtilityClass;
import net.ranides.assira.functional.special.AnyFunction;
import net.ranides.assira.functional.Compositor;
import net.ranides.assira.functional.Compositor.CompositorBuilder;
import net.ranides.assira.functional.Compositor.CompositorScope;
import net.ranides.assira.functional.FunctionUtils;
import net.ranides.assira.functional.Functions.Function0;
import net.ranides.assira.generic.Ref;
import net.ranides.assira.reflection.IClass;

import java.text.MessageFormat;
import java.util.ArrayList;
import java.util.List;
import java.util.Optional;
import java.util.function.Supplier;

/**
 * PreparedException encapsulates in one object exception class, message and optional arguments.
 * It is useful if we want to simplify throw statements inside method bodies.
 *
 * It uses {@link MessageFormat#format}
 * Message patterns should use 0-based argument indexes.
 *
 * PreparedException must be constructed using builder.
 *
 * Functions.Function3<Integer, Integer, String, IllegalArgumentException> error =
 *      PreparedException
 *          .runtime(IllegalArgumentException.class)
 *          .hideHelpers()
 *          .message("Expected {0} but {1} is passed inside {2}.")
 *          .hide(BigDecimal.class)
 *          .params()
 *             .withParam(Integer.class)
 *             .withParam(Integer.class)
 *             .withParam(String.class)
 *          .function();
 *
 * @author Ranides Atterwim {@literal <ranides@gmail.com>}
 */
@UtilityClass public class PreparedException {

    /**
     * Prepare new unchecked exception.
     *
     * @param type type of exception
     * @param <E> type of exception
     * @return ExceptionBuilder
     */
    public static <E extends RuntimeException> ExceptionBuilder<E> runtime(Class<E> type) {
        return new ExceptionBuilder<>(IClass.typeinfo(type));
    }

    /**
     * Prepare new checked exception.
     *
     * @param type type of exception
     * @param <E> type of exception
     * @return ExceptionBuilder
     */
    public static <E extends Exception> ExceptionBuilder<E> checked(Class<E> type) {
        return new ExceptionBuilder<>(IClass.typeinfo(type));
    }

    /**
     * Builder pattern for PreparedException.
     *
     * @param <T>
     */
    @Setter
    @Accessors(fluent = true)
    @RequiredArgsConstructor(access = AccessLevel.PRIVATE)
    public static class ExceptionBuilder<T extends Throwable> {
        private final IClass<T> type;
        private String message;
        private final List<String> hideFrames = new ArrayList<>();

        /**
         * Remove from exception stacktrace helper methods, to reduce noise.
         *
         * @return this
         */
        public ExceptionBuilder<T> hideHelpers() {
            return this
                .hide(Compositor.class)
                .hide(FunctionUtils.class)
                .hide(Optional.class)
                .hide(Ref.class)
                .hide(PreparedException.class);
        }

        /**
         * Remove from exception stacktrace methods of specified class (including inner classes).
         *
         * @param ignoreClass class to ignore
         * @return this
         */
        public ExceptionBuilder<T> hide(Class<?> ignoreClass) {
            return hide(ignoreClass.getName());
        }

        /**
         * Remove from exception stacktrace methods of specified class (including inner classes).
         *
         * @param className class to ignore
         * @return this
         */
        public ExceptionBuilder<T> hide(String className) {
            hideFrames.add(className);
            return this;
        }

        /**
         * Define parameters required to generate error message.
         *
         * @return this
         */
        public CompositorScope<ExceptionBuilder<T>, Object, T>.Compositor0<Object, Object, Object, Object, T> params() {
            return Compositor.compose(new Decorator());
        }

        /**
         * Build prepared exception.
         *
         * @return construct exception factory, for later use
         */
        public Supplier<T> function() {
            return () -> construct0(message);
        }

        /**
         * This decorator is used to create prepared exceptions which build message text using additional arguments
         *
         * @since 2021-05-05
         */
        private class Decorator extends Compositor.CompositorDecorator<ExceptionBuilder<T>, Object, T> {
            @Override
            public AnyFunction<T> after(CompositorBuilder<Object, T> function) {
                return args -> {
                    try {
                        return construct0(MessageFormat.format(message, args));
                    } catch (IllegalArgumentException cause) {
                        throw new RuntimeException(message, cause);
                    }
                };
            }

            @Override
            public Function0<T> after(Function0<T> action) {
                return () -> construct0(message);
            }
        }

        private T construct0(String text) {
                T exception = type.construct(text);
                if (!hideFrames.isEmpty()) {
                    cleanupStackTrace(exception);
                }
                return exception;
        }

        private void cleanupStackTrace(Throwable exception) {
            StackTraceElement[] cleaned = TraceUtils.getFrames()
                .stream()
                .skip(1)
                .filter(this::isVisible)
                .toArray(StackTraceElement[]::new);
            exception.setStackTrace(cleaned);
        }

        private boolean isVisible(StackTraceElement frame) {
            String className = frame.getClassName();
            return hideFrames.stream().noneMatch(className::startsWith);
        }
    }
}
