<?php
namespace addons\login_area_whitelist\ip;

class Reader
{
    private $file;
    private $fileSize = 0;
    private $nodeCount = 0;
    private $nodeOffset = 0;
    private $meta = [];
    private $database = "";
    private $v4offset = 0;
    private $v6offsetCache = [];
    const IPV4 = 1;
    const IPV6 = 2;
    public function __construct($database)
    {
        $this->database = $database;
        $this->init();
    }
    private function init()
    {
        ini_set("memory_limit", "2000M");
        if (is_readable($this->database) === false) {
            throw new \InvalidArgumentException("The IP Database file \"" . $this->database . "\" does not exist or is not readable.");
        }
        $this->file = @fopen($this->database, "rb");
        if ($this->file === false) {
            throw new \InvalidArgumentException("IP Database File opening \"" . $this->database . "\".");
        }
        $this->fileSize = @filesize($this->database);
        if ($this->fileSize === false) {
            throw new \UnexpectedValueException("Error determining the size of \"" . $this->database . "\".");
        }
        list($metaLength) = unpack("N", fread($this->file, 4));
        $text = fread($this->file, $metaLength);
        $this->meta = json_decode($text, 1);
        if (isset($this->meta["fields"]) === false || isset($this->meta["languages"]) === false) {
            throw new \Exception("IP Database metadata error.");
        }
        $fileSize = 4 + $metaLength + $this->meta["total_size"];
        if ($fileSize != $this->fileSize) {
            throw new \Exception("IP Database size error.");
        }
        $this->nodeCount = $this->meta["node_count"];
        $this->nodeOffset = 4 + $metaLength;
    }
    public function find($ip, $language)
    {
        if (is_resource($this->file) === false) {
            throw new \BadMethodCallException("IPIP DB closed.");
        }
        if (isset($this->meta["languages"][$language]) === false) {
            throw new \InvalidArgumentException("language : " . $language . " not support.");
        }
        if (filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_IPV4 | FILTER_FLAG_IPV6) === false) {
            throw new \InvalidArgumentException("The value \"" . $ip . "\" is not a valid IP address.");
        }
        if (strpos($ip, ".") !== false && !$this->supportV4()) {
            throw new \InvalidArgumentException("The Database not support IPv4 address.");
        }
        if (strpos($ip, ":") !== false && !$this->supportV6()) {
            throw new \InvalidArgumentException("The Database not support IPv6 address.");
        }
        try {
            $node = $this->findNode($ip);
            if (0 < $node) {
                $data = $this->resolve($node);
                $values = explode("\t", $data);
                return array_slice($values, $this->meta["languages"][$language], count($this->meta["fields"]));
            }
        } catch (\Exception $e) {
            return NULL;
        }
    }
    public function findMap($ip, $language)
    {
        $array = $this->find($ip, $language);
        if (NULL === $array) {
            return NULL;
        }
        return array_combine($this->meta["fields"], $array);
    }
    private function findNode($ip)
    {
        $binary = inet_pton($ip);
        $bitCount = strlen($binary) * 8;
        $key = substr($binary, 0, 2);
        $node = 0;
        $index = 0;
        if ($bitCount === 32) {
            if ($this->v4offset === 0) {
                for ($i = 0; $i < 96 && $node < $this->nodeCount; $i++) {
                    if (80 <= $i) {
                        $idx = 1;
                    } else {
                        $idx = 0;
                    }
                    $node = $this->readNode($node, $idx);
                    if ($this->nodeCount < $node) {
                        return 0;
                    }
                }
                $this->v4offset = $node;
            } else {
                $node = $this->v4offset;
            }
        } else {
            if (isset($this->v6offsetCache[$key])) {
                $index = 16;
                $node = $this->v6offsetCache[$key];
            }
        }
        for ($i = $index; $i < $bitCount; $i++) {
            if ($this->nodeCount <= $node) {
                break;
            }
            $node = $this->readNode($node, 1 & (255 & ord($binary[$i >> 3])) >> 7 - $i % 8);
            if ($i == 15) {
                $this->v6offsetCache[$key] = $node;
            }
        }
        if ($node === $this->nodeCount) {
            return 0;
        }
        if ($this->nodeCount < $node) {
            return $node;
        }
        throw new \Exception("find node failed.");
    }
    private function readNode($node, $index)
    {
        return unpack("N", $this->read($this->file, $node * 8 + $index * 4, 4))[1];
    }
    private function resolve($node)
    {
        $resolved = $node - $this->nodeCount + $this->nodeCount * 8;
        if ($this->fileSize <= $resolved) {
            return NULL;
        }
        $bytes = $this->read($this->file, $resolved, 2);
        list($size) = unpack("N", str_pad($bytes, 4, "\0", STR_PAD_LEFT));
        $resolved += 2;
        return $this->read($this->file, $resolved, $size);
    }
    public function close()
    {
        if (is_resource($this->file) === true) {
            fclose($this->file);
        }
    }
    private function read($stream, $offset, $length)
    {
        if (0 < $length) {
            if (fseek($stream, $offset + $this->nodeOffset) === 0) {
                $value = fread($stream, $length);
                if (strlen($value) === $length) {
                    return $value;
                }
            }
            throw new \Exception("The Database file read bad data.");
        }
        return "";
    }
    public function supportV6()
    {
        return ($this->meta["ip_version"] & self::IPV6) === self::IPV6;
    }
    public function supportV4()
    {
        return ($this->meta["ip_version"] & self::IPV4) === self::IPV4;
    }
    public function getMeta()
    {
        return $this->meta;
    }
    public function getBuildTime()
    {
        return $this->meta["build"];
    }
}

?>