<?php

namespace PragmaRX\Google2FA;

use PragmaRX\Google2FA\Exceptions\SecretKeyTooShortException;
use PragmaRX\Google2FA\Support\Base32;
use PragmaRX\Google2FA\Support\Constants;
use PragmaRX\Google2FA\Support\QRCode;

class Google2FA
{
    use QRCode, Base32;

    /**
     * Length of the Token generated.
     */
    protected $oneTimePasswordLength = 6;

    /**
     * Interval between key regeneration.
     */
    protected $keyRegeneration = 30;

    /**
     * Secret.
     */
    protected $secret;

    /**
     * Window.
     */
    protected $window = 1; // Keys will be valid for 60 seconds

    /**
     * Find a valid One Time Password.
     *
     * @param $secret
     * @param $key
     * @param $window
     * @param $startingTimestamp
     * @param $timestamp
     * @param string $oldTimestamp
     *
     * @return bool
     */
    public function findValidOTP($secret, $key, $window, $startingTimestamp, $timestamp, $oldTimestamp = Constants::ARGUMENT_NOT_SET)
    {
        for (; $startingTimestamp <= $timestamp + $this->getWindow($window); $startingTimestamp++) {
            if (hash_equals($this->oathHotp($secret, $startingTimestamp), $key)) {
                return
                    $oldTimestamp === Constants::ARGUMENT_NOT_SET
                        ? true
                        : $startingTimestamp;
            }
        }

        return false;
    }

    /**
     * Generate a digit secret key in base32 format.
     *
     * @param int    $length
     * @param string $prefix
     *
     * @return string
     */
    public function generateSecretKey($length = 16, $prefix = '')
    {
        return $this->generateBase32RandomKey($length, $prefix);
    }

    /**
     * Get the current one time password for a key.
     *
     * @param $secret
     *
     * @return string
     */
    public function getCurrentOtp($secret)
    {
        return $this->oathHotp($secret, $this->getTimestamp());
    }

    /**
     * Get key regeneration.
     *
     * @return mixed
     */
    public function getKeyRegeneration()
    {
        return $this->keyRegeneration;
    }

    /**
     * Get OTP length.
     *
     * @return mixed
     */
    public function getOneTimePasswordLength()
    {
        return $this->oneTimePasswordLength;
    }

    /**
     * Get secret.
     *
     * @param string|null $secret
     *
     * @return mixed
     */
    public function getSecret($secret = null)
    {
        return
            is_null($secret)
            ? $this->secret
            : $secret;
    }

    /**
     * Returns the current Unix Timestamp divided by the $keyRegeneration
     * period.
     *
     * @return int
     **/
    public function getTimestamp()
    {
        return (int) floor(microtime(true) / $this->keyRegeneration);
    }

    /**
     * Get the OTP window.
     *
     * @param null|int $window
     *
     * @return mixed
     */
    public function getWindow($window = null)
    {
        return
            is_null($window)
                ? $this->window
                : $window;
    }

    /**
     * Make a window based starting timestamp.
     *
     * @param $window
     * @param $timestamp
     * @param $oldTimestamp
     *
     * @return mixed
     */
    private function makeStartingTimestamp($window, $timestamp, $oldTimestamp)
    {
        return $oldTimestamp === Constants::ARGUMENT_NOT_SET
            ? $timestamp - $this->getWindow($window)
            : max($timestamp - $this->getWindow($window), $oldTimestamp + 1);
    }

    /**
     * Get/use a starting timestamp for key verification.
     *
     * @param string|int|null $timestamp
     *
     * @return int
     */
    protected function makeTimestamp($timestamp = null)
    {
        if (is_null($timestamp)) {
            return $this->getTimestamp();
        }

        return (int) $timestamp;
    }

    /**
     * Takes the secret key and the timestamp and returns the one time
     * password.
     *
     * @param string $secret  - Secret key in binary form.
     * @param int    $counter - Timestamp as returned by getTimestamp.
     *
     * @throws SecretKeyTooShortException
     *
     * @return string
     */
    public function oathHotp($secret, $counter)
    {
        $secret = $this->base32Decode($this->getSecret($secret));

        if (strlen($secret) < 8) {
            throw new SecretKeyTooShortException();
        }

        // Counter must be 64-bit int
        $bin_counter = pack('N*', 0, $counter);

        $hash = hash_hmac('sha1', $bin_counter, $secret, true);

        return str_pad($this->oathTruncate($hash), $this->getOneTimePasswordLength(), '0', STR_PAD_LEFT);
    }

    /**
     * Extracts the OTP from the SHA1 hash.
     *
     * @param string $hash
     *
     * @return int
     **/
    public function oathTruncate($hash)
    {
        $offset = ord($hash[19]) & 0xf;

        $temp = unpack('N', substr($hash, $offset, 4));

        return substr($temp[1] & 0x7fffffff, -$this->getOneTimePasswordLength());
    }

    /**
     * Remove invalid chars from a base 32 string.
     *
     * @param $string
     *
     * @return mixed
     */
    public function removeInvalidChars($string)
    {
        return preg_replace('/[^'.Constants::VALID_FOR_B32.']/', '', $string);
    }

    /**
     * Setter for the enforce Google Authenticator compatibility property.
     *
     * @param mixed $enforceGoogleAuthenticatorCompatibility
     *
     * @return $this
     */
    public function setEnforceGoogleAuthenticatorCompatibility($enforceGoogleAuthenticatorCompatibility)
    {
        $this->enforceGoogleAuthenticatorCompatibility = $enforceGoogleAuthenticatorCompatibility;

        return $this;
    }

    /**
     * Set key regeneration.
     *
     * @param mixed $keyRegeneration
     */
    public function setKeyRegeneration($keyRegeneration)
    {
        $this->keyRegeneration = $keyRegeneration;
    }

    /**
     * Set OTP length.
     *
     * @param mixed $oneTimePasswordLength
     */
    public function setOneTimePasswordLength($oneTimePasswordLength)
    {
        $this->oneTimePasswordLength = $oneTimePasswordLength;
    }

    /**
     * Set secret.
     *
     * @param mixed $secret
     */
    public function setSecret($secret)
    {
        $this->secret = $secret;
    }

    /**
     * Set the OTP window.
     *
     * @param mixed $window
     */
    public function setWindow($window)
    {
        $this->window = $window;
    }

    /**
     * Verifies a user inputted key against the current timestamp. Checks $window
     * keys either side of the timestamp.
     *
     * @param string          $key          - User specified key
     * @param null|string     $secret
     * @param null|int        $window
     * @param null|int        $timestamp
     * @param null|string|int $oldTimestamp
     *
     * @return bool|int
     */
    public function verify($key, $secret = null, $window = null, $timestamp = null, $oldTimestamp = Constants::ARGUMENT_NOT_SET)
    {
        return $this->verifyKey(
            $secret,
            $key,
            $window,
            $timestamp,
            $oldTimestamp
        );
    }

    /**
     * Verifies a user inputted key against the current timestamp. Checks $window
     * keys either side of the timestamp.
     *
     * @param string          $secret
     * @param string          $key          - User specified key
     * @param null|int        $window
     * @param null|int        $timestamp
     * @param null|string|int $oldTimestamp
     *
     * @return bool|int
     */
    public function verifyKey($secret, $key, $window = null, $timestamp = null, $oldTimestamp = Constants::ARGUMENT_NOT_SET)
    {
        $timestamp = $this->makeTimestamp($timestamp);

        return $this->findValidOTP(
           $secret,
           $key,
           $window,
           $this->makeStartingTimestamp($window, $timestamp, $oldTimestamp),
           $timestamp,
           $oldTimestamp
       );
    }

    /**
     * Verifies a user inputted key against the current timestamp. Checks $window
     * keys either side of the timestamp, but ensures that the given key is newer than
     * the given oldTimestamp. Useful if you need to ensure that a single key cannot
     * be used twice.
     *
     * @param string   $secret
     * @param string   $key          - User specified key
     * @param int      $oldTimestamp - The timestamp from the last verified key
     * @param int|null $window
     * @param int|null $timestamp
     *
     * @return bool|int - false (not verified) or the timestamp of the verified key
     **/
    public function verifyKeyNewer($secret, $key, $oldTimestamp, $window = null, $timestamp = null)
    {
        return $this->verifyKey($secret, $key, $window, $timestamp, $oldTimestamp);
    }
}
