package com.github.zxl0714.redismock;

import com.github.zxl0714.redismock.expecptions.EOFException;
import com.github.zxl0714.redismock.expecptions.ParseErrorException;
import com.google.common.annotations.VisibleForTesting;
import com.google.common.base.Preconditions;
import com.google.common.io.ByteArrayDataOutput;
import com.google.common.io.ByteStreams;

import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStream;

/**
 * Created by Xiaolu on 2015/4/20.
 */
public class RedisCommandParser {

    private final InputStream messageInput;

    @VisibleForTesting
    RedisCommandParser(String stringInput) {
        this(new ByteArrayInputStream(stringInput.getBytes()));
    }

    @VisibleForTesting
    RedisCommandParser(InputStream messageInput) {
        Preconditions.checkNotNull(messageInput);

        this.messageInput = messageInput;
    }

    @VisibleForTesting
    byte consumeByte() throws EOFException {
        int b;
        try {
            b = messageInput.read();
        } catch (IOException e) {
            throw new EOFException();
        }
        if (b == -1) {
            throw new EOFException();
        }
        return (byte) b;
    }

    @VisibleForTesting
    void expectByte(byte c) throws ParseErrorException, EOFException {
        if (consumeByte() != c) {
            throw new ParseErrorException();
        }
    }

    @VisibleForTesting
    long consumeLong() throws ParseErrorException {
        byte c;
        long ret = 0;
        boolean hasLong = false;
        while (true) {
            try {
                c = consumeByte();
            } catch (EOFException e) {
                throw new ParseErrorException();
            }
            if (c == '\r') {
                break;
            }
            if (!isNumber(c)) {
                throw new ParseErrorException();
            }
            ret = ret * 10 + c - '0';
            hasLong = true;
        }
        if (!hasLong) {
            throw new ParseErrorException();
        }
        return ret;
    }

    @VisibleForTesting
    Slice consumeSlice(long len) throws ParseErrorException {
        ByteArrayDataOutput bo = ByteStreams.newDataOutput();
        for (long i = 0; i < len; i++) {
            try {
                bo.write(consumeByte());
            } catch (EOFException e) {
                throw new ParseErrorException();
            }
        }
        return new Slice(bo.toByteArray());
    }

    @VisibleForTesting
    long consumeCount() throws ParseErrorException, EOFException {
        expectByte((byte) '*');
        try {
            long count = consumeLong();
            expectByte((byte) '\n');
            return count;
        } catch (EOFException e) {
            throw new ParseErrorException();
        }
    }

    @VisibleForTesting
    Slice consumeParameter() throws ParseErrorException {
        try {
            expectByte((byte) '$');
            long len = consumeLong();
            expectByte((byte) '\n');
            Slice para = consumeSlice(len);
            expectByte((byte) '\r');
            expectByte((byte) '\n');
            return para;
        } catch (EOFException e) {
            throw new ParseErrorException();
        }
    }

    private static boolean isNumber(byte c) {
        return '0' <= c && c <= '9';
    }

    @VisibleForTesting
    static RedisCommand parse(String stringInput) throws ParseErrorException, EOFException {
        Preconditions.checkNotNull(stringInput);

        return parse(new ByteArrayInputStream(stringInput.getBytes()));
    }

    public static RedisCommand parse(InputStream messageInput) throws ParseErrorException, EOFException {
        Preconditions.checkNotNull(messageInput);

        RedisCommandParser parser = new RedisCommandParser(messageInput);
        long count = parser.consumeCount();
        if (count == 0) {
            throw new ParseErrorException();
        }
        RedisCommand command = new RedisCommand();
        for (long i = 0; i < count; i++) {
            command.addParameter(parser.consumeParameter());
        }
        return command;
    }
}