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

namespace BoFilTest.Domain
{
    public class MeasurementEntry
    {
        public StepDescription Step { get; set; }

        /// <summary>
        /// When this entry was measured
        /// </summary>
        public DateTime Time { get; set; }

        /// <summary>
        /// Relative time in the test, without pauses
        /// </summary>
        public TimeSpan RelativeRunningTime { get; set; }

        /// <summary>
        /// Values measured for this entry
        /// </summary>
        public MeasuredValues Values { get; set; }
    }

    public class MeasurementProtocol
    {
        private static NLog.Logger logger = NLog.LogManager.GetCurrentClassLogger();

        private readonly List<MeasurementEntry> entries;
        private readonly EventBus eventBus;
        private Gram tare;
        private bool recordingPaused;

        public MeasurementProtocol(EventBus eventBus)
        {
            this.entries = new List<MeasurementEntry>();
            this.eventBus = eventBus;
            this.tare = Gram.From(0.0);
            this.recordingPaused = false;

            this.eventBus.Subscribe<TestStarted>(message =>
            {
                this.tare = Gram.From(0.0);
                this.entries.Clear();
                this.recordingPaused = false;
                CalibrationCurveIndex = message.CalibrationCurveIndex;
            });

            this.eventBus.Subscribe<StepChanged>(message =>
            {
                if (!message.Step.RecordValues)
                {
                    logger.Info("Recording paused...");
                    recordingPaused = true;
                }
            });

            this.eventBus.Subscribe<NewValues>(message =>
            {
                ProcessNewValues(step: message.Step, time: message.Time, values: message.Values);
            });

            this.eventBus.Subscribe<TestFinished>(message =>
            {
                GasIntegral = message.GasIntegral.HasValue ? Option.Some((double)message.GasIntegral.Value) : Option.None<double>();
            });
        }

        public IEnumerable<MeasurementEntry> All => entries;
        public Option<double> GasIntegral { get; private set; }
        public int CalibrationCurveIndex { get; set; }

        void SetTareFrom(MeasuredValues values)
        {
            if (values.Weight.HasValue)
                this.tare = values.Weight.ValueOrFailure();
            else
                logger.Warn("Unable to get tare at test start. No balance value present.");
        }

        MeasuredValues ApplyTare(MeasuredValues values)
        {
            return new MeasuredValues(values.Weight.Map(x => x.Minus(this.tare)), values.Flow, values.Pressure, values.ExtraValue);
        }

        void ProcessNewValues(StepDescription step, DateTime time, MeasuredValues values)
        {
            if (step.IsRecording && !this.entries.Any())
            {
                // First real recorded value? Use for weight tare
                SetTareFrom(values);
            }

            var withTare = ApplyTare(values);
            this.eventBus.Publish(new NewValuesWithTare(step, withTare));

            // No need to record in the protocol?
            if (!step.IsRecording)
                return;

            // This logic removes "gaps" in the recording from the timeline
            TimeSpan relative = TimeSpan.Zero;
            if (this.entries.Any())
            {
                var last = this.entries.Last();
                if (recordingPaused)
                {
                    relative = last.RelativeRunningTime;
                }
                else
                {
                    relative = last.RelativeRunningTime.Add(time.Subtract(last.Time));
                }
            }

            if (recordingPaused)
            {
                logger.Info("Recording resumed");
                recordingPaused = false;
            }

            var newEntry = new MeasurementEntry { Step = step, Time = time, RelativeRunningTime = relative, Values = withTare };
            this.entries.Add(newEntry);
            this.eventBus.Publish(newEntry);
        }
    }
}
