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

import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;

import lombok.experimental.UtilityClass;
import net.ranides.assira.events.EventListener;
import net.ranides.assira.functional.checked.CheckedConsumer;
import net.ranides.assira.functional.checked.CheckedSupplier;
import net.ranides.assira.trace.ExceptionUtils;
import net.ranides.assira.trace.LoggerUtils;

import java.io.Closeable;
import java.io.Flushable;
import java.io.IOException;
import java.util.function.Consumer;

/**
 * Utility methods for I/O operations, with rudimentary exception handling.
 *
 * @author Ranides Atterwim {@literal <ranides@gmail.com>}
 */
@UtilityClass
public class IOUtils {
    
    private static final org.slf4j.Logger LOGGER = LoggerUtils.getLogger();

    /**
     * If provided exception is IOException then returns it.
     * In other cases it returns IOException with provided cause.
     *
     * @param cause cause
     * @return IOException
     */
    public static IOException wrap(Throwable cause) {
        return (cause instanceof IOException) ? (IOException)cause : new IOException(cause);
    }

    /**
     * Throws provided exception, converting them to IOException if necessary.
     * It uses sneaky throw.
     *
     * @see ExceptionUtils#rethrow(Throwable)
     * @param cause cause
     * @return exception
     */
    public static RuntimeException rethrow(Throwable cause) {
        return ExceptionUtils.rethrow(wrap(cause));
    }

    /**
     * Adds "current" exception to suppressed list stored in "first"
     *
     * @param first first
     * @param current current
     * @return "first"
     */
    public static IOException addSuppressed(IOException first, Throwable current) {
        if(null == first) {
            first = new IOException("There is one or more internal IOException. Check suppressed exceptions.");
        }
        first.addSuppressed(current);
        return first;
    }

    /**
     * Closes provided resource and returns "true" if resource is not "null".
     *
     * This method is null-safe.
     *
     * @param object object
     * @return boolean
     * @throws IOException on error
     */
    public static boolean close(Closeable object) throws IOException {
        if(null != object) {
            object.close();
            return true;
        }
        return false;
    }

    /**
     * Closes provided resource and returns "true" if resource is not "null".
     * Sends exception to provided "handler". If handler is "null", it uses default logger.
     *
     * This method is null-safe.
     *
     * @param object object
     * @param handler handler
     * @return boolean
     */
    public static boolean close(Closeable object, Consumer<IOException> handler) {
        try {
            return close(object);
        } catch(IOException cause) {
            if(null != handler) {
                handler.accept(cause);
            } else {
                LOGGER.warn("Exception ignored on #close", cause);
            }
            return false;
        }
    }

    /**
     * Closes provided resource and returns "true" if at least one resource is not null.
     * It does not stop processing after first exception.
     * If more than one exception is thrown then it is added to supressed list.
     *
     * This method is null-safe.
     *
     * @param objects objects
     * @return boolean
     * @throws IOException on error
     */
    public static boolean closeAll(Closeable... objects) throws IOException {
        IOException error = null;
        boolean closed = false;

        for(Closeable object : objects) {
            try {
                closed |= close(object);
            } catch(IOException cause) {
                error = addSuppressed(error, cause);
            }
        }

        if(error != null) {
            throw error;
        }
        return closed;
    }

    /**
     * Closes provided resource and returns "true" if resource is not "null".
     * It supports Closeable and AutoCloseable objects.
     *
     * This method is null-safe.
     *
     * @param object object
     * @return boolean
     * @throws Exception on error
     */
    public static boolean closeAny(Object object) throws Exception {
        if(object instanceof Closeable) {
            ((Closeable)object).close();
            return true;
        }
        if(object instanceof AutoCloseable) {
            ((AutoCloseable)object).close();
            return true;
        }
        return false;
    }

    /**
     * Closes provided resource and returns "true" if resource is not "null".
     * Sends exception to provided "handler". If handler is "null", it uses default logger.
     * It supports Closeable and AutoCloseable objects.
     *
     * This method is null-safe.
     *
     * @param object object
     * @param handler handler
     * @return boolean
     */
    public static boolean closeAny(Object object, Consumer<Exception> handler) {
        try {
            return closeAny(object);
        } catch(Exception cause) {
            if(null != handler) {
                handler.accept(cause);
            } else {
                LOGGER.warn("Exception ignored on #close", cause);
            }
            return false;
        }
    }

    /**
     * Flushes provided resource and returns "true" if resource is not "null".
     *
     * This method is null-safe.
     *
     * @param object object
     * @return boolean
     * @throws IOException on error
     */
    public static boolean flush(Flushable object) throws IOException {
        if(null != object) {
            object.flush();
            return true;
        }
        return false;
    }

    /**
     * Flushes provided resource and returns "true" if resource is not "null".
     * Sends exception to provided "listener". If listener is "null", it uses default logger.
     *
     * This method is null-safe.
     *
     * @param object object
     * @param listener listener
     * @return boolean
     */
    public static boolean flush(Flushable object, EventListener<? super IOEvent.Failure> listener) {
        try {
            return flush(object);
        } catch(IOException cause) {
            if(null != listener) {
                listener.handleEvent(IOEvent.failure(cause));
            } else {
                LOGGER.warn("Exception ignored on #flush", cause);
            }
            return false;
        }
    }

    /**
     * Flushes provided resource and returns "true" if resource is not "null".
     * It supports Flushable objects.
     *
     * This method is null-safe.
     *
     * @param object object
     * @return boolean
     * @throws Exception on error
     */
    public static boolean flushAny(Object object) throws Exception {
        if(object instanceof Flushable) {
            ((Flushable)object).flush();
            return true;
        }
        return false;
    }

    /**
     * Flushes provided resource and returns "true" if resource is not "null".
     * Sends exception to provided "handler". If handler is "null", it uses default logger.
     * It supports Flushable objects.
     * 
     * This method is null-safe.
     *
     * @param object object
     * @param handler handler
     * @return boolean
     */
    public static boolean flushAny(Object object, Consumer<Exception> handler) {
        try {
            return flushAny(object);
        } catch(Exception cause) {
            if(null != handler) {
                handler.accept(cause);
            } else {
                LOGGER.warn("Exception ignored on #flush", cause);
            }
            return false;
        }
    }
    
    @SuppressWarnings({"DoubleCheckedLocking"})
    @SuppressFBWarnings({"DC_DOUBLECHECK","IS2_INCONSISTENT_SYNC"})
    static final class SharedHandle<T> implements IOHandle<T> {
        
        private final CheckedSupplier<T,IOException> open;
        
        private final CheckedConsumer<T,IOException> close;

        private SharedWrapper<T> ref = null;

        @SuppressWarnings("unchecked")
        SharedHandle(CheckedSupplier<T,IOException> open) {
            this.close = resource -> {
                if(resource instanceof Closeable) {
                    ((Closeable)resource).close();
                }
            };
            this.open = open;
        }

        SharedHandle(CheckedConsumer<T,IOException> close, CheckedSupplier<T,IOException> open) {
            this.close = close;
            this.open = open;
        }
        
        @Override
        public void close() {
            SharedWrapper<T> temp = ref;
            if (temp == null) {
                synchronized(this) {
                    temp = ref;
                    if (temp == null) {
                        return;
                    }
                }
            }
            if(null != ref.value) {
                close.$accept(ref.value);
            }
        }

        @Override
        public T get() throws IOException {
            SharedWrapper<T> temp = ref;
            if (temp == null) {
                synchronized(this) {
                    temp = ref;
                    if (temp == null) {
                        ref = temp = new SharedWrapper<>(open.get());
                    }
                }
            }
            return temp.value;
        }

        @Override
        public boolean opened() {
            SharedWrapper<T> temp = ref;
            if (temp == null) {
                synchronized(this) {
                    temp = ref;
                    if (temp == null) {
                        return false;
                    }
                }
            }
            return null != temp.value;
        }
        
        private static final class SharedWrapper<T> {

            public final T value;

            public SharedWrapper(T value) {
                this.value = value;
            }

        }

    }

}
