/*
 * Copyright (c) 2002-2011 JSCAPE
 * 1147 S. 53rd Pl., Mesa, Arizona, 85206, U.S.A.
 * All rights reserved.
 *
 * This software is the confidential and proprietary information of
 * JSCAPE. ("Confidential Information").  You shall not disclose such
 * Confidential Information and shall use it only in accordance with
 *  the terms of the license agreement you entered into with JSCAPE.
 */

package com.jscape.util.io.primitive;

import com.jscape.util.ArrayTools;
import com.jscape.util.io.ByteArrayOutputStream;
import com.jscape.util.io.DataCodec;

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

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

/**
 * @author Alex Usun
 */
public class LineCodec
        implements DataCodec<String> {

    private final byte[] eol;
    private final Charset charset;

    public static void writeValue(byte[] data, int offset, int length, byte[] eol, OutputStream out)
            throws IOException {
        out.write(data, offset, length);
        out.write(eol);
    }

    public static void writeValue(byte[] data, byte[] eol, OutputStream out)
            throws IOException {
        writeValue(data, 0, data.length, eol, out);
    }

    public static void writeValue(String value, byte[] eol, Charset charset, OutputStream out)
            throws IOException {
        byte[] data = value.getBytes(charset);
        writeValue(data, eol, out);
    }

    public static int readValue(byte[] buffer, int offset, int length, byte[] eol, InputStream in)
            throws IOException {
        int read;
        int i = offset;
        int lineLength = 0;
        boolean eolReached = false;
        boolean somethingRead = false;
        while (!eof(read = in.read())) {
            buffer[i++] = (byte) read;
            ++lineLength;
            somethingRead = true;

            if (eolReached(buffer, lineLength, offset + lineLength, eol)) {
                eolReached = true;
                break;
            } else if (lineLength >= length) {
                break;
            }
        }

        if (eolReached) {
            lineLength -= eol.length;
        }

        return somethingRead ? lineLength : -1;
    }

    public static int readValue(byte[] buffer, byte[] eol, InputStream in)
            throws IOException {
        return readValue(buffer, 0, buffer.length, eol, in);
    }

    public static String readValue(byte[] eol, Charset charset, InputStream in)
            throws IOException {
        ByteArrayOutputStream buffer = new ByteArrayOutputStream();

        int read;
        while (!eof(read = in.read())) {
            buffer.write(read);
            if (eolReached(buffer, eol)) {
                break;
            }
        }

        if (!eof(read)) {
            buffer.position(buffer.position() - eol.length);
        }

        return eof(read) && buffer.isEmpty() ? null : new String(buffer.buffer(), 0, buffer.position(), charset);
    }

    public static String readValue(Charset charset, InputStream in)
            throws IOException {
        if (!in.markSupported()) {
            throw new IOException("Unsupported input stream type: markSupported() required");
        }

        ByteArrayOutputStream buffer = new ByteArrayOutputStream();

        int read;
        while (!eof(read = in.read())) {
            buffer.write(read);

            if (eolReached(buffer)) {
                skipPossibleLineFeed(buffer, in);
                break;
            }
        }

        if (!eof(read)) {
            buffer.position(buffer.position() - 1);
        }

        return eof(read) && buffer.isEmpty() ? null : new String(buffer.buffer(), 0, buffer.position(), charset);
    }

    private static boolean eof(int i) {
        return i == -1;
    }

    private static boolean eolReached(byte[] buffer, int dataLength, int bufferLength, byte[] eol) {
        return dataLength >= eol.length && ArrayTools.ends(buffer, eol, bufferLength);
    }

    private static boolean eolReached(ByteArrayOutputStream buffer, byte[] eol) {
        return buffer.position() >= eol.length && ArrayTools.ends(buffer.buffer(), eol, buffer.position());
    }

    private static boolean eolReached(ByteArrayOutputStream buffer) {
        byte lastByte = buffer.buffer()[buffer.position() - 1];
        return lastByte == '\r' || lastByte == '\n';
    }

    private static void skipPossibleLineFeed(ByteArrayOutputStream buffer, InputStream in)
            throws IOException {
        byte lastByte = buffer.buffer()[buffer.position() - 1];

        if (lastByte != '\r') {
            return;
        }

        in.mark(1);
        int read = in.read();
        if (!eof(read) && read != '\n') {
            in.reset();
        }
    }

    public LineCodec(byte[] eol, Charset charset) {
        assertNotNull(eol);
        this.eol = eol;

        assertNotNull(charset);
        this.charset = charset;
    }

    public String read(InputStream in)
            throws IOException {
        return readValue(this.eol, this.charset, in);
    }

    public void write(String value, OutputStream out)
            throws IOException {
        writeValue(value, this.eol, this.charset, out);
    }
}
