<?php

namespace Buckhill\OTP;

abstract class AbstractOTP
{
    protected $secret;
    protected $conversion_method;


    /**
     * @param string $secret
     *
     * @return $this
     */
    public function setSecret($secret = '')
    {
        if(!is_string($secret))
        {
            throw new \InvalidArgumentException(sprintf("Invalid value provided for secret. Expected string, got: %s.", gettype($secret)));
        }

        $this->secret = $secret;

        return $this;
    }


    /**
     * @param bool|true $convert
     *
     * @return string
     */
    public function getSecret($convert = true)
    {
        if(true === $convert)
        {
            return $this->convert($this->secret);
        }

        return $this->secret;
    }


    /**
     * @param callable $conversion
     * @return $this
     */
    public function setConversionMethod(callable $conversion)
    {
        $this->conversion_method = $conversion;

        return $this;
    }


    /**
     * @return bool
     */
    public function hasConversionMethod()
    {
        return is_callable($this->conversion_method);
    }


    /**
     * @return callable
     * @throws \BadMethodCallException
     */
    public function getConversionMethod()
    {
        if(!is_callable($this->conversion_method))
        {
            throw new \BadMethodCallException("There is no conversion method set, yet ::getConversionMethod() is called. Please, use ::setConversionMethod() first.");
        }

        return $this->conversion_method;
    }


    /**
     * @param $secret
     * @return string
     */
    public function convert($secret)
    {
        return call_user_func_array($this->getConversionMethod(), [$secret]);
    }


    /**
     * @param $counter
     * @return string
     */
    public function binary($counter)
    {
        if(!is_int($counter))
        {
            throw new \InvalidArgumentException(sprintf("Invalid value for the counter. Expected integer, got: %s", gettype($counter)));
        }

        // Convert the decimal number to hexadecimal
        $hex = dechex($counter);

        // Note - this method works with values up to 2^64 - 1, which is sufficient for more than "most" use cases
        if(strlen($hex) > 16)
        {
            throw new \InvalidArgumentException("Counter value too large.");
        }

        // This is now a string, we have to zero-fill it so it's 16 chars in length = 8 bytes (2 chars = 1 byte), which we will convert to binary format using pack()
        return pack('H*', str_pad($hex, 16, "0", STR_PAD_LEFT));
    }


    /**
     * Performs the hash truncation as specified per RFC @ https://www.ietf.org/rfc/rfc4226.txt
     * Section 5.4. describes the algorithm
     *
     * @param $hash
     * @param int $length
     * @return string
     */
    protected function truncate($hash, $length = 6)
    {
        $offset = ord($hash[strlen($hash) - 1]) & 0xf;

        $value = (
            ((ord($hash[$offset + 0]) & 0x7f) << 24) |
            ((ord($hash[$offset + 1]) & 0xff) << 16) |
            ((ord($hash[$offset + 2]) & 0xff) << 8) |
            (ord($hash[$offset + 3]) & 0xff)
        );

        return (string)substr($value, -1 * $length);
    }


    /**
     * @param string $algo
     * @param $secret
     * @param $counter
     * @return string
     */
    protected function hmac($algo = 'sha1', $secret, $counter)
    {
        return hash_hmac($algo, $counter, $secret, true);
    }
}