package com.github.davidmoten.rx2;

import java.io.BufferedInputStream;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
import java.util.Arrays;
import java.util.concurrent.Callable;
import java.util.zip.ZipEntry;
import java.util.zip.ZipInputStream;

import com.github.davidmoten.rx2.util.ZippedEntry;

import io.reactivex.Emitter;
import io.reactivex.Flowable;
import io.reactivex.Single;
import io.reactivex.functions.BiConsumer;
import io.reactivex.functions.Consumer;
import io.reactivex.functions.Function;

public final class Bytes {

    private static final int DEFAULT_BUFFER_SIZE = 8192;

    private Bytes() {
        // prevent instantiation
    }

    /**
     * Returns a Flowable stream of byte arrays from the given
     * {@link InputStream} between 1 and {@code bufferSize} bytes.
     * 
     * @param is
     *            input stream of bytes
     * @param bufferSize
     *            max emitted byte array size
     * @return a stream of byte arrays
     */
    public static Flowable<byte[]> from(final InputStream is, final int bufferSize) {
        return Flowable.generate(new Consumer<Emitter<byte[]>>() {
            @Override
            public void accept(Emitter<byte[]> emitter) throws Exception {
                byte[] buffer = new byte[bufferSize];
                int count = is.read(buffer);
                if (count == -1) {
                    emitter.onComplete();
                } else if (count < bufferSize) {
                    emitter.onNext(Arrays.copyOf(buffer, count));
                } else {
                    emitter.onNext(buffer);
                }
            }
        });
    }

    public static Flowable<byte[]> from(File file) {
        return from(file, 8192);
    }

    public static Flowable<byte[]> from(final File file, final int size) {
        Callable<InputStream> resourceFactory = new Callable<InputStream>() {

            @Override
            public InputStream call() throws FileNotFoundException {
                return new BufferedInputStream(new FileInputStream(file), size);
            }
        };
        Function<InputStream, Flowable<byte[]>> observableFactory = new Function<InputStream, Flowable<byte[]>>() {

            @Override
            public Flowable<byte[]> apply(InputStream is) {
                return from(is, size);
            }
        };
        return Flowable.using(resourceFactory, observableFactory, InputStreamCloseHolder.INSTANCE, true);
    }

    private static final class InputStreamCloseHolder {
        static final Consumer<InputStream> INSTANCE = new Consumer<InputStream>() {

            @Override
            public void accept(InputStream is) throws IOException {
                is.close();
            }
        };
    }

    /**
     * Returns a Flowable stream of byte arrays from the given
     * {@link InputStream} of {@code 8192} bytes. The final byte array may be
     * less than {@code 8192} bytes.
     * 
     * @param is
     *            input stream of bytes
     * @return a stream of byte arrays
     */
    public static Flowable<byte[]> from(InputStream is) {
        return from(is, DEFAULT_BUFFER_SIZE);
    }

    public static Flowable<ZippedEntry> unzip(final File file) {
        Callable<ZipInputStream> resourceFactory = new Callable<ZipInputStream>() {
            @Override
            public ZipInputStream call() throws FileNotFoundException {
                return new ZipInputStream(new FileInputStream(file));
            }
        };
        Function<ZipInputStream, Flowable<ZippedEntry>> observableFactory = ZipHolder.OBSERVABLE_FACTORY;
        Consumer<ZipInputStream> disposeAction = ZipHolder.DISPOSER;
        return Flowable.using(resourceFactory, observableFactory, disposeAction);
    }

    public static Flowable<ZippedEntry> unzip(final InputStream is) {
        return unzip(new ZipInputStream(is));
    }

    public static Flowable<ZippedEntry> unzip(final ZipInputStream zis) {

        return Flowable.generate(new Consumer<Emitter<ZippedEntry>>() {
            @Override
            public void accept(Emitter<ZippedEntry> emitter) throws IOException {
                ZipEntry zipEntry = zis.getNextEntry();
                if (zipEntry != null) {
                    emitter.onNext(new ZippedEntry(zipEntry, zis));
                } else {
                    // end of stream so eagerly close the stream (might not be a
                    // good idea since this method did not create the zis
                    zis.close();
                    emitter.onComplete();
                }
            }
        });

    }

    public static Single<byte[]> collect(Flowable<byte[]> source) {
        return source.collect(BosCreatorHolder.INSTANCE, BosCollectorHolder.INSTANCE).map(BosToArrayHolder.INSTANCE);
    }

    public static Function<Flowable<byte[]>, Single<byte[]>> collect() {
        return new Function<Flowable<byte[]>, Single<byte[]>>() {
            @Override
            public Single<byte[]> apply(Flowable<byte[]> source) throws Exception {
                return collect(source);
            }
        };
    }

    private static final class BosCreatorHolder {
        static final Callable<ByteArrayOutputStream> INSTANCE = new Callable<ByteArrayOutputStream>() {

            @Override
            public ByteArrayOutputStream call() {
                return new ByteArrayOutputStream();
            }
        };
    }

    private static final class BosCollectorHolder {
        static final BiConsumer<ByteArrayOutputStream, byte[]> INSTANCE = new BiConsumer<ByteArrayOutputStream, byte[]>() {

            @Override
            public void accept(ByteArrayOutputStream bos, byte[] bytes) throws IOException {
                bos.write(bytes);
            }
        };
    }

    private static final class BosToArrayHolder {
        static final Function<ByteArrayOutputStream, byte[]> INSTANCE = new Function<ByteArrayOutputStream, byte[]>() {
            @Override
            public byte[] apply(ByteArrayOutputStream bos) {
                return bos.toByteArray();
            }
        };
    }

    private static final class ZipHolder {
        static final Consumer<ZipInputStream> DISPOSER = new Consumer<ZipInputStream>() {

            @Override
            public void accept(ZipInputStream zis) throws IOException {
                zis.close();
            }
        };
        final static Function<ZipInputStream, Flowable<ZippedEntry>> OBSERVABLE_FACTORY = new Function<ZipInputStream, Flowable<ZippedEntry>>() {
            @Override
            public Flowable<ZippedEntry> apply(ZipInputStream zis) {
                return unzip(zis);
            }
        };
    }

}