package qiniu.ip17mon;

import java.io.*;
import java.net.HttpURLConnection;
import java.net.URL;
import java.nio.charset.Charset;
import java.util.InputMismatchException;

public final class Locator implements ILocator {
    public static final String VERSION = "0.2.2";
    private static final Charset Utf8 = Charset.forName("UTF-8");
    private final byte[] ipData;
    private final int textOffset;
    private final int[] index;
    private final int[] indexData;
    private final int[] textStartIndex;
    private final short[] textLengthIndex;

    private Locator(byte[] data) {
        this(data, (data[4] == 0 && data[5] == 0 && data[6] == 0 && data[7] == 0) && data[8] == 0 && data[9] == 0);
    }

    private Locator(byte[] data, boolean datx) {
        this.ipData = data;
        int offset = bigEndian(data, 0);
        this.index = new int[256];
        if (datx) {

            textOffset = offset - 256 * 256 * 4;

            for (int i = 0; i < 256; i++) {
                index[i] = littleEndian(data, 4 + i * 4 * 256);
            }

            int nidx = (textOffset - 4 - 256 * 256 * 4) / 9;
            indexData = new int[nidx];
            textStartIndex = new int[nidx];
            textLengthIndex = new short[nidx];

            for (int i = 0, off = 0; i < nidx; i++) {
                off = 4 + 256 * 256 * 4 + i * 9;
                indexData[i] = bigEndian(ipData, off);
                textStartIndex[i] = ((int) ipData[off + 6] & 0xff) << 16 | ((int) ipData[off + 5] & 0xff) << 8
                        | ((int) ipData[off + 4] & 0xff);
                textLengthIndex[i] = (short) (((int) ipData[off + 7] & 0xff) << 8 | ((int) ipData[off + 8] & 0xff));
            }
        } else {

            for (int i = 0; i < 256; i++) {
                index[i] = littleEndian(data, 4 + i * 4);
            }
            textOffset = offset - 1024;

            int nIdx = (textOffset - 4 - 1024) / 8;
            indexData = new int[nIdx];
            textStartIndex = new int[nIdx];
            textLengthIndex = new short[nIdx];

            for (int i = 0, off = 0; i < nIdx; i++) {
                off = 4 + 1024 + i * 8;
                indexData[i] = bigEndian(ipData, off);
                textStartIndex[i] = ((int) ipData[off + 6] & 0xff) << 16 | ((int) ipData[off + 5] & 0xff) << 8
                        | ((int) ipData[off + 4] & 0xff);
                textLengthIndex[i] = ipData[off + 7];
            }
        }
    }

    static int bigEndian(byte[] data, int offset) {
        int a = (((int) data[offset]) & 0xff);
        int b = (((int) data[offset + 1]) & 0xff);
        int c = (((int) data[offset + 2]) & 0xff);
        int d = (((int) data[offset + 3]) & 0xff);
        return (a << 24) | (b << 16) | (c << 8) | d;
    }

    static int littleEndian(byte[] data, int offset) {
        int a = (((int) data[offset]) & 0xff);
        int b = (((int) data[offset + 1]) & 0xff);
        int c = (((int) data[offset + 2]) & 0xff);
        int d = (((int) data[offset + 3]) & 0xff);
        return (d << 24) | (c << 16) | (b << 8) | a;
    }

    static byte parseOctet(String ipPart) {
        // Note: we already verified that this string contains only hex digits.
        int octet = Integer.parseInt(ipPart);
        // Disallow leading zeroes, because no clear standard exists on
        // whether these should be interpreted as decimal or octal.
        if (octet < 0 || octet > 255 || (ipPart.startsWith("0") && ipPart.length() > 1)) {
            throw new NumberFormatException("invalid ip part");
        }
        return (byte) octet;
    }

    static byte[] textToNumericFormatV4(String str) {
        String[] s = str.split("\\.");
        if (s.length != 4) {
            throw new NumberFormatException("the ip is not v4");
        }
        byte[] b = new byte[4];
        b[0] = parseOctet(s[0]);
        b[1] = parseOctet(s[1]);
        b[2] = parseOctet(s[2]);
        b[3] = parseOctet(s[3]);
        return b;
    }

    static LocationInfo buildInfo(byte[] bytes, int offset, int len) {
        String str = new String(bytes, offset, len, Utf8);
        String[] ss = str.split("\t", -1);
        if (ss.length == 4) {
            return new LocationInfo(ss[0], ss[1], ss[2], "");
        } else if (ss.length >= 5) {
            return new LocationInfo(ss[0], ss[1], ss[2], ss[4]);
        } else if (ss.length == 3) {
            return new LocationInfo(ss[0], ss[1], ss[2], "");
        } else if (ss.length == 2) {
            return new LocationInfo(ss[0], ss[1], "", "");
        }
        return null;
    }

    public static Locator loadFromNet(String netPath) throws IOException {
        URL url = new URL(netPath);
        HttpURLConnection httpConn = (HttpURLConnection) url.openConnection();
        httpConn.setConnectTimeout(3000);
        httpConn.setReadTimeout(30 * 1000);
        int responseCode = httpConn.getResponseCode();
        if (responseCode != HttpURLConnection.HTTP_OK) {
            return null;
        }

        int length = httpConn.getContentLength();
        if (length <= 0 || length > 64 * 1024 * 1024) {
            throw new InputMismatchException("invalid ip data");
        }
        InputStream is = httpConn.getInputStream();
        byte[] data = new byte[length];
        int downloaded = 0;
        int read = 0;
        while (downloaded < length) {
            try {
                read = is.read(data, downloaded, length - downloaded);
            } catch (IOException e) {
                is.close();
                throw new IOException("read error");
            }
            if (read < 0) {
                is.close();
                throw new IOException("read error");
            }
            downloaded += read;
        }

        is.close();

        String path = url.getPath();
        if (path.toLowerCase().endsWith("datx")) {
            return loadBinary(data, true);
        } else if (path.toLowerCase().endsWith("dat")) {
            return loadBinary(data, false);
        } else {
            return loadBinaryUnkown(data);
        }
    }

    public static Locator loadFromLocal(String filePath) throws IOException {
        File f = new File(filePath);
        FileInputStream fi = new FileInputStream(f);
        byte[] b = new byte[(int) f.length()];
        try {
            fi.read(b);
        } catch (IOException e) {
            e.printStackTrace();
            try {
                fi.close();
            } catch (IOException e1) {
                e1.printStackTrace();
            }
            throw e;
        }
        fi.close();

        if (filePath.toLowerCase().endsWith("datx")) {
            return loadBinary(b, true);
        } else if (filePath.toLowerCase().endsWith("dat")) {
            return loadBinary(b, false);
        } else {
            return loadBinaryUnkown(b);
        }
    }

    public static Locator loadFromStream(InputStream in, boolean x) throws Exception {
        ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
        byte[] buffer = new byte[16 * 1024];
        int n;

        while ((n = in.read(buffer)) != -1) {
            byteArrayOutputStream.write(buffer, 0, n);
        }

        return loadBinary(byteArrayOutputStream.toByteArray(), x);
    }

    public static Locator loadFromStreamUnkown(InputStream in) throws Exception {
        ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
        byte[] buffer = new byte[16 * 1024];
        int n;

        while ((n = in.read(buffer)) != -1) {
            byteArrayOutputStream.write(buffer, 0, n);
        }

        return loadBinaryUnkown(byteArrayOutputStream.toByteArray());
    }

    public static Locator loadFromStreamOld(InputStream in) throws Exception {
        ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
        byte[] buffer = new byte[16 * 1024];
        int n;

        while ((n = in.read(buffer)) != -1) {
            byteArrayOutputStream.write(buffer, 0, n);
        }

        return loadBinaryOld(byteArrayOutputStream.toByteArray());
    }

    public static Locator loadBinary(byte[] ipdb, boolean x) {
        return new Locator(ipdb, x);
    }

    public static Locator loadBinaryUnkown(byte[] ipdb) {
        return new Locator(ipdb);
    }

    public static Locator loadBinaryOld(byte[] ipdb) {
        return loadBinary(ipdb, false);
    }

    public static void main(String[] args) {
        if (args == null || args.length < 2) {
            System.out.println("locator ipfile ip");
            return;
        }
        try {
            Locator l = loadFromLocal(args[0]);
            System.out.println(l.find(args[1]));
        } catch (IOException e) {
            e.printStackTrace();
        }

    }

    private int findIndexOffset(long ip, int start, int end) {
        int mid = 0;
        while (start < end) {
            mid = (start + end) / 2;
            long l = 0xffffffffL & ((long) indexData[mid]);
            if (ip > l) {
                start = mid + 1;
            } else {
                end = mid;
            }
        }
        long l = ((long) indexData[end]) & 0xffffffffL;
        if (l >= ip) {
            return end;
        }
        return start;
    }

    public LocationInfo find(String ip) {
        byte[] b;
        try {
            b = textToNumericFormatV4(ip);
        } catch (Exception e) {
            return null;
        }
        return find(b);
    }

    public LocationInfo find(byte[] ipBin) {
        int end = indexData.length - 1;
        int a = 0xff & ((int) ipBin[0]);
        if (a != 0xff) {
            end = index[a + 1];
        }
        long ip = (long) bigEndian(ipBin, 0) & 0xffffffffL;
        int idx = findIndexOffset(ip, index[a], end);
        int off = textStartIndex[idx];
        return buildInfo(ipData, textOffset + off, 0xffff & (int) textLengthIndex[idx]);
    }

    public LocationInfo find(int address) {
        byte[] addr = new byte[4];

        addr[0] = (byte) ((address >> 24) & 0xff);
        addr[1] = (byte) ((address >> 16) & 0xff);
        addr[2] = (byte) ((address >> 8) & 0xff);
        addr[3] = (byte) (address & 0xff);

        return find(addr);
    }

    public void checkDb() throws IOException {
        byte[] addr = new byte[4];
        try {
            for (long x = 0; x < 0xffffffffL; x++) {
                addr[0] = (byte) ((x >> 24) & 0xff);
                addr[1] = (byte) ((x >> 16) & 0xff);
                addr[2] = (byte) ((x >> 8) & 0xff);
                addr[3] = (byte) (x & 0xff);
                find(addr);
            }
        } catch (Exception e) {
            throw new IOException(e.getMessage());
        }
    }
}