<?php

namespace pictpostpersonal;

use Exception;

/**
 * HTTPリクエスト情報を扱うクラス (PHP 7.1対応版)
 */
class Request implements RequestInterface
{
    /**
     * @var string リクエストメソッド(例: "get","post","put","delete"など)
     */
    public $method;

    /**
     * @var string リクエストURI全体(例: "/command/args?foo=bar")
     */
    public $request;

    public $request_url;

    /**
     * @var string URLの先頭セグメント(例: "/command" の "command" 部分)
     */
    public $command;

    /**
     * @var array メソッドに応じたパラメータ (GET, POST, PUT等)
     */
    public $args_raw = [];

    /**
     * @var array urlPatternMatch() 成功時に抽出されたパラメータ
     */
    public $path_args = [];

    /**
     * @var string クエリパラメータを除去したURL (例: "/command/args")
     */
    public $url;

    /**
     * @var string 階層深に応じて "../" を組み合わせた相対パス
     */
    public $rel;

    /**
     * @var array クッキー配列
     */
    public $cookie = [];

    /**
     * @var array Fileクラスの配列
     */
    private $files = [];

    /**
     * @var array JSONの内容(contentstypeがjsonの場合のみ)
     */
    public $json = null;

    // パラメータフィルタ定数
    public const FILTER_NONE    = 0;
    public const FILTER_INT     = 1;
    public const FILTER_BOOLEAN = 2;

    //スーパーグローバル変数のエイリアス
    private $GET;
    private $POST;
    private $SERVER;
    private $FILES;
    private $COOKIE;

    private $input_stream;

    /**
     * コンストラクタ
     * 通常はスーパーグローバル変数をそのまま使用しますが、
     * テストや特定の環境では引数で値を渡すことも可能です。
     */
    public function __construct($GET = null, $POST = null, $SERVER = null, $FILES = null, $COOKIE = null, $input_stream = null)
    {
        // スーパーグローバル変数の値渡し
        $this->GET    = ($GET !== null)    ? $GET    : $_GET;
        $this->POST   = ($POST !== null)   ? $POST   : $_POST;
        $this->SERVER = ($SERVER !== null) ? $SERVER : $_SERVER;
        $this->FILES  = ($FILES !== null)  ? $FILES  : $_FILES;
        $this->COOKIE = ($COOKIE !== null) ? $COOKIE : $_COOKIE;
        $this->input_stream = $input_stream !== null ? $input_stream : null;
        // 初期化処理を呼び出す
        $this->initialize();
    }

    /**
     * リクエストの生データを取得する
     * @return string 生のリクエストデータ
     */
    protected function getInputStream(): string
    {
        if ($this->input_stream !== null) {
            return $this->input_stream;
        }
        return file_get_contents('php://input');
    }

    /**
     * 初期化処理
     * リクエストURIの解析、コマンドの抽出、パラメータの取得などを行う
     */
    private function initialize()
    {
        // リクエストURI (例: "/command/args?foo=bar")
        $this->request = isset($this->SERVER['REQUEST_URI']) ? $this->SERVER['REQUEST_URI'] : '/';


        // SCRIPT_NAME からベースパスを除去
        $script_name = isset($this->SERVER['SCRIPT_NAME']) ? $this->SERVER['SCRIPT_NAME'] : '';
        $base_path   = str_replace(basename($script_name), '', $script_name);
        if ($base_path !== '/' && $base_path !== '') {
            $request_url = '/' . str_replace($base_path, '', $this->request);
        } else {
            $request_url = $this->request;
        }

        $this->request_url = SITE_URL . $request_url;

        // URL末尾のスラッシュ除去＆クエリパラメータを切り捨て
        $tmp = explode('?', $request_url, 2);
        $url_path = rtrim($tmp[0], '/');
        $this->url = ($url_path === '') ? '/' : $url_path;

        // GETの c= がある場合はURLを上書き (mod_rewriteなし環境想定)
        if (isset($this->GET['c'])) {
            $command       = $this->SERVER['QUERY_STRING'];
            $parse_path     = parse_url($command, PHP_URL_PATH);
            $this->url     = str_replace('c=', '', $parse_path !== null ? $parse_path : '');
            $parsed_query   = parse_url($command, PHP_URL_QUERY);
            if ($parsed_query !== null) {
                $parsed_query = str_replace('&amp;', '&', $parsed_query);
                parse_str($parsed_query, $this->GET); // $this->GETを書き換え
            }
        } elseif (isset($this->POST['c'])) {
            // POSTでc= が来た場合もURLを上書き
            $command = $this->POST['c'];
            $this->url = $command;
        }

        // mod_rewrite 使えない環境で index.php/path/info を想定
        if ($script_name === '/index.php' && isset($this->SERVER['PATH_INFO'])) {
            $this->url = $this->SERVER['PATH_INFO'];
        }

        // URLの先頭セグメント(コマンド) を抽出
        $path_parts   = explode('/', $this->url);
        array_shift($path_parts); // 先頭(空文字)を除去
        $this->command = count($path_parts) > 0 ? array_shift($path_parts) : '/';
        if ($this->command === '') {
            $this->command = '/';
        }

        // リクエストメソッド
        $this->method = isset($this->SERVER['REQUEST_METHOD'])
            ? strtolower($this->SERVER['REQUEST_METHOD'])
            : 'get';

        // パラメータ割り当て
        switch ($this->method) {
            case 'post':
                $this->args_raw = $this->POST;
                break;
            case 'get':
                $this->args_raw = $this->GET;
                break;
            case 'put':
            case 'patch':
            case 'delete':
                $input = $this->getInputStream();
                $data = [];
                parse_str($input, $data);
                $this->args_raw = $data;
                break;
            default:
                // 未定義メソッド
                $this->args_raw = [];
                break;
        }


        // JSON形式かどうかを判定
        $content_type = isset($this->SERVER['CONTENT_TYPE']) ? $this->SERVER['CONTENT_TYPE'] : '';
        if (strpos($content_type, 'application/json') === 0) {
            $json = $this->getInputStream();
            if ($json === '') {
                $this->json = null;
            } else {
                $decoded = json_decode($json, true);

                if (json_last_error() !== JSON_ERROR_NONE) {
                    throw new Exception('無効なJSON: ' . json_last_error_msg());
                }
                $this->json = $decoded;
            }
        }


        // 相対パス "../" 計算
        $this->rel = str_repeat('../', count($path_parts));

        // Cookie
        $this->cookie = $this->COOKIE;

        // ファイル
        $this->files = [];
        foreach ($this->FILES as $field_name => $file_data) {
            if (is_array($file_data['name'])) {
                foreach ($file_data['name'] as $index => $name) {
                    if ($file_data['error'][$index] === UPLOAD_ERR_NO_FILE) {
                        continue;
                    }
                    $tmp_file = [
                        'name' =>  $file_data['name'][$index],
                        'tmp_name' => $file_data['tmp_name'][$index],
                        'error' => $file_data['error'][$index],
                        'type' => $file_data['type'][$index]
                    ];
                    $this->files[$field_name][] = new File($tmp_file);
                }
            } else {
                if ($file_data['error'] === UPLOAD_ERR_NO_FILE) {
                    continue;
                }
                $this->files[$field_name][] = new File($file_data);
            }
        }
    }

    /**
     * URLパターンを正規表現化してマッチ判定
     * パターン例: "/command/{id}" -> 実URLとマッチすると $this->pathArgs に格納
     */
    public function urlPatternMatch(string $url_pattern): bool
    {

        // {name} -> ([^/]+) に置き換え
        $pattern_regex = preg_replace('/\{[a-zA-Z0-9_]+\}/', '([^/]+)', $url_pattern);

        if (preg_match('#^' . $pattern_regex . '$#', $this->url, $matches)) {
            // パラメータ名だけ抽出
            preg_match_all('/\{([a-zA-Z0-9_]+)\}/', $url_pattern, $arg_names);
            $args = [];
            foreach ($arg_names[1] as $idx => $arg_name) {
                // +1 したインデックスでマッチ結果を取得
                $value = isset($matches[$idx + 1]) ? $matches[$idx + 1] : null;
                $args[$arg_name] = $value;
                $args[$idx]     = $value;
            }
            $this->path_args = $args;
            return true;
        }
        return false;
    }

    /**
     * GET/POSTなどから取得したパラメータを返す(簡易フィルタ付き)
     *
     * @param string $name
     * @param $defaultValue
     * @param int   $filter  FILTER_NONE / FILTER_INT / FILTER_BOOLEAN
     * @param $filterOptions
     * @return mixed
     * @throws Exception
     */
    public function getArgs(
        string $name,
        $default_value = null,
        int $filter = self::FILTER_NONE,
        $filter_options = 0
    ) {
        $value = isset($this->path_args[$name])
            ? $this->path_args[$name]
            : (isset($this->args_raw[$name]) ? $this->args_raw[$name] : $default_value);

        if ($filter === self::FILTER_NONE) {
            return $value;
        }

        switch ($filter) {
            case self::FILTER_INT:
                $raw_filter = FILTER_VALIDATE_INT;
                break;
            case self::FILTER_BOOLEAN:
                $raw_filter = FILTER_VALIDATE_BOOLEAN;
                break;
            default:
                $raw_filter = FILTER_DEFAULT;
                break;
        }
        $result = filter_var($value, $raw_filter, $filter_options);
        return $result;
    }

    public function hasArgs(string $name): bool
    {
        return isset($this->path_args[$name])
            ? $this->path_args[$name]
            : (isset($this->args_raw[$name]));
    }

    /**
     * 指定Cookieを取得 (単純実装)
     */
    public function getCookie(string $name, $default_value = null)
    {
        return isset($this->cookie[$name]) ? $this->cookie[$name] : $default_value;
    }

    /**
     * 単一ファイル取得
     */
    public function getFile(string $name, $index = null): ?File
    {
        if (!isset($this->files[$name])) {
            return null;
        }

        if ($index === null) {
            $index = 0;
        }
        if (isset($this->files[$name][$index])) {
            return $this->files[$name][$index];
        }
        return null;
    }

    /**
     * 複数ファイルをまとめて取得
     */
    public function getFiles(string $name = ''): array
    {
        if ($name === '') {
            return $this->files;
        }

        if (!isset($this->files[$name])) {
            return [];
        }
        return $this->files[$name];
    }

    /**
     * アップロードされたファイルにエラーがあるか確認する
     *
     * @return bool アップロードエラーが1つでもある場合はtrue、すべて正常な場合はfalse
     */
    public function hasUploadErrors()
    {
        foreach ($this->files as $files) {
            foreach ($files as $file) {
                if ($file->getError() !== 0) {
                    return true;
                }
            }
        }
        return false;
    }
}
