using System;

namespace OtpProviderClient
{
    /// <summary>
    /// Provides Time-based One Time Passwords RFC 6238.
    /// </summary>
    public class Totp_Provider
    {
        /// <summary>
        /// Time reference for TOTP generation.
        /// </summary>
        private static readonly DateTime UnixEpoch = new DateTime(1970, 1, 1, 0, 0, 0, DateTimeKind.Utc);

        /// <summary>
        /// Duration of generation of each totp, in seconds.
        /// </summary>
        private int _Duration;

        /// <summary>
        /// Length of the generated totp.
        /// </summary>
        private int _Length;

        private TimeSpan _TimeCorrection;
        /// <summary>
        /// Sets the time span that is used to match the server's UTC time to ensure accurate generation of Time-based One Time Passwords.
        /// </summary>
        public TimeSpan TimeCorrection { set { _TimeCorrection = value; } }

        /// <summary>
        /// Instanciates a new Totp_Generator.
        /// </summary>
        /// <param name="Duration">Duration of generation of each totp, in seconds.</param>
        /// <param name="Length">Length of the generated totp.</param>
        public Totp_Provider(int Duration, int Length)
        {
            if (!(Duration > 0)) throw new Exception("Invalid Duration."); //Throws an exception if the duration is invalid as the class cannot work without it.
            _Duration = Duration; //Defines variable from argument.
            if (!((Length > 5) && (Length < 9))) throw new Exception("Invalid Length."); //Throws an exception if the length is invalid as the class cannot work without it.
            _Length = Length; //Defines variable from argument.
            _TimeCorrection = TimeSpan.Zero; //Defines variable from non-constant default value.
        }

        /// <summary>
        /// Returns current time with correction int UTC format.
        /// </summary>
        public DateTime Now
        {
            get
            {
                return DateTime.UtcNow - _TimeCorrection; //Computes current time minus time correction giving the corrected time.
            }
        }

        /// <summary>
        /// Returns the time remaining before counter incrementation.
        /// </summary>
        public int Timer
        {
            get
            {
                var n = (_Duration - (int)((Now - UnixEpoch).TotalSeconds % _Duration)); //Computes the seconds left before counter incrementation.
                return n == 0 ? _Duration : n; //Returns timer value from 30 to 1.
            }
        }

        /// <summary>
        /// Returns number of intervals that have elapsed.
        /// </summary>
        public long Counter
        {
            get
            {
                var ElapsedSeconds = (long)Math.Floor((Now - UnixEpoch).TotalSeconds); //Compute current counter for current time.
                return ElapsedSeconds / _Duration; //Applies specified interval to computed counter.
            }
        }

        /// <summary>
        /// Converts an unsigned integer to binary data.
        /// </summary>
        /// <param name="n">Unsigned Integer.</param>
        /// <returns>Binary data.</returns>
        private byte[] GetBytes(ulong n)
        {
            byte[] b = new byte[8]; //Math.
            b[0] = (byte)(n >> 56); //Math.
            b[1] = (byte)(n >> 48); //Math.
            b[2] = (byte)(n >> 40); //Math.
            b[3] = (byte)(n >> 32); //Math.
            b[4] = (byte)(n >> 24); //Math.
            b[5] = (byte)(n >> 16); //Math.
            b[6] = (byte)(n >> 8);  //Math.
            b[7] = (byte)(n);       //Math.
            return b;
        }

        /// <summary>
        /// Generate a Totp using provided binary data.
        /// </summary>
        /// <param name="key">Binary data.</param>
        /// <returns>Time-based One Time Password.</returns>
        public string Generate(byte[] key)
        {
            System.Security.Cryptography.HMACSHA1 hmac = new System.Security.Cryptography.HMACSHA1(key, true); //Instanciates a new hash provider with a key.
            byte[] hash = hmac.ComputeHash(GetBytes((ulong)Counter)); //Generates hash from key using counter.
            hmac.Clear(); //Clear hash instance securing the key.

            int offset = hash[hash.Length - 1] & 0xf;           //Math.
            int binary =                                        //Math.
                ((hash[offset] & 0x7f) << 24)                   //Math.
                | ((hash[offset + 1] & 0xff) << 16)             //Math.
                | ((hash[offset + 2] & 0xff) << 8)              //Math.
                | (hash[offset + 3] & 0xff);                    //Math.

            int password = binary % Convert.ToInt32(Math.Pow(10, _Length)); //Math.
            return password.ToString(new string('0', _Length)); //Math.
        }
    }

    /// <summary>
    /// Provides time correction for Time-based One Time Passwords that require accurate DateTime syncronisation with server.
    /// </summary>
    public class TimeCorrection_Provider
    {
        /// <summary>
        /// Timer providing the delay between each time correction check.
        /// </summary>
        private System.Timers.Timer _Timer;

        /// <summary>
        /// Thread which handles the time correction check.
        /// </summary>
        private System.Threading.Thread Task;

        private bool _Enable;
        /// <summary>
        /// Defines weither or not the class will attempt to get time correction from the server.
        /// </summary>
        public bool Enable { get { return _Enable; } set { _Enable = value; _Timer.Enabled = value; } }

        private static int _Interval = 60;
        /// <summary>
        /// Gets or sets the interval in minutes between each online checks for time correction.
        /// </summary>
        /// <value>Time</value>
        public static int Interval { get { return _Interval; } set { _Interval = value; } }
        private long _IntervalStretcher;

        private volatile string _Url;
        /// <summary>
        /// Returns the URL this instance is using to checks for time correction.
        /// </summary>
        public string Url { get { return _Url; } }

        private TimeSpan _TimeCorrection;
        /// <summary>
        /// Returns the time span between server UTC time and this computer's UTC time of the last check for time correction.
        /// </summary>
        public TimeSpan TimeCorrection { get { return _TimeCorrection; } }

        private DateTime _LastUpdateDateTime;
        /// <summary>
        /// Returns the date and time in universal format of the last online check for time correction.
        /// </summary>
        public DateTime LastUpdateDateTime { get { return _LastUpdateDateTime; } }

        private bool _LastUpdateSucceded = false;
        /// <summary>
        /// Returns true if the last check for time correction was successful.
        /// </summary>
        public bool LastUpdateSucceded { get { return _LastUpdateSucceded; } }
        
        /// <summary>
        /// Instanciates a new Totp_TimeCorrection using the specified URL to contact the server.
        /// </summary>
        /// <param name="Url">URL of the server to get check.</param>
        /// <param name="Enable">Enable or disable the time correction check.</param>
        public TimeCorrection_Provider(string Url, bool Enable = true)
        {
            if (Url == string.Empty) throw new Exception("Invalid URL."); //Throws exception if the URL is invalid as the class cannot work without it.
            _Url = Url; //Defines variable from argument.
            _Enable = Enable; //Defines variable from argument.
            _LastUpdateDateTime = DateTime.MinValue; //Defines variable from non-constant default value.
            _TimeCorrection = TimeSpan.Zero; //Defines variable from non-constant default value.
            _Timer = new System.Timers.Timer(); //Instanciates timer.
            _Timer.Elapsed += Timer_Elapsed; //Handles the timer event
            _Timer.Interval = 1000; //Defines the timer interval to 1 seconds.
            _Timer.Enabled = _Enable; //Defines the timer to run if the class is initially enabled.
            Task = new System.Threading.Thread(Task_Thread); //Instanciate a new task.
            if (_Enable) Task.Start(); //Starts the new thread if the class is initially enabled.
        }

        /// <summary>
        /// Task that occurs every time the timer's interval has elapsed.
        /// </summary>
        private void Timer_Elapsed(object sender, EventArgs e)
        {
            _IntervalStretcher++; //Increments timer.
            if (_IntervalStretcher >= (60 * _Interval)) //Checks if the specified delay has been reached.
            {
                _IntervalStretcher = 0; //Resets the timer.
                Task_Do(); //Attempts to run a new task
            }
        }

        /// <summary>
        /// Instanciates a new task and starts it.
        /// </summary>
        /// <returns>Informs if reinstanciation of the task has succeeded or not. Will fail if the thread is still active from a previous time correction check.</returns>
        private bool Task_Do()
        {
            if (!Task.IsAlive) //Checks if the task is still running.
            {
                Task = new System.Threading.Thread(Task_Thread); //Instanciate a new task.
                Task.Start(); //Starts the new thread.
                return true; //Informs if successful
            }
            return false; //Informs if failed
        }

        /// <summary>
        /// Event that occurs when the timer has reached the required value. Attempts to get time correction from the server.
        /// </summary>
        private void Task_Thread()
        {
            try
            {
                var WebClient = new System.Net.WebClient(); //WebClient to connect to server.
                WebClient.DownloadData(_Url); //Downloads the server's page using HTTP or HTTPS.
                var DateHeader = WebClient.ResponseHeaders.Get("Date"); //Gets the date from the HTTP header of the downloaded page.
                _TimeCorrection = DateTime.UtcNow - DateTime.Parse(DateHeader, System.Globalization.CultureInfo.InvariantCulture.DateTimeFormat).ToUniversalTime(); //Compares the downloaded date to the systems date giving us a timespan.
                _LastUpdateSucceded = true; //Informs that the date check has succeeded.
            }
            catch (Exception)
            {
                _LastUpdateSucceded = false; //Informs that the date check has failed.
            }
            _LastUpdateDateTime = DateTime.Now; //Informs when the last update has been attempted (succeeded or not).
        }

        /// <summary>
        /// Perform a time correction check, may a few seconds.
        /// </summary>
        /// <param name="ResetTimer">Resets the timer to 0. Occurs even if the attempt to attempt a new time correction fails.</param>
        /// <param name="ForceCheck">Attempts to get time correction even if disabled.</param>
        /// <returns>Informs if the time correction check was attempted or not. Will fail if the thread is still active from a previous time correction check.</returns>
        public bool CheckNow(bool ResetTimer = true, bool ForceCheck = false)
        {
            if (ResetTimer) //Checks if the timer should be reset.
            {
                _IntervalStretcher = 0; //Resets the timer.
            }
            if (ForceCheck || _Enable) //Checks if this check is forced or if time correction is enabled.
            {
                return Task_Do(); //Attempts to run a new task and informs if attempt to attemp is a success of fail
            }
            return false; //Informs if not attempted to attempt
        }
    }

    /// <summary>
    /// Utility to deal with Base32 encoding and decoding.
    /// </summary>
    /// <remarks>
    /// http://tools.ietf.org/html/rfc4648
    /// </remarks>
    public static class Base32
    {
        /// <summary>
        /// The number of bits in a base32 encoded character.
        /// </summary>
        private const int encodedBitCount = 5;
        /// <summary>
        /// The number of bits in a byte.
        /// </summary>
        private const int byteBitCount = 8;
        /// <summary>
        /// A string containing all of the base32 characters in order.
        /// This allows a simple indexof or [index] to convert between
        /// a numeric value and an encoded character and vice versa.
        /// </summary>
        private const string encodingChars = "ABCDEFGHIJKLMNOPQRSTUVWXYZ234567";
        /// <summary>
        /// Takes a block of data and converts it to a base 32 encoded string.
        /// </summary>
        /// <param name="data">Input data.</param>
        /// <returns>base 32 string.</returns>
        public static string Encode(byte[] data)
        {
            if (data == null)
                throw new ArgumentNullException();
            if (data.Length == 0)
                throw new ArgumentNullException();

            // The output character count is calculated in 40 bit blocks.  That is because the least
            // common blocks size for both binary (8 bit) and base 32 (5 bit) is 40.  Padding must be used
            // to fill in the difference.
            int outputCharacterCount = (int)Math.Ceiling(data.Length / (decimal)encodedBitCount) * byteBitCount;
            char[] outputBuffer = new char[outputCharacterCount];

            byte workingValue = 0;
            short remainingBits = encodedBitCount;
            int currentPosition = 0;

            foreach (byte workingByte in data)
            {
                workingValue = (byte)(workingValue | (workingByte >> (byteBitCount - remainingBits)));
                outputBuffer[currentPosition++] = encodingChars[workingValue];

                if (remainingBits <= byteBitCount - encodedBitCount)
                {
                    workingValue = (byte)((workingByte >> (byteBitCount - encodedBitCount - remainingBits)) & 31);
                    outputBuffer[currentPosition++] = encodingChars[workingValue];
                    remainingBits += encodedBitCount;
                }

                remainingBits -= byteBitCount - encodedBitCount;
                workingValue = (byte)((workingByte << remainingBits) & 31);
            }

            // If we didn't finish, write the last current working char.
            if (currentPosition != outputCharacterCount)
                outputBuffer[currentPosition++] = encodingChars[workingValue];

            // RFC 4648 specifies that padding up to the end of the next 40 bit block must be provided
            // Since the outputCharacterCount does account for the paddingCharacters, fill it out.
            while (currentPosition < outputCharacterCount)
            {
                // The RFC defined paddinc char is '='.
                outputBuffer[currentPosition++] = '=';
            }

            return new string(outputBuffer);
        }

        /// <summary>
        /// Takes a base 32 encoded value and converts it back to binary data.
        /// </summary>
        /// <param name="base32">Base 32 encoded string.</param>
        /// <returns>Binary data.</returns>
        public static byte[] Decode(string base32)
        {
            if (string.IsNullOrEmpty(base32))
                throw new ArgumentNullException();

            var unpaddedBase32 = base32.ToUpperInvariant().TrimEnd('=');
            foreach (var c in unpaddedBase32)
            {
                if (encodingChars.IndexOf(c) < 0)
                    throw new ArgumentException("Base32 contains illegal characters.");
            }

            // we have already removed the padding so this will tell us how many actual bytes there should be.
            int outputByteCount = unpaddedBase32.Length * encodedBitCount / byteBitCount;
            byte[] outputBuffer = new byte[outputByteCount];

            byte workingByte = 0;
            short bitsRemaining = byteBitCount;
            int mask = 0;
            int arrayIndex = 0;

            foreach (char workingChar in unpaddedBase32)
            {
                int encodedCharacterNumericValue = encodingChars.IndexOf(workingChar);

                if (bitsRemaining > encodedBitCount)
                {
                    mask = encodedCharacterNumericValue << (bitsRemaining - encodedBitCount);
                    workingByte = (byte)(workingByte | mask);
                    bitsRemaining -= encodedBitCount;
                }
                else
                {
                    mask = encodedCharacterNumericValue >> (encodedBitCount - bitsRemaining);
                    workingByte = (byte)(workingByte | mask);
                    outputBuffer[arrayIndex++] = workingByte;
                    workingByte = (byte)(encodedCharacterNumericValue << (byteBitCount - encodedBitCount + bitsRemaining));
                    bitsRemaining += byteBitCount - encodedBitCount;
                }
            }

            return outputBuffer;
        }
    }
}