using BoFilTest.Utils;
using Optional;
using Optional.Unsafe;
using System;
using System.Collections.Generic;
using System.Threading;

namespace BoFilTest.Domain
{
    // Very simple state machine that guides through the test steps as its states
    public class TestProcess
    {
        private readonly EventBus eventBus;
        private readonly List<TestStep> steps;
        private readonly TimeSpan acquisitionInterval;
        private readonly IHardwareControl hardwareControl;
        private int currentStep;
        private bool markForContinue;
        private DateTime lastStart;
        private bool timeoutHit;
        private Option<TimeSpan> countDown;
        private readonly TestTimings timings;
        private bool started;
        private readonly Ticker ticker;

        public TestProcess(EventBus eventBus, List<TestStep> steps, TimeSpan acquisitionInterval, IHardwareControl hardwareControl)
        {
            this.eventBus = eventBus;
            this.steps = steps;
            this.acquisitionInterval = acquisitionInterval;
            this.hardwareControl = hardwareControl;
            this.currentStep = -1; // start from the null state for orthogonal initialization of the null state
            this.markForContinue = true;
            this.lastStart = new DateTime();
            this.timeoutHit = false;
            this.countDown = Option.None<TimeSpan>();
            this.timings = new TestTimings();
            this.started = false;
            this.ticker = new Ticker();
        }
                
        // Let the system advance to the next step
        public void MarkForContinue()
        {
            // NOTE: I'm not entirely sure anymore why I went to do the transition with this boolean and the polling.
            // Maybe just because of the background process notion?
            // Maybe just do it directly?
            this.markForContinue = true;            
        }

        // Whole test finished?
        public bool IsFinished()
        {
            return this.currentStep >= this.steps.Count;
        }

        public Option<TestStep> GetCurrentStep()
        {
            if (this.currentStep < 0 || this.currentStep >= this.steps.Count)
                return Option.None<TestStep>();

            return Option.Some(this.steps[this.currentStep]);
        }

        TimeDisplay timeDisplay;

        TimeDisplay TimeDisplay
        {
            get
            {
                return timeDisplay;
            }
            set
            {
                if (!value.Equals(timeDisplay))
                {
                    timeDisplay = value;
                    this.eventBus.Publish(timeDisplay);
                }
            }
        }

        void UpdateTime(TestStep step)
        {
            var runningTime = DateTime.Now - this.lastStart;
            var warn = false;
            this.countDown.MatchSome(countDown =>
            {
                if (runningTime.CompareTo(countDown) > 0)
                {
                    warn = true;
                    if (!timeoutHit)
                    {
                        timeoutHit = true;
                        this.eventBus.Publish(new TimeoutHit());
                    }
                }
                else
                {
                    runningTime = countDown.Subtract(runningTime);
                }
            });

            TimeDisplay = new TimeDisplay(FormatTime(runningTime), warn);            
        }

        void UpdateStep(TestStep step)
        {
            this.ticker.Tick(time =>
            {
                RecordAndPublish(step.Description, time);
            });
        }

        void RecordAndPublish(StepDescription step, DateTime timestamp)
        {
            this.eventBus.Publish(new NewValues(step, timestamp, this.hardwareControl.ReadAll()));
        }
        
        void RecordAndPublish(StepDescription step)
        {
            RecordAndPublish(step, DateTime.Now);
        }

        public string FormatTime(TimeSpan time)
        {
            char DecimalSeparator = Convert.ToChar(Thread.CurrentThread.CurrentCulture.NumberFormat.NumberDecimalSeparator);
            return String.Format("{0:#0.}:{1:00.}{3}{2:0.}", time.Minutes, time.Seconds, Math.Floor(time.Milliseconds / 100.0), DecimalSeparator);
        }

        private Option<StepDescription> GetAssociatedStepForTransition(Option<StepDescription> from, Option<StepDescription> to)
        {
            if (from.HasValue && from.ValueOrFailure().IsRecording)
            {
                return from;
            }
            else
            {
                if (to.HasValue && to.ValueOrFailure().IsRecording)
                    return to;
            }
            return Option.None<StepDescription>();
        }

        private void RecordForTransition(Option<StepDescription> from, Option<StepDescription> to)
        {
            GetAssociatedStepForTransition(from, to).MatchSome(step =>
            {
                RecordAndPublish(step);
                ticker.Reset();
            });
        }

        private void SignalStart(StepDescription step)
        {
            this.ticker.Start(this.acquisitionInterval);
            this.eventBus.Publish(new TestStarted(DateTime.Now,
                hardwareControl.ReadAll(), hardwareControl.GasFlowMeasure.CalibrationCurveIndex));
        }

        private bool TransitionSteps()
        {
            var now = DateTime.Now;

            // Leave the old state
            var lastState = GetCurrentStep();
            lastState.MatchSome(current =>
            {
                current.Done(this.timings, now.Subtract(this.lastStart));
            });

            // Transition
            ++this.currentStep;
            this.markForContinue = false;
            this.lastStart = now;
            this.timeoutHit = false;

            var nextState = GetCurrentStep();
            RecordForTransition(lastState.Map(x => x.Description), nextState.Map(x => x.Description));

            // BOF-63, pretty hacky: reset integration value when last step was not integrating and current is
            if (!lastState.Map(x => x.Description.IsIntegrating).ValueOr(false) &&
                nextState.Map(x => x.Description.IsIntegrating).ValueOr(false))
            {
                hardwareControl.GasFlowMeasure.ResetIntegral();
            }

            if (IsFinished())
            {
                ticker.Stop();
                // BOF-68 Read integral before cleaning up hardware 
                var integral = hardwareControl.GasFlowMeasure.ReadIntegral();
                hardwareControl.Cleanup();
                this.eventBus.Publish(new TestFinished(this.timings, this.steps, integral));
                return false;
            }

            // Enter the new state
            TestStep step = this.steps[this.currentStep];
            if (!started)
            {
                SignalStart(step.Description);
                started = true;
            }

            this.countDown = step.HasCountDown ? Option.Some(step.EvaluateCountDown(timings)) : Option.None<TimeSpan>();
            this.eventBus.Publish(new StepChanged(step, this.steps));
            return true;
        }

        // Pulse this process. Returns true while it has things to do.
        public bool Poll()
        {
            if (IsFinished())
                return false;

            GetCurrentStep().MatchSome(current =>
            {
                UpdateTime(current);
                UpdateStep(current);
            });

            if (!markForContinue)
                return true;

            // Got a state transition
            return TransitionSteps();
        }
    }
}
