package ru.yandex.webmaster3.core.util;

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

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 it.unimi.dsi.fastutil.chars.CharArrays;
import it.unimi.dsi.fastutil.io.FastByteArrayOutputStream;
import org.apache.commons.io.IOUtils;
import org.joda.time.DateTime;
import org.joda.time.format.DateTimeFormatter;

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

/**
 * @author aherman
 */
public class ByteStreamUtil {
    public static long readLongLE(byte[] b, int offset) {
        return (((b[offset + 7] & 0xFFL) << 56) +
                ((b[offset + 6] & 0xFFL) << 48) +
                ((b[offset + 5] & 0xFFL) << 40) +
                ((b[offset + 4] & 0xFFL) << 32) +
                ((b[offset + 3] & 0xFFL) << 24) +
                ((b[offset + 2] & 0xFFL) << 16) +
                ((b[offset + 1] & 0xFFL) <<  8) +
                ((b[offset + 0] & 0xFFL)));
    }

    public static int readIntLE(byte[] b, int offset) {
        return (((b[offset + 3] & 0xFF) << 24) +
                ((b[offset + 2] & 0xFF) << 16) +
                ((b[offset + 1] & 0xFF) << 8) +
                ((b[offset + 0] & 0xFF)));
    }

    public static int readUInt16LE(byte[] b, int offset) {
        return (((b[offset + 1] & 0xFF) << 8) +
                ((b[offset + 0] & 0xFF)));
    }

    public static int readUInt8(byte[] b, int offset) {
        return b[offset] & 0xFF;
    }

    public static void writeIntLE(byte[] b, int offset, int value) {
        b[offset + 0] = (byte) value;
        b[offset + 1] = (byte) (value >> 8);
        b[offset + 2] = (byte) (value >> 16);
        b[offset + 3] = (byte) (value >> 24);
    }

    public static void writeLongLE(byte[] b, int offset, long value) {
        b[offset + 0] = (byte) (value & 0xFFL);
        b[offset + 1] = (byte) ((value >> 8) & 0xFFL);
        b[offset + 2] = (byte) ((value >> 16) & 0xFFL);
        b[offset + 3] = (byte) ((value >> 24) & 0xFFL);
        b[offset + 4] = (byte) ((value >> 32) & 0xFFL);
        b[offset + 5] = (byte) ((value >> 40) & 0xFFL);
        b[offset + 6] = (byte) ((value >> 48) & 0xFFL);
        b[offset + 7] = (byte) ((value >> 56) & 0xFFL);
    }

    public static void writeLongBE(byte[] b, int offset, long value) {
        b[offset + 7] = (byte) (value & 0xFFL);
        b[offset + 6] = (byte) ((value >> 8) & 0xFFL);
        b[offset + 5] = (byte) ((value >> 16) & 0xFFL);
        b[offset + 4] = (byte) ((value >> 24) & 0xFFL);
        b[offset + 3] = (byte) ((value >> 32) & 0xFFL);
        b[offset + 2] = (byte) ((value >> 40) & 0xFFL);
        b[offset + 1] = (byte) ((value >> 48) & 0xFFL);
        b[offset + 0] = (byte) ((value >> 56) & 0xFFL);
    }

    public static void writeHostId(FastByteArrayOutputStream os, WebmasterHostId hostId) {
        write(os, 'h');
        write(os, 't');
        write(os, 't');
        write(os, 'p');
        if (hostId.getSchema() == WebmasterHostId.Schema.HTTPS) {
            write(os, 's');
        }
        write(os, ':');
        String punycodeHostname = hostId.getPunycodeHostname();
        writeASCIIString(os, punycodeHostname);
        write(os, ':');
        writeInt(os, 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(FastByteArrayOutputStream os, long value) {
        if (value == Long.MIN_VALUE) {
            write(os, '-');
            write(os, '9');
            write(os, '2');
            write(os, '2');
            write(os, '3');
            write(os, '3');
            write(os, '7');
            write(os, '2');
            write(os, '0');
            write(os, '3');
            write(os, '6');
            write(os, '8');
            write(os, '5');
            write(os, '4');
            write(os, '7');
            write(os, '7');
            write(os, '5');
            write(os, '8');
            write(os, '0');
            write(os, '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);
        }

        if (minusSign) {
            write(os, '-');
        }

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

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

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

    public static void writeFloat(FastByteArrayOutputStream os, float value) {
        writeASCIIString(os, Float.toString(value));
    }

    public static void writeDouble(FastByteArrayOutputStream os, double value) {
        writeASCIIString(os, Double.toString(value));
    }

    public static WebmasterHostId readUrlAsHostId(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[256];
            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 readHostId(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 = readInt(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 readInt(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 readLong(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 Float readFloat(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);
        }
        return Floats.tryParse(baos.toString());
    }

    public static Double readDouble(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);
        }
        return Doubles.tryParse(baos.toString());
    }

    public static void writeASCIIString(FastByteArrayOutputStream os, String s) {
        for (int i = 0; i < s.length(); i++) {
            write(os, s.charAt(i));
        }
    }

    public static String readASCIIString(InputStream is) throws IOException {
        char[] chars = new char[32];
        int pos = 0;
        int i;
        while ((i = is.read()) != -1) {
            if (pos >= chars.length) {
                chars = CharArrays.ensureCapacity(chars, chars.length * 2);
            }
            chars[pos++] = (char) i;
        }
        return new String(chars, 0, pos);
    }

    public static Optional<DateTime> readDateTime(InputStream is, DateTimeFormatter dateTimeFormatter) {
        ByteArrayOutputStream baos = new ByteArrayOutputStream();
        try {
            if (IOUtils.copy(is, baos) == 0) {
                return Optional.empty();
            }
        } catch (IOException e) {
            // should not happen
            throw new RuntimeException("Unable to float int", e);
        }
        return Optional.of(dateTimeFormatter.parseDateTime(baos.toString()));
    }

    private static void write(FastByteArrayOutputStream os, int b) {
        os.write(b);
    }

    private static void write(FastByteArrayOutputStream os, char b) {
        os.write(b);
    }

    private static void write(FastByteArrayOutputStream os, byte b) {
        os.write(b);
    }
}
