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

import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.util.Arrays;

import com.google.common.primitives.Doubles;
import com.google.common.primitives.Floats;
import com.google.common.primitives.Ints;
import com.google.common.primitives.Longs;
import org.apache.commons.io.IOUtils;

import ru.yandex.webmaster3.core.data.WebmasterHostId;

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

    public static void writeHostId(BAOutputStream baos, WebmasterHostId hostId) {
        baos.write('h');
        baos.write('t');
        baos.write('t');
        baos.write('p');
        if (hostId.getSchema() == WebmasterHostId.Schema.HTTPS) {
            baos.write('s');
        }
        baos.write(':');
        String punycodeHostname = hostId.getPunycodeHostname();
        baos.writeASCII(punycodeHostname);
        baos.write(':');
        writeInt(baos, hostId.getPort());
    }

    private static final long[] LONG_SIZE = {
            0L,
            9L,
            99L,
            999L,
            9999L,
            99999L,
            999999L,
            9999999L,
            99999999L,
            999999999L,
            9999999999L,
            99999999999L,
            999999999999L,
            9999999999999L,
            99999999999999L,
            999999999999999L,
            9999999999999999L,
            99999999999999999L,
            999999999999999999L,
            Long.MAX_VALUE
    };
    private static final byte[] NUMBERS = {'0', '1', '2', '3', '4', '5', '6', '7', '8', '9'};

//    public static void writeLong(BAOutputStream baos, long value) {
//        baos.writeASCII(Long.toString(value));
//    }

    public static void writeLong(BAOutputStream baos, long value) {
        if (value == Long.MIN_VALUE) {
            baos.write('-');
            baos.write('9');
            baos.write('2');
            baos.write('2');
            baos.write('3');
            baos.write('3');
            baos.write('7');
            baos.write('2');
            baos.write('0');
            baos.write('3');
            baos.write('6');
            baos.write('8');
            baos.write('5');
            baos.write('4');
            baos.write('7');
            baos.write('7');
            baos.write('5');
            baos.write('8');
            baos.write('0');
            baos.write('8');
            return;
        }
        boolean minusSign = value < 0;
        int d19=0, d18=0, d17=0, d16=0, d15=0, d14=0, d13=0, d12=0, d11=0, d10=0, d9=0, d8=0, d7=0, d6=0, d5=0, d4=0, d3=0, d2=0, d1=0;

        if (value < 0) {
            value = -value;
        }
        int size = Arrays.binarySearch(LONG_SIZE, value);
        if (size < 0) {
            size = -size - 1;
        }
        long v;
        int r;

        switch (20 - size) {
            case 1: v = value / 10; r = (int) (value - ((v << 3) + (v << 1))); d1 = r; value = v;
            case 2: v = value / 10; r = (int) (value - ((v << 3) + (v << 1))); d2 = r; value = v;
            case 3: v = value / 10; r = (int) (value - ((v << 3) + (v << 1))); d3 = r; value = v;
            case 4: v = value / 10; r = (int) (value - ((v << 3) + (v << 1))); d4 = r; value = v;
            case 5: v = value / 10; r = (int) (value - ((v << 3) + (v << 1))); d5 = r; value = v;
            case 6: v = value / 10; r = (int) (value - ((v << 3) + (v << 1))); d6 = r; value = v;
            case 7: v = value / 10; r = (int) (value - ((v << 3) + (v << 1))); d7 = r; value = v;
            case 8: v = value / 10; r = (int) (value - ((v << 3) + (v << 1))); d8 = r; value = v;
            case 9: v = value / 10; r = (int) (value - ((v << 3) + (v << 1))); d9 = r; value = v;
            case 10: v = value / 10; r = (int) (value - ((v << 3) + (v << 1))); d10 = r; value = v;
            case 11: v = value / 10; r = (int) (value - ((v << 3) + (v << 1))); d11 = r; value = v;
            case 12: v = value / 10; r = (int) (value - ((v << 3) + (v << 1))); d12 = r; value = v;
            case 13: v = value / 10; r = (int) (value - ((v << 3) + (v << 1))); d13 = r; value = v;
            case 14: v = value / 10; r = (int) (value - ((v << 3) + (v << 1))); d14 = r; value = v;
            case 15: v = value / 10; r = (int) (value - ((v << 3) + (v << 1))); d15 = r; value = v;
            case 16: v = value / 10; r = (int) (value - ((v << 3) + (v << 1))); d16 = r; value = v;
            case 17: v = value / 10; r = (int) (value - ((v << 3) + (v << 1))); d17 = r; value = v;
            case 18: v = value / 10; r = (int) (value - ((v << 3) + (v << 1))); d18 = r; value = v;
            case 19: d19 = (int) (value);
        }
        int t;
        t = d1; d1 = d19; d19 = t;
        t = d2; d2 = d18; d18 = t;
        t = d3; d3 = d17; d17 = t;
        t = d4; d4 = d16; d16 = t;
        t = d5; d5 = d15; d15 = t;
        t = d6; d6 = d14; d14 = t;
        t = d7; d7 = d13; d13 = t;
        t = d8; d8 = d12; d12 = t;
        t = d9; d9 = d11; d11 = t;

        if (minusSign) {
            baos.write('-');
        }

        int i = size;
        baos.write(NUMBERS[d1]); i--;
        if (i > 0) {baos.write(NUMBERS[d2]); i--;}
        if (i > 0) {baos.write(NUMBERS[d3]); i--;}
        if (i > 0) {baos.write(NUMBERS[d4]); i--;}
        if (i > 0) {baos.write(NUMBERS[d5]); i--;}
        if (i > 0) {baos.write(NUMBERS[d6]); i--;}
        if (i > 0) {baos.write(NUMBERS[d7]); i--;}
        if (i > 0) {baos.write(NUMBERS[d8]); i--;}
        if (i > 0) {baos.write(NUMBERS[d9]); i--;}
        if (i > 0) {baos.write(NUMBERS[d10]); i--;}
        if (i > 0) {baos.write(NUMBERS[d11]); i--;}
        if (i > 0) {baos.write(NUMBERS[d12]); i--;}
        if (i > 0) {baos.write(NUMBERS[d13]); i--;}
        if (i > 0) {baos.write(NUMBERS[d14]); i--;}
        if (i > 0) {baos.write(NUMBERS[d15]); i--;}
        if (i > 0) {baos.write(NUMBERS[d16]); i--;}
        if (i > 0) {baos.write(NUMBERS[d17]); i--;}
        if (i > 0) {baos.write(NUMBERS[d18]); i--;}
        if (i > 0) {baos.write(NUMBERS[d19]);}
    }

    public static void writeInt(BAOutputStream baos, int value) {
//        baos.writeASCII(Integer.toString(value));
        writeLong(baos, (long) value);
    }

    public static void writeInt(BAOutputStream baos, int value, int minSymbols) {
        String valueS = Integer.toString(value);
        while (minSymbols > valueS.length()) {
            baos.write('0');
            minSymbols--;
        }
        baos.writeASCII(valueS);
    }

    public static void writeBoolean(BAOutputStream baos, boolean value) {
        if(value) {
            writeInt(baos, 1);
        } else {
            writeInt(baos, 0);
        }
    }

    public static void writeFloat(BAOutputStream baos, float value) {
        baos.writeASCII(Float.toString(value));
    }

    public static void writeDouble(BAOutputStream baos, double value) {
        baos.writeASCII(Double.toString(value));
    }

    public static WebmasterHostId parseUrlAsHostId(InputStream is) {
        try {
            WebmasterHostId.Schema schema = WebmasterHostId.Schema.HTTP;
            int ch;

            ch = is.read();
            if (ch != 'h' && ch != 'H') {
                return null;
            }
            ch = is.read();
            if (ch != 't' && ch != 'T') {
                return null;
            }
            ch = is.read();
            if (ch != 't' && ch != 'T') {
                return null;
            }
            ch = is.read();
            if (ch != 'p' && ch != 'P') {
                return null;
            }
            ch = is.read();
            if (ch == 's' || ch == 'S') {
                schema = WebmasterHostId.Schema.HTTPS;
                if (is.read() != ':') {
                    return null;
                }
            } else if (ch != ':') {
                return null;
            }

            ch = is.read();
            if (ch != '/') {
                return null;
            }
            ch = is.read();
            if (ch != '/') {
                return null;
            }

            char[] buffer = new char[2048];
            int position = 0;
            boolean hasPort = false;

            while ((ch = is.read()) >= 0) {
                if ((ch >= 'a' && ch <= 'z') || (ch >= '0' && ch <= '9' )) {
                    buffer[position++] = (char) ch;
                } else if ((ch >= 'A' && ch <= 'Z')) {
                    buffer[position++] = (char) ('a' + (ch - 'A'));
                } else if (ch == '-' || ch == '_') {
                    buffer[position++] = (char) ch;
                } else if (ch == '.') {
                    buffer[position++] = (char) ch;
                } else if (ch == ':') {
                    hasPort = true;
                    break;
                } else if (ch == '/') {
                    break;
                } else {
                    return null;
                }
            }

            String hostname = new String(buffer, 0, position);
            int port = 0;

            if (hasPort) {
                while ((ch = is.read()) >= 0) {
                    if (ch >= '0' && ch <= '9') {
                        port *= 10;
                        port += ch - '0';
                    } else {
                        break;
                    }
                }
            }

            if (port == 0) {
                port = schema.getDefaultPort();
            }
            return WebmasterHostId.createNoLowerCase(schema, hostname, port);
        } catch (IOException e) {
            return null;
        }
    }

    public static WebmasterHostId parseHostId(InputStream is) {
        int position = 0;
        char[] buffer = new char[255];
        WebmasterHostId.Schema schema = WebmasterHostId.Schema.HTTP;

        int value;
        try {
            value = is.read();
            if (value != 'h') {
                return null;
            }
            value = is.read();
            if (value != 't') {
                return null;
            }
            value = is.read();
            if (value != 't') {
                return null;
            }
            value = is.read();
            if (value != 'p') {
                return null;
            }
            value = is.read();
            if (value == 's') {
                schema = WebmasterHostId.Schema.HTTPS;
                if (is.read() != ':') {
                    return null;
                }
            } else if (value != ':') {
                return null;
            }
            do {
                value = is.read();
                if (value == -1) {
                    return null;
                }
                if (value == ':') {
                    break;
                }
                if ((value >= 'a' && value <= 'z') || (value >= '0' && value <= '9') || value == '.' || value == '_'
                        || value == '-')
                {
                    buffer[position++] = (char) value;
                    continue;
                }
                return null;
            } while (true);

            Integer port = parseInt(is);
            if (port == null) {
                return null;
            }

            return WebmasterHostId.createNoLowerCase(schema, new String(buffer, 0, position), port);
        } catch (IOException e) {
            // should not happen
            throw new RuntimeException("Unable to read hostId", e);
        }
    }

    public static Integer parseInt(InputStream is) {
        ByteArrayOutputStream baos = new ByteArrayOutputStream();
        try {
            if (IOUtils.copy(is, baos) == 0) {
                return null;
            }
        } catch (IOException e) {
            // should not happen
            throw new RuntimeException("Unable to parse int", e);
        }
        return Ints.tryParse(baos.toString());
    }

    public static Long parseLong(InputStream is) {
        ByteArrayOutputStream baos = new ByteArrayOutputStream();
        try {
            if (IOUtils.copy(is, baos) == 0) {
                return null;
            }
        } catch (IOException e) {
            // should not happen
            throw new RuntimeException("Unable to parse long", e);
        }
        return Longs.tryParse(baos.toString());
    }

    public static Long parseUnsignedLong(InputStream is) {
        ByteArrayOutputStream baos = new ByteArrayOutputStream();
        try {
            if (IOUtils.copy(is, baos) == 0) {
                return null;
            }
        } catch (IOException e) {
            // should not happen
            throw new RuntimeException("Unable to parse long", e);
        }
        return Long.parseUnsignedLong(baos.toString());
    }

    public static Float parseFloat(InputStream is) {
        ByteArrayOutputStream baos = new ByteArrayOutputStream();
        try {
            if (IOUtils.copy(is, baos) == 0) {
                return null;
            }
        } catch (IOException e) {
            // should not happen
            throw new RuntimeException("Unable to float int", e);
        }
        String s = baos.toString();
        switch (s) {
            case "nan":
                return Float.NaN;
            case "inf":
                return Float.POSITIVE_INFINITY;
            case "-inf":
                return Float.NEGATIVE_INFINITY;
            default:
                return Floats.tryParse(s);
        }
    }

    public static Double parseDouble(InputStream is) {
        ByteArrayOutputStream baos = new ByteArrayOutputStream();
        try {
            if (IOUtils.copy(is, baos) == 0) {
                return null;
            }
        } catch (IOException e) {
            // should not happen
            throw new RuntimeException("Unable to parse double", e);
        }
        String s = baos.toString();
        switch (s) {
            case "nan":
                return Double.NaN;
            case "inf":
                return Double.POSITIVE_INFINITY;
            case "-inf":
                return Double.NEGATIVE_INFINITY;
            default:
                return Doubles.tryParse(s);
        }
    }

}
