using Plugin.BLE.Abstractions.Contracts;
using Serilog;
using System;
using System.Diagnostics;
using System.Threading.Tasks;
using TINK.Model.Bike.BluetoothLock;
using TINK.Model.Device;
using TINK.Services.BluetoothLock.Exception;
using TINK.Services.BluetoothLock.Tdo;
using Xamarin.Essentials;

namespace TINK.Services.BluetoothLock.BLE
{
    public class LockItPolling : LockItBase
    {
        public LockItPolling(IDevice device, IAdapter adapter, ICipher cipher) : base(device, adapter, cipher)
        {
        }

        /// <summary> Reconnects to device. </summary>
        /// <remarks> Consists of a bluetooth connect plus invocation of an authentication sequence. </remarks>
        /// <param name="authInfo">Info required to connect.</param>
        /// <param name="connectTimeout">Timeout to apply when connecting to bluetooth lock.</param>
        /// <returns>True if connecting succeeded, false if not.</returns>
        public override async Task ReconnectAsync(
            LockInfoAuthTdo authInfo,
            TimeSpan connectTimeout)
            => await ReconnectAsync(
                authInfo,
                connectTimeout,
                () => new LockItPolling(Device, Adapter, Cipher));

        /// <summary> Connects to device. </summary>
        /// <param name="authInfo">Info required to connect.</param>
        /// <param name="device">Device with must be connected.</param>
        /// <returns>True if connecting succeeded, false if not.</returns>
        public static async Task<LockItBase> Authenticate(
            IDevice device,
            LockInfoAuthTdo authInfo,
            IAdapter adapter,
            ICipher cipher)
            => await Authenticate(
                device,
                authInfo,
                adapter,
                cipher,
                () => new LockItPolling(device, adapter, cipher));

        /// <summary> Opens lock. </summary>
        /// <returns> Locking state.</returns>
        public async override Task<LockitLockingState?> OpenAsync()
        {
            if (DeviceInfo.Platform != DevicePlatform.Unknown && MainThread.IsMainThread == false)
            {
                throw new System.Exception("Can not open lock. Bluetooth code must be run on main thread");
            }

            var info = await GetLockStateAsync();
            if (info?.State == null)
            {
                // Device not reachable.
                Log.ForContext<LockItPolling>().Information("Can not open lock. Device is not reachable (get state).");
                return await Task.FromResult((LockitLockingState?)null);
            }

            if (info.State.Value.GetLockingState() == LockingState.Open)
            {
                // Lock is already open.
                Log.ForContext<LockItPolling>().Information("No need to open lock. Lock is already open.");
                return await Task.FromResult((LockitLockingState?)null);
            }

            Log.ForContext<LockItPolling>().Debug($"Request to closed lock. Current lockikng state is {info}, counter value {ActivateLockWriteCounter}.");

            var result = await OpenCloseLock(
                true); // Close lock);

            if (!result)
            {
                // State did not change. Return previous state.
                Log.ForContext<LockItPolling>().Information($"Opening lock failed.");
                return await Task.FromResult(info.State.Value);
            }

            info = await GetLockStateAsync();
            if (info?.State == null)
            {
                // Device not reachable.
                Log.ForContext<LockItPolling>().Information($"State after open command unknown. Device is not reachable (get state).");
                return await Task.FromResult((LockitLockingState?)null);
            }

            var watch = new Stopwatch();
            watch.Start();

            while (info?.State != null
                && info.State.Value != LockitLockingState.CouldntOpenBoldBlocked
                && info.State.Value != LockitLockingState.Open
                && watch.Elapsed < TimeSpan.FromMilliseconds(OPEN_CLOSE_TIMEOUT_MS))
            {
                info = await GetLockStateAsync(true); // While opening lock seems not always respond to reading operations.
                Log.ForContext<LockItPolling>().Information($"Current lock state is {info?.State.Value}.");
            }

            if (info == null)
            {
                return null;
            }

            switch (info.State.Value)
            {
                case LockitLockingState.Open:
                    return info.State.Value;

                case LockitLockingState.CouldntOpenBoldBlocked:
                    // Expected error. ILockIt count not be opened (Spoke blocks lock, ....)
                    throw new CouldntOpenBoldIsBlockedException();

                case LockitLockingState.Unknown:
                    // Expected error. ILockIt count not be opened (Spoke has blocked/ blocks lock, ....)
                    throw new CouldntOpenBoldWasBlockedException();

                default:
                    // Comprises values
                    // - LockitLockingState.Closed 
                    // - LockitLockingState.CouldntCloseMoving (should never happen because command open was send)
                    // - LockitLockingState.CouldntCloseBoldBlocked (should never happen because command open was send)
                    // Internal error which sould never occure. Lock refuses to open but connection is ok.
                    throw new CouldntOpenInconsistentStateExecption(info.State.Value.GetLockingState());
            }
        }

        /// <summary> Close the lock.</summary>
        /// <returns>Locking state.</returns>
        public async override Task<LockitLockingState?> CloseAsync()
        {
            if (DeviceInfo.Platform != DevicePlatform.Unknown && MainThread.IsMainThread == false)
            {
                throw new System.Exception("Can not close lock. Bluetooth code must be run on main thread");
            }

            // Get current state
            var info = await GetLockStateAsync();
            if (info?.State == null)
            {
                // Device not reachable.
                Log.ForContext<LockItPolling>().Error("Can not close lock. Device is not reachable (get state).");
                return await Task.FromResult((LockitLockingState?)null);
            }

            if (info.State.Value.GetLockingState() == LockingState.Closed)
            {
                // Lock is already closed.
                Log.ForContext<LockItPolling>().Error("No need to close lock. Lock is already closed.");
                return await Task.FromResult(info.State.Value);
            }

            Log.ForContext<LockItPolling>().Debug($"Request to closed lock. Current state is {info}, counter value {ActivateLockWriteCounter}.");

            var result = await OpenCloseLock(false); // Close lock
            if (!result)
            {
                // State did not change. Return previous state.
                Log.ForContext<LockItPolling>().Information($"Closing lock failed.");
                return await Task.FromResult(info.State.Value);
            }

            // Get lock state until either lock state chaneges or until log gets unreachable.
            info = await GetLockStateAsync();
            if (info?.State == null)
            {
                // Device not reachable.
                Log.ForContext<LockItPolling>().Information($"Lock state after close command unknown.");
                return await Task.FromResult((LockitLockingState?)null);
            }

            var watch = new Stopwatch();
            watch.Start();

            while (info.State != null
                && info.State.Value != LockitLockingState.CouldntCloseBoldBlocked
                && info.State.Value != LockitLockingState.CouldntCloseMoving
                && info.State.Value != LockitLockingState.Closed
                && watch.Elapsed < TimeSpan.FromMilliseconds(OPEN_CLOSE_TIMEOUT_MS))
            {
                info = await GetLockStateAsync(true); ; // While closing lock seems not always respond to reading operations.
                Log.ForContext<LockItPolling>().Information($"Current lock state is {info?.State.Value}.");
            }

            if (info == null)
            {
                return null;
            }

            switch (info.State.Value)
            {
                case LockitLockingState.CouldntCloseBoldBlocked:
                    // Expected error. ILockIt could not be closed (Spoke blocks lock, ....)
                    throw new CouldntCloseBoldBlockedException();

                case LockitLockingState.CouldntCloseMoving:
                    // Expected error. ILockIt could not be closed (bike is moving)
                    throw new CounldntCloseMovingException();

                case LockitLockingState.Closed:
                    // Everything is ok.
                    return info.State.Value;

                default:
                    // Comprises values
                    // - LockitLockingState.Open 
                    // - LockitLockingState.Unknown
                    // - LockitLockingState.CouldntOpenBoldBlocked
                    // Internal error which sould never occurre. Lock refuses to close but connection is ok.                    
                    throw new CouldntCloseInconsistentStateExecption(info.State.Value.GetLockingState());
            }
        }
    }
}