package ru.yandex.webmaster3.storage.util.yt;

import java.io.IOException;
import java.io.InputStream;
import java.util.List;

import org.apache.commons.io.IOUtils;
import org.apache.commons.io.output.NullOutputStream;

/**
 * @author aherman
 */
class SchemafulDsvParser<T> implements InterruptableIterator<T> {
    private final YtRowMapper<T> rowMapper;
    private FieldIterator fieldIterator;
    private T nextValue;
    private long row = -1;

    private NullOutputStream streamEndConsumer = new NullOutputStream();
    private byte[] copyBuffer = new byte[4 * 1024];

    SchemafulDsvParser(InputStream is, YtRowMapper<T> rowMapper) {
        this.fieldIterator = new FieldIterator(is, 4 * 1024);
        this.rowMapper = rowMapper;
    }

    SchemafulDsvParser(InputStream is, YtRowMapper<T> rowMapper, int size) {
        this.fieldIterator = new FieldIterator(is, size);
        this.rowMapper = rowMapper;
    }

    @Override
    public long getRow() {
        return row;
    }

    @Override
    public void close() throws IOException {
        this.fieldIterator = null;
    }

    @Override
    public boolean hasNext() throws InterruptedException, IOException, YtException {
        if(nextValue != null) {
            return true;
        }
        if (fieldIterator.hasNext()) {
            nextValue = readNextValue();
        }
        return nextValue != null;
    }

    @Override
    public T next() throws InterruptedException, IOException, YtException {
        T result = nextValue;
        if (!fieldIterator.hasNext()) {
            nextValue = null;
            row++;
            return result;
        }

        nextValue = readNextValue();
        fieldIterator.checkRefill();
        row++;
        return result;
    }

    private T readNextValue() throws IOException {
        List<String> columns = rowMapper.getColumns();
        for (int i = 0; i < columns.size(); i++) {
            String name = columns.get(i);
            if (!fieldIterator.hasNext()) {
                throw new IOException("Invalid format");
            }
            InputStream valueIS = fieldIterator.next();
            rowMapper.nextField(name, valueIS);
            consumeLeftovers(valueIS);

            if (i < columns.size() - 1) {
                if (fieldIterator.getDelimiter() != Delimiter.FIELD_END) {
                    throw new IOException("Invalid format");
                }
            }
        }
        if (fieldIterator.getDelimiter() != Delimiter.ROW_END) {
            throw new IOException("Invalid format");
        }
        return rowMapper.rowEnd();
    }

    private long consumeLeftovers(InputStream valueIS) throws IOException {
        return IOUtils.copyLarge(valueIS, streamEndConsumer, copyBuffer);
    }

    static class FieldIterator {
        private final InputStream is;

        private final Buffer rawBuffer;
        private final Buffer cleanBuffer;

        private boolean parentStreamEnded = false;

        private UnescapedInputStream unescapedInputStream;

        FieldIterator(InputStream is, int size) {
            this.is = is;
            this.rawBuffer = new Buffer(size);
            this.cleanBuffer = new Buffer(size);
            this.unescapedInputStream = new UnescapedInputStream();
        }

        boolean hasNext() throws IOException {
            if (parentStreamEnded && rawBuffer.isEmpty()) {
                return false;
            }
            return true;
        }

        public InputStream next() {
            cleanBuffer.reset();
            unescapedInputStream.fieldEnded = false;
            return unescapedInputStream;
        }

        Delimiter getDelimiter() throws IOException {
            if (parentStreamEnded && rawBuffer.isEmpty()) {
                return Delimiter.ROW_END;
            }
            if (rawBuffer.isEmpty()) {
                throw new IOException("Corrupted stream");
            }
            int b = rawBuffer.read();
            if (b == '\t') {
                return Delimiter.FIELD_END;
            }
            if (b == '\n') {
                return Delimiter.ROW_END;
            }
            throw new IOException("Corrupted stream");
        }

        void checkRefill() throws IOException {
            if (rawBuffer.isEmpty()) {
                rawBuffer.reset();
                fillRawBuffer();
            }
        }

        private void fillRawBuffer() throws IOException {
            int bytesToRead = rawBuffer.freeSpace();
            int read = rawBuffer.fill(is, bytesToRead);
            if (read < bytesToRead) {
                parentStreamEnded = true;
            }
        }

        boolean unescape() throws IOException {
            boolean fieldEnded = false;
            while (!rawBuffer.isEmpty() && cleanBuffer.freeSpace() > 0) {
                byte b = rawBuffer.peek();
                if (b == '\t' || b == '\n') {
                    fieldEnded = true;
                    break;
                }

                if (b == '\\') {
                    if (rawBuffer.available() == 1) {
                        break;
                    }

                    rawBuffer.read();
                    byte escapedCode = rawBuffer.read();
                    switch (escapedCode) {
                        case 't': b = '\t'; break;
                        case 'n': b = '\n'; break;
                        case '0': b = '\0'; break;
                        case '\\': b = '\\'; break;
                        default:
                            throw new IOException("Corrupted stream, unknown escape sequence: \\" + (char) escapedCode);
                    }
                } else {
                    rawBuffer.read();
                }
                cleanBuffer.append(b);
            }
            return fieldEnded;
        }

        private class UnescapedInputStream extends InputStream {
            private boolean fieldEnded = false;

            @Override
            public int read() throws IOException {
                if (cleanBuffer.available() > 0) {
                    return cleanBuffer.read();
                }
                if (!refill()) {
                    return -1;
                }

                return cleanBuffer.read();
            }

            private boolean refill() throws IOException {
                if (fieldEnded) {
                    return false;
                }
                cleanBuffer.reset();

                if (rawBuffer.isEmpty()) {
                    rawBuffer.reset();
                    fillRawBuffer();
                }

                fieldEnded = unescape();
                if (!fieldEnded && rawBuffer.available() == 1) {
                    byte last = rawBuffer.getLast();
                    rawBuffer.reset();
                    rawBuffer.append(last);
                    fillRawBuffer();
                    fieldEnded = unescape();
                }

                if (cleanBuffer.isEmpty()) {
                    return false;
                }
                return true;
            }

            @Override
            public int read(byte[] b, int off, int len) throws IOException {
                if (cleanBuffer.available() > 0) {
                    return cleanBuffer.copyTo(b, off, len);
                }
                if (!refill()) {
                    return -1;
                }

                return cleanBuffer.copyTo(b, off, len);
            }
        }
    }

    static class Buffer {
        final byte[] buffer;
        int position;
        int end;

        public Buffer(int size) {
            this.buffer = new byte[size];
        }

        public boolean isEmpty() {
            return position == end;
        }

        public int available() {
            return end - position;
        }

        public int freeSpace() {
            return buffer.length - end;
        }

        public void reset() {
            this.position = 0;
            this.end = 0;
        }

        public int fill(InputStream is, int length) throws IOException {
            if (end + length > buffer.length) {
                throw new IllegalStateException();
            }
            int read = IOUtils.read(is, buffer, end, length);
            this.end += read;
            return read;
        }

        public int copyTo(byte[] b, int offset, int length) {
            int l = Math.min(available(), length);
            System.arraycopy(buffer, position, b, offset, l);
            position += l;
            return l;
        }

        public void append(byte b) {
            buffer[end++] = b;
        }

        public byte getLast() {
            return buffer[end - 1];
        }

        public byte peek() {
            return buffer[position];
        }

        public byte read() {
            return buffer[position++];
        }
    }

    enum Delimiter {
        FIELD_END,
        ROW_END,
    }
}
