<?php
/**
 * Created by PhpStorm.
 * User: crose
 * Date: 1/9/16
 * Time: 10:26 PM
 */

namespace IMATHUZH\Qfq\Core\Store;


use IMATHUZH\Qfq\Core\Exception\Thrower;
use IMATHUZH\Qfq\Core\Helper\KeyValueStringParser;
use IMATHUZH\Qfq\Core\Helper\OnArray;
use IMATHUZH\Qfq\Core\Helper\Path;
use IMATHUZH\Qfq\Core\Typo3\T3Handler;


/**
 * Class Sip
 * @package Store
 */
class Sip {

    private $phpUnit = false;
    private $staticUniqId = false;

    /**
     * @param bool|false $phpUnit
     */
    public function __construct($phpUnit = false) {

        #TODO: rewrite $phpUnit to: "if (!defined('PHPUNIT_QFQ')) {...}"
        $this->phpUnit = $phpUnit;
    }


    /**
     * @param string $queryString Possible variants:
     *                            * (DEPRECATED) http://www.math.uzh.ch/index.php?a=1&s=4b3403665fea6&r=45&type=99&id=person#pill12
     *                            * (DEPRECATED) index.php?a=1&s=4b3403665fea6&r=45&type=99&id=person#pill12
     *                            * ?a=1&s=4b3403665fea6&r=45&type=99&id=person#pill12
     *                            * a=1&s=4b3403665fea6&r=45&type=99&id=person#pill12
     *                            * form?a=1&b=foo
     *
     * @param string $mode Possible values: RETURN_URL|RETURN_SIP
     *
     * @param string $phpScriptName
     * @return string/array
     *  * mode=RETURN_URL: return complete URL
     *  * mode=RETURN_SIP: returns only the sip
     *  * mode=RETURN_ARRAY: returns array with url ('sipUrl') and all decoded and created parameters.
     * @throws \CodeException
     * @throws \UserFormException
     */
    public function queryStringToSip($queryString, $mode = RETURN_URL, $phpScriptName = INDEX_PHP) {

        $clientArray = array();
        $sipArray = array();

        // Split URL parameter:
        $paramArray = KeyValueStringParser::parse($queryString, "=", "&");

        // Split parameter between Script, Client and SIP
        $anchor = '';
        $script = $this->splitParamClientSip($paramArray, $clientArray, $sipArray, $anchor);
        if ($anchor != '') {
            $anchor = '#' . $anchor;
        }

        // If no 'r' is specified: define r=0
        // Bug #9121: No default
//        if (!isset($sipArray[SIP_RECORD_ID])) {
//            $sipArray[SIP_RECORD_ID] = 0;
//        }

        // Generate keyname for $_SESSION[]
        $sipParamString = $this->buildParamStringFromArray($sipArray);

        $sessionParamSip = Session::get($sipParamString);
        if ($sessionParamSip === false) {
            // Not found: create new entry
            $s = $this->sipUniqId('badcaffee1234');
            Session::set($sipParamString, $s);
            Session::set($s, $sipParamString);
        } else {
            $s = $sessionParamSip;
        }

        // Append SIP to final parameter
        $clientArray[CLIENT_SIP] = $s;

        if ($script[0] === '?') {
            if (T3Handler::useSlugsInsteadOfPageAlias() && $phpScriptName === INDEX_PHP && array_key_exists('id', $clientArray)) {

                // if 'id=...' is used: compute slug if possible or throw exception
                $store = Store::getInstance();
                $pageId = $store->getVar(TYPO3_PAGE_ID, STORE_TYPO3);
                $pageSlug = $store->getVar(TYPO3_PAGE_SLUG, STORE_TYPO3);
                $editFormPage = $store->getVar(SYSTEM_EDIT_FORM_PAGE, STORE_SYSTEM);
                if (is_numeric($clientArray['id']) && is_numeric($pageId) && intval($clientArray['id']) === intval($pageId)) {
                    $script = Path::urlApp($pageSlug) . $script;
                    unset($clientArray['id']);
                } elseif (!is_numeric($clientArray['id'])) {
                    $script = Path::urlApp($clientArray['id']) . $script;
                    unset($clientArray['id']);
                } else {
                    Thrower::userReportException('Deprecated alias/id url used.', 'Tried to render an url using page alias/id which is deprecated. Query string: ' . $queryString);
                }
            } else {
                $script = $phpScriptName . $script;
            }
        }

        $clientArray[SIP_SIP_URL] = $script . OnArray::toString($clientArray) . $anchor;

        switch ($mode) {
            case RETURN_URL:
                $rc = $clientArray[SIP_SIP_URL];
                break;
            case RETURN_SIP:
                $rc = $s;
                break;
            case RETURN_ARRAY:
                $rc = array_merge($clientArray, $sipArray);
                break;
            default:
                throw new \CodeException('Unknown Mode: "' . $mode . '"', ERROR_UNKNOWN_MODE);
        }

        return $rc;
    }

    /**
     * Splits the $paramArray in &$clientArray and &$sipArray. $sipArray contains all key/values pairs which do not
     * belong to Typo3.
     *
     * @param array $paramArray
     * @param array $clientArray
     * @param array $sipArray
     *
     * @param string $anchor
     *
     * @return string
     * @throws \CodeException
     */
    private function splitParamClientSip(array $paramArray, array &$clientArray, array &$sipArray, &$anchor) {

        $script = '';
        $anchor = '';

        // Possible variants:
        //   http://www.math.uzh.ch/index.php?a=1&s=4b3403665fea6&r=45&type=99&id=person
        //   index.php?a=1&s=4b3403665fea6&r=45&type=99&id=person
        //   ?a=1&s=4b3403665fea6&r=45&type=99&id=person
        //   a=1&s=4b3403665fea6&r=45&type=99&id=person
        $flagFirst = true;
        foreach ($paramArray as $key => $value) {

            // first key/value pair: a) save potential existing url/script part, b) if no keyname is specified, call it 'id'.
            if ($flagFirst) {
                $flagFirst = false;

                $script = $this->splitAndFix($key, $value);
            }

            //Check for anker in Parameter
            $anchorArray = explode('#', $value, 2);
            if (!empty($anchorArray[1])) {
                $value = $anchorArray[0];
                $anchor = $anchorArray[1];
            }

            // copy every parameter either to $clientArray or to $sipArray
            switch ($key) {
                //special T3 parameter.
                case SIP_EXCLUDE_L:
                case SIP_EXCLUDE_TYPE:
                case SIP_EXCLUDE_ID:
                case SIP_EXCLUDE_XDEBUG_SESSION_START:
                    $clientArray[$key] = $value;
                    break;
                case CLIENT_SIP:
                    if ($this->getQueryStringFromSip($value) === false) {
                        throw new \CodeException('SIP Parameter ist not allowed to be stored as a regular URL Parameter', ERROR_SIP_NOT_ALLOWED_AS_PARAM);
                    }
                    $clientArray[$key] = $value;
                    break;
                default:
                    // Values in SIP should not be url encoded, unless it is a password (see #20279)
                    $sipArray[$key] = ($key === SYSTEM_LDAP_1_PASSWORD) ? $value : urldecode($value);
                    break;
            }
        }

        return $script;
    }

    /**
     * Fix first parameter mix of hostname / script / Get parameter and optional missing keyname
     *
     * @param $key
     * @param $value
     *
     * @return string Part upto first '?',
     */
    private function splitAndFix(&$key, &$value) {

        $script = '';
        $tmpArray = explode('?', $key, 2);

        if (count($tmpArray) > 1) {
            $script = $tmpArray[0];
            $key = $tmpArray[1]; // new key, now without the hostname / scriptname.
        }
        $script .= '?';


        // empty value: keyname omitted - define keyname as 'id'
        if ($value === '') {
            $value = $key;
            $key = 'id';
        }

        return $script;
    }

    /**
     * Takes the values form an array and creates a urlparamstring. Skip values which should not passed to the
     * urlparamstring.
     * - SIP_TARGET_URL is necessary for 'delete' links (via 'report') - may be unecessary in other situations.
     *
     * @param array $sipArray
     *
     * @return string
     */
    private function buildParamStringFromArray(array $sipArray) {
        $tmpArray = array();

        foreach ($sipArray as $key => $value) {
            switch ($key) {
                case SIP_SIP:
//                case SIP_MODE_ANSWER:
//                case SIP_TABLE:
                case SIP_URLPARAM:
                    break;

                case SIP_TARGET_URL:  // Do not skip this param. Necessary for delete links (via 'report') - specifies the target where to jump after delete,php has been called (plain HTML, not AJAX)
                default:
                    $tmpArray[$key] = $value;
                    break;
            }
        }

        OnArray::sortKey($tmpArray);

        return OnArray::toString($tmpArray);
    }

    /**
     * Returns a new uniqid (unique per session), which will be used as a SIP identifier.
     *
     * @param bool|false $staticUniqId
     *
     * @return bool|string
     */
    public function sipUniqId($staticUniqId = false) {

        if ($this->phpUnit) {
            if ($staticUniqId !== false) {
                $this->staticUniqId = $staticUniqId;
            }

            return $this->staticUniqId;
        }

        $sip = uniqid();

        // It seems there is a chance that uniqid() is not unique: http://php.net/manual/en/function.uniqid.php
        // Check if the newly created uniqid() is unique for the current user (sufficient).
        while (Session::get($sip) !== false) {
            $sip = uniqid();
        }

        return $sip;
    }

    /**
     * Update the SIP in the Session according $sipArray.
     *
     * @param array $sipArray
     */
    public function updateSipToSession(array $sipArray) {
        $sip = $sipArray[SIP_SIP];

        // Remove old entry, cause the the 'key' will change.
        $sipParamStringOld = Session::get($sipArray[SIP_SIP]);
        Session::unsetItem($sipParamStringOld);

        // Generate keyname for $_SESSION[]
        $sipParamStringNew = $this->buildParamStringFromArray($sipArray);

        Session::set($sip, $sipParamStringNew);
        Session::set($sipParamStringNew, $sip);
    }

    /**
     * Remove one key (incl value) from SIP. Do not change the SIP.
     *
     * set: sip['badcaffee1234']="r=123&action=start" >> sip['badcaffee1234']="r=123"
     * unset: sip['r=123&action=start']
     * set: sip("r=123")= 'badcaffee1234'
     *
     * @param string $s
     * @param string $key
     */
    public function removeKeyFromSip($s, $key) {

        // Get old entry
        $sipParamStringOld = Session::get($s);

        $arr = explode('&', $sipParamStringOld);

        // Find key and remove from array
        foreach ($arr as $idx => $value) {
            $tokenArr = explode('=', $value, 2);
            if (($tokenArr[0] ?? '') == $key) {
                unset($arr[$idx]);
            }
        }

        $sipParamStringNew = implode('&', $arr);

        // Remove old
        Session::unsetItem($sipParamStringOld);

        // Set new
        Session::set($s, $sipParamStringNew);
        Session::set($sipParamStringNew, $s);
    }

    /**
     * Retrieve Params stored in $_SESSION[$s]
     *
     * @param $s
     *
     * @return array Parameter Array
     * @throws \CodeException
     * @throws \UserFormException
     * @throws \UserReportException
     */
    public function getVarsFromSip($s) {

        static $exceptionAlreadyThrown = false;

        # Check if parameter is manipulated
        if (strlen($s) != SIP_TOKEN_LENGTH) {
            Config::attackDetectedExitNow(array(), 'Invalid SIP token length: ' . strlen($s) . " _GET['s']=" . htmlentities($s));
        }

        // Validate: Check if still the same fe_user is logged in.
//        $this->checkFeUserUid();

        # Check if index 's' exists.
        $sessionVar = Session::get($s);

        if ($sessionVar === false) {

            if ($exceptionAlreadyThrown) {
                // There is already an exception throw for Store SIP: just return an empty array and do not throw a new exception.
                return array();
            }
            $exceptionAlreadyThrown = true;
            throw new \UserFormException("SIP '$s' not registered - please reload the previous site and try again.", ERROR_SIP_NOT_FOUND);
        }

        // Decode parameter
        return KeyValueStringParser::parse($sessionVar, "=", "&");
    }

    /**
     * Formats the content of sip. Per variable one line. Decode base64 encoded variables.
     * If $vars is an array, iterate over it.
     * If $vars is a string, than this is the SIP - retrieve parameter from SIP and process those.
     *
     * @param string|array $vars
     *
     * @return string
     * @throws \CodeException
     * @throws \UserFormException
     * @throws \UserReportException
     */
    public function debugSip($vars) {

        if (!is_array($vars)) {
            $vars = $this->getVarsFromSip($vars);
        }

        // Detect and decode base64 content
        $len = strlen(SIP_PREFIX_BASE64);
        foreach ($vars as $key => $value) {
            if (substr($key, 0, $len) == SIP_PREFIX_BASE64) {
                $vars[$key] = base64_decode($value);
            }
        }

        return OnArray::toString($vars, ' = ', PHP_EOL, "'");
    }

    /**
     * Returns the sip for the given querystring. The querystring has to be sorted.
     *
     * @param $queryString
     *
     * @return mixed
     */
    public function getSipFromQueryString($queryString) {
        return Session::get($queryString);
    }

    /**
     * Returns the querystring for the given $sip
     *
     * @param $sip
     *
     * @return bool
     */
    public function getQueryStringFromSip($sip) {
        return Session::get($sip);
    }

}