<?php
/**
 * A request represents a http request
 *
 * Currently, superglobals such as $_GET, $_POST and $_SERVER are accessed
 * either through getvar() or directly, this is bad practice and hard to make
 * testable. Eventually, this class must replace all of this (and more).
 *
 * This file is part of Zoph.
 *
 * Zoph is free software; you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation; either version 2 of the License, or
 * (at your option) any later version.
 *
 * Zoph is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 * You should have received a copy of the GNU General Public License
 * along with Zoph; if not, write to the Free Software
 * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301  USA
 *
 * @package Zoph
 * @author Jeroen Roos
 */

namespace web;

use ArrayAccess;
use breadcrumb;
use generic\variable;
use user;

/**
 * The request class is used to access request-related variables
 * such as $_GET, $_POST and $_SERVER.
 *
 * In the future $_FILES and $_COOKIE will be added.
 *
 * A variable can be accessed through ArrayAccess ($request["variable"] or object
 * access ($request->variable);
 *
 * @package Zoph
 * @author Jeroen Roos
 */
class request implements ArrayAccess {
    /** @var holds $_GET variables */
    private $get;

    /** @var holds $_POST variables */
    private $post;

    /** @var holds $_SERVER variables */
    private $server;

    /** @var holds $_FILES variables */
    private $files;

    /** @var holds $_COOKIE variables */
    private $cookie;

    /** @var request vars, holds $_GET for GET requests and $_POST for POST requests
             actually, a POST request can have GET variables as well, but this has
             always been how Zoph works, so for now I am not changing this, note
             that this is *different* from the $_REQUEST superglobals - hence it's
             not called $request */
    private $requestVars;

    /** @var queryString holds query string */
    private $queryString;

    /** @var array always pass these fields when cleaning vars */
    private const FIELDS = array("_action", "_order", "_dir", "_cols", "_rows", "_random", "_filename", "_maxfiles", "_maxsize", "_off", "_filenum", "_index", "_qs");

    /**
     * Create object
     * @param array array of variables, can contain GET, POST and SERVER
     */
    public function __construct(array $vars) {
        foreach ([ "GET", "POST", "SERVER", "FILES", "COOKIE" ] as $var) {
            if (isset($vars[$var])) {
                $value=new variable($vars[$var]);
                $prop=strtolower($var);
                $this->$prop=$value->input();
            }
        }
        $this->buildRequest();

        $this->queryString = new queryString($this->getServerVar("QUERY_STRING"), $this["_qs"]);

        $this->breadcrumbs();
    }

    /**
     * Get the name of the current page (without .php)
     * @return string name
     */
    public function getPage() {
        return preg_replace("/\/(\w+)\.php/", '${1}', $this->getServerVar("SCRIPT_NAME"));
    }

    /**
     * Create object and fill with superglobals
     * @return request new request
     */
    public static function create() : self {
        return new self(array(
            "GET"       =>  $_GET,
            "POST"      =>  $_POST,
            "SERVER"    => $_SERVER,
            "FILES"     => $_FILES,
            "COOKIE"    => $_COOKIE
        ));
    }

    /**
     * Fill the REQUESTVARS property with either the GET variables
     * OR the POST variables.
     * Note that this behaviour is different from PHP's $_REQUEST superglobal
     */
    private function buildRequest() : void {
        if (!empty($this->get)) {
            $this->requestVars=&$this->get;
        } else {
            $this->requestVars=&$this->post;
        }
    }

    /**
     * For ArrayAccess: does the offset exist
     * @param mixed offset
     * @return bool offset exists
     */
    public function offsetExists(mixed $off) : bool {
        return (isset($this->get[$off]) || isset($this->post[$off]));
    }

    /**
     * For ArrayAccess: Get value of parameter
     * if $_GET parameter is available, return it, if it is not but $_POST is available
     * return that, otherwise null
     * @param mixed offset
     * @return mixed value
     */
    public function offsetGet(mixed $off) : mixed {
        if (isset($this->get[$off])) {
            return $this->get[$off];
        } else if (isset($this->post[$off])) {
            return $this->post[$off];
        } else {
            return null;
        }
    }

    /**
     * For ArrayAccess: Set value of parameter
     * @param mixed offset
     * @param mixed value
     */
    public function offsetSet(mixed $off, mixed $val) : void {
        if (isset($this->post[$off])) {
            $this->post[$off]=$val;
        } else if (isset($this->get[$off])) {
            $this->get[$off]=$val;
        } else {
            $this->post[$off]=$val;
        }
    }

    /**
     * For ArrayAccess: Unset value of parameter
     * @param mixed offset
     */
    public function offsetUnset(mixed $off) : void {
        unset($this->get[$off]);
        unset($this->post[$off]);
    }

    /**
     * For ObjectAccess: Get value of parameter
     * if $_GET parameter is available, return it, if it is not but $_POST is available
     * return that, otherwise null
     * @param mixed offset
     * @return mixed value
     */
    public function __get(mixed $off) : mixed {
        return $this->offsetGet($off);
    }

    /**
     * Get RequestVars
     * @return array requestvars
     */
    public function getRequestVars() : array {
        return (array) $this->requestVars;
    }

    /**
     * Return the query string, urlencoded, it can be passed via an URL
     */
    public function getEncodedQueryString() : string {
        return $this->queryString->encode();
    }

    /**
     * Get the query string that was used to get to this page
     */
    public function getQueryString() : string {
        return $this->queryString;
    }

    /**
     * Sometimes a form passes a previous query string as part of the data
     * this is needed to return to the original page. For example, if you have performed
     * a search and click on a photo, you're not simply sent to that photo,
     * but the query string for that photo contains the original search
     * to return to the search after the photo was updated, you need to retrieve that
     * query string through this function.
     */
    public function getPassedQueryString() : string {
        return (string) $this->queryString->getPassed();
    }

    /**
     * Clean the query string by passing regexes
     * For example removing "_crumb" and "_action":
     * this->cleanQueryString(array("/_crumb=\d+&?/","/_action=\w+&?/"))
     * @param array regex to use for cleaning
     * @return string cleaned query string
     */
    public function cleanQueryString(array $regexes) : string {
        $qs = $this->getQueryString();
        foreach ($regexes as $regex) {
            $qs = preg_replace($regex, "", $qs);
        }
        return $qs;
    }

    /**
     * Get the return query string
     * This could be the passed query string ("_qs") or this function could
     * clean the current query string, removing "_crumb" and "_action"
     */
    public function getReturnQueryString() : string {
        return $this->queryString->getReturn();
    }

    /**
     * Get $_SERVER variables
     * @param string Variable to return
     * @return mixed value
     */
    public function getServerVar(string $var) : mixed {
        if (isset($this->server[$var])) {
            return $this->server[$var];
        } else {
            return null;
        }
    }

    /**
     * Get uploaded files
     * @param string name of upload field
     * @return array uploaded files
     */
    public function getFiles(string $var) : ?array {
        if (isset($this->files[$var])) {
            return $this->files[$var];
        } else {
            return null;
        }
    }

    /**
     * Get $_POST variables
     * @param Variable to return
     * @return mixed value
     */
    public function getPostVar(mixed $var) : mixed {
        if (isset($this->post[$var])) {
            return $this->post[$var];
        } else {
            return null;
        }
    }

   /**
    * Update requestvars
    * Update a variable to a new value, remove ignored keys
    * always removes PHPSESSID and _crumb variables
    * @param mixed string variable (key) to add / update | array array of key => value pairs to update
    * @param mixed value of the new /updated variable | ignored when $new is array
    * @param array list of key names to remove
    * @return array updated variables
    */
    public function getUpdatedVars(mixed $new, mixed $val = null, array $ignore = array(), bool $clean = true) {
        $ignore[] = "PHPSESSID";
        $ignore[] = "_crumb";

        if ($clean) {
            $vars = $this->getRequestVarsClean();
        } else {
            $vars = $this->getRequestVars();
        }

        foreach ($ignore as $key) {
            unset($vars[$key]);
        }

        if (is_array($new)) {
            foreach ($new as $key => $value) {
                $vars[$key] = $value;
            }
        } else {
            $vars[$new] = $val;
        }

        return $vars;
    }

    /**
     * Remove any params without values and operator params without corresponding
     * fields (e.g. _album_id_op when there is no _album_id).  This can be called
     * once after a search is performed.  It allows for shorter urls that are
     * more readable and easier to debug.
     * @param array remove this field
     */
    public function getRequestVarsClean(array $remove = array()) : array {
        $cleanVars = array();

        foreach ($this->getRequestVars() as $var => $value) {
            if (in_array($var, $remove)) {
                continue;
            } else if (in_array($var, self::FIELDS)) {
                $cleanVars[$var] = $value;
                continue;
            } else if (substr($var, 0, 1) == "_") {
                continue;
            }

            if (is_array($value)) {
                foreach ($value as $i => $val) {
                    if (empty($val)) {
                        continue;
                    }
                    if (!isset($cleanVars[$var])) {
                        $cleanVars[$var]=array();
                    }
                    $cleanVars[$var][$i]=$val;
                    foreach ($this->getRelatedVars($var, $i) as $related => $value) {
                        if (!isset($cleanVars[$related])) {
                            $cleanVars[$related]=array();
                        }
                        $cleanVars[$related][$i]=$value;
                    }

                }
            } else {
                if (empty($value)) {
                    continue;
                }
                $cleanVars[$var]=$value;
                foreach ($this->getRelatedVars($var) as $related => $value) {
                    $cleanVars[$related]=$value;
                }
            }
        }
        return $cleanVars;

    }

    private function getRelatedVars(string $var, int $index = null) {
        $return = array();
        foreach ($this->getRequestVars() as $related => $value) {
            if  (substr($related, 1, strlen($var)) == $var) {
                if (is_array($value) && $index !== null && isset($value[$index])) {
                    $return[$related] = $value[$index];
                } else if (!is_array($value) && $index === null && isset($value)) {
                    $return[$related] = $value;
                }
            }
        }
        return $return;
    }

    private function breadcrumbs() {
        if ((user::getCurrent())?->prefs->get("show_breadcrumbs")) {
            breadcrumb::init();

            if ($this["_clear_crumbs"]) {
                breadcrumb::eat(0);
            } else if ($this["_crumb"]) {
                breadcrumb::eat($this["_crumb"]);
            }
        }
    }
}
