<?php

namespace Aerys;

use Amp\Struct;

class Options {
    use Struct;

    const MAX_DEFLATE_ENABLE_CACHE_SIZE = 1024;

    private $debug;
    private $user = null;
    private $maxConnections = 1000;
    private $connectionsPerIP = 30; // IPv4: /32, IPv6: /56 (per RFC 6177)
    private $maxRequestsPerConnection = 1000; // set to PHP_INT_MAX to disable
    private $connectionTimeout = 6; // seconds
    private $defaultContentType = "text/html"; // can be vhost
    private $defaultTextCharset = "utf-8"; // can be vhost
    private $sendServerToken = false;
    private $socketBacklogSize = 128;
    private $normalizeMethodCase = true;
    private $maxConcurrentStreams = 20;
    private $maxFramesPerSecond = 60;
    private $allowedMethods = ["GET", "POST", "PUT", "PATCH", "HEAD", "OPTIONS", "DELETE"];
    private $deflateEnable;
    private $deflateContentTypes = '#^(?:text/.*+|[^/]*+/xml|[^+]*\+xml|application/(?:json|(?:x-)?javascript))$#i';
    private $configPath = null;

    private $maxFieldLen = 16384; // this must be strictly less than maxBodySize
    private $maxInputVars = 200;
    private $maxBodySize = 131072;
    private $maxHeaderSize = 32768;
    private $ioGranularity = 32768; // recommended: at least 16 KB
    private $softStreamCap = 131072; // should be multiple of outputBufferSize

    //@link http://webmasters.stackexchange.com/questions/31750/what-is-recommended-minimum-object-size-for-deflate-performance-benefits
    private $deflateMinimumLength = 860; // can be vhost
    private $deflateBufferSize = 8192; // can be vhost
    private $chunkBufferSize = 8192; // can be vhost
    private $outputBufferSize = 8192; // can be vhost

    private $shutdownTimeout = 3000; // milliseconds

    private $_dynamicCache;

    private $__initialized = false;

    public function __construct() {
        $this->deflateEnable = \extension_loaded("zlib");

        $this->_dynamicCache = new class {
            public $deflateContentTypes = [];
        };
    }

    /**
     * Allow retrieval of "public" properties.
     *
     * @param string $property
     * @return mixed Returns the value of the requested property
     * @throws \Error If an unknown property is requested
     */
    public function __get(string $property) {
        if (\property_exists($this, $property)) {
            return $this->{$property};
        }
        // Use \Amp\Struct::generateStructPropertyError() to get a nice message
        // with a possible suggestion for the correct name
        throw new \Error(
            $this->generateStructPropertyError($property)
        );
    }

    /**
     * Prevent external code from modifying our "public" time/date properties.
     *
     * @param string $property
     * @param mixed $value
     * @throws \Error If an unknown property is requested
     */
    public function __set(string $property, $value) {
        if (!\property_exists($this, $property)) {
            // Use \Amp\Struct::generateStructPropertyError() to get a nice message
            // with a possible suggestion for the correct name
            throw new \Error(
                $this->generateStructPropertyError($property)
            );
        }

        if ($this->__initialized) {
            throw new \RuntimeException("Cannot alter options after initialization");
        }

        $setter = "set" . \ucfirst($property);
        if (\method_exists($this, $setter)) {
            return $this->{$setter}($value);
        }
        $this->{$property} = $value;
    }

    private function setDebug(bool $flag) {
        if ($this->debug !== null) {
            throw new \Error(
                "Debug mode can't be altered at run-time"
            );
        }

        $this->debug = $flag;
    }

    // Note this is a required string|null parameter, just = null because we don't have proper union types with null yet...
    private function setUser(string $user = null) {
        $this->user = $user;
    }

    private function setMaxConnections(int $count) {
        if ($count < 1) {
            throw new \Error(
                "Max connections setting must be greater than or equal to one"
            );
        }

        $this->maxConnections = $count;
    }

    private function setConnectionsPerIP(int $count) {
        if ($count < 1) {
            throw new \Error(
                "Connections per IP maximum must be greater than or equal to one"
            );
        }

        $this->connectionsPerIP = $count;
    }

    private function setMaxRequestsPerConnection(int $count) {
        if ($count < 1) {
            throw new \Error(
                "Maximum number of requests on a connection must be greater than zero. Set it to PHP_INT_MAX to allow an unlimited amount of requests"
            );
        }
        $this->maxRequestsPerConnection = $count;
    }

    private function setConnectionTimeout(int $seconds) {
        if ($seconds < 1) {
            throw new \Error(
                "Keep alive timeout setting must be greater than or equal to one second"
            );
        }

        $this->connectionTimeout = $seconds;
    }

    private function setDefaultContentType(string $contentType) {
        $this->defaultContentType = $contentType;
    }

    private function setDefaultTextCharset(string $charset) {
        $this->defaultTextCharset = $charset;
    }

    private function setSendServerToken(bool $flag) {
        $this->sendServerToken = $flag;
    }

    private function setSocketBacklogSize(int $backlog) {
        if ($backlog < 16) {
            throw new \Error(
                "Socket backlog size setting must be greater than or equal to 16"
            );
        }

        $this->socketBacklogSize = $backlog;
    }

    private function setNormalizeMethodCase(bool $flag) {
        $this->normalizeMethodCase = $flag;
    }

    private function setMaxInputVars(int $bytes) {
        if ($bytes < 0) {
            throw new \Error(
                "Max input vars must be a positive integer"
            );
        }

        $this->maxInputVars = $bytes;
    }

    private function setMaxFieldLen(int $bytes) {
        if ($bytes <= 0) {
            throw new \Error(
                "Max field length must be greater than zero bytes"
            );
        }

        $this->maxFieldLen = $bytes;
    }


    private function setMaxBodySize(int $bytes) {
        if ($bytes < 0) {
            throw new \Error(
                "Max body size setting must be greater than or equal to zero"
            );
        }

        $this->maxBodySize = $bytes;
    }

    private function setMaxHeaderSize(int $bytes) {
        if ($bytes <= 0) {
            throw new \Error(
                "Max header size setting must be greater than zero"
            );
        }

        $this->maxHeaderSize = $bytes;
    }

    private function setSoftStreamCap(int $bytes) {
        if ($bytes < 0) {
            throw new \Error(
                "Soft stream cap setting must be greater than or equal to zero"
            );
        }

        $this->softStreamCap = $bytes;
    }

    private function setMaxConcurrentStreams(int $streams) {
        if ($streams <= 0) {
            throw new \Error(
                "Max number of concurrent streams setting must be greater than zero"
            );
        }

        $this->maxConcurrentStreams = $streams;
    }

    private function setMaxFramesPerSecond(int $frames) {
        if ($frames <= 0) {
            throw new \Error(
                "Max number of HTTP/2 frames per second setting must be greater than zero"
            );
        }

        $this->maxFramesPerSecond = $frames;
    }

    private function setIoGranularity(int $bytes) {
        if ($bytes <= 0) {
            throw new \Error(
                "IO granularity setting must be greater than zero"
            );
        }

        $this->ioGranularity = $bytes;
    }

    private function setAllowedMethods(array $allowedMethods) {
        foreach ($allowedMethods as $key => $method) {
            if (!\is_string($method)) {
                throw new \Error(
                    \sprintf(
                        "Invalid type at key %s of allowed methods array: %s",
                        $key,
                        \is_object($method) ? \get_class($method) : \gettype($method)
                    )
                );
            }
            if ($method === "") {
                throw new \Error(
                    "Invalid empty HTTP method at key {$key} of allowed methods array"
                );
            }
        }
        if (!\in_array("GET", $allowedMethods)) {
            throw new \Error(
                "Servers must support GET as an allowed HTTP method"
            );
        }
        if (!\in_array("HEAD", $allowedMethods)) {
            throw new \Error(
                "Servers must support HEAD as an allowed HTTP method"
            );
        }

        $this->allowedMethods = $allowedMethods;
    }

    private function setDeflateEnable(bool $flag) {
        if ($flag && !\extension_loaded("zlib")) {
            throw new \Error(
                "Cannot enable deflate negotiation: zlib module required"
            );
        }

        $this->deflateEnable = $flag;
    }

    private function setDeflateContentTypes(string $regex) {
        if (!$regex || preg_match($regex, "") === false) {
            throw new \DomainException(
                "Deflate content types must be a valid regex"
            );
        }

        $this->deflateContentTypes = $regex;
    }

    private function setDeflateMinimumLength(int $bytes) {
        if ($bytes < 0) {
            throw new \Error(
                "Deflate minimum length must be greater than or equal to zero bytes"
            );
        }

        $this->deflateMinimumLength = $bytes;
    }

    private function setDeflateBufferSize(int $bytes) {
        if ($bytes <= 0) {
            throw new \Error(
                "Deflate buffer size must be greater than zero bytes"
            );
        }

        $this->deflateBufferSize = $bytes;
    }

    private function setChunkBufferSize(int $bytes) {
        if ($bytes <= 0) {
            throw new \Error(
                "Chunk buffer size must be greater than zero bytes"
            );
        }

        $this->chunkBufferSize = $bytes;
    }

    private function setOutputBufferSize(int $bytes) {
        if ($bytes <= 0) {
            throw new \Error(
                "Output buffer size must be greater than zero bytes"
            );
        }

        $this->outputBufferSize = $bytes;
    }

    private function setShutdownTimeout(int $milliseconds) {
        if ($milliseconds < 0) {
            throw new \Error(
                "Shutdown timeout size must be greater than or equal to zero"
            );
        }

        $this->shutdownTimeout = $milliseconds;
    }

    private function setConfigPath(string $config) {
        if ($this->configPath !== null) {
            throw new \Error(
                "Config path can't be altered at run-time"
            );
        }

        $this->configPath = $config;
    }
}
