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

import java.io.PrintWriter;
import java.io.StringWriter;
import java.util.List;
import java.util.Optional;
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;
import net.ranides.assira.collection.iterators.ForwardSpliterator;
import net.ranides.assira.events.EventListener;
import net.ranides.assira.events.Events;
import net.ranides.assira.functional.CheckedRunnable;

/**
 *
 * @author ranides
 */
public final class ExceptionUtils {

    private ExceptionUtils() { }
    
    public static String toString(Throwable cause) {
        StringWriter out = new StringWriter();
        cause.printStackTrace(new PrintWriter(out));
        return out.toString();
    }
	
	public static Stream<Throwable> getCauseStream(Throwable cause) {
		return StreamSupport.stream(new CauseSpliterator(cause), false);
	}
	
	public static List<Throwable> getCauseList(Throwable cause) {
		return getCauseStream(cause).collect(Collectors.toList());
    }

    public static String getStackTrace(Throwable cause) {
        StringWriter result = new StringWriter();
        PrintWriter writer = new PrintWriter(result);
        cause.printStackTrace(writer);
        writer.flush();
        return result.toString();
    }

    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;
		}

	}
    
    public static <E extends Exception> GroupExceptions<E> group(Supplier<E> supplier) {
        return new GroupExceptions<>(supplier);
    }
    
    public static <E extends Exception> ConsumeExceptions<E> consume(Consumer<E> consumer) {
        return new ConsumeExceptions<>(consumer);
    }
    
    public static <E extends Exception> ObserveExceptions observe(EventListener<? super Events.Failure> listener) {
        return new ObserveExceptions(listener);
    }
    
    public static class ObserveExceptions {
        
        private final EventListener<? super Events.Failure> listener;
        
        ObserveExceptions(EventListener<? super Events.Failure> listener) {
            this.listener = listener;
        }
        
        public <T extends Exception> ObserveExceptions run(CheckedRunnable<T> action) {
            try {
                action.run();
            } catch(Exception cause) {
                listener.handleEvent(Events.failure(cause));
            }
            return this;
        }
        
    }
    
    public static class ConsumeExceptions<E extends Exception> {
        
        private final Consumer<E> consumer;
        
        ConsumeExceptions(Consumer<E> consumer) {
            this.consumer = consumer;
        }
        
        public ConsumeExceptions<E> run(CheckedRunnable<E> action) {
            try {
                action.run();
            } catch(Exception cause) {
                consumer.accept((E)cause);
            }
            return this;
        }
        
    }
    
    public static class GroupExceptions<E extends Exception> {
    
        private final Supplier<E> supplier;
        
        private E exception;
        
        GroupExceptions(Supplier<E> supplier) {
            this.supplier = supplier;
        }
        
        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;
        }
        
        public Optional<E> cause() {
            return Optional.ofNullable(exception);
        }
        
        public void rethrow() throws E {
            if(null != exception) {
                throw exception;
            }
        }
        
    }
    
}
