using System.Text.Json;
using System.Text.Json.Serialization;

namespace TrafficLights
{
    public class TrafficLightSimulatorService : IHostedService, IDisposable
    {
        const long TICKS_PER_SECOND = 10000000;
        const int UPDATES_PER_SIMULATED_SECOND = 4;

        public class State
        {
            [JsonPropertyName("duration")]
            public int Duration { get; set; } = -1;
            [JsonPropertyName("north")]
            public string North { get; set; } = "";
            [JsonPropertyName("south")]
            public string South { get; set; } = "";
            [JsonPropertyName("east")]
            public string East { get; set; } = "";
            [JsonPropertyName("west")]
            public string West { get; set; } = "";
            [JsonPropertyName("north-right")]
            public string NorthRight { get; set; } = "";
        }

        public class Period
        {
            [JsonPropertyName("name")]
            public string Name { get; set; } = "";
            [JsonPropertyName("verbose-name")]
            public string VerboseName { get; set; } = "";
            [JsonPropertyName("timestart")]
            public string TimeStartString { get; set; } = "";
            public TimeOnly TimeStart;
            [JsonPropertyName("states")]
            public List<State> States { get; set; } = new List<State>();
        }
        public class Status
        {
            [JsonPropertyName("currentPeriod")]
            public Period CurrentPeriod {  get; set; }
            [JsonPropertyName("currentState")]
            public State CurrentState { get; set; }
            [JsonPropertyName("currentStateIndex")]
            public int CurrentStateIndex {  get; set; }
            [JsonPropertyName("secondsUntilChangeover")]
            public double SecondsUntilChangeover { get; set; }
            [JsonPropertyName("simulationTime")]
            public string SimulationTime {  get; set; }

            public Status(Period currentPeriod, State currentState, int currentStateIndex, double secondsUntilChangeover, TimeOnly simulationTime)
            {
                CurrentPeriod = currentPeriod;
                CurrentState = currentState;
                CurrentStateIndex = currentStateIndex;
                SecondsUntilChangeover = secondsUntilChangeover;
                SimulationTime = simulationTime.ToLongTimeString();
            }
        }

        private readonly ILogger<TrafficLightSimulatorService> _logger;
        private IConfiguration _configuration;
        private Timer? _timer = null;

        private List<Period>? _periodData;
        private TimeOnly _startSimulationTime;
        private TimeSpan _runTimeSpan;
        private double _timeFactor;
        private long _startTicks;
        private int _currentPeriodIndex;
        private int _currentStateIndex;
        private long _nextChangeoverTicks;
        private DateTime _startSimulationDateTime;
        private DateTime _currentSimulationDateTime;

        private Mutex _mut = new Mutex();

        public TrafficLightSimulatorService(ILogger<TrafficLightSimulatorService> logger, IConfiguration configuration)
        {
            _logger = logger;
            _configuration = configuration;
        }

        private int _getPeriodIndex(TimeOnly testTime)
        {
            if (_periodData is null)
            {
                return -1;
            }
            for (int index = 0; index < _periodData.Count;  index++)
            {
                if (index == _periodData.Count-1)
                {
                    return index;
                }
                if (testTime >= _periodData[index].TimeStart && testTime < _periodData[index+1].TimeStart)
                {
                    return index;
                }
            }
            // Technically shouldn't be necessary
            return _periodData.Count - 1;
        }

        public Task StartAsync(CancellationToken stoppingToken)
        {
            bool parseSuccess = false;

            _logger.LogInformation("Traffic Light Simulator Service Running.");

            var periodConfigurationFilename = _configuration.GetValue<string>("PeriodConfigurationFile");
            _logger.LogInformation(string.Format("Using periods configuration file: '{0}'", periodConfigurationFilename));

            var periodConfigurationStream = File.OpenRead(periodConfigurationFilename);

            _periodData = JsonSerializer.Deserialize<List<Period>>(periodConfigurationStream);

            if (_periodData is null)
            {
                _logger.LogError("Error loading period data - return value is null.");
                return Task.CompletedTask;
            }

            _logger.LogInformation(_periodData.GetType().ToString());

            foreach (Period period in _periodData)
            {
                parseSuccess = TimeOnly.TryParse(period.TimeStartString, out period.TimeStart);
                _logger.LogInformation(string.Format("Period found with name: '{0}', timestart: '{1}', parsed timestart: {2}, parse success: {3}", period.Name, period.TimeStartString, period.TimeStart.ToString(), parseSuccess));
                foreach (State state in period.States)
                {
                    // _logger.LogInformation(string.Format("  state with duration: {0}", state.Duration));
                }
            }

            // From configuration: 
            parseSuccess = TimeOnly.TryParse(_configuration.GetValue<string>("StartTime"), out _startSimulationTime);
            _runTimeSpan = new TimeSpan(_configuration.GetValue<long>("RunTime") * TICKS_PER_SECOND);
            _timeFactor = _configuration.GetValue<double>("TimeFactor");

            DateTime now = DateTime.UtcNow;
            _startTicks = now.Ticks;

            _startSimulationDateTime = new DateTime(1900, 1, 1, _startSimulationTime.Hour, _startSimulationTime.Minute, _startSimulationTime.Second);
            _currentSimulationDateTime = _startSimulationDateTime;

            _currentPeriodIndex = _getPeriodIndex(TimeOnly.FromDateTime(_startSimulationDateTime));
            _currentStateIndex = 0;

            _nextChangeoverTicks = _startTicks + (long)((_periodData[_currentPeriodIndex].States[_currentStateIndex].Duration * TICKS_PER_SECOND) / _timeFactor);

            _logger.LogInformation(string.Format("Start period: {0}", _periodData[_currentPeriodIndex].Name));
            _logger.LogInformation(string.Format("Start ticks: {0}", _startTicks));
            _logger.LogInformation(string.Format("Next changeover ticks: {0}", _nextChangeoverTicks));
            _logger.LogInformation(string.Format("Next changeover second(s): {0}", TimeSpan.FromTicks(_nextChangeoverTicks - _startTicks).ToString()));
            _logger.LogInformation(string.Format("Start simulation DT: {0}", _startSimulationDateTime.ToString()));

            _timer = new Timer(DoWork, null, 0, (int)((1000 / UPDATES_PER_SIMULATED_SECOND) / _timeFactor)); // Value is in milliseconds

            return Task.CompletedTask;
        }

        private void DoWork(object? state)
        {
            if (_periodData is null)
            {
                return;
            }

            _mut.WaitOne();

            DateTime now = DateTime.UtcNow;
            long nowTicks = now.Ticks;

            _currentSimulationDateTime = _startSimulationDateTime + (TimeSpan.FromTicks(nowTicks - _startTicks) * _timeFactor);

            if (nowTicks >= _nextChangeoverTicks)
            {
                int timedPeriodIndex = _getPeriodIndex(TimeOnly.FromDateTime(_currentSimulationDateTime));

                if (timedPeriodIndex != _currentPeriodIndex && _currentStateIndex == _periodData[_currentPeriodIndex].States.Count - 1)
                {
                    _logger.LogInformation(string.Format("{0} : {1} : Changing period from {2} to {3}", TimeOnly.FromDateTime(_currentSimulationDateTime).ToString(), _periodData[_currentPeriodIndex].Name, _currentPeriodIndex, timedPeriodIndex));
                    // Current sequence complete, therefore safe to change periods
                    _currentPeriodIndex = timedPeriodIndex;
                    _currentStateIndex = 0;
                }
                else
                {
                    int nextStateIndex = (_currentStateIndex + 1) % _periodData[_currentPeriodIndex].States.Count;
                    _logger.LogInformation(string.Format("{0} : {1} : Changing state from {2} to {3}", TimeOnly.FromDateTime(_currentSimulationDateTime).ToString(), _periodData[_currentPeriodIndex].Name, _currentStateIndex, nextStateIndex));
                    _currentStateIndex = nextStateIndex;
                }
                _nextChangeoverTicks = nowTicks + (long)((_periodData[_currentPeriodIndex].States[_currentStateIndex].Duration * TICKS_PER_SECOND) / _timeFactor);
                _logger.LogInformation(string.Format("  Next changeover in {0} simulation second(s)", _periodData[_currentPeriodIndex].States[_currentStateIndex].Duration));
            }

            _mut.ReleaseMutex();
        }

        public Status GetStatus()
        {
            _mut.WaitOne();
            var currentStatus = new Status(
                _periodData[_currentPeriodIndex], 
                _periodData[_currentPeriodIndex].States[_currentStateIndex], 
                _currentStateIndex, 
                (_nextChangeoverTicks - DateTime.UtcNow.Ticks) / (double)TICKS_PER_SECOND,
                TimeOnly.FromDateTime(_currentSimulationDateTime));
            _mut.ReleaseMutex();
            return currentStatus;
        }

        public Task StopAsync(CancellationToken stoppingToken)
        {
            _logger.LogInformation("Traffic Light Simulator Service is stopping.");

            _timer?.Change(Timeout.Infinite, 0);

            return Task.CompletedTask;
        }

        public void Dispose() 
        {
            _timer?.Dispose();
        }
    }
}
