/// <summary>
 /// Initializes a new instance of the <see cref="FeedParameters"/> class.
 /// </summary>
 /// <param name="lastGpsDataToken">The latest <see cref="LogRecord" /> token</param>
 /// <param name="lastStatusDataToken">The latest <see cref="StatusData" /> token</param>
 /// <param name="lastFaultDataToken">The latest <see cref="FaultData" /> token</param>
 /// <param name="feedStartOption">The <see cref="Common.FeedStartOption" /> to use.</param>
 /// <param name="feedStartSpecificTimeUTC">If <paramref name="feedStartOption"/> is set to <see cref="Common.FeedStartOption.SpecificTime" />, the date and time at which to start the data feeds.</param>
 public FeedParameters(long?lastGpsDataToken, long?lastStatusDataToken, long?lastFaultDataToken, Common.FeedStartOption feedStartOption, DateTime?feedStartSpecificTimeUTC = null)
 {
     LastGpsDataToken    = lastGpsDataToken;
     LastStatusDataToken = lastStatusDataToken;
     LastFaultDataToken  = lastFaultDataToken;
     FeedStartOption     = feedStartOption;
     if (feedStartSpecificTimeUTC == null)
     {
         FeedStartSpecificTimeUTC = DateTime.MinValue;
     }
     else if (feedStartSpecificTimeUTC > DateTime.UtcNow)
     {
         ConsoleUtility.LogWarning($"The value of '{feedStartSpecificTimeUTC}' specified for FeedStartSpecificTimeUTC is in the future. As such, the current date and time will be used instead.");
         FeedStartSpecificTimeUTC = DateTime.UtcNow;
     }
     else
     {
         FeedStartSpecificTimeUTC = (DateTime)feedStartSpecificTimeUTC;
     }
 }
        static async Task Main()
        {
            IList <ConfigItem> configItems;
            bool           trackSpecificVehicles = false;
            IList <Device> devicesToTrack        = null;

            Common.FeedStartOption feedStartOption         = Common.FeedStartOption.CurrentTime;
            DateTime              feedStartSpecificTimeUTC = DateTime.MinValue;
            int                   feedIntervalSeconds      = 60;
            IList <Diagnostic>    diagnosticsToTrack       = null;
            string                parentOutputFolder;
            string                outputFolder;
            int                   maximumFileSizeBytes = 1024000;
            GeotabDataOnlyPlanAPI api;

            string username = "";
            string password = "";
            string server   = "my.geotab.com";
            string database = "";
            string title    = "";

            try
            {
                // Set title.
                title         = AppDomain.CurrentDomain.FriendlyName.Replace(".", " ");
                Console.Title = title;
                ConsoleUtility.LogUtilityStartup(title);

                // Request MyGeotab credentials and database name.
                server   = ConsoleUtility.GetUserInput($"MyGeotab server");
                database = ConsoleUtility.GetUserInput($"Database to run examples against.").ToLower();
                username = ConsoleUtility.GetUserInput($"MyGeotab username");
                password = ConsoleUtility.GetUserInputMasked($"MyGeotab password");

                // Create Geotab Data-Only Plan API instance and authenticate.
                api = new GeotabDataOnlyPlanAPI(server, database, username, password);
                ConsoleUtility.LogInfoStart("Authenticating...");
                await api.AuthenticateAsync();

                ConsoleUtility.LogOk();

                // Load configuration information from the config file.
                configItems = GetConfigItems("configuration");

                // Validate output folder and create subfolder for output files.
                parentOutputFolder = configItems.Where(configItem => configItem.Key == ArgNameOutputFolder).FirstOrDefault().Value;
                if (!Directory.Exists(parentOutputFolder))
                {
                    throw new ArgumentException($"The specified output folder, '{parentOutputFolder}', does not exist.");
                }
                DirectoryInfo directoryInfo = new DirectoryInfo(parentOutputFolder);
                string        subfolderName = $"Output_{DateTime.Now.ToString("yyyyMMdd_HHmmss")}";
                directoryInfo.CreateSubdirectory(subfolderName);
                outputFolder = Path.Combine(directoryInfo.FullName, subfolderName);

                // Validate and set maximum file size.
                string maxFileSizeMBString = configItems.Where(configItem => configItem.Key == ArgNameMaximumFileSizeMB).FirstOrDefault().Value;
                if (int.TryParse(maxFileSizeMBString, out int maxFileSizeMB))
                {
                    if (maxFileSizeMB > 0)
                    {
                        maximumFileSizeBytes = maxFileSizeMB * Common.MegabyteToByteMultiplier;
                    }
                }

                // Get the vehicle tracking option.
                string vehicleTrackingOption = configItems.Where(configItem => configItem.Key == ArgNameVehicleTrackingOption).FirstOrDefault().Value;
                switch (vehicleTrackingOption)
                {
                case nameof(Common.VehicleTrackingOption.Reporting):
                    trackSpecificVehicles = false;
                    break;

                case nameof(Common.VehicleTrackingOption.Specific):
                    trackSpecificVehicles = true;
                    break;

                default:
                    break;
                }

                // If specific vehicles are to be tracked, validate the supplied list of device IDs against the current database.  Discard any supplied items that are not valid device IDs.  If no valid device IDs are supplied, switch back to tracking vehicles that are currently reporting data.
                if (trackSpecificVehicles == true)
                {
                    ConsoleUtility.LogInfo("Validating SpecificVehiclesToTrack...");
                    string   vehicleListSupplied = configItems.Where(configItem => configItem.Key == ArgNameSpecificVehiclesToTrack).FirstOrDefault().Value;
                    string[] vehicleList         = vehicleListSupplied.Split(",");
                    devicesToTrack = new List <Device>();
                    IList <Device> devices = await GetAllDevicesAsync(api);

                    for (int vehicleListIndex = 0; vehicleListIndex < vehicleList.Length; vehicleListIndex++)
                    {
                        string vehicleDeviceId = vehicleList[vehicleListIndex];
                        Device checkedDevice   = devices.Where(device => device.Id.ToString() == vehicleDeviceId).FirstOrDefault();
                        if (checkedDevice == null)
                        {
                            ConsoleUtility.LogListItem($"Note - The following is not a valid device Id", $"{vehicleDeviceId}", Common.ConsoleColorForUnchangedData, Common.ConsoleColorForErrors);
                            continue;
                        }
                        devicesToTrack.Add(checkedDevice);
                    }
                    if (devicesToTrack.Count == 0)
                    {
                        ConsoleUtility.LogWarning($"No valid device IDs have been entered. Switching to tracking of vehicles that are currently reporting data.");
                        trackSpecificVehicles = false;
                    }
                }

                // Get diagnostics to be tracked.
                ConsoleUtility.LogInfo("Validating DiagnosticsToTrack...");
                string   diagnosticListSupplied = configItems.Where(configItem => configItem.Key == ArgNameDiagnosticsToTrack).FirstOrDefault().Value;
                string[] diagnosticList         = diagnosticListSupplied.Split(",");
                diagnosticsToTrack = new List <Diagnostic>();
                IList <Diagnostic> diagnostics = await GetAllDiagnosticsAsync(api);

                for (int diagnosticListIndex = 0; diagnosticListIndex < diagnosticList.Length; diagnosticListIndex++)
                {
                    string     diagnosticId      = diagnosticList[diagnosticListIndex];
                    Diagnostic checkedDiagnostic = diagnostics.Where(diagnostic => diagnostic.Id.ToString() == diagnosticId).FirstOrDefault();
                    if (checkedDiagnostic == null)
                    {
                        ConsoleUtility.LogListItem($"Note - The following is not a valid diagnostic Id", $"{diagnosticId}", Common.ConsoleColorForUnchangedData, Common.ConsoleColorForErrors);
                        continue;
                    }
                    diagnosticsToTrack.Add(checkedDiagnostic);
                }
                if (diagnosticsToTrack.Count == 0)
                {
                    ConsoleUtility.LogWarning($"No valid diagnostic IDs have been entered. As such, no diagnostics will be tracked.");
                }

                // Get the feed start option.
                string feedStartOptionString = configItems.Where(configItem => configItem.Key == ArgNameFeedStartOption).FirstOrDefault().Value;
                switch (feedStartOptionString)
                {
                case nameof(Common.FeedStartOption.CurrentTime):
                    feedStartOption = Common.FeedStartOption.CurrentTime;
                    break;

                case nameof(Common.FeedStartOption.FeedResultToken):
                    feedStartOption = Common.FeedStartOption.FeedResultToken;
                    break;

                case nameof(Common.FeedStartOption.SpecificTime):
                    string feedStartSpecificTimeUTCString = configItems.Where(configItem => configItem.Key == ArgNameFeedStartSpecificTimeUTC).FirstOrDefault().Value;
                    if (DateTime.TryParse(feedStartSpecificTimeUTCString, null, System.Globalization.DateTimeStyles.RoundtripKind, out feedStartSpecificTimeUTC) == false)
                    {
                        ConsoleUtility.LogWarning($"The value of '{feedStartSpecificTimeUTCString}' specified for FeedStartSpecificTimeUTC is invalid. As such, the current date and time will be used instead.");
                        feedStartOption = Common.FeedStartOption.CurrentTime;
                    }
                    else
                    {
                        feedStartOption = Common.FeedStartOption.SpecificTime;
                    }
                    break;

                default:
                    break;
                }

                // Get the feed interval.
                string feedIntervalSecondsString = configItems.Where(configItem => configItem.Key == ArgNameFeedIntervalSeconds).FirstOrDefault().Value;
                if (int.TryParse(feedIntervalSecondsString, out int feedIntervalSecondsInt))
                {
                    if (feedIntervalSecondsInt < ShortestAllowedFeedIntervalSeconds)
                    {
                        ConsoleUtility.LogListItem($"Note - The specified FeedIntervalSeconds value of '{feedIntervalSecondsString}' is less then the shortest allowed value of '{ShortestAllowedFeedIntervalSeconds.ToString()}'.  FeedIntervalSeconds will be set to:", ShortestAllowedFeedIntervalSeconds.ToString(), Common.ConsoleColorForUnchangedData, Common.ConsoleColorForErrors);
                        feedIntervalSeconds = ShortestAllowedFeedIntervalSeconds;
                    }
                    else
                    {
                        feedIntervalSeconds = feedIntervalSecondsInt;
                    }
                }
                else
                {
                    ConsoleUtility.LogListItem($"Note - The specified FeedIntervalSeconds value of '{feedIntervalSecondsString}' is invalid.  FeedIntervalSeconds will be set to:", ShortestAllowedFeedIntervalSeconds.ToString(), Common.ConsoleColorForUnchangedData, Common.ConsoleColorForErrors);
                    feedIntervalSeconds = ShortestAllowedFeedIntervalSeconds;
                }

                // Instantiate a DatabaseWorker to start processing the data feeds.
                bool   continuous        = true;
                Worker worker            = new DatabaseWorker(username, password, database, server, parentOutputFolder, outputFolder, maximumFileSizeBytes, feedIntervalSeconds, feedStartOption, feedStartSpecificTimeUTC, trackSpecificVehicles, devicesToTrack, diagnosticsToTrack);
                var    cancellationToken = new CancellationTokenSource();
                Task   task = Task.Run(async() => await worker.DoWorkAsync(continuous), cancellationToken.Token);
                if (continuous && Console.ReadLine() != null)
                {
                    worker.RequestStop();
                    cancellationToken.Cancel();
                }
            }
            catch (Exception ex)
            {
                ConsoleUtility.LogError(ex);
            }
            finally
            {
                ConsoleUtility.LogUtilityShutdown(title);
                Console.ReadKey();
            }
        }
        /// <summary>
        /// Initializes a new instance of the <see cref="DatabaseWorker"/> class.
        /// </summary>
        /// <param name="user">The user.</param>
        /// <param name="password">The password.</param>
        /// <param name="database">The database.</param>
        /// <param name="server">The server.</param>
        /// <param name="dataFeedTokenFolder">The folder where data feed token files are to be written.</param>
        /// <param name="outputFolder">The folder where output files are to be written.</param>
        /// <param name="maximumFileSizeInBytes">The maximum size, in bytes, that a file may reach before a new file is started.</param>
        /// <param name="feedIntervalSeconds">The number of seconds to wait, after processing a batch of feed results, before executing the next iteration of GetFeed() calls.</param>
        /// <param name="feedStartOption">The <see cref="Common.FeedStartOption" /> to use.</param>
        /// <param name="feedStartSpecificTimeUTC">If <paramref name="feedStartOption"/> is set to <see cref="Common.FeedStartOption.SpecificTime"/>, the date and time at which to start the data feeds.</param>
        /// <param name="trackSpecificVehicles">Whether to track specific vehicles or vehicles that are reporting data.</param>
        /// <param name="devicesToTrack">If <paramref name="trackSpecificVehicles"/> is <c>true</c>, the list of <see cref="Device"/>s to track.</param>
        /// <param name="diagnosticsToTrack">The <see cref="Diagnostic"/>s that are to be tracked.</param>
        public DatabaseWorker(string user, string password, string database, string server, string dataFeedTokenFolder, string outputFolder, long maximumFileSizeInBytes, int feedIntervalSeconds, Common.FeedStartOption feedStartOption, DateTime?feedStartSpecificTimeUTC = null, bool trackSpecificVehicles = false, IList <Device> devicesToTrack = null, IList <Diagnostic> diagnosticsToTrack = null)
            : base()
        {
            // Validate input.
            if (trackSpecificVehicles == true && (devicesToTrack == null || devicesToTrack.Count == 0))
            {
                throw new ArgumentException($"'trackSpecificVehicles' is set to 'true', but 'devicesToTrack' is null or empty.");
            }

            DataFeedTokenFolder    = dataFeedTokenFolder;
            OutputFolder           = outputFolder;
            MaximumFileSizeInBytes = maximumFileSizeInBytes;
            TrackSpecificVehicles  = trackSpecificVehicles;
            DevicesToTrack         = (List <Device>)devicesToTrack;
            DiagnosticsToTrack     = (List <Diagnostic>)diagnosticsToTrack;

            // Determine whether to use StatusData and/or FaultData feeds based on the diagnostics, if any, that are to be tracked.
            if (DiagnosticsToTrack != null && DiagnosticsToTrack.Count > 0)
            {
                if (DiagnosticsToTrack.Where(diagnosticToTrack => diagnosticToTrack.DiagnosticType == DiagnosticType.Sid || diagnosticToTrack.DiagnosticType == DiagnosticType.Pid || diagnosticToTrack.DiagnosticType == DiagnosticType.SuspectParameter || diagnosticToTrack.DiagnosticType == DiagnosticType.ObdFault || diagnosticToTrack.DiagnosticType == DiagnosticType.GoFault || diagnosticToTrack.DiagnosticType == DiagnosticType.ObdWwhFault || diagnosticToTrack.DiagnosticType == DiagnosticType.ProprietaryFault || diagnosticToTrack.DiagnosticType == DiagnosticType.LegacyFault).Any())
                {
                    useFaultDataFeed = true;
                }
                if (DiagnosticsToTrack.Where(diagnosticToTrack => diagnosticToTrack.DiagnosticType == DiagnosticType.GoDiagnostic || diagnosticToTrack.DiagnosticType == DiagnosticType.DataDiagnostic).Any())
                {
                    useStatusDataFeed = true;
                }
            }

            // Build token file paths.
            faultDataTokenFilePath  = Path.Combine(DataFeedTokenFolder, FaultDataTokenFilename);
            gpsTokenFilePath        = Path.Combine(DataFeedTokenFolder, GpsTokenFilename);
            statusDataTokenFilePath = Path.Combine(DataFeedTokenFolder, StatusDataTokenFilename);

            // If feeds are to be started based on feed result token, read previously-written token values from their respective files.
            long faultDataToken  = 0;
            long gpsToken        = 0;
            long statusDataToken = 0;

            if (feedStartOption == Common.FeedStartOption.FeedResultToken)
            {
                if (File.Exists(faultDataTokenFilePath))
                {
                    using (StreamReader faultDataTokenFileReader = new(faultDataTokenFilePath))
                    {
                        String faultDataTokenString = faultDataTokenFileReader.ReadToEnd();
                        _ = long.TryParse(faultDataTokenString, out faultDataToken);
                    }
                }
                if (File.Exists(gpsTokenFilePath))
                {
                    using (StreamReader gpsTokenFileReader = new(gpsTokenFilePath))
                    {
                        String gpsTokenString = gpsTokenFileReader.ReadToEnd();
                        _ = long.TryParse(gpsTokenString, out gpsToken);
                    }
                }
                if (File.Exists(statusDataTokenFilePath))
                {
                    using (StreamReader statusDataTokenFileReader = new(statusDataTokenFilePath))
                    {
                        String statusDataTokenString = statusDataTokenFileReader.ReadToEnd();
                        _ = long.TryParse(statusDataTokenString, out statusDataToken);
                    }
                }
            }

            // Instantiate FeedParameters and FeedProcessor objects.
            feedParameters      = new FeedParameters(gpsToken, statusDataToken, faultDataToken, feedStartOption, feedStartSpecificTimeUTC);
            FeedIntervalSeconds = feedIntervalSeconds;
            feedProcessor       = new FeedProcessor(server, database, user, password, useFaultDataFeed, useStatusDataFeed);
            TrackedVehicles     = new List <TrackedVehicle>();
        }