<?php

namespace CookieSession\Storage;

use CookieSession\Encrypter\Encrypter;
use CookieSession\Storage\Serialization\Collection;
use Hashids\Hashids;
use Illuminate\Http\Request;
use Symfony\Component\HttpFoundation\Cookie;

abstract class AbstractStorage
{
    protected $chunk_size;
    protected $chunks;
    protected $encrypter = null;
    protected $hashids = null;
    protected $to_store;

    public function __construct($chunk_size = 4096, $chunks = 40)
    {
        $this->chunk_size = $chunk_size;
        $this->chunks = $chunks;
    }


    /**
     * @return $this
     */
    abstract function encrypt();

    /**
     * @return $this
     */
    abstract function decrypt();

    /**
     * @return $this
     */
    abstract function compress();

    /**
     * @return $this
     */
    abstract function decompress();

    /**
     * @return $this
     */
    abstract function encode();

    /**
     * @return $this
     */
    abstract function decode();


    /**
     * @return $this
     */
    abstract function jsonDecode();

    /**
     * @param $string
     *
     * @return mixed
     * @throws \Exception
     */
    abstract function sign($string);

    /**
     * @param $string
     * @param $signature
     *
     * @return bool
     * @throws \Exception
     */
    abstract function verify($string, $signature);

    /**
     * @param $string
     *
     * @return $this
     */
    public function set($string)
    {
        $this->to_store = $string;

        return $this;
    }


    /**
     * @return string
     */
    public function get()
    {
        return $this->to_store;
    }


    /**
     * @param int $size
     *
     * @return $this
     */
    public function setChunkSize($size = 4096)
    {
        $this->chunk_size = $size;

        return $this;
    }


    /**
     * @param int $chunks
     *
     * @return $this
     */
    public function setChunks($chunks = 40)
    {
        $this->chunks = $chunks;

        return $this;
    }


    /**
     * @param $string
     *
     * @return array
     * @throws \Exception
     */
    public function chunk($string)
    {
        if(!is_string($string))
        {
            throw new \Exception("chunk() method accepts only string paremeters. You provided: ". gettype($string));
        }

        $len = strlen($string);
        $max_size = $this->chunks * $this->chunk_size;

        if($len > $max_size)
        {
            throw new \Exception(sprintf("The string provided exceeds maximum allowed size. Allowed: %s, string length: %s", $max_size, $len));
        }

        $chunks = str_split($string, $this->chunk_size);

        $result = [];

        foreach($chunks as $name => $value)
        {
            if($this->hasHashids())
            {
                $chunk_name = $this->getHashids()->encode_hex($name);
            }
            else
            {
                $chunk_name = $name;
            }

            $result[] = [
                'name' => $chunk_name,
                'value' => $value
            ];
        }

        return $result;
    }

    /**
     * @param Encrypter $encrypter
     *
     * @return $this
     */
    public function setEncrypter(Encrypter $encrypter)
    {
        $this->encrypter = $encrypter;

        return $this;
    }


    /**
     * @return Encrypter|null
     * @throws \Exception
     */
    public function getEncrypter()
    {
        if(!$this->encrypter instanceof Encrypter)
        {
            throw new \Exception("No instance of Encrypter has been set. Unable to encrypt without Encrypter object");
        }

        return $this->encrypter;
    }


    /**
     * @param Hashids $hashids
     *
     * @return $this
     */
    public function setHashids(Hashids $hashids)
    {
        $this->hashids = $hashids;

        return $this;
    }


    /**
     * @return bool
     */
    public function hasHashids()
    {
        return ($this->hashids instanceof Hashids);
    }


    /**
     * @return Hashids|null
     * @throws \Exception
     */
    public function getHashids()
    {
        if(!$this->hashids instanceof Hashids)
        {
            throw new \Exception("No instance of Hashids has been set. Unable to retrieve Hashids object.");
        }

        return $this->hashids;
    }

    /**
     * @param string $string
     *
     * @return Collection
     * @throws \Exception
     */
    public function serialize($string = '')
    {
        if(!is_string($string))
        {
            throw new \Exception("serialize() method accepts only string parameters. You provided: " . gettype($string));
        }

        // Change the IV
        $this->getEncrypter()->autoSetIV();

        // This is the string which will be saved into chunks
        $serialized = $this->set($string)->encrypt()->compress()->encode()->get();

        // This is the signature of the above string, computed using PKI
        $signature = $this->sign($serialized);
        $encoded_signature = $this->set($signature)->encode()->get();

        // This is the array holding the strings split into chunks. Array is enumerated by numeric indexes
        // This is what will go into "storage cookies". Array index = cookie name, array value = cookie value.
        // The array indexes are hashed using hashids, so that their index isn't immediately obvious.
        $chunks = $this->chunk($serialized);

        $collection = new Collection();

        $collection
            ->setSignature($encoded_signature)
            ->setLength(strlen($serialized))
            ->setChunks($chunks);

        $metadata = [
            'signature' => $encoded_signature,
            'size' => $collection->getSize()
        ];

        $collection->setMetadata($metadata);

        $metadata = json_encode($metadata);
        $encrypted_metadata = $this->set($metadata)->encrypt()->compress()->encode()->get();

        return $collection->setEncryptedMetadata($encrypted_metadata);
    }


    /**
     * @param Request $request
     * @param         $cookie_name
     *
     * @return array
     * @throws \Exception
     */
    public function unserializeFromRequest(Request $request, $cookie_name)
    {
        if(!$request->hasCookie($cookie_name))
        {
            return false;
        }

        $metadata = $this->set($request->cookie($cookie_name))->decode()->decompress()->decrypt()->jsonDecode()->get();

        if(!isset($metadata['signature']) || !isset($metadata['size']) || !is_int($metadata['size']))
        {
            throw new \Exception("Invalid cookie metadata.");
        }

        $data       = $this->getDataFrom($request, $metadata['size']);
        $signature  = $this->set($metadata['signature'])->decode()->get();

        if(!$this->getEncrypter()->verify($data, $signature))
        {
            throw new \Exception("Signature does not match the data. Possible tampering with cookies?");
        }

        return $this->set($data)->decode()->decompress()->decrypt()->jsonDecode()->get();
    }


    public function unserialize($metadata, $data)
    {
        $metadata   = $this->set($metadata)->decode()->decompress()->decrypt()->get();
        $data       = $this->set($data)->decode()->decompress()->decrypt()->get();

    }


    /**
     * @param Request $request
     * @param int     $size
     *
     * @return string
     * @throws \Exception
     */
    protected function getDataFrom(Request $request, $size = 0)
    {
        $data = '';
        $hashids = $this->getHashids();

        for($i = 0; $i < $size; $i++)
        {
            $cookie = $request->cookie($hashids->encode_hex($i));

            if(!is_null($cookie))
            {
                $data .= $cookie;
            }
        }

        return $data;
    }
}