using System;
using System.Collections.Concurrent;
using System.Threading;
using System.Threading.Tasks;

namespace CircuitBreaker.SignalProc.SignalProcessor
{
    public class InMemoryCircuitSignalProcessor : ICircuitSignalProcessor, IDisposable
    {
        public event CircuitStateChanged Closed;
        public event CircuitStateChanged HalfOpened;
        public event CircuitStateChanged Opened;

        public event CircuitSignalProcessed SignalProcessed;

        private readonly BlockingCollection<CircuitSignal> bColl;
        private readonly int successThreshold;
        private readonly int failThreshold;
        private readonly TimeSpan timeoutTimeSpan;
        private readonly Timer timeoutTimer;

        private int failCounter;
        private int successCounter;

        private bool isReadingContinue;

        private bool isDisposed;

        public CircuitState CurrentState { get; private set; }

        public InMemoryCircuitSignalProcessor(
            int successThreshold,
            int failThreshold,
            TimeSpan timeoutTimeSpan)
        {
            this.failThreshold = failThreshold;
            this.timeoutTimeSpan = timeoutTimeSpan;
            this.successThreshold = successThreshold;

            CurrentState = CircuitState.Closed;
            var cbQueue = new ConcurrentQueue<CircuitSignal>();
            bColl = new BlockingCollection<CircuitSignal>(cbQueue);

            timeoutTimer = new Timer(TimeoutExpired, null, 
                -1, (int)timeoutTimeSpan.TotalMilliseconds);

            isReadingContinue = true;

            var taskQueueReader = new Task(QueueReader);
            taskQueueReader.Start();
        }

        public void HandleSuccess()
        {
            bColl.Add(CircuitSignal.Create(true, null));
        }

        public void HandleSuccess(string sign)
        {
            bColl.Add(CircuitSignal.Create(true, sign));
        }

        public void HandleFail()
        {
            bColl.Add(CircuitSignal.Create(false, null));
        }

        public void HandleFail(string sign)
        {
            bColl.Add(CircuitSignal.Create(false, sign));
        }

        private void QueueReader()
        {
            while (isReadingContinue)
            {
                var cs = bColl.Take();
                if (cs.Signal)
                    SuccessInternal(cs);
                else
                    FailInternal(cs);

                SignalProcessed?.Invoke(this, new SignalProcessedEventArgs { CircuitSignal = cs });
            }
        }

        private void SuccessInternal(CircuitSignal cs)
        {
            switch (CurrentState)
            {
                case CircuitState.Closed:
                case CircuitState.Open:
                    return;
                case CircuitState.HalfOpen:
                    successCounter = successCounter + 1;
                    if (successCounter >= successThreshold)
                    {
                        ToClosed(cs);
                    }
                    return;
            }
        }

        private void FailInternal(CircuitSignal cs)
        {
            switch (CurrentState)
            {
                case CircuitState.Closed:
                    failCounter = failCounter + 1;
                    if (failCounter >= failThreshold)
                    {
                        ToOpen(cs);
                    }
                    return;
                case CircuitState.HalfOpen:
                    ToOpen(cs);
                    return;
                case CircuitState.Open:
                    return;
            }
        }

        private void TimeoutExpired(object state)
        {
            ToHalfOpen();
        }

        private void ToHalfOpen()
        {
            CurrentState = CircuitState.HalfOpen;
            timeoutTimer.Change(-1, -1);
            successCounter = 0;

            HalfOpened?.Invoke(this, new StateChangedEventArgs { State = CurrentState });
        }

        private void ToClosed(CircuitSignal cs)
        {
            CurrentState = CircuitState.Closed;
            timeoutTimer.Change(-1, -1);
            failCounter = 0;

            Closed?.Invoke(this, new StateChangedEventArgs { Sign = cs.Sign, State = CurrentState });
        }

        private void ToOpen(CircuitSignal cs)
        {
            CurrentState = CircuitState.Open;
            timeoutTimer.Change((int)timeoutTimeSpan.TotalMilliseconds, Int32.MaxValue);

            Opened?.Invoke(this, new StateChangedEventArgs { Sign = cs.Sign, State = CurrentState });
        }

        public void ResetProcessor()
        {
            //TODO: implement
            throw new NotImplementedException();
        }

        public void Dispose()
        {
            Dispose(true);
        }

        protected virtual void Dispose(bool disposing)
        {
            //TODO: improve disposing
            if (!isDisposed)
            {
                if (disposing)
                {
                    isReadingContinue = false;
                }
                bColl?.Dispose();

                isDisposed = true;
            }
        }
    }
}