package com.fos.core.geoip;

import java.io.File;
import java.io.IOException;
import java.io.RandomAccessFile;
import java.io.UnsupportedEncodingException;
import java.net.InetAddress;
import java.nio.ByteOrder;
import java.nio.MappedByteBuffer;
import java.nio.channels.FileChannel;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.StringTokenizer;

import org.apache.log4j.Logger;
import org.geo.split.RegionEntity;
import org.geo.split.RegionSplit;

import com.fos.core.conf.GConstants;
import com.maxmind.geoip2.DatabaseReader;
import com.maxmind.geoip2.model.CityResponse;
import com.maxmind.geoip2.record.City;
import com.maxmind.geoip2.record.Country;
import com.maxmind.geoip2.record.Subdivision;

/**
 * 
 * 通过IP查询地理信息
 * @team BBDTEK Dev Team
 * @version 1.0
 * @time 2016-04-06
 * 
 */
public final class IPSeeker {
    private final Object mutex = new Object();
    private final Logger LOG = Logger.getLogger(IPSeeker.class);
    
    private class IPLocation {
        public String country = null;
        public String area = null;
        
        public IPLocation() {
            
        }
        public IPLocation getCopy() {
            IPLocation ret = new IPLocation();
            ret.country = this.country;
            ret.area = this.area;
            return ret;
        }
    }
    
    private final Map<String, IPLocation> ipCache = new HashMap<String, IPLocation>();
    private final IPLocation loc = new IPLocation();
    private final byte[] buf100  = new byte[100];
    private final byte[] buf4    = new byte[4];
    private final byte[] buf3    = new byte[3];
    private RandomAccessFile ipFile = null;
    private MappedByteBuffer mbb    = null;
    private long ipBegin = 0L;
    private long ipEnd   = 0L;
    private DatabaseReader reader = null;
    
    private static IPSeeker INS = new IPSeeker();
    
    private IPSeeker() {
        String dir = GConstants.getValue("geoip.db.dir", null);
        try {
            if(dir == null || dir.length() == 0){
                dir = IPSeeker.class.getResource("/").getPath();
                if (dir != null && dir.contains("WEB-INF")) {// 是WEB项目的时候获取WebContent下的路径
                    dir = dir.substring(0, dir.indexOf("WEB-INF"))+"res"+GConstants.FS;
                } else {// 非WEB项目获取当前路径
                    File file = new File("");
                    dir = file.getAbsolutePath()+GConstants.FS+"res"+GConstants.FS;
                }
            }
            File db = new File(dir+"QQWry.dat");
            LOG.info("加载QQWry.dat数据："+db.getAbsolutePath());
            if(db.exists()){
                this.ipFile = new RandomAccessFile(db, "r");
                if (this.ipFile != null) {
                    this.ipBegin = readLong4(0L);
                    this.ipEnd = readLong4(4L);
                    if ((this.ipBegin == -1L) || (this.ipEnd == -1L)) {
                        this.ipFile.close();
                        this.ipFile = null;
                    }
                }
                LOG.info("成功加载QQWry.dat数据库");
            }
        } catch (Exception e) {
            LOG.error("QQWry.dat数据库不可用");
            e.printStackTrace();
        }
        try {
            File db = new File(dir+"GeoLite2-City.mmdb");
            LOG.info("加载GeoLite2-City.mmdb数据："+db.getAbsolutePath());
            if(db.exists()){
                reader = new DatabaseReader.Builder(db).build();
                LOG.info("成功加载GeoLite2-City.mmdb数据库");
            }
        } catch (Exception e) {
            // TODO Auto-generated catch block
            e.printStackTrace();
            LOG.error("GeoLite2-City.mmdb数据库不可用");
        }
    }

    public static IPSeeker getInstance() {
        return INS;
    }
    public static Address getAddress(String ip) {
        String fullName = IPSeeker.getInstance().getCountry(ip);// 通过纯真数据库获取IP的位置名称
        if (fullName == null) {
            fullName = IPSeeker.getInstance().getGeoLiteCity(ip);// 通过GeoLite库获取IP的位置名称
        }
        if (fullName == null) {
            return null;
        }
        RegionEntity region = RegionSplit.singleton(fullName);// 格式化地理名称
        if(region == null || region.getRegion()==null || region.getRegion().isEmpty())
            return null;
        Address address = new Address(region.getRegion(), region.getPostcode());
        if("中国".equals(address.getCountry()) == false){// 非中国地区使用GeoLite库
            
            fullName = IPSeeker.getInstance().getGeoLiteCity(ip);
            if(fullName != null && fullName.startsWith("中国") == false){// 非中国地区
                
                return new Address(fullName, null);
            }
        }
        return address;
    }
    /**
     * 通过GeoLite库获取IP的位置名称
     * @param ip
     * @return
     */
    public String getGeoLiteCity(String ip){
        if(reader == null)
            return null;
        synchronized (mutex) {
            try {
                
                InetAddress ipAddress = InetAddress.getByName(ip);
                
                CityResponse response = reader.city(ipAddress);
                Country country = response.getCountry();
                String temp = country.getNames().get("zh-CN");
                String fullName = temp == null?"":temp;
                
                Subdivision subdivision = response.getMostSpecificSubdivision();
                
                temp = subdivision.getNames().get("zh-CN");
                fullName = temp == null? fullName:fullName+" "+temp;
                
                
                City city = response.getCity();
                temp = city.getNames().get("zh-CN");
                fullName = temp == null? fullName:fullName+" "+temp;
                
                if(fullName == null || fullName.length() == 0)
                    return null;
                return fullName;
            } catch (Exception e) {
                // TODO Auto-generated catch block
                e.printStackTrace();
            }
            return null;
        }
    }
    public List<IPEntry> getIPEntriesDebug(String s) {
        List<IPEntry> ret = new ArrayList<IPEntry>();
        long endOffset = this.ipEnd + 4L;
        for (long offset = this.ipBegin + 4L; offset <= endOffset; offset += 7L) {
            long temp = readLong3(offset);
            if (temp != -1L) {
                IPLocation ipLoc = getIPLocation(temp);
                if (s == null || (ipLoc.country.indexOf(s) != -1) || (ipLoc.area.indexOf(s) != -1)) {
                    IPEntry entry = new IPEntry();
                    entry.setCountry(ipLoc.country);
                    entry.setArea(ipLoc.area);

                    readIP(offset - 4L, this.buf4);
//                    entry.beginIp = IPSeeker.getIpStringFromBytes(this.buf4);
                    entry.setBeginIp(IPSeeker.getIpStringFromBytes(this.buf4));
                    readIP(temp, this.buf4);
//                    entry.endIp = IPSeeker.getIpStringFromBytes(this.buf4);
                    entry.setEndIp(IPSeeker.getIpStringFromBytes(this.buf4));
                    ret.add(entry);
                }
            }
        }
        return ret;
    }

    public List<IPEntry> getIPEntries(String s) {
        List<IPEntry> ret = new ArrayList<IPEntry>();
        try {
            if (this.mbb == null) {
                FileChannel fc = this.ipFile.getChannel();
                this.mbb = fc.map(FileChannel.MapMode.READ_ONLY, 0L, this.ipFile.length());
                this.mbb.order(ByteOrder.LITTLE_ENDIAN);
            }
            int endOffset = (int) this.ipEnd;
            for (int offset = (int) this.ipBegin + 4; offset <= endOffset; offset += 7) {
                int temp = readInt3(offset);
                if (temp != -1) {
                    IPLocation ipLoc = getIPLocation(temp);
                    if (s == null || (ipLoc.country.indexOf(s) != -1) || (ipLoc.area.indexOf(s) != -1)) {
                        IPEntry entry = new IPEntry();
//                        entry.country = ipLoc.country;
//                        entry.area = ipLoc.area;
                        entry.setCountry(ipLoc.country);
                        entry.setArea(ipLoc.area);
                        readIP(offset - 4, this.buf4);
//                        entry.beginIp = IPSeeker.getIpStringFromBytes(this.buf4);
                        entry.setBeginIp(IPSeeker.getIpStringFromBytes(this.buf4));
                        readIP(temp, this.buf4);
//                        entry.endIp = IPSeeker.getIpStringFromBytes(this.buf4);
                        entry.setEndIp(IPSeeker.getIpStringFromBytes(this.buf4));
                        ret.add(entry);
                    }
                }
            }
        } catch (IOException e) {
            e.printStackTrace();
        } catch (Exception e) {
            e.printStackTrace();
        }
        return ret;
    }
    private int readInt3(int offset) {
        this.mbb.position(offset);
        return this.mbb.getInt() & 0xFFFFFF;
    }

    private int readInt3() {
        return this.mbb.getInt() & 0xFFFFFF;
    }

    public String getCountry(byte[] ip) {
        if (this.ipFile == null) {
            return null;
        }
        String ipStr = IPSeeker.getIpStringFromBytes(ip);
        if (this.ipCache.containsKey(ipStr)) {
            IPLocation ipLoc = (IPLocation) this.ipCache.get(ipStr);
            return ipLoc.country;
        }
        IPLocation ipLoc = getIPLocation(ip);
        this.ipCache.put(ipStr, ipLoc.getCopy());
        return ipLoc.country;
    }

    public String getCountry(String ip) {
        return getCountry(IPSeeker.getIpByteArrayFromString(ip));
    }

    public String getArea(byte[] ip) {
        if (this.ipFile == null) {
            return null;
        }
        String ipStr = IPSeeker.getIpStringFromBytes(ip);
        if (this.ipCache.containsKey(ipStr)) {
            IPLocation ipLoc = (IPLocation) this.ipCache.get(ipStr);
            return ipLoc.area;
        }
        IPLocation ipLoc = getIPLocation(ip);
        this.ipCache.put(ipStr, ipLoc.getCopy());
        return ipLoc.area;
    }

    public String getArea(String ip) {
        return getArea(IPSeeker.getIpByteArrayFromString(ip));
    }

    private IPLocation getIPLocation(byte[] ip) {
        IPLocation info = null;
        long offset = locateIP(ip);
        if (offset != -1L) {
            info = getIPLocation(offset);
        }
        if (info == null) {
            info = new IPLocation();
            info.country = null;
            info.area = null;
        }
        return info;
    }

    private long readLong4(long offset) {
        long ret = 0L;
        try {
            this.ipFile.seek(offset);
            ret |= this.ipFile.readByte() & 0xFF;
            ret |= this.ipFile.readByte() << 8 & 0xFF00;
            ret |= this.ipFile.readByte() << 16 & 0xFF0000;
            return ret | this.ipFile.readByte() << 24 & 0xFF000000;
        } catch (IOException e) {
        }
        return -1L;
    }

    private long readLong3(long offset) {
        long ret = 0L;
        try {
            this.ipFile.seek(offset);
            this.ipFile.readFully(this.buf3);
            ret |= this.buf3[0] & 0xFF;
            ret |= this.buf3[1] << 8 & 0xFF00;
            return ret | this.buf3[2] << 16 & 0xFF0000;
        } catch (IOException e) {
        }
        return -1L;
    }

    private long readLong3() {
        long ret = 0L;
        try {
            this.ipFile.readFully(this.buf3);
            ret |= this.buf3[0] & 0xFF;
            ret |= this.buf3[1] << 8 & 0xFF00;
            return ret | this.buf3[2] << 16 & 0xFF0000;
        } catch (IOException e) {
        }
        return -1L;
    }

    private void readIP(long offset, byte[] ip) {
        try {
            this.ipFile.seek(offset);
            this.ipFile.readFully(ip);
            byte temp = ip[0];
            ip[0] = ip[3];
            ip[3] = temp;
            temp = ip[1];
            ip[1] = ip[2];
            ip[2] = temp;
        } catch (IOException e) {
            e.printStackTrace();
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    private void readIP(int offset, byte[] ip) {
        this.mbb.position(offset);
        this.mbb.get(ip);
        byte temp = ip[0];
        ip[0] = ip[3];
        ip[3] = temp;
        temp = ip[1];
        ip[1] = ip[2];
        ip[2] = temp;
    }

    private int compareIP(byte[] ip, byte[] beginIp) {
        for (int i = 0; i < 4; i++) {
            int r = compareByte(ip[i], beginIp[i]);
            if (r != 0) {
                return r;
            }
        }
        return 0;
    }

    private int compareByte(byte b1, byte b2) {
        if ((b1 & 0xFF) > (b2 & 0xFF)) {
            return 1;
        }
        if ((b1 ^ b2) == 0) {
            return 0;
        }
        return -1;
    }

    private long locateIP(byte[] ip) {
        long m = 0L;

        readIP(this.ipBegin, this.buf4);
        int r = compareIP(ip, this.buf4);
        if (r == 0) {
            return this.ipBegin;
        }
        if (r < 0) {
            return -1L;
        }
        long i = this.ipBegin;
        for (long j = this.ipEnd; i < j;) {
            m = getMiddleOffset(i, j);
            readIP(m, this.buf4);
            r = compareIP(ip, this.buf4);
            if (r > 0) {
                i = m;
            } else if (r < 0) {
                if (m == j) {
                    j -= 7L;
                    m = j;
                } else {
                    j = m;
                }
            } else {
                return readLong3(m + 4L);
            }
        }
        m = readLong3(m + 4L);
        readIP(m, this.buf4);
        r = compareIP(ip, this.buf4);
        if (r <= 0) {
            return m;
        }
        return -1L;
    }

    private long getMiddleOffset(long begin, long end) {
        long records = (end - begin) / 7L;
        records >>= 1;
        if (records == 0L) {
            records = 1L;
        }
        return begin + records * 7L;
    }

    private IPLocation getIPLocation(long offset) {
        synchronized (mutex) {
            try {
                this.ipFile.seek(offset + 4L);

                byte b = this.ipFile.readByte();
                if (b == 1) {
                    long countryOffset = readLong3();

                    this.ipFile.seek(countryOffset);

                    b = this.ipFile.readByte();
                    if (b == 2) {
                        this.loc.country = readString(readLong3());
                        this.ipFile.seek(countryOffset + 4L);
                    } else {
                        this.loc.country = readString(countryOffset);
                    }
                    this.loc.area = readArea(this.ipFile.getFilePointer());
                } else if (b == 2) {
                    this.loc.country = readString(readLong3());
                    this.loc.area = readArea(offset + 8L);
                } else {
                    this.loc.country = readString(this.ipFile.getFilePointer() - 1L);
                    this.loc.area = readArea(this.ipFile.getFilePointer());
                }
                return this.loc;
            } catch (IOException e) {
                e.printStackTrace();
            } catch (Exception e) {
                e.printStackTrace();
            }
            return null;
        }
    }

    private IPLocation getIPLocation(int offset) {
        synchronized (mutex) {
            this.mbb.position(offset + 4);

            byte b = this.mbb.get();
            if (b == 1) {
                int countryOffset = readInt3();

                this.mbb.position(countryOffset);

                b = this.mbb.get();
                if (b == 2) {
                    this.loc.country = readString(readInt3());
                    this.mbb.position(countryOffset + 4);
                } else {
                    this.loc.country = readString(countryOffset);
                }
                this.loc.area = readArea(this.mbb.position());
            } else if (b == 2) {
                this.loc.country = readString(readInt3());
                this.loc.area = readArea(offset + 8);
            } else {
                this.loc.country = readString(this.mbb.position() - 1);
                this.loc.area = readArea(this.mbb.position());
            }
            return this.loc;
        }
    }

    private String readArea(long offset) throws IOException {
        this.ipFile.seek(offset);
        byte b = this.ipFile.readByte();
        if ((b == 1) || (b == 2)) {
            long areaOffset = readLong3(offset + 1L);
            if (areaOffset == 0L) {
                return null;
            }
            return readString(areaOffset);
        }
        return readString(offset);
    }

    private String readArea(int offset) {
        this.mbb.position(offset);
        byte b = this.mbb.get();
        if ((b == 1) || (b == 2)) {
            int areaOffset = readInt3();
            if (areaOffset == 0) {
                return null;
            }
            return readString(areaOffset);
        }
        return readString(offset);
    }

    private String readString(long offset) {
        try {
            this.ipFile.seek(offset);

            int i = 0;
            for (this.buf100[i] = this.ipFile.readByte(); this.buf100[i] != 0; this.buf100[(++i)] = this.ipFile.readByte()) {
            }
            if (i != 0) {
                return IPSeeker.getString(this.buf100, 0, i, "GBK");
            }
        } catch (IOException e) {
            e.printStackTrace();
        } catch (Exception e) {
            e.printStackTrace();
        }
        return "";
    }

    private String readString(int offset) {
        try {
            this.mbb.position(offset);

            int i = 0;
            for (this.buf100[i] = this.mbb.get(); this.buf100[i] != 0; this.buf100[(++i)] = this.mbb.get()) {
            }
            if (i != 0) {
                return IPSeeker.getString(this.buf100, 0, i, "GBK");
            }
        } catch (IllegalArgumentException e) {
            e.printStackTrace();
        } catch (Exception e) {
            e.printStackTrace();
        }
        return "";
    }
    
    public static String getIpStringFromBytes(byte[] ip) {
        try {
            StringBuilder buf = new StringBuilder();
            buf.delete(0, buf.length());
            buf.append(ip[0] & 0xFF);
            buf.append('.');
            buf.append(ip[1] & 0xFF);
            buf.append('.');
            buf.append(ip[2] & 0xFF);
            buf.append('.');
            buf.append(ip[3] & 0xFF);
            return buf.toString();
        } catch (Exception e) {
            // TODO: handle exception
            e.printStackTrace();
        }
        return null;
    }

    public static byte[] getIpByteArrayFromString(String ip) {
        byte[] ret = new byte[4];
        StringTokenizer st = new StringTokenizer(ip, ".");
        try {
            ret[0] = ((byte) (Integer.parseInt(st.nextToken()) & 0xFF));
            ret[1] = ((byte) (Integer.parseInt(st.nextToken()) & 0xFF));
            ret[2] = ((byte) (Integer.parseInt(st.nextToken()) & 0xFF));
            ret[3] = ((byte) (Integer.parseInt(st.nextToken()) & 0xFF));
        } catch (Exception e) {
            e.printStackTrace();
        }
        return ret;
    }

    public static String getString(byte[] b, int offset, int len, String encoding) {
        try {
            return new String(b, offset, len, encoding);
        } catch (UnsupportedEncodingException e) {
            e.printStackTrace();
        } catch (Exception e) {
            e.printStackTrace();
        }
        return new String(b, offset, len);
    }
}
