/*
 * @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.experimental.UtilityClass;
import net.ranides.assira.collection.iterators.ForwardSpliterator;
import net.ranides.assira.events.EventListener;
import net.ranides.assira.events.Events;
import net.ranides.assira.functional.checked.CheckedRunnable;
import net.ranides.assira.generic.Ref;

import java.io.PrintWriter;
import java.io.StringWriter;
import java.util.List;
import java.util.function.Consumer;
import java.util.function.Supplier;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import java.util.stream.StreamSupport;

/**
 * Utility methods for Exception objects.
 *
 * @author ranides
 */
@UtilityClass
public class ExceptionUtils {

    /**
     * Converts exception to String.
     *
     * It returns formatted stacktrace, but exact format is undefined.
     * The point of the method is to return readable and meaningful description of exception.
     *
     * @param cause cause
     * @return String
     */
    public static String toString(Throwable cause) {
        return getStackTrace(cause);
    }

    /**
     * Returns stream with chain of cause exceptions.
     *
     * @param cause cause
     * @return stream
     */
	public static Stream<Throwable> getCauseStream(Throwable cause) {
		return StreamSupport.stream(new CauseSpliterator(cause), false);
	}

    /**
     * Returns list with chain of cause exceptions.
     *
     * @param cause cause
     * @return list
     */
	public static List<Throwable> getCauseList(Throwable cause) {
		return getCauseStream(cause).collect(Collectors.toList());
    }

    /**
     * Returns formatted stacktrace as String
     *
     * @param cause cause
     * @return string
     */
    public static String getStackTrace(Throwable cause) {
        StringWriter result = new StringWriter();
        PrintWriter writer = new PrintWriter(result);
        cause.printStackTrace(writer);
        writer.flush();
        return result.toString();
    }

    /**
     * It is sneaky throw.
     * It throws any passed exception, without declaring it, even check one.
     *
     * It can be used by simple call:
     * ExceptionUtils.rethrow(new IOException(...))
     *
     * But preferred way is to write:
     * throw ExceptionUtils.rethrow(new IOException(...))
     *
     * Second form tells the compiler, that we throw unconditionally,
     * so flow analysis will work correctly.
     *
     * @param cause cause
     * @return should be ingored
     */
    public static RuntimeException rethrow(Throwable cause) {
        new ExceptionErasure<RuntimeException>().rethrow(cause);
        
        // not reachable, wyjątek bez wrappowania leci linię wyżej
        throw new RuntimeException(cause);  // NOPMD
    }
    
    private static class ExceptionErasure<T extends Throwable> { // NOPMD
        
        @SuppressWarnings("unchecked")
        private void rethrow(Throwable exception) throws T {
            throw (T) exception;
        }

    }
	
	private static final class CauseSpliterator extends ForwardSpliterator<Throwable> {
		
		private Throwable current;
		
		public CauseSpliterator(Throwable current) {
			this.current = current;
		}

		@Override
		public boolean tryAdvance(Consumer<? super Throwable> action) {
            if(null == current) {
                return false;
            }
            action.accept(current);
            current = current.getCause();
            return true;
		}

	}

    /**
     * It creates executor, which will collect all thrown exceptions as
     * suppressed objects inside "E" created by provided supplier.
     * Supplier will be called by executor only if some exception is thrown at least once.
     *
     * Returned executor allows to run any code by calling {@link GroupExceptions#run(CheckedRunnable)}
     *
     * @param supplier supplier
     * @param <E> E
     * @return executor
     */
    public static <E extends Exception> GroupExceptions<E> group(Supplier<E> supplier) {
        return new GroupExceptions<>(supplier);
    }

    /**
     * It creates executor, which will send all thrown exceptions to consumer.
     *
     * Returned executor allows to run any code by calling {@link ConsumeExceptions#run(CheckedRunnable)}
     *
     * @param consumer consumer
     * @param <E> E
     * @return executor
     */
    public static <E extends Exception> ConsumeExceptions<E> consume(Consumer<E> consumer) {
        return new ConsumeExceptions<>(consumer);
    }

    /**
     * It creates executor, which will send all thrown exceptions to event listener.
     * Type of generated event is: {@link Events.Failure}.
     *
     * Returned executor allows to run any code by calling {@link ConsumeExceptions#run(CheckedRunnable)}
     *
     * @param listener listener
     * @param <E> E
     * @return executor
     */
    public static <E extends Exception> ObserveExceptions observe(EventListener<? super Events.Failure> listener) {
        return new ObserveExceptions(listener);
    }

    /**
     * Executor which allows execution of arbitrary code and sends events about thrown exceptions.
     */
    public static class ObserveExceptions {
        
        private final EventListener<? super Events.Failure> listener;
        
        ObserveExceptions(EventListener<? super Events.Failure> listener) {
            this.listener = listener;
        }

        /**
         * Executes code and catches thrown exceptions.
         * You can chain calls to this method.
         *
         * @param action action
         * @param <T> T
         * @return this
         */
        public <T extends Exception> ObserveExceptions run(CheckedRunnable<T> action) {
            try {
                action.run();
            } catch(Exception cause) {
                listener.handleEvent(Events.failure(cause));
            }
            return this;
        }
        
    }

    /**
     * Executor which allows execution of arbitrary code and sends thrown exceptions to consumer.
     *
     * @param <E> E
     */
    public static class ConsumeExceptions<E extends Exception> {
        
        private final Consumer<E> consumer;
        
        ConsumeExceptions(Consumer<E> consumer) {
            this.consumer = consumer;
        }

        /**
         * Executes code and catches thrown exceptions.
         * You can chain calls to this method.
         *
         * @param action action
         * @return this
         */
        @SuppressWarnings("unchecked")
        public ConsumeExceptions<E> run(CheckedRunnable<E> action) {
            try {
                action.run();
            } catch(Exception cause) {
                consumer.accept((E)cause);
            }
            return this;
        }
        
    }

    /**
     * Executor which allows execution of arbitrary code and collects thrown exceptions.
     *
     * Exceptions are collected as "suppressed" inside parent exception created by "supplier".
     *
     * Parent exception is created only if at least one exception is thrown.
     *
     * @param <E>
     */
    public static class GroupExceptions<E extends Exception> {
    
        private final Supplier<E> supplier;
        
        private E exception;
        
        GroupExceptions(Supplier<E> supplier) {
            this.supplier = supplier;
        }

        /**
         * Executes code and catches thrown exceptions.
         * You can chain calls to this method.
         *
         * @param action action
         * @param <T> T
         * @return this
         */
        public <T extends Exception> GroupExceptions<E> run(CheckedRunnable<T> action) {
            try {
                action.run();
            } catch(Exception cause) {
                if(exception == null) {
                    exception = supplier.get();
                }
                exception.addSuppressed(cause);
            }
            return this;
        }

        /**
         * Returns "container exception" with cause exceptions added as "suppressed"
         * If there was no error then it returns none
         *
         * @return optional
         */
        public Ref<E> exception() {
            return Ref.ofNullable(exception);
        }

        /**
         * Throws "container exception" if there was any error at execution time.
         * If there was no error then it returns "false".
         *
         * @throws E E
         * @return boolean
         */
        public boolean rethrow() throws E {
            if(null != exception) {
                throw exception;
            }
            return false;
        }
        
    }
    
}
