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

import org.apache.commons.io.IOUtils;

import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.util.Optional;

/**
 * @author aherman
 */
public class ClickhouseEscapeUtils {

    public static String trimAndEscape(String text) {
        return escape(text, true);
    }

    public static String escape(String text, boolean forLikeExpression) {
        StringBuilder sb = new StringBuilder(text.length() * 2);
        for (int i = 0; i < text.length(); i++) {
            char ch = text.charAt(i);
            switch (ch) {
                case '\b': break;
                case '\f': break;
                case '\r': break;
                case '\n': break;
                case '\t': break;
                case '\0': break;
                case '\'': sb.append("\\\'"); break;
                case '\\': sb.append("\\\\"); break;
                case '%':
                    if (forLikeExpression) {
                        sb.append("\\\\%");
                        break;
                    }
                case '_':
                    if (forLikeExpression) {
                        sb.append("\\\\_");
                        break;
                    }
                default:
                    sb.append(ch);
            }
        }
        return sb.toString();
    }

    public static String escapeString(String text) {
        SimpleByteArrayOutputStream baos = new SimpleByteArrayOutputStream(text.length());
        try {
            IOUtils.copy(IOUtils.toInputStream(text, StandardCharsets.UTF_8), escape(baos));
            return new String(baos.toByteArray(), StandardCharsets.UTF_8);
        } catch (IOException e) {
            // Should never happen
            throw new RuntimeException("Unable to escape string", e);
        }
    }

    public static BAInputStream unescape(BAInputStream is) {
        return new UnescapingInputStream(is);
    }

    public static BAOutputStream escape(BAOutputStream os) {
        return new EscapingOutputStream(os);
    }

    /**
     *
     * @param is
     * @param unquoteValue указывать true, если читается дата или строка
     * @return
     */
    public static ArrayParser unescapedArray(BAInputStream is, boolean unquoteValue) {
        return new ArrayParser(is, unquoteValue);
    }

    public static class ArrayParser {
        private final BAInputStream is;
        private final boolean unquoteValue;

        private boolean haveBufferedChar = false;
        private boolean readStarted = false;
        private int buffered = 0;

        public ArrayParser(BAInputStream is, boolean unquoteValue) {
            this.is = is;
            this.unquoteValue = unquoteValue;
        }

        private int read() {
            readStarted = true;
            if (!haveBufferedChar) {
                buffered = is.read();
            }
            haveBufferedChar = false;
            return buffered;
        }

        private void rollbackLastRead() {
            if (!readStarted) {
                throw new IllegalStateException("Cannot rollback: no read ever performed");
            }
            haveBufferedChar = true;
        }

        /**
         * @return Unescaped input stream, representing single value, or none - if array consumed
         */
        public Optional<BAInputStream> nextValue() {
            boolean sawArrayStart = readStarted;

            int firstChar = read();

            switch (firstChar) {
                case '[':
                    if (read() == ']') {
                        //array is empty
                        return Optional.empty();
                    } else {
                        ArrayParser.this.rollbackLastRead();
                    }
                    sawArrayStart = true; //and continue reading
                case ',':
                    if (!sawArrayStart) {
                        throw new RuntimeException("Expected array start, found '" + ((char) firstChar) + "'");
                    }
                    if (unquoteValue) {
                        int quote = read();
                        if (quote != '\'') {
                            throw new RuntimeException("Expected ''', found '" + quote + "'");
                        }
                    }
                    return Optional.of(new BAInputStream() {
                        private boolean consumed = false;

                        @Override
                        public int read() {
                            if (consumed) {
                                return -1;
                            }
                            int ch = ArrayParser.this.read();
                            switch (ch) {
                                case -1:
                                    consumed = true;
                                    return -1;
                                case ',':
                                case ']':
                                    ArrayParser.this.rollbackLastRead();
                                    consumed = true;
                                    return -1;
                                case '\'':
                                    if (!ArrayParser.this.unquoteValue) {
                                        return ch;
                                    } else {
                                        consumed = true;
                                        return -1;
                                    }
                                case '\\':
                                    ch = ArrayParser.this.read();
                                    if (ch == -1) {
                                        throw new RuntimeException("EOF found after \\");
                                    }
                                    return ch;
                                default:
                                    return ch;
                            }
                        }

                        @Override
                        public void resetPosition() {
                            throw new UnsupportedOperationException();
                        }
                    });
                case ']':
                    if (!sawArrayStart) {
                        throw new RuntimeException("Expected array start, found '" + ((char) firstChar) + "'");
                    }
                    return Optional.empty();
                default:
                    if (sawArrayStart) {
                        throw new RuntimeException("Expected ',' or ']', found: '" + ((char) firstChar) + "'");
                    } else {
                        throw new RuntimeException("Expected '[', found: '" + ((char) firstChar) + "'");
                    }
            }
        }
    }

    private static class UnescapingInputStream extends BAInputStream {
        private final BAInputStream is;

        private UnescapingInputStream(BAInputStream is) {
            this.is = is;
        }

        @Override
        public int read() {
            int value = is.read();
            if (value == -1) {
                return -1;
            }
            if (value != '\\') {
                return value;
            }
            value = is.read();
            if (value == -1) {
                throw new RuntimeException("Broken escape sequence");
            }
            switch (value) {
                case 'b': return '\b';
                case 'f': return '\f';
                case 'r': return '\r';
                case 'n': return '\n';
                case 't': return '\t';
                case '0': return '\0';
                case '\'': return '\'';
                case '\\': return '\\';
            }
            throw new RuntimeException("Broken escape sequence");
        }

        @Override
        public void resetPosition() {
            is.resetPosition();
        }
    }

    private static class EscapingOutputStream extends BAOutputStream {
        private BAOutputStream os;

        private EscapingOutputStream(BAOutputStream os) {
            this.os = os;
        }

        public void writeByte(byte b) {
            switch (b) {
                case '\b': os.write('\\'); os.write('b'); break;
                case '\f': os.write('\\'); os.write('f'); break;
                case '\r': os.write('\\'); os.write('r'); break;
                case '\n': os.write('\\'); os.write('n'); break;
                case '\t': os.write('\\'); os.write('t'); break;
                case '\0': os.write('\\'); os.write('0'); break;
                case '\'': os.write('\\'); os.write('\''); break;
                case '\\': os.write('\\'); os.write('\\'); break;

                default:
                    os.writeByte(b);
            }
        }

        @Override
        public int position() {
            return os.position();
        }

        @Override
        public void reset() {
            os.reset();
        }
    }
}
