package com.tiesheng.util.ip2region;

import cn.hutool.core.io.FileUtil;
import cn.hutool.core.io.IoUtil;
import cn.hutool.core.io.resource.ClassPathResource;

import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.io.RandomAccessFile;
import java.nio.charset.StandardCharsets;

/**
 * ip db searcher class (Not thread safe)
 *
 * @author chenxin<chenxin619315 @ gmail.com>
 */
public class Ip2Region {


    private static Ip2Region ip2Region;
    /**
     * db config
     */
    private final DbConfig dbConfig;
    /**
     * db file access handler
     */
    private RandomAccessFile raf = null;
    /**
     * header blocks buffer
     */
    private long[] headerSip = null;
    private int[] headerPtr = null;
    private int headerLength;
    /**
     * super blocks info
     */
    private long firstIndexPtr = 0;
    private long lastIndexPtr = 0;
    private int totalIndexBlocks = 0;
    /**
     * for memory mode
     * the original db binary string
     */
    private byte[] dbBinStr = null;

    /**
     * construct class
     */
    public Ip2Region() {
        this.dbConfig = new DbConfig();
        try {
            ClassPathResource cpr = new ClassPathResource("ipdb/ip2region.db");
            InputStream inputStream = cpr.getStream();
            File tempFile = File.createTempFile("ip2region", ".db");
            try {
                FileUtil.writeFromStream(inputStream, tempFile);
            } finally {
                IoUtil.close(inputStream);
            }
            raf = new RandomAccessFile(tempFile, "r");
        } catch (Exception ignored) {
        }
    }

    /**
     * 获取单例
     *
     * @return
     */
    public static Ip2Region getInstance() {
        if (ip2Region == null) {
            synchronized (Ip2Region.class) {
                if (ip2Region == null) {
                    ip2Region = new Ip2Region();
                }
            }
        }
        return ip2Region;
    }

    /**
     * get the region with an int ip address with memory binary search algorithm
     *
     * @param ip
     * @throws IOException
     */
    public DataBlock memorySearch(long ip) throws IOException {
        int blen = IndexBlock.getIndexBlockLength();
        if (dbBinStr == null) {
            dbBinStr = new byte[(int) raf.length()];
            raf.seek(0L);
            raf.readFully(dbBinStr, 0, dbBinStr.length);

            //initialize the global vars
            firstIndexPtr = IpUtil.getIntLong(dbBinStr, 0);
            lastIndexPtr = IpUtil.getIntLong(dbBinStr, 4);
            totalIndexBlocks = (int) ((lastIndexPtr - firstIndexPtr) / blen) + 1;
        }

        //search the index blocks to define the data
        int l = 0, h = totalIndexBlocks;
        long sip, eip, dataptr = 0;
        while (l <= h) {
            int m = (l + h) >> 1;
            int p = (int) (firstIndexPtr + m * blen);

            sip = IpUtil.getIntLong(dbBinStr, p);
            if (ip < sip) {
                h = m - 1;
            } else {
                eip = IpUtil.getIntLong(dbBinStr, p + 4);
                if (ip > eip) {
                    l = m + 1;
                } else {
                    dataptr = IpUtil.getIntLong(dbBinStr, p + 8);
                    break;
                }
            }
        }

        //not matched
        if (dataptr == 0) {
            return null;
        }

        //get the data
        int dataLen = (int) ((dataptr >> 24) & 0xFF);
        int dataPtr = (int) ((dataptr & 0x00FFFFFF));
        int cityId = (int) IpUtil.getIntLong(dbBinStr, dataPtr);
        String region = new String(dbBinStr, dataPtr + 4, dataLen - 4, StandardCharsets.UTF_8);

        return new DataBlock(cityId, region, dataPtr);
    }

    /**
     * get the region throught the ip address with memory binary search algorithm
     *
     * @return DataBlock
     * @throws IOException
     */
    public DataBlock memorySearch() throws IOException {
        return memorySearch(null);
    }

    /**
     * get the region throught the ip address with memory binary search algorithm
     *
     * @param ip
     * @return DataBlock
     * @throws IOException
     */
    public DataBlock memorySearch(String ip) throws IOException {
        return memorySearch(IpUtil.ip2long(ip));
    }


    /**
     * get by index ptr
     *
     * @param ptr
     * @throws IOException
     */
    public DataBlock getByIndexPtr(long ptr) throws IOException {
        raf.seek(ptr);
        byte[] buffer = new byte[12];
        raf.readFully(buffer, 0, buffer.length);
        long extra = IpUtil.getIntLong(buffer, 8);

        int dataLen = (int) ((extra >> 24) & 0xFF);
        int dataPtr = (int) ((extra & 0x00FFFFFF));

        raf.seek(dataPtr);
        byte[] data = new byte[dataLen];
        raf.readFully(data, 0, data.length);

        int cityId = (int) IpUtil.getIntLong(data, 0);
        String region = new String(data, 4, data.length - 4, StandardCharsets.UTF_8);

        return new DataBlock(cityId, region, dataPtr);
    }

    /**
     * get the region with an int ip address with b-tree algorithm
     *
     * @param ip
     * @throws IOException
     */
    public DataBlock btreeSearch(long ip) throws IOException {
        //check and load the header
        if (headerSip == null) {
            raf.seek(8L);
            byte[] b = new byte[dbConfig.getTotalHeaderSize()];
            raf.readFully(b, 0, b.length);

            //fill the header
            int len = b.length >> 3, idx = 0;
            headerSip = new long[len];
            headerPtr = new int[len];
            long startIp, dataPtr;
            for (int i = 0; i < b.length; i += 8) {
                startIp = IpUtil.getIntLong(b, i);
                dataPtr = IpUtil.getIntLong(b, i + 4);
                if (dataPtr == 0) {
                    break;
                }

                headerSip[idx] = startIp;
                headerPtr[idx] = (int) dataPtr;
                idx++;
            }

            headerLength = idx;
        }

        //1. define the index block with the binary search
        if (ip == headerSip[0]) {
            return getByIndexPtr(headerPtr[0]);
        } else if (ip == headerSip[headerLength - 1]) {
            return getByIndexPtr(headerPtr[headerLength - 1]);
        }

        int l = 0, h = headerLength, sptr = 0, eptr = 0;
        while (l <= h) {
            int m = (l + h) >> 1;

            //perfetc matched, just return it
            if (ip == headerSip[m]) {
                if (m > 0) {
                    sptr = headerPtr[m - 1];
                    eptr = headerPtr[m];
                } else {
                    sptr = headerPtr[m];
                    eptr = headerPtr[m + 1];
                }

                break;
            }

            //less then the middle value
            if (ip < headerSip[m]) {
                if (m == 0) {
                    sptr = headerPtr[m];
                    eptr = headerPtr[m + 1];
                    break;
                } else if (ip > headerSip[m - 1]) {
                    sptr = headerPtr[m - 1];
                    eptr = headerPtr[m];
                    break;
                }
                h = m - 1;
            } else {
                if (m == headerLength - 1) {
                    sptr = headerPtr[m - 1];
                    eptr = headerPtr[m];
                    break;
                } else if (ip <= headerSip[m + 1]) {
                    sptr = headerPtr[m];
                    eptr = headerPtr[m + 1];
                    break;
                }
                l = m + 1;
            }
        }

        //match nothing just stop it
        if (sptr == 0) {
            return null;
        }

        //2. search the index blocks to define the data
        int blockLen = eptr - sptr, blen = IndexBlock.getIndexBlockLength();
        //include the right border block
        byte[] iBuffer = new byte[blockLen + blen];
        raf.seek(sptr);
        raf.readFully(iBuffer, 0, iBuffer.length);

        l = 0;
        h = blockLen / blen;
        long sip, eip, dataptr = 0;
        while (l <= h) {
            int m = (l + h) >> 1;
            int p = m * blen;
            sip = IpUtil.getIntLong(iBuffer, p);
            if (ip < sip) {
                h = m - 1;
            } else {
                eip = IpUtil.getIntLong(iBuffer, p + 4);
                if (ip > eip) {
                    l = m + 1;
                } else {
                    dataptr = IpUtil.getIntLong(iBuffer, p + 8);
                    break;
                }
            }
        }

        //not matched
        if (dataptr == 0) {
            return null;
        }

        //3. get the data
        int dataLen = (int) ((dataptr >> 24) & 0xFF);
        int dataPtr = (int) ((dataptr & 0x00FFFFFF));

        raf.seek(dataPtr);
        byte[] data = new byte[dataLen];
        raf.readFully(data, 0, data.length);

        int cityId = (int) IpUtil.getIntLong(data, 0);
        String region = new String(data, 4, data.length - 4, StandardCharsets.UTF_8);

        return new DataBlock(cityId, region, dataPtr);
    }

    /**
     * get the region throught the ip address with b-tree search algorithm
     *
     * @param ip
     * @return DataBlock
     * @throws IOException
     */
    public DataBlock btreeSearch(String ip) {
        try {
            return btreeSearch(IpUtil.ip2long(ip));
        } catch (IOException ignored) {
        }
        return new DataBlock(0, "未知IP");
    }

    /**
     * get the region with a int ip address with binary search algorithm
     *
     * @param ip
     * @throws IOException
     */
    public DataBlock binarySearch(long ip) throws IOException {
        int blen = IndexBlock.getIndexBlockLength();
        if (totalIndexBlocks == 0) {
            raf.seek(0L);
            byte[] superBytes = new byte[8];
            raf.readFully(superBytes, 0, superBytes.length);
            //initialize the global vars
            firstIndexPtr = IpUtil.getIntLong(superBytes, 0);
            lastIndexPtr = IpUtil.getIntLong(superBytes, 4);
            totalIndexBlocks = (int) ((lastIndexPtr - firstIndexPtr) / blen) + 1;
        }

        //search the index blocks to define the data
        int l = 0, h = totalIndexBlocks;
        byte[] buffer = new byte[blen];
        long sip, eip, dataptr = 0;
        while (l <= h) {
            int m = (l + h) >> 1;
            raf.seek(firstIndexPtr + m * blen);
            raf.readFully(buffer, 0, buffer.length);
            sip = IpUtil.getIntLong(buffer, 0);
            if (ip < sip) {
                h = m - 1;
            } else {
                eip = IpUtil.getIntLong(buffer, 4);
                if (ip > eip) {
                    l = m + 1;
                } else {
                    dataptr = IpUtil.getIntLong(buffer, 8);
                    break;
                }
            }
        }

        //not matched
        if (dataptr == 0) {
            return null;
        }

        //get the data
        int dataLen = (int) ((dataptr >> 24) & 0xFF);
        int dataPtr = (int) ((dataptr & 0x00FFFFFF));

        raf.seek(dataPtr);
        byte[] data = new byte[dataLen];
        raf.readFully(data, 0, data.length);

        int cityId = (int) IpUtil.getIntLong(data, 0);
        String region = new String(data, 4, data.length - 4, StandardCharsets.UTF_8);

        return new DataBlock(cityId, region, dataPtr);
    }

    /**
     * get the region throught the ip address with binary search algorithm
     *
     * @param ip
     * @return DataBlock
     * @throws IOException
     */
    public DataBlock binarySearch(String ip) throws IOException {
        return binarySearch(IpUtil.ip2long(ip));
    }

    /**
     * get the db config
     *
     * @return DbConfig
     */
    public DbConfig getDbConfig() {
        return dbConfig;
    }

    /**
     * close the db
     *
     * @throws IOException
     */
    public void close() throws IOException {
        headerSip = null;
        headerPtr = null;
        dbBinStr = null;
        if (raf != null) {
            raf.close();
        }
    }

}
