<?php

declare (strict_types=1);
/*
 * The MIT License (MIT)
 *
 * Copyright (c) 2014-2019 Spomky-Labs
 *
 * This software may be modified and distributed under the terms
 * of the MIT license.  See the LICENSE file for details.
 */
namespace ExpressionEngine\Dependency\OTPHP;

use ExpressionEngine\Dependency\Assert\Assertion;
use ExpressionEngine\Dependency\ParagonIE\ConstantTime\Base32;
use RuntimeException;
use function ExpressionEngine\Dependency\Safe\ksort;
use function ExpressionEngine\Dependency\Safe\sprintf;
abstract class OTP implements OTPInterface
{
    use ParameterTrait;
    protected function __construct(?string $secret, string $digest, int $digits)
    {
        $this->setSecret($secret);
        $this->setDigest($digest);
        $this->setDigits($digits);
    }
    public function getQrCodeUri(string $uri, string $placeholder) : string
    {
        $provisioning_uri = \urlencode($this->getProvisioningUri());
        return \str_replace($placeholder, $provisioning_uri, $uri);
    }
    /**
     * The OTP at the specified input.
     */
    protected function generateOTP(int $input) : string
    {
        $hash = \hash_hmac($this->getDigest(), $this->intToByteString($input), $this->getDecodedSecret(), \true);
        $hmac = \array_values(\unpack('C*', $hash));
        $offset = $hmac[\count($hmac) - 1] & 0xf;
        $code = ($hmac[$offset + 0] & 0x7f) << 24 | ($hmac[$offset + 1] & 0xff) << 16 | ($hmac[$offset + 2] & 0xff) << 8 | $hmac[$offset + 3] & 0xff;
        $otp = $code % 10 ** $this->getDigits();
        return \str_pad((string) $otp, $this->getDigits(), '0', \STR_PAD_LEFT);
    }
    public function at(int $timestamp) : string
    {
        return $this->generateOTP($timestamp);
    }
    /**
     * @param array<string, mixed> $options
     */
    protected function filterOptions(array &$options) : void
    {
        foreach (['algorithm' => 'sha1', 'period' => 30, 'digits' => 6] as $key => $default) {
            if (isset($options[$key]) && $default === $options[$key]) {
                unset($options[$key]);
            }
        }
        ksort($options);
    }
    /**
     * @param array<string, mixed> $options
     */
    protected function generateURI(string $type, array $options) : string
    {
        $label = $this->getLabel();
        Assertion::string($label, 'The label is not set.');
        Assertion::false($this->hasColon($label), 'Label must not contain a colon.');
        $options = \array_merge($options, $this->getParameters());
        $this->filterOptions($options);
        $params = \str_replace(['+', '%7E'], ['%20', '~'], \http_build_query($options));
        return sprintf('otpauth://%s/%s?%s', $type, \rawurlencode((null !== $this->getIssuer() ? $this->getIssuer() . ':' : '') . $label), $params);
    }
    private function getDecodedSecret() : string
    {
        try {
            return Base32::decodeUpper($this->getSecret());
        } catch (\Exception $e) {
            throw new RuntimeException('Unable to decode the secret. Is it correctly base32 encoded?');
        }
    }
    private function intToByteString(int $int) : string
    {
        $result = [];
        while (0 !== $int) {
            $result[] = \chr($int & 0xff);
            $int >>= 8;
        }
        return \str_pad(\implode(\array_reverse($result)), 8, "\x00", \STR_PAD_LEFT);
    }
    protected function compareOTP(string $safe, string $user) : bool
    {
        return \hash_equals($safe, $user);
    }
}
