<?php

namespace Buckhill\Yubico\Modes\Yubikey;


use Buckhill\Yubico\Exceptions\ReplayException;
use Buckhill\Yubico\Exceptions\YubikeyValidationException;

class DecryptionResult
{
    const CRC = 0xf0b8;
    const PRIVATE_ID_LENGTH = 12;

    protected $decrypted_otp;
    protected $yubikey;
    protected $decrypted_data;
    protected $crc_calculated;

    /**
     * DecryptionResult constructor.
     *
     * @param $decrypted
     */
    public function __construct($decrypted, Yubikey $key)
    {
        $this->decrypted_otp = $decrypted;
        $this->yubikey = $key;

        $this->parse();
    }

    public function validate()
    {
        return $this->validateAgainst($this->yubikey);
    }


    /**
     * @param Yubikey $key
     *
     * @return $this
     * @throws ReplayException
     * @throws YubikeyValidationException
     */
    public function validateAgainst(Yubikey  $key)
    {
        if(!$this->verifyCRC())
        {
            throw new YubikeyValidationException(sprintf("Incorrect CRC. Expected %s, got %s.", static::CRC, $this->crc_calculated));
        }

        if($key->getPrivateId() !== $this->getPrivateId())
        {
            throw new YubikeyValidationException(sprintf("Invalid Private ID found in OTP. Need: %s, got: %s", $key->getPrivateId(), $this->getPrivateId()));
        }

        if($this->getUseCounter() === $key->getUseCounter())
        {
            if($key->getSessionCounter() > $this->getSessionCounter())
            {
                throw new ReplayException(sprintf("Replay detected. Session counter in key is larger than session counter in OTP (%d > %d)",
                    $key->getSessionCounter(), $this->getSessionCounter()));
            }
        }
        elseif($key->getUseCounter() >= $this->getUseCounter())
        {
            throw new ReplayException(sprintf("Replay detected. Usage counter in OTP: %d. Usage counter in Yubikey: %d",
                $this->getUseCounter(), $key->getUseCounter()));
        }

        return $this;
    }


    /**
     * @return bool
     */
    public function verifyCRC()
    {
        $crc = 0xffff;
        $token = $this->decrypted_otp;

        for ($i = 0; $i < 16; $i++ )
        {
            $b = hexdec($token[$i * 2] . $token[($i * 2) + 1]);

            $crc = $crc ^ ($b & 0xff);

            for ($j = 0; $j < 8; $j++)
            {
                $n = $crc & 1;

                $crc = $crc >> 1;

                if ($n != 0)
                {
                    $crc = $crc ^ 0x8408;
                }
            }
        }

        $this->crc_calculated = $crc;

        return $crc === static::CRC;
    }


    /**
     * @return string
     */
    public function getPrivateId()
    {
        return $this->fromData('private_id');
    }


    /**
     * @return int
     */
    public function getUseCounter()
    {
        return $this->fromData('use_counter');
    }


    /**
     * @return int
     */
    public function getSessionCounter()
    {
        return $this->fromData('session_counter');
    }


    /**
     * @return int
     */
    public function getTimeLow()
    {
        return $this->fromData('time_low');
    }


    /**
     * @return int
     */
    public function getTimeHigh()
    {
        return $this->fromData('time_high');
    }


    /**
     * @return int
     */
    public function getTimestamp()
    {
        return $this->fromData('timestamp');
    }


    /**
     * @return int
     */
    public function getCRC()
    {
        return $this->fromData('crc');
    }

    /**
     * @return $this
     */
    protected function parse()
    {
        $otp = $this->decrypted_otp;
        $start = 0;

        $private_id = substr($otp, $start, self::PRIVATE_ID_LENGTH);
        $start += self::PRIVATE_ID_LENGTH;

        $use_counter = hexdec(substr($otp, $start + 2, 2) . substr($otp, $start, 2)) & 0xffff;
        $start += 4;

        $timelow = hexdec(substr($otp, $start + 2, 2) . substr($otp, $start, 2)) & 0xffff;
        $start += 4;

        $timehigh = hexdec(substr($otp, $start, 2)) & 0xff;
        $start += 2;

        $session_use = hexdec(substr($otp, $start, 2)) & 0xff;
        $start += 2;

        $random = hexdec(substr($otp, $start + 2, 2) . substr($otp, $start, 2));
        $start += 4;

        $crc = hexdec(substr($otp, $start + 2, 2) . substr($otp, $start, 2));

        $timestamp = ($timehigh << 16) + $timelow;

        $counter = ($use_counter << 8) + $session_use;

        $this->decrypted_data = [
            'private_id'        => $private_id,
            'use_counter'       => $use_counter,
            'session_counter'   => $session_use,
            'time_low'          => $timelow,
            'time_high'         => $timehigh,
            'timestamp'         => $timestamp,
            'random'            => $random,
            'crc'               => $crc,
            'total_counter'     => $counter
        ];

        return $this;
    }


    /**
     * @param      $key
     * @param null $default
     *
     * @return null
     */
    protected function fromData($key, $default = null)
    {
        return isset($this->decrypted_data[$key]) ? $this->decrypted_data[$key] : $default;
    }
}
