<?php
/**
 * Created by PhpStorm.
 * User: jiuli
 * Date: 2018/5/3
 * Time: 下午4:30
 */

namespace Mainto\RpcServer\RpcServer;

use Closure;
use Exception;
use Illuminate\Contracts\Session\Session;
use Illuminate\Support\Arr;
use Illuminate\Support\Str;
use Mainto\RpcServer\Exceptions\RpcNotSupportException;
use Mainto\RpcServer\Exceptions\RpcRuntimeException;
use stdClass;

class RpcSession implements Session {
    /**
     * effective time
     */
    const EFFECTIVE_TIME = 43200;

    /**
     * renew time
     */
    const RENEWAL_TIME = 3600;

    /**
     * The session handler
     * @var RpcSessionHandlerInterface
     */
    protected static ?RpcSessionHandlerInterface $handler = null;

    /**
     * The session ID.
     *
     * @var string
     */
    protected string $id;

    /**
     * The session name.
     *
     * @var string
     */
    protected string $name;

    /**
     * The session attributes.
     *
     * @var array
     */
    protected array $attributes = [];

    /**
     * Session store started status.
     *
     * @var bool
     */
    protected bool $started = false;

    /**
     * Session is exists
     *
     * @var bool
     */
    protected bool $exists = false;

    /**
     * Session is effective
     *
     * @var bool
     */
    protected bool $effective = false;

    /**
     * expire time
     * @var int
     */
    protected int $expireTime = 0;

    /**
     * RpcSessionBag constructor.
     * @param $sessionId
     */
    public function __construct ($sessionId) {
        if (!static::$handler) {
            if (!app()->has(RpcSessionHandlerInterface::class)) {
                throw new RpcRuntimeException("please register session handler at first");
            }

            static::$handler = app(RpcSessionHandlerInterface::class);
        }

        $this->id = $sessionId;
    }

    /**
     * Start the session, reading the data from a handler.
     *
     * @return bool
     */
    public function start () {
        $this->loadSession();

        return $this->started = true;
    }

    /**
     * Load the session data from the handler.
     *
     * @return void
     */
    protected function loadSession () {
        $this->attributes = array_merge($this->attributes, $this->readFromHandler());
    }

    /**
     * Read the session data from the handler.
     *
     * @return array
     */
    protected function readFromHandler () {
        if (!$this->id) return [];

        if ($data = static::$handler->getSessionStringBySessionId($this->getSaveName())) {
            $data = @unserialize($this->prepareForUnserialize($data));

            if ($data !== false && !is_null($data) && is_array($data)) {
                $this->setExists(true);
                $this->expireTime = $data['t'] ?? 0;
                return $data['d'] ?? [];
            }
        }

        $this->expireTime = time() + static::EFFECTIVE_TIME;
        return [];
    }

    /**
     * Get the current save session name.
     *
     * @return string
     */
    protected function getSaveName () {
        return "session_{$this->getId()}";
    }

    /**
     * Get the current session ID.
     *
     * @return string
     */
    public function getId () {
        return $this->id;
    }

    /**
     * Set the session ID.
     *
     * @param string $id
     * @return void
     */
    public function setId ($id) {
        throw new RpcNotSupportException("current not support set session id");
    }

    /**
     * Prepare the raw string data from the session for unserialization.
     *
     * @param string $data
     * @return string
     */
    protected function prepareForUnserialize ($data) {
        return $data;
    }

    /**
     * set session is exists
     *
     * @param $exists
     */
    protected function setExists ($exists) {
        $this->exists = $exists;
    }

    /**
     * Save the session data to storage.
     */
    public function save () {
        if ((time() + static::RENEWAL_TIME) > $this->expireTime) {
            if (!$this->expireTime) $this->expireTime = time();
            $this->expireTime += static::EFFECTIVE_TIME;
            $this->effective = true;
        }

        if ($this->effective) {
            if (!$this->id) return;

            static::$handler->updateSession(
                $this->getSaveName(),
                $this->prepareForStorage(serialize([
                    't' => $this->expireTime,
                    'd' => $this->attributes,
                ])),
                $this->expireTime - time()
            );
        }

        $this->started = false;
    }

    /**
     * Prepare the serialized session data for storage.
     *
     * @param string $data
     * @return string
     */
    protected function prepareForStorage ($data) {
        return $data;
    }

    /**
     * Get all of the session data.
     *
     * @return array
     */
    public function all () {
        return $this->attributes;
    }

    /**
     * Checks if a key exists.
     *
     * @param string|array $key
     * @return bool
     */
    public function exists ($key) {
        $placeholder = new stdClass();

        return !collect(is_array($key) ? $key : func_get_args())->contains(function ($key) use ($placeholder) {
            return $this->get($key, $placeholder) === $placeholder;
        });
    }

    /**
     * Get an item from the session.
     *
     * @param string $key
     * @param mixed $default
     * @return mixed
     */
    public function get ($key, $default = null) {
        return Arr::get($this->attributes, $key, $default);
    }

    /**
     * Checks if a key is present and not null.
     *
     * @param string|array $key
     * @return bool
     */
    public function has ($key) {
        return !collect(is_array($key) ? $key : func_get_args())->contains(function ($key) {
            return is_null($this->get($key));
        });
    }

    /**
     * Get the value of a given key and then forget it.
     *
     * @param string $key
     * @param string $default
     * @return mixed
     */
    public function pull ($key, $default = null) {
        return Arr::pull($this->attributes, $key, $default);
    }

    /**
     * Replace the given session attributes entirely.
     *
     * @param array $attributes
     * @return void
     */
    public function replace (array $attributes) {
        $this->effective = true;
        $this->put($attributes);
    }

    /**
     * Put a key / value pair or array of key / value pairs in the session.
     *
     * @param string|array $key
     * @param mixed $value
     * @return void
     */
    public function put ($key, $value = null) {
        if (!is_array($key)) {
            $key = [$key => $value];
        }

        $this->effective = true;
        foreach ($key as $arrayKey => $arrayValue) {
            Arr::set($this->attributes, $arrayKey, $arrayValue);
        }
    }

    /**
     * Get an item from the session, or store the default value.
     *
     * @param string $key
     * @param \Closure $callback
     * @return mixed
     */
    public function remember ($key, Closure $callback) {
        if (!is_null($value = $this->get($key))) {
            return $value;
        }

        $this->effective = true;
        return tap($callback(), function ($value) use ($key) {
            $this->put($key, $value);
        });
    }

    /**
     * Push a value onto a session array.
     *
     * @param string $key
     * @param mixed $value
     * @return void
     */
    public function push ($key, $value) {
        $array = $this->get($key, []);

        $array[] = $value;

        $this->effective = true;
        $this->put($key, $array);
    }

    /**
     * Decrement the value of an item in the session.
     *
     * @param string $key
     * @param int $amount
     * @return int
     */
    public function decrement ($key, $amount = 1) {
        $this->effective = true;
        return $this->increment($key, $amount * -1);
    }

    /**
     * Increment the value of an item in the session.
     *
     * @param string $key
     * @param int $amount
     * @return mixed
     */
    public function increment ($key, $amount = 1) {
        $this->effective = true;
        $this->put($key, $value = $this->get($key, 0) + $amount);

        return $value;
    }

    /**
     * Remove an item from the session, returning its value.
     *
     * @param string $key
     * @return mixed
     */
    public function remove ($key) {
        $this->effective = true;
        return Arr::pull($this->attributes, $key);
    }

    /**
     * Remove one or many items from the session.
     *
     * @param string|array $keys
     * @return void
     */
    public function forget ($keys) {
        $this->effective = true;
        Arr::forget($this->attributes, $keys);
    }

    /**
     * Flush the session data and regenerate the ID.
     *
     * @return bool
     * @throws Exception
     */
    public function invalidate () {
        $this->flush();

        return $this->migrate(true);
    }

    /**
     * Remove all of the items from the session.
     *
     * @return void
     */
    public function flush () {
        $this->effective = true;
        $this->attributes = [];
    }

    /**
     * renew the session
     *
     * @return void
     */
    public function renew () {
        $this->effective = true;
        $this->expireTime = time() + static::EFFECTIVE_TIME;
    }

    /**
     * Generate a new session ID for the session.
     *
     * @param bool $destroy
     * @return bool
     * @throws Exception
     */
    public function migrate ($destroy = false) {
        if ($destroy) {
            if (!$this->id) return true;

            static::$handler->deleteSession($this->getSaveName());
        }

        $this->effective = false;
        $this->setExists(false);

        return true;
    }

    /**
     * test the session is exists
     *
     * @return bool
     */
    public function isSessionExists () {
        return $this->exists;
    }

    /**
     * Determine if the session has been started.
     *
     * @return bool
     */
    public function isStarted () {
        return $this->started;
    }

    /**
     * Get the name of the session.
     *
     * @return string
     */
    public function getName () {
        return $this->name;
    }

    /**
     * Get the expire time of the session
     *
     * @return int
     */
    public function getExpireTime (): int {
        return $this->expireTime;
    }

    /**
     * Set the name of the session.
     *
     * @param string $name
     * @return void
     */
    public function setName ($name) {
        $this->name = $name;
    }

    /**
     * Get the CSRF token value.
     *
     * @return string
     */
    public function token () {
        return $this->get('_token');
    }

    /**
     * Regenerate the CSRF token value.
     *
     * @return void
     */
    public function regenerateToken () {
        $this->put('_token', Str::random(40));
    }

    /**
     * Get the previous URL from the session.
     *
     * @return string|null
     */
    public function previousUrl () {
        return $this->get('_previous.url');
    }

    /**
     * Set the "previous" URL in the session.
     *
     * @param string $url
     * @return void
     */
    public function setPreviousUrl ($url) {
        $this->put('_previous.url', $url);
    }

    /**
     * Get the underlying session handler implementation.
     *
     * @return \SessionHandlerInterface
     */
    public function getHandler () {
        return null;
    }

    /**
     * Determine if the session handler needs a request.
     *
     * @return bool
     */
    public function handlerNeedsRequest () {
        return false;
    }

    /**
     * Set the request on the handler instance.
     *
     * @param \Illuminate\Http\Request $request
     * @return void
     */
    public function setRequestOnHandler ($request) {
        //        if ($this->handlerNeedsRequest()) {
        //            $this->handler->setRequest($request);
        //        }
    }
}