using Optional;
using System;
using System.Collections.Generic;

namespace BoFilTest.Domain
{
    public class TestStepList
    {
        private readonly List<TestStep> list;

        public static bool IsZero(TimeSpan time)
        {
            return TimeSpan.Zero.Equals(time);
        }

        private static TestStep MakePause()
        {
            return new TestStep(new StepDescription(StepType.Pause), (timings, duration) => { });
        }

        TestStepList()
        {
            this.list = new List<TestStep>();
        }

        private void Pause()
        {
            list.Add(MakePause());
        }

        private void Record(StepType type, Action<TestTimings, TimeSpan> done)
        {
            list.Add(new TestStep(new StepDescription(type), done));
        }

        private void Record(StepType type, int counter, Action<TestTimings, TimeSpan> done)
        {
            list.Add(new TestStep(new StepDescription(type, counter), done));
        }

        private void Record(StepType type, Func<TestTimings, TimeSpan> countDown, Action<TestTimings, TimeSpan> done)
        {
            list.Add(new TestStep(new StepDescription(type), countDown, done));
        }

        private void Record(StepType type, TimeSpan countDown, Action<TestTimings, TimeSpan> done)
        {
            Record(type, (timings) => countDown, done);
        }

        private void Record(StepType type, int counter, Func<TestTimings, TimeSpan> countDown, Action<TestTimings, TimeSpan> done)
        {
            list.Add(new TestStep(new StepDescription(type, counter), countDown, done));
        }

        private List<TestStep> Steps => this.list;

        /**
         * Domain rules here!
         **/
        public static List<TestStep> Build(ITestSpecification spec)
        {
            var result = new TestStepList();

            result.Pause();
            result.Record(StepType.CakeFormation, (timings, span) =>
            {
                timings.CakeFormationTime = span;
            });

            int counter = 0;
            var hasLateSteps = !IsZero(spec.Pressing) || !IsZero(spec.Vaporization);
            if (spec.WashingPassCount > 0 || hasLateSteps)
            {
                AddOptionalInterimDemoisturizing(spec, result, 0);
            }

            for (int i = 0; i < spec.WashingPassCount; ++i)
            {
                result.Pause();
                result.Record(StepType.Washing, counter, (timings, duration) =>
                {
                    timings.CakeWash.Add(duration);

                });

                // BOF-22, only add the last ID when they are not directly before final demoisturizing
                if (i + 1 != spec.WashingPassCount || hasLateSteps)
                {
                    AddOptionalInterimDemoisturizing(spec, result, counter + 1);
                }
                ++counter;
            }

            if (!IsZero(spec.Pressing))
            {
                result.Pause();

                result.Record(StepType.Pressing, spec.Pressing, (timings, duration) =>
                {
                    timings.Press = Option.Some(duration);
                });

                result.Pause();
            }

            if (!IsZero(spec.Vaporization))
            {
                // Do not add two subsequent pauses
                if (IsZero(spec.Pressing))
                    result.Pause();

                result.Record(StepType.Vaporization, spec.Vaporization, (timings, duration) =>
                {
                    timings.Vaporization = Option.Some(duration);
                });
            }

            if (spec.UseGasBreakthrough)
            {
                result.Record(StepType.GasBreakthrough, (timings, duration) =>
                {
                    timings.GasBreakthrough = Option.Some(duration);
                });
            }

            result.Record(StepType.Demoisturizing, (timings) =>
            {
                var baseTime = TimeSpan.FromSeconds(timings.CakeFormationTime.TotalSeconds * spec.DemoisturizationRatio);
                return timings.GasBreakthrough.Map(gasBreakthrough => { return baseTime.Subtract(gasBreakthrough); }).ValueOr(baseTime);
            },
            (timings, duration) =>
            {
                timings.Demoisturizing = timings.GasBreakthrough.Map(gasBreakthrough => duration.Add(gasBreakthrough)).ValueOr(duration);
            });

            return result.Steps;
        }

        private static void AddOptionalInterimDemoisturizing(ITestSpecification spec, TestStepList result, int counter)
        {
            if (IsZero(spec.IntermediateDemoisturizingTime))
                return;

            result.Record(StepType.IntermediateDemoisturizing, counter,
                (timings) => { return spec.IntermediateDemoisturizingTime; },
                (timings, duration) => { timings.IntermediateDemoisturizing.Add(duration); });
        }
    }
}
