package ru.yandex.webmaster3.storage.util;

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

import com.google.common.primitives.Ints;
import org.apache.commons.io.IOUtils;
import org.apache.commons.lang3.tuple.Pair;

import ru.yandex.webmaster3.storage.util.clickhouse2.BAInputStream;

/**
 * @author aherman
 */
public class YamrTableReader implements Iterable<Pair<BAInputStream.RandomAccessBAInputStream, BAInputStream.RandomAccessBAInputStream>> {
    private final InputStream is;
    private final byte[] buffer;

    private int rowStart;
    private int position;

    private int end;
    private boolean streamEnd = false;

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

    @Override
    public Iterator<Pair<BAInputStream.RandomAccessBAInputStream, BAInputStream.RandomAccessBAInputStream>> iterator() {
        return new YtIterator();
    }

    private int readFieldLength() {
        ensure(4);
        int length = Ints.fromBytes(
                buffer[position + 3],
                buffer[position + 2],
                buffer[position + 1],
                buffer[position]
        );
        position += 4;
        return length;
    }

    private void read(int length) {
        ensure(length);
        position += length;
    }

    private boolean ensure(int length) {
        int rowSize = (position - rowStart) + length;
        if (rowSize > buffer.length) {
            throw new RuntimeException("Insufficient buffer size: " + rowSize);
        }
        if (position + length >= end) {
            return refill();
        } else {
            return true;
        }
    }

    private boolean refill() {
        if (streamEnd) {
            return false;
        }
        if (rowStart > 0) {
            if (rowStart < end) {
                System.arraycopy(buffer, rowStart, buffer, 0, end - rowStart);
                end -= rowStart;
                position -= rowStart;
                rowStart = 0;
            } else {
                rowStart = 0;
                position = 0;
                end = 0;
            }
        }
        try {
            int read = IOUtils.read(is, buffer, end, buffer.length - end);
            if (read <= 0) {
                streamEnd = true;
            } else {
                end += read;
            }
        } catch (IOException e) {
            throw new RuntimeException("Unable to read data", e);
        }
        return true;
    }

    private Pair<BAInputStream.RandomAccessBAInputStream, BAInputStream.RandomAccessBAInputStream> readNextRow() {
        if (streamEnd && rowStart == end) {
            return null;
        }
        int keyLength = readFieldLength();
        read(keyLength);
        int valueLength = readFieldLength();
        read(valueLength);
        Pair<BAInputStream.RandomAccessBAInputStream, BAInputStream.RandomAccessBAInputStream> result = Pair.of(
                new Stream(rowStart + 4, keyLength),
                new Stream(rowStart + 4 + keyLength + 4, valueLength)
        );
        rowStart += 4 + keyLength + 4 + valueLength;
        return result;
    }

    private class YtIterator implements Iterator<Pair<BAInputStream.RandomAccessBAInputStream, BAInputStream.RandomAccessBAInputStream>> {
        private Pair<BAInputStream.RandomAccessBAInputStream, BAInputStream.RandomAccessBAInputStream> nextPair;

        @Override
        public boolean hasNext() {
            nextPair = readNextRow();
            return nextPair != null;
        }

        @Override
        public Pair<BAInputStream.RandomAccessBAInputStream, BAInputStream.RandomAccessBAInputStream> next() {
            return nextPair;
        }
    }

    private class Stream extends BAInputStream.RandomAccessBAInputStream {
        private final int start;
        private final int size;
        private int streamPosition;

        private Stream(int start, int size) {
            this.start = start;
            this.size = size;
            this.streamPosition = start;
        }

        @Override
        public int size() {
            return size;
        }

        @Override
        public byte byteAt(int position) {
            return buffer[start + position];
        }

        @Override
        public int read() {
            if (streamPosition == size + start) {
                return -1;
            }
            return buffer[streamPosition++] & 0xFF;
        }

        @Override
        public void resetPosition() {
            streamPosition = start;
        }
    }
}
