package com.jscape.util.io.primitive;

import com.jscape.util.ArrayTools;
import com.jscape.util.IOTools;
import com.jscape.util.SuppressObfuscation;
import com.jscape.util.io.ByteArrayOutputStream;

import java.io.EOFException;
import java.io.IOException;
import java.io.InputStream;
import java.nio.charset.Charset;

import static com.jscape.util.Parameters.assertGreater;
import static com.jscape.util.Parameters.assertNotNull;

/**
 * The common line reader. Provides services for reading lines ending with '\r', '\n' or '\r\n'.
 *
 * @author Alex Usun
 */
@SuppressObfuscation
public class CommonLineReader
        implements AutoCloseable {

    private static final int CR = '\r';
    private static final int LF = '\n';

    private final InputStream in;
    private final int maxLineLength;
    private final ByteArrayOutputStream buffer;
    private int previousChar;

    /**
     * Reads an expected line using specified reader.
     *
     * @param reader a reader
     *
     * @return a line
     *
     * @throws IOException  if an error occurs
     * @throws EOFException if no lines are present in reader
     */
    public static byte[] readLine(CommonLineReader reader)
            throws IOException {
        byte[] data = reader.readLine();

        if (data == null) {
            throw new EOFException();
        }

        return data;
    }

    /**
     * Reads an expected line using specified reader.
     *
     * @param reader  a reader
     * @param charset a charset
     *
     * @return a line
     *
     * @throws IOException  if an error occurs
     * @throws EOFException if no lines are present in reader
     */
    public static String readLine(CommonLineReader reader, Charset charset)
            throws IOException {
        String data = reader.readLine(charset);

        if (data == null) {
            throw new EOFException();
        }

        return data;
    }

    /**
     * Creates a new reader instance.
     *
     * @param in            an input stream
     * @param maxLineLength max line length
     */
    public CommonLineReader(InputStream in, int maxLineLength) {
        assertNotNull(in);
        this.in = in;

        assertGreater(maxLineLength, 0, "Bad max line length.");
        this.maxLineLength = maxLineLength;

        this.buffer = new ByteArrayOutputStream();
    }

    /**
     * Creates a new reader instance with Integer.MAX_VALUE line length.
     *
     * @param in an input stream
     */
    public CommonLineReader(InputStream in) {
        this(in, Integer.MAX_VALUE);
    }

    /**
     * Reads a next line.
     *
     * @return next line bytes or <code>null</code> if EOF is reached
     *
     * @throws IOException if an I/O error occurs
     */
    public byte[] readLine()
            throws IOException {
        this.buffer.reset();

        int ch;
        while ((ch = this.in.read()) != IOTools.EOF) {
            if (ch == CR) {
                this.previousChar = ch;
                return makeLine();
            } else if (ch == LF && this.previousChar == CR) {
                this.previousChar = ch;
            } else if (ch == LF) {
                this.previousChar = ch;
                return makeLine();
            } else {
                this.buffer.write(ch);
            }

            if (this.buffer.position() >= this.maxLineLength) {
                return makeLine();
            }
        }

        return this.buffer.isEmpty() ? null : makeLine();
    }

    public int readLine(byte[] buffer)
            throws IOException {
        int read = 0;
        int ch;
        while ((ch = this.in.read()) != IOTools.EOF) {
            if (ch == CR) {
                this.previousChar = ch;
                return read;
            } else if (ch == LF && this.previousChar == CR) {
                this.previousChar = ch;
            } else if (ch == LF) {
                this.previousChar = ch;
                return read;
            } else {
                buffer[read++] = (byte) ch;
            }

            if (read >= buffer.length) {
                return read;
            }
        }

        return read != 0 ? read : IOTools.EOF;
    }

    /**
     * Reads a next line as string.
     *
     * @param charset a charset
     *
     * @return next line string or <code>null</code> if EOF is reached
     *
     * @throws IOException if an I/O error occurs
     */
    public String readLine(Charset charset)
            throws IOException {
        this.buffer.reset();

        int ch;
        while ((ch = this.in.read()) != IOTools.EOF) {
            if (ch == CR) {
                this.previousChar = ch;
                return makeLine(charset);
            } else if (ch == LF && this.previousChar == CR) {
                this.previousChar = ch;
            } else if (ch == LF) {
                this.previousChar = ch;
                return makeLine(charset);
            } else {
                this.buffer.write(ch);
            }

            if (this.buffer.position() >= this.maxLineLength) {
                return makeLine(charset);
            }
        }

        return this.buffer.isEmpty() ? null : makeLine(charset);
    }

    public void close() {
        IOTools.close(this.in);
        this.buffer.reset();
    }

    public void consumeLF()
            throws IOException {
        if (this.previousChar != CR) {
            return;
        }

        int ch = this.in.read();

        if (ch == LF) {
            this.previousChar = LF;
        }
    }

    @Override
    public String toString() {
        return "CommonLineReader {"
                + "in=" + this.in
                + ", maxLineLength=" + this.maxLineLength
                + ", buffer=" + this.buffer
                + ", previousEol=" + this.previousChar
                + '}';
    }

    private byte[] makeLine() {
        return ArrayTools.copy(this.buffer.buffer(), 0, this.buffer.position());
    }

    private String makeLine(Charset charset) {
        return new String(this.buffer.buffer(), 0, this.buffer.position(), charset);
    }
}
