/********************************************
*           Program.cs                      *
********************************************/
/*******************************************************************************************************************************************************
*    All Project Files Copyright © 2025, 2026 by The ep5 Educational Broadcasting Foundation                                                                 *
*                                                                                                                                                      *
*    Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met:    *
*                                                                                                                                                      *
*        →  Redistributions of source code must retain the above copyright notice, this list of conditions, and the following disclaimer:              *
*        →  Redistributions in binary form must reproduce the above copyright notice, this list of conditions, and the following disclaimer in the     *
*           documentation and/or other materials provided with the distribution.                                                                       *
*        →  Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote products derived from this    *
*           software without specific prior written permission.                                                                                        *
*                                                                                                                                                      *
*    THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS “AS IS” AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED     *
*    TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO  EVENT SHALL THE COPYRIGHT HOLDER OR     *
*    CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,         *
*    PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF         *
*    LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT, INCLUDING NEGLIGENCE OR OTHERWISE, ARISING IN ANY WAY OUT OF THE USE OF THIS           *
*    SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.                                                                                      *
*******************************************************************************************************************************************************/

//  Written/updated 24 July 2026 by David Fisher
//  Copyright © 2025, 2026 by The ep5 Educational Broadcasting Foundation; all rights reserved

//  NOTE: THE EXECUTION TIME FOR EACH EVENT IS SET AT THE EARLY BOUND TIME; LATER BOUND TIME IS NOT YET USED IN THIS VERSION. CREATE A FUNCTION TO
//  GENERATE A RANDOM TIME VALUE BETWEEN THESE BOUNDS, AT RUN TIME. THIS MUST OCCUR WHEN THE PROGRAM IS RUNNING, AS THE VALUE MUST REMAIN UNKNOWN.
//  THIS WILL PREVENT PREDICTABLE OPERATION WHEN THE PROGRAM IS STARTED AT THE SAME TIME EACH DAY. IF A FIXED TASK EXECUTION TIME IS REQUIRED, THEN
//  SET BOTH BOUNDS TO THAT VALUE.
//  
//  TO BE DONE: PROVIDE MEANS OF TYING EXECUTION TIME TO SUNRISE-SUNSET TABLE IN DATABASE;
//              ADD SERILOG OUTPUT FOR DEBUGGING PURPOSES;
//              ADD XML COMMENTS TO ALL METHODS AND CLASSES;
//              MAKE DATA DICTIONARY TO GIVE HUMAN-SIGNIFICANT NAMES TO DIGITAL I/O CHANNELS;
//              IMPROVE ERROR HANDLING THROUGHOUT PROGRAM.


using Microsoft.Data.SqlClient;
using Modbus.Device;
using Serilog;
using Serilog.Events;
using Serilog.Formatting.Json;
using System.Diagnostics;
using System.Net.Sockets;
using static System.Console;

namespace TaskScheduler

{
    /// <summary>
    ///     This program reads a task schedule from a SQL Server database table and executes the tasks by controlling digital outputs while also monitoring digital inputs.
    ///     The control logic is expressed in this module rather than in a separate BusinessRules module, as the logic is simple, straightforward, and specific to the task scheduling function.
    /// </summary>
    internal class Program
    {
        /// <summary>
        ///     The Task struct defines the structure of a scheduled task, including its recipe ID, channel number, day of the week, time bounds, digital value, notes, and status.
        ///     It contains all the data required to define and execute a single task in the schedule.
        /// </summary>
        /// <remarks name="channelNmbr">For the testbed hardware configuration, there are 32 separate digital outputs, identified as 0 through 31</remarks>
        /// <remarks name="doDayOfWeek">0 = all seven days; 1 = Sunday, 7 = Saturday</remarks>
        /// <remarks name="earlyBound">The earliest time of day that the task may be executed</remarks>
        /// <remarks name="laterBound">The latest time of day that the task may be executed.</remarks>remarks?
        /// <remarks name="usage">If both bounds are the same, the task is to be executed at that specific time; otherwise, the task is to be executed at a random time between the two bounds.
        ///     Determination of the random value occurs at run-time.
        /// </remarks>
        /// <remarks name="digitalValue">0 = turn output OFF; 1 = turn output ON</remarks>
        /// <remarks name="notes">Any notes associated with the task; can be left blank (null).</remarks>
        /// <remarks name="status">1 = active and used; 0 = active but not executed</remarks>
        public struct Task
        {
            public int recipeID;
            public int channelNmbr;
            public byte doDayOfWeek;
            public TimeOnly earlyBound;
            public TimeOnly laterBound;
            public byte digitalValue;
            public string notes;
            public byte status;
        }

        public static List<Task> eventsList = [];
        public static Task eventRecord;
        public static int recipeNmbr;

        static void Main(string[] args)
        {
            Log.Logger = new LoggerConfiguration()
                            // add console as logging target
                            .WriteTo.Console()
                            // add a logging target for warnings and higher severity  logs
                            // structured in JSON format
                            .WriteTo.File(new JsonFormatter(), "important.json", restrictedToMinimumLevel: LogEventLevel.Warning)
                            // add a rolling file for all logs
                            .WriteTo.File("all-.logs", rollingInterval: RollingInterval.Day)
                            // set default minimum level
                            .MinimumLevel.Debug()
                            .CreateLogger();

            int counter = 0;
            //int result = 0;
            //bool action;
            string stepState;
            string currentStatus;

            // Provide a recipe containing time-correct event data to work with
            Process ExternalProcess = new();
            ExternalProcess.StartInfo.FileName = @"P:\TableFill - WEEKLY\bin\Debug\net10.0-windows10.0.17763.0\TableFill.exe";
            ExternalProcess.StartInfo.WindowStyle = ProcessWindowStyle.Hidden;
            ExternalProcess.Start();
            ExternalProcess.WaitForExit();

            recipeNmbr = Convert.ToInt16(args[0]);
            TcpClient client = CreateTcpClient(Data.ipAddress, Data.port);                  // NModbus4 initialization
            ModbusIpMaster modbusMaster = ModbusIpMaster.CreateIp(client);
            MakeHardStop.HardStop(modbusMaster, Data.coilOutputStartAddress);               // Ensure clean start with all outputs OFF
            //WriteLine("Created Modbus master.");
            Data.numberOfDigitalInputs = (ushort)MachineState.digitalInputChannels.Length;
            Data.numberOfDigitalOutputs = (ushort)MachineState.digitalOutputChannels.Length;
            // Read recipe data from SQL Server and close connection after storing in List<T>
            SqlConnection connection = new("Server = .; Database = ep5BAStest; Integrated Security = SSPI; TrustServerCertificate = true");
            connection.Open();
            using SqlCommand command = new($"select * from dbo.ControlEvent where recipeID = {recipeNmbr} order by doDayOfWeek, earlyBound", connection);
            SqlDataReader dataReader = command.ExecuteReader();
            while (dataReader.Read())
            {
                eventRecord.recipeID = (int)dataReader["recipeID"];
                eventRecord.doDayOfWeek = (byte)dataReader["doDayOfWeek"];
                eventRecord.earlyBound = TimeOnly.FromTimeSpan((TimeSpan)dataReader["earlyBound"]);
                eventRecord.laterBound = TimeOnly.FromTimeSpan((TimeSpan)dataReader["laterBound"]);
                eventRecord.channelNmbr = (int)dataReader["channelNmbr"];
                eventRecord.digitalValue = (byte)dataReader["digitalValue"];
                eventRecord.status = (byte)dataReader["status"];
                eventRecord.notes = (string)dataReader["notes"];
                if (eventRecord.digitalValue == 1)
                    stepState = "ON";
                else
                    stepState = "OFF";
                if (eventRecord.status == 1)
                    currentStatus = "active";
                else
                    currentStatus = "inactive";
                WriteLine("do at {0}\tchannel {1}\t{2}\t{3}\t{4}", eventRecord.earlyBound.ToString("hh:mm:ss.fff"), eventRecord.channelNmbr, stepState, currentStatus, eventRecord.notes);
                counter++;
                eventsList.Add(eventRecord);
            }
            dataReader.Close();
            connection.Close();
            if (eventsList.Count == counter)
                WriteLine($"\nThis recipe contains {counter} steps.");
            else
            {
                WriteLine("Discrepancy between List<T> count and record counter!");         // Nominal error trap; does nothing beyond notification
                WriteLine($"List length is {eventsList.Count} and counter is {counter}.");
                WriteLine("Press the <ANY> key to continue...");
                _ = ReadKey(true);
            }
            // Run main event loop
            int indx = 0;
            while (indx < counter)
            { 
                // If operator quits before completion of task list
                if (KeyAvailable)
                {
                    char c = ReadKey(true).KeyChar;
                    if (c is 'q' or 'Q' or (char)ConsoleKey.Escape)
                    {
                        MakeHardStop.HardStop(modbusMaster, Data.coilOutputStartAddress);
                        break;
                    }
                }
                // Read the inputs to keep the connection alive
                IOread.ReadAllDigitalInputs(modbusMaster);
                // Wait for the next event to trigger
                if (TimeOnly.FromDateTime(DateTime.Now) > eventsList[indx].earlyBound)
                {
                    if (eventsList[indx].digitalValue == 1)
                        MachineState.digitalOutputChannels[eventsList[indx].channelNmbr] = true;
                    else
                        MachineState.digitalOutputChannels[eventsList[indx].channelNmbr] = false;
                    indx++;
                }
                // Write all output values
                IOwrite.WriteAllDigitalOutputs(modbusMaster);
            }
            WriteLine("\n\nRecipe has been completed. Program terminates here. See ya...\n\n");
            MakeHardStop.HardStop(modbusMaster, Data.coilOutputStartAddress);
            return;
        }

        /// <summary>
        ///     This function creates and returns a TCP client connected to the specified IP address and port.
        /// </summary>
        /// <param name="ipAddress">The IP address is the fixed location in dotted-quad format of the fieldbus coupler</param>
        /// <param name="port">The port ID is the fixed port spec that is used for Modbus communication</param>
        /// <returns>The "TcpClient" returned by this function is used when setting up the Modbus master</returns>
        private static TcpClient CreateTcpClient(string ipAddress, int port)
        {
            // Simple attempt at debugging network issues
            try
            {
                return new TcpClient(Data.ipAddress, Data.port);
            }
            catch (Exception ex)
            {
                WriteLine($"Error creating TCP client: {ex.Message}");
                throw;                                                                      // Rethrow to handle it in the Main method
            }
        }
    }
}