using Microscopic_Traffic_Simulator___Model.Utilities;
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Threading;

namespace Microscopic_Traffic_Simulator___Model.SimulationControl
{
    /// <summary>
    /// Class controlling performing simulation actions by simulation calendar.
    /// </summary>
    public class Simulation : IDisposable
    {
        /// <summary>
        /// Random instance for simulation and all simulation actions.
        /// </summary>
        private Random random;

        /// <summary>
        /// Simulation timer.
        /// </summary>
        private Timer timer;

        /// <summary>
        /// Currently valid token for simulation actions. The token is changed when the simulation
        /// is paused or stopped. 
        /// </summary>
        private int validToken = 0;

        /// <summary>
        /// This object is used as lock object for preventing two events performing at one time.
        /// Also it wraps the sections where there are operations with tokens. It prevents from simultaneous 
        /// execution of simulation control actions with scheduled pause.
        /// </summary>
        private object eventsLock = new object();

        /// <summary>
        /// DateTime of last starting of simulation timer or last change of simulation speed.
        /// </summary>
        private DateTime dateTimeOfLastAction;

        /// <summary>
        /// Model time of last tick of simulation timer or last change of simulation run.
        /// </summary>
        private TimeSpan currentModelTime;
        /// <summary>
        /// Model time of last tick of simulation timer or last change of simulation run.
        /// </summary>        
        public TimeSpan CurrentModelTime { get { return currentModelTime; } }

        /// <summary>
        /// Simulation model time when the simulation will be paused.
        /// </summary>
        private TimeSpan modelTimeToPause = TimeSpan.MaxValue;
        /// <summary>
        /// Simulation model time when the simulation will be paused.
        /// </summary>        
        public TimeSpan ModelTimeToPause
        {
            get { return modelTimeToPause; }
            set { modelTimeToPause = value; }
        }

        /// <summary>
        /// An event informing about changing of model time.
        /// </summary>
        public event EventHandler ModelTimeChanged;

        /// <summary>
        /// An event informing about applying of pause by the simulation control.
        /// </summary>
        public event EventHandler SimulationPaused;

        /// <summary>
        /// An event informing about finishing of simulation action.
        /// </summary>
        public event EventHandler SingleSimulationActionPerformed;

        /// <summary>
        /// Event of simulation speed change.
        /// </summary>
        public event EventHandler<SimulationSpeedChangeEventArgs> SimulationSpeedChanged;

        /// <summary>
        /// Event of activation or deactivation of maximum simulation speed.
        /// </summary>
        public event EventHandler IsMaxSimulationSpeedChange;

        /// <summary>
        /// Current simulation status of simulation.
        /// </summary>                
        private SimulationState simulationState = SimulationState.NotRunning;
        /// <summary>
        /// Current simulation status of simulation.
        /// </summary>
        public SimulationState SimulationState { get { return simulationState; } }

        /// <summary>
        /// Seed which is used in the simulation.
        /// </summary>
        private int usedSeed;
        /// <summary>
        /// Seed which is used in the simulation.
        /// </summary>
        public int UsedSeed { get { return usedSeed; } }

        /// <summary>
        /// Minimum possible simulation speed.
        /// </summary>
        private const double MinimumSimulationSpeed = 0.001;
        /// <summary>
        /// Maximum possible simulation speed.
        /// </summary>
        private const double MaximumSimulationSpeed = 10000.0;
        /// <summary>
        /// Current simulation speed in model time speed divided by computer/user time speed
        /// </summary>
        private double simulationSpeed = 1.0;
        /// <summary>
        /// Current simulation speed in model time speed divided by computer/user time speed
        /// </summary>
        public double SimulationSpeed
        {
            get { return simulationSpeed; }
            set
            {
                if (value < MinimumSimulationSpeed || value > MaximumSimulationSpeed)
                {
                    return;
                }                
                double oldSimulationSpeed = simulationSpeed;
                lock (eventsLock)
                {
                    if (simulationState == SimulationState.Running)
                    {
                        PauseTimer();
                    }
                    simulationSpeed = value;
                    if (simulationState == SimulationState.Running)
                    {
                        StartOrResumeTimer();
                    }
                }
                if (oldSimulationSpeed != simulationSpeed)
                    OnSimulationSpeedChange(oldSimulationSpeed, simulationSpeed);
            }
        }

        /// <summary>
        /// Indicates whether the simulation speed is maximal.
        /// </summary>
        private bool isMaxSimulationSpeed;
        /// <summary>
        /// Indicates whether the simulation speed is maximal.
        /// </summary>        
        public bool IsMaxSimulationSpeed
        {
            get { return isMaxSimulationSpeed; }
            set
            {
                if (isMaxSimulationSpeed != value)
                {
                    isMaxSimulationSpeed = value;
                    if (isMaxSimulationSpeed)
                    {
                        if (simulationState == SimulationState.Running)
                        {
                            lock (eventsLock)
                            {
                                validToken++;
                            }
                            //needed to test the simulation state again because scheduled pause might be performed
                            //just before the locked section right above. Then do not resume timer.
                            if (simulationState == SimulationState.Running)
                            {
                                StartOrResumeTimer();
                            }
                        }
                    }
                    OnIsMaxSimulationSpeedChange();
                }
            }
        }

        /// <summary>
        /// Checks whether model time of next action is higher than model time to pause.
        /// </summary>
        private bool IsModelTimeToPauseReached
        {
            get
            {
                if (simulationActions.Count == 0)
                    return false;
                else
                    return simulationActions.Top.Key.Time >= modelTimeToPause;
            }
        }

        /// <summary>
        /// Heap storing simulation calendar actions to perform.
        /// </summary>
        private Heap<SimulationEventGeneratorKey, ISimulationEventsGenerator> simulationActions =
            new Heap<SimulationEventGeneratorKey, ISimulationEventsGenerator>();

        /// <summary>
        /// List of objects which generates repeating actions
        /// </summary>
        private List<ISimulationEventsGenerator> simulationActionGenerators =
            new List<ISimulationEventsGenerator>();

        /// <summary>
        /// Initialize simulation by list of simulation action generators and seed for 
        /// initialization of random instance. The timer is also initialized.
        /// </summary>
        /// <param name="simulationEventGenerators">List of simulation action generators.</param>        
        internal Simulation(List<ISimulationEventsGenerator> simulationEventGenerators)
        {
            this.simulationActionGenerators = simulationEventGenerators;            
        }

        /// <summary>
        /// The new action for current action generator is pushed to calendar.
        /// </summary>
        /// <param name="simulationEventsGenerator">Reference to current action generator.</param>
        private void PushNewActionToCalendarForCurrentActionGenerator(
            ISimulationEventsGenerator simulationEventsGenerator)
        {
            TimeSpan timeToNextAction = simulationEventsGenerator.GetTimeToNextAction(random);
            if (timeToNextAction != TimeSpan.MaxValue)
            {
                simulationActions.Push(new SimulationEventGeneratorKey(currentModelTime + timeToNextAction, 
                    simulationEventsGenerator.Priority), simulationEventsGenerator);
            }
        }

        /// <summary>
        /// Initialize simulation run by getting times for next action from all generators and
        /// inserting them to simulation calendar.
        /// <param name="seed">Seed for initialization of random instance. When null it will be generated according
        /// to tick counter.</param>
        /// </summary>
        private void InitializeSimulationRun(int? seed)
        {
            Debug.Assert(simulationActions.Count == 0);
            Debug.Assert(currentModelTime == TimeSpan.Zero);
            Debug.Assert(simulationState == SimulationState.NotRunning);
            foreach (ISimulationEventsGenerator i in simulationActionGenerators)
                PushNewActionToCalendarForCurrentActionGenerator(i);
            if (seed == null)
                usedSeed = (int)(DateTime.Now.Ticks & 0x0000FFFF);
            else 
                usedSeed = seed.Value;
            random = new Random(usedSeed);
        }

        /// <summary>
        /// Run simulation. If the simulation is in the beginning initialize the simulation run.
        /// <param name="seed">Seed to be used in the simulation.</param>
        /// </summary>
        internal void Run(int? seed = null)
        {
            Debug.Assert(simulationState == SimulationState.NotRunning ||
                simulationState == SimulationState.Paused);
            if (simulationState == SimulationState.NotRunning)
            {                
                InitializeSimulationRun(seed);
            }
            simulationState = SimulationState.Running;
            ResetModelTimeToPauseIfReached();
            StartOrResumeTimer();
        }

        /// <summary>
        /// Perform one simulation step. If the simulation is in the beginning 
        /// initialize the simulation run.
        /// <param name="seed">Seed to be used in the simulation.</param>
        /// </summary>
        internal void StepForward(int? seed = null)
        {
            Debug.Assert(simulationState == SimulationState.NotRunning ||
                   simulationState == SimulationState.Paused);
            if (simulationState == SimulationState.NotRunning)
            {
                InitializeSimulationRun(seed);
                simulationState = SimulationState.Paused;
            }
            ResetModelTimeToPauseIfReached();
            RunStep();
        }

        /// <summary>
        /// Pause simulation.        
        /// </summary>
        internal void Pause()
        {
            lock (eventsLock)
            {
                if (simulationState == SimulationState.Paused ||
                    simulationState == SimulationState.NotRunning)
                {
                    Debug.Assert(true,
                        "Possible simultaneous call of Pause command and scheduled pause or bug in code.");
                    return;
                }
                simulationState = SimulationState.Paused;
                PauseTimer();
            }
        }

        /// <summary>
        /// Stop simulation. The calendar will be cleared.
        /// </summary>
        internal void Stop()
        {
            lock (eventsLock)
            {
                if (simulationState == SimulationState.NotRunning)
                {
                    Debug.Assert(true,
                        "Possible simultaneous call of Stop command and scheduled pause or bug in code.");
                    return;
                }
                simulationState = SimulationState.NotRunning;
                StopTimer();
                simulationActions.Clear();
                ResetModelTimeToPause();
            }
        }

        /// <summary>
        /// Set timer to next action according to current date time and model time. This method
        /// should be called to set timer after starting or resuming simulation or after changing
        /// simulation speed. Last used token is updated to current valid token.
        /// </summary>
        private void StartOrResumeTimer()
        {
            TimeSpan timeToBeSetInTimer = TimeSpan.Zero;
            if (!isMaxSimulationSpeed && simulationActions.Count > 0)
            {
                dateTimeOfLastAction = DateTime.Now;
                timeToBeSetInTimer = (simulationActions.Top.Key.Time - currentModelTime).Divide(simulationSpeed);
                if (timeToBeSetInTimer < TimeSpan.Zero)
                    timeToBeSetInTimer = TimeSpan.Zero;
            }
            timer = new Timer(Tick, validToken, timeToBeSetInTimer, Timeout.InfiniteTimeSpan);
        }

        /// <summary>
        /// Get following action with its model time from simulation calendar. From generator of
        /// action is computed time of the new action which is also added to calendar.
        /// </summary>
        /// <returns>Generator of action.</returns>
        private ISimulationEventsGenerator GetGeneratorToPerformAnActionAndStoreNewOne()
        {
            if (simulationActions.Count == 0)
                return null;
            var currentEvent = simulationActions.Pop();
            currentModelTime = currentEvent.Key.Time;
            PushNewActionToCalendarForCurrentActionGenerator(currentEvent.Value);
            return currentEvent.Value;
        }

        /// <summary>
        /// Run one simulation action. 
        /// </summary>
        private void RunStep()
        {
            new Thread(() =>
            {
                lock (eventsLock) //prevent from potential simultaneous executing of Run or Stop command.
                {
                    var currentActionGenerator = GetGeneratorToPerformAnActionAndStoreNewOne();
                    if (currentActionGenerator == null)
                        return;
                    currentActionGenerator.PerformAction(random);
                    currentModelTime = simulationActions.Top.Key.Time;
                    OnModelTimeChanged();
                    OnSingleSimulationActionPerformed();
                }
            }).Start();
        }

        /// <summary>
        /// Method is run by timer. The token of last used action is checked for case that 
        /// pause/stop/restart was pressed. The current simulation action is performed and before it
        /// the timer for next simulation action is set.        
        /// </summary>
        /// <param name="state">State containing token in the tick.</param>
        private void Tick(object state)
        {
            lock (eventsLock)
            {
                if ((int)state != validToken || simulationActions.Count == 0)
                {
                    return;
                }
                if (IsModelTimeToPauseReached || simulationActions.Top.Value.IsPauseScheduled)
                {
                    Pause();
                    OnSimulationPaused();
                    return;
                }
                var currentActionGenerator = GetGeneratorToPerformAnActionAndStoreNewOne();                
                StartOrResumeTimer();
                OnModelTimeChanged();
                currentActionGenerator.PerformAction(random);
            }
        }

        /// <summary>
        /// When simulation is continued or stopped after reaching the model time to pause
        /// the model time to pause is reset.
        /// </summary>
        private void ResetModelTimeToPauseIfReached()
        {
            if (IsModelTimeToPauseReached)
            {
                ResetModelTimeToPause();
            }
        }

        /// <summary>
        /// Reset model time to reset.
        /// </summary>
        private void ResetModelTimeToPause()
        {
            modelTimeToPause = TimeSpan.MaxValue;
        }

        /// <summary>
        /// Update times based on current simulation speed.
        /// </summary>
        private void UpdateTimes()
        {
            if ((isMaxSimulationSpeed || IsModelTimeToPauseReached) && simulationActions.Count > 0)
                currentModelTime = simulationActions.Top.Key.Time;
            else
                currentModelTime += (DateTime.Now - dateTimeOfLastAction).Multiply(simulationSpeed);
        }

        /// <summary>
        /// Pasue timer by increasing valid token and updating times.
        /// </summary>
        private void PauseTimer()
        {
            UpdateTimes();
            OnModelTimeChanged();
            validToken++;
        }

        /// <summary>
        /// Stop timer by increasing valid token and resetting current model time.
        /// </summary>
        private void StopTimer()
        {
            currentModelTime = TimeSpan.Zero;
            OnModelTimeChanged();
            validToken++;
        }

        /// <summary>
        /// If there is any listening client the event about changing the model time is fired.
        /// </summary>
        private void OnModelTimeChanged()
        {
            if (ModelTimeChanged != null)
                ModelTimeChanged(this, EventArgs.Empty);
        }

        /// <summary>
        /// If there is any listening client the event about pause applied by the simulation control
        /// is fired.
        /// </summary>
        private void OnSimulationPaused()
        {
            if (SimulationPaused != null)
                SimulationPaused(this, EventArgs.Empty);
        }

        /// <summary>
        /// If there is any listening client the event about finishing of simulation action is fired.
        /// </summary>
        private void OnSingleSimulationActionPerformed()
        {
            if (SingleSimulationActionPerformed != null)
                SingleSimulationActionPerformed(this, EventArgs.Empty);
        }

        /// <summary>
        /// Fires simulation speed change event.
        /// </summary>
        /// <param name="oldSimulationSpeed">Old simulation speed.</param>
        /// <param name="newSimulationSpeed">New simulation speed.</param>
        private void OnSimulationSpeedChange(double oldSimulationSpeed, double newSimulationSpeed)
        {
            if (SimulationSpeedChanged != null)
            {
                SimulationSpeedChanged(this, new SimulationSpeedChangeEventArgs()
                {
                    OldSimulationSpeed = oldSimulationSpeed,
                    NewSimulationSpeed = newSimulationSpeed
                });
            }
        }

        /// <summary>
        /// Fires an event informing about activation/deactivation of maximum simulation speed.
        /// </summary>
        private void OnIsMaxSimulationSpeedChange()
        {
            if (IsMaxSimulationSpeedChange != null)
                IsMaxSimulationSpeedChange(this, EventArgs.Empty);
        }

        /// <summary>
        /// Dispose disposable fields.
        /// </summary>
        /// <param name="disposing">Flag indicating whether to release managed resources.</param>
        protected virtual void Dispose(bool disposing)
        {
            if (disposing)
            {
                if (timer != null)
                {
                    timer.Dispose();
                    timer = null;
                }
            }
        }

        /// <summary>
        /// Disposes cellular topology resources.
        /// </summary>
        public void Dispose()
        {
            Dispose(true);
            GC.SuppressFinalize(this);
        }
    }
}