<?php

namespace Buckhill\RFC6238;


use Buckhill\OTP\AbstractOTP;

/**
 * Class OTP
 * @package Buckhill\RFC6238
 */
class OTP extends AbstractOTP
{
    /**
     * The secret used for hashing
     *
     * @var null|string
     */
    protected $secret;

    /**
     * Hash algorithm used for the hash_hmac
     *
     * @var string
     */
    protected $algo;

    /**
     * Interval is the value during which the OTP will yield the same value. It's basically a value that says "this OTP will be valid for next 30 seconds"
     * If the value of interval, then the window during which OTP is valid is 30 seconds. Increase this value via constructor or setInterval() method
     *
     * @var int
     */
    protected $interval;

    /**
     * @param null $secret
     * @param int $interval
     */
    public function __construct($secret = null, $interval = 30, $algo = 'sha1')
    {
        $this->secret = $secret;
        $this->algo = $algo;

        $this->setInterval($interval);
    }

    /**
     * @param null $timestamp
     * @param int $digits
     * @return string
     */
    public function getOTP($timestamp = null, $digits = 6)
    {
        $timecode   = $this->getTimeCode((is_null($timestamp) || !is_int($timestamp)) ? time() : $timestamp);
        $secret     = $this->getSecret();
        $hash       = $this->getHash($secret, $this->binary($timecode));

        return $this->truncate($hash, $digits);
    }

    /**
     * Note: make sure to convert the secret to base32 if you are using Google Authenticator
     *
     * @param $name
     * @param $secret
     * @return string
     */
    public function getProvisioningURI($name = '', $secret = '')
    {
        if(empty($secret) || !is_string($secret))
        {
            throw new \InvalidArgumentException("Invalid value provided for secret. Expecting non-empty string.");
        }

        return sprintf("otpauth://totp/%s?secret=%s", urlencode($name), $secret);
    }

    /**
     * @param $otp
     * @param null $timestamp
     * @param int $digits
     * @return bool
     */
    public function verify($otp, $timestamp = null, $digits = 6)
    {
        $calculated = $this->at($timestamp, $digits);

        return (strcmp($otp, $calculated) === 0);
    }

    /**
     * @param null $timestamp
     * @param int $digits
     * @return string
     */
    public function at($timestamp = null, $digits = 6)
    {
        return $this->getOTP($timestamp, $digits);
    }

    /**
     * @param $secret
     * @param $timecode
     * @return string
     */
    public function getHash($secret, $timecode)
    {
        return $this->hmac($this->algo, $secret, $timecode);
    }

    /**
     * @param int $interval
     * @return $this
     * @throws \InvalidArgumentException
     */
    public function setInterval($interval = 30)
    {
        if(!is_int($interval))
        {
            throw new \InvalidArgumentException("Interval must be an integer.");
        }

        if($interval <= 0)
        {
            throw new \InvalidArgumentException("Interval must be larger than 0.");
        }

        $this->interval = $interval;

        return $this;
    }

    /**
     * Since interval is managed via setInterval() method, we are 100% sure it's integer that's > 0
     *
     * @return int
     */
    public function getInterval()
    {
        return $this->interval;
    }

    /**
     * @param $timestamp
     * @return int
     */
    protected function getTimeCode($timestamp)
    {
        if(!is_int($timestamp))
        {
            throw new \InvalidArgumentException("Timestamp must be an integer.");
        }

        if($timestamp <= 0)
        {
            throw new \InvalidArgumentException("Timestamp must be larger than 0");
        }

        return (int)($timestamp / $this->getInterval());
    }
}