package com.github.vincentrussell.json.datagenerator.impl;

import com.google.common.base.Charsets;
import org.apache.commons.io.FileUtils;
import org.apache.commons.io.IOUtils;

import java.io.ByteArrayInputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.RandomAccessFile;
import java.io.StringWriter;
import java.util.Arrays;

/**
 * {@link OutputStream} that will write to an byte array before overflowing to a file
 */
public class ByteArrayBackupToFileOutputStream extends OutputStream {

    private static final int DEFAULT_INITIAL_BUFFER_SIZE = 1028;
    private static final int DEFAULT_SIZE_BEFORE_OVER_FLOW = 1024000;
    private byte[] buf;

    private int count;
    private final int sizeBeforeOverFlow;
    private File file = null;
    private FileOutputStream fileOutputStream = null;
    private long lastMark = 0;

    /**
     * constructor with default settings
     */
    public ByteArrayBackupToFileOutputStream() {
        this(DEFAULT_INITIAL_BUFFER_SIZE, DEFAULT_SIZE_BEFORE_OVER_FLOW);
    }

    /**
     * constructor
     * @param initialBufferSize initial buffer size in bytes
     * @param sizeBeforeOverFlow size in bytes before overflow to file
     */
    public ByteArrayBackupToFileOutputStream(final int initialBufferSize,
        final int sizeBeforeOverFlow) {
        if (sizeBeforeOverFlow < 0) {
            throw new IllegalArgumentException(
                "Negative initial sizeBeforeOverFlow: " + sizeBeforeOverFlow);
        }
        buf = new byte[initialBufferSize];
        this.sizeBeforeOverFlow = sizeBeforeOverFlow;
    }

    private void ensureCapacity(final int minCapacity) throws IOException {
        if (buf != null && minCapacity - buf.length > 0 && minCapacity <= sizeBeforeOverFlow) {
            grow(minCapacity);
            return;
        }

        if (minCapacity > sizeBeforeOverFlow && file == null) {
            file = File.createTempFile("temp", "temp");
            fileOutputStream = new FileOutputStream(file);
            fileOutputStream.write(buf);
            buf = null;
            return;
        }
    }

    private void grow(final int minCapacity) {
        // overflow-conscious code
        int oldCapacity = buf.length;
        int newCapacity = oldCapacity << 1;
        if (newCapacity > sizeBeforeOverFlow) {
            newCapacity = sizeBeforeOverFlow;
        }
        if (newCapacity - minCapacity < 0) {
            newCapacity = minCapacity;
        }
        if (newCapacity < 0) {
            if (minCapacity < 0) { // overflow
                throw new OutOfMemoryError();
            }
            newCapacity = Integer.MAX_VALUE;
        }
        buf = Arrays.copyOf(buf, newCapacity);
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public synchronized void write(final int b) throws IOException {
        ensureCapacity(count + 1);
        if (buf == null) {
            fileOutputStream.write(b);
            return;
        }
        buf[count] = (byte) b;
        count += 1;
    }

    /**
     * remove one byte from the written outputstream
     * @throws IOException if there is no more buffer to unwrite from
     */
    public void unwrite() throws IOException {
        if (count == 0) {
            throw new IOException("Pushback buffer overflow");
        }
        if (buf == null) {
            @SuppressWarnings("resource") RandomAccessFile randomAccessFile =
                new RandomAccessFile(file, "rw");
            randomAccessFile.setLength(file.length() - 1);
            return;
        }


        buf[--count] = (byte) 0;
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public synchronized void write(final byte[] b, final int off, final int len)
        throws IOException {
        if ((off < 0) || (off > b.length) || (len < 0) || ((off + len) - b.length > 0)) {
            throw new IndexOutOfBoundsException();
        }
        ensureCapacity(count + len);
        if (buf == null) {
            fileOutputStream.write(b);
            return;
        }
        System.arraycopy(b, off, buf, count, len);
        count += len;
    }

    /**
     * get the number or bytes written to the {@link OutputStream}
     * @return the size
     */
    public synchronized int size() {
        return count;
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public synchronized String toString() {
        if (buf == null) {
            try {
                try (InputStream inputStream = new FileInputStream(file)) {
                    StringWriter writer = new StringWriter();
                    IOUtils.copy(inputStream, writer, "UTF-8");
                    return writer.toString();
                }
            } catch (IOException e) {
                throw new RuntimeException(e);
            }
        } else {
            return new String(buf, 0, count, Charsets.UTF_8);
        }
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public void close() throws IOException {
        if (fileOutputStream != null) {
            fileOutputStream.close();
        }
        if (file != null) {
            FileUtils.forceDelete(file);
        }
    }

    /**
     * create an {@link InputStream} based on the data written to the {@link OutputStream}
     * @return the new {@link InputStream}
     * @throws IOException if the {@link OutputStream} can not be flushed or closed
     */
    public InputStream getNewInputStream() throws IOException {
        if (buf != null) {
            return new ByteArrayInputStream(Arrays.copyOf(buf, count));
        } else {
            fileOutputStream.flush();
            return new FileInputStream(file);
        }
    }

    /**
     * Marks the current position in this input stream.
     * @throws IOException if the length of {@link OutputStream} cannot be retrieved
     */
    public void mark() throws IOException {
        lastMark = getLength();
    }

    /**
     * Repositions this stream to the position at the time the mark method was
     * last called on this input stream.
     * @throws IOException if the length of {@link OutputStream} cannot be set
     */
    public void reset() throws IOException {
        if (lastMark <= 0) {
            throw new IOException("mark has not been set yet.");
        }
        setLength(lastMark);
    }

    private void shrink(final int newCapacity) {
        buf = Arrays.copyOf(buf, newCapacity);
        count = newCapacity;
    }

    /**
     * set the length of the {@link OutputStream}
     * @param length the length in bytes
     * @throws IOException if the length is greater than the {@link OutputStream} size
     */
    public void setLength(final long length) throws IOException {
        if (buf != null) {
            if (length > buf.length) {
                throw new IllegalStateException(
                    "length: " + length + " is greater than buffer length");
            }
            shrink((int) length);
        } else {
            if (length > file.length()) {
                throw new IllegalStateException(
                    "length: " + length + " is greater than file length");
            }
            @SuppressWarnings("resource") RandomAccessFile randomAccessFile =
                new RandomAccessFile(file, "rw");
            randomAccessFile.setLength(length);
            fileOutputStream = new FileOutputStream(file, true);
        }

    }

    /**
     * get the byte length of the {@link OutputStream}
     * @return the length in bytes
     */
    public long getLength() {
        if (buf != null) {
            return count;
        } else {
            return file.length();
        }
    }
}