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

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

import org.apache.commons.io.IOUtils;
import org.apache.commons.io.output.NullOutputStream;
import org.jetbrains.annotations.NotNull;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
 * @author aherman
 */
class SchemafulDsvParser2<T> extends InputStream implements InterruptableIterator<T> {
    private static final Logger log = LoggerFactory.getLogger(SchemafulDsvParser2.class);

    private final YtRowMapper<T> rowMapper;

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

    private final DelimiterSplittingBuffer buffer;

    State state = State.ROW_END;

    private T next = null;

    private long row = -1;

    public SchemafulDsvParser2(InputStream parentStream, YtRowMapper<T> rowMapper) {
        this(parentStream, rowMapper, 4 * 1024);
    }

    public SchemafulDsvParser2(InputStream parentStream, YtRowMapper<T> rowMapper, int bufferSize) {
        this.rowMapper = rowMapper;
        this.buffer = new DelimiterSplittingBuffer(parentStream, bufferSize);
    }

    private T readNext() throws IOException {
        next = null;
        if (buffer.availableBeforeDelimiter() == -1) {
            state = State.STREAM_END;
            return null;
        }

        List<String> columns = rowMapper.getColumns();
        expectState(State.ROW_END);
        state = State.COLUMN_START;
//        log.debug("Row start: {} {} {}", row, buffer.streamPosition, state);
        for (String column : columns) {
            expectState(State.COLUMN_START);
//            log.debug("Column start: {} {} {}", row, buffer.streamPosition, state);
            rowMapper.nextField(column, this);
//            log.debug("After mapper: {} {} {}", row, buffer.streamPosition, state);

            // Consume skipped rows
            IOUtils.copyLarge(this, nullStream, copyBuffer);
//            log.debug("After copy: {} {} {}", row, buffer.streamPosition, state);

            if (state == State.COLUMN_END) {
                state = State.COLUMN_START;
            }
        }
//        log.debug("Row end: {} {} {}", row, buffer.streamPosition, state);
        expectState(State.ROW_END);
        return rowMapper.rowEnd();
    }

    private void expectState(State expectedState) throws IOException {
        if (state != expectedState) {
            throw new IOException("Broken stream, wrong state: row=" + row + " expected " + expectedState + " actual " + state);
        }
    }

    @Override
    public boolean hasNext() throws InterruptedException, IOException, YtException {
        if(next != null) {
            return true;
        }
        if (state == State.STREAM_END) {
            return false;
        }
        next = readNext();
        return next != null;
    }

    @Override
    public T next() throws InterruptedException, IOException, YtException {
        if (next == null) {
            throw new NoSuchElementException();
        }
        T result = next;
        next = readNext();
        row++;
        return result;
    }

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

    @Override
    public int read() throws IOException {
        if (state == State.COLUMN_END || state == State.ROW_END) {
            return -1;
        }

        if (buffer.availableBeforeDelimiter() >= 1) {
            return buffer.readOne();
        }
        UnescapedByte unescaped = buffer.unescapeOne();
        switch (unescaped) {
            case ZERO_BYTE: return 0;
            case NEW_LINE_BYTE: return '\n';
            case TAB_BYTE: return '\t';
            case SLASH_BYTE: return '\\';
        }
        if (unescaped == UnescapedByte.COLUMN_END) {
            state = State.COLUMN_END;
            return -1;
        }
        if (unescaped == UnescapedByte.ROW_END) {
            state = State.ROW_END;
            return -1;
        }
        throw new IOException("Broken stream, row=" + row);
    }

    @Override
    public int read(@NotNull byte[] b, int off, int len) throws IOException {
        if (state == State.COLUMN_END || state == State.ROW_END) {
            return -1;
        }
        int copied = buffer.copyTo(b, off, len);
        if (copied == 0) {
            UnescapedByte unescaped = buffer.unescapeOne();
            switch (unescaped) {
                case COLUMN_END: state = State.COLUMN_END; return -1;
                case ROW_END: state = State.ROW_END;return -1;
                case ZERO_BYTE: b[off] = 0; return 1;
                case NEW_LINE_BYTE: b[off] = '\n'; return 1;
                case TAB_BYTE: b[off] = '\t'; return 1;
                case SLASH_BYTE: b[off] = '\\'; return 1;
                default:
                    throw new IOException("Broken stream, row=" + row);
            }
        } else {
            return copied;
        }
    }

    @Override
    public void close() throws IOException {
    }

    static class DelimiterSplittingBuffer {
        private final InputStream is;
        private long streamPosition = 0;
        private final byte[] buffer;
        private boolean parentEof = false;
        private boolean eof = false;

        private int position;
        private int end;
        private int nextDelimiter;

        public DelimiterSplittingBuffer(InputStream is, int bufferSize) {
            this.is = is;
            this.buffer = new byte[bufferSize];
        }

        public void fill() throws IOException {
            int available = available();
            if (available > 3) {
                return;
            }

            if (available > 0) {
                System.arraycopy(buffer, position, buffer, 0, available);
                end = available;
            } else {
                end = 0;
            }
            position = 0;

            if (!parentEof) {
                int read = is.read(buffer, end, buffer.length - end);
                if (read <= 0) {
                    parentEof = true;
                } else {
                    end += read;
                }
            }
            if (available() == 0) {
                eof = true;
            }
            nextDelimiter = -1;
            findDelimiter();
        }

        private void findDelimiter() {
            if (position <= nextDelimiter) {
                return;
            }

            for (int i = position; i < end; i++) {
                byte b = buffer[i];
                if (b == '\n' || b == '\t' || b == '\\') {
                    nextDelimiter = i;
                    return;
                }
            }
            nextDelimiter = -1;
        }

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

        int availableBeforeDelimiter() throws IOException {
            if (eof) {
                return -1;
            }
            fill();
            if (eof) {
                return -1;
            }
            if (nextDelimiter >= 0) {
                return nextDelimiter - position;
            }
            return available();
        }

        public int readOne() throws IOException {
            if (eof) {
                return -1;
            }
            fill();
            if (eof) {
                return -1;
            }

            if (availableBeforeDelimiter() < 1) {
                throw new IOException("position must not look at column/row end, streamPosition=" + streamPosition);
            }
            int result = buffer[position++] & 0xFF;
            streamPosition++;

            findDelimiter();
            return result;
        }

        public UnescapedByte unescapeOne() throws IOException {
            if (eof) {
                return UnescapedByte.STREAM_END;
            }
            fill();
            if (eof) {
                return UnescapedByte.STREAM_END;
            }

            if (available() < 1) {
                throw new IOException("Broken stream, expected delimiter, streamPosition=" + streamPosition);
            }
            byte b =  buffer[position++];
            streamPosition++;

            UnescapedByte result = UnescapedByte.COLUMN_END;
            if (b == '\t') {
                result = UnescapedByte.COLUMN_END;
            } else if (b == '\n') {
                result = UnescapedByte.ROW_END;
            } else if (b == '\\') {
                if (available() < 1) {
                    throw new IOException("Broken escape sequence, missing second code, streamPosition=" + streamPosition);
                }
                byte b1 = buffer[position++];
                streamPosition++;

                switch (b1) {
                    case '0': result = UnescapedByte.ZERO_BYTE; break;
                    case 't': result = UnescapedByte.TAB_BYTE; break;
                    case 'n': result = UnescapedByte.NEW_LINE_BYTE; break;
                    case '\\': result = UnescapedByte.SLASH_BYTE; break;
                    default:
                        throw new IOException("Broken escape sequence, unknown second code, streamPosition=" + streamPosition + " b1=" + Integer.toHexString(b1 & 0xFF));
                }
            }
            findDelimiter();
            return result;
        }

        public int copyTo(byte[] buf, int off, int len) throws IOException {
            if (eof) {
                return -1;
            }
            fill();
            if (eof) {
                return -1;
            }

            int copyLength = Math.min(len, availableBeforeDelimiter());
            if (copyLength == 0) {
                return 0;
            }
            System.arraycopy(buffer, position, buf, off, copyLength);
            position += copyLength;
            streamPosition += copyLength;

            return copyLength;
        }
    }

    enum UnescapedByte {
        STREAM_END,
        COLUMN_END,
        ROW_END,

        NEW_LINE_BYTE,
        TAB_BYTE,
        SLASH_BYTE,
        ZERO_BYTE,
    }

    enum State {
        COLUMN_START,
        COLUMN_END,
        ROW_END,
        STREAM_END
    }
}
