using System; using System.Collections.Generic; using System.IO; using System.Linq; using System.Net.Http; using System.Text; using System.Threading.Tasks; using System.Windows.Forms; using CarManagerV3.Classes; namespace CarManagerV3.Util { /// /// Provides car manufacturer and model autocompletion data for the application. /// Manages fetching, caching, and retrieval of car brand and model information from remote sources and local storage. /// /// /// This class maintains a static list of car manufacturers and their models, supports fetching updated data from remote URLs, /// and caches the data locally for offline use. It implements retry logic to prevent excessive network requests. /// internal class CarCompletions { /// /// A list of URLs that should be used to fetch the json file with autocompletion data. /// Multiple URLs are provided to ensure that if one source is unavailable, the others can be used as a fallback. The application should attempt to fetch the data from each URL in order until it successfully retrieves the data or exhausts all options. /// private static readonly string[] carDataFileUrls = { "https://static.clsw.app/carmgm/merged-cars.json" }; /// /// The filename for the cached car completion data in the user's data directory. /// private static readonly string carCompletionDataFileName = "car_data.json"; /// /// The timestamp of the last fetch attempt for car completion data. /// Used to implement retry throttling to prevent excessive network requests. /// private static DateTime lastFetchAttempt = Properties.Settings.Default.LastFetchedAutoCompletions; /// /// The minimum time interval that must elapse between fetch attempts. /// Prevents excessive retry attempts when the remote source is unavailable. /// private static readonly TimeSpan fetchRetryInterval = Properties.Settings.Default.FetchAutoCompletionsInterval; // Minimum interval between fetch attempts /// /// Gets the full file path for the car completion data file. /// Ensures that the user data directory exists before returning the path. /// /// The full file path to the car completion data file in the user's data directory. private static string getCarCompletionDataFilePath() { var userDataDir = Properties.Settings.Default.DataLocation; SafeManager.EnsureDirectoryExists(userDataDir); return Path.Combine(userDataDir, carCompletionDataFileName); } /// /// The static list of car manufacturers and their available models. /// This list serves as the default data and is updated when fetching from remote sources or local cache. /// public static List carBrands = new List { new CarManufacturer("Toyota") { Models = new List { "Camry", "Corolla", "RAV4" } }, new CarManufacturer("Honda") { Models = new List { "Civic", "Accord", "CR-V" } }, new CarManufacturer("Ford") { Models = new List { "F-150", "Mustang", "Escape" } }, new CarManufacturer("Chevrolet") { Models = new List { "Silverado", "Malibu", "Equinox" } }, new CarManufacturer("BMW") { Models = new List { "3 Series", "5 Series", "X5" } }, new CarManufacturer("Mercedes-Benz") { Models = new List { "C-Class", "E-Class", "GLC" } }, new CarManufacturer("Audi") { Models = new List { "A4", "A6", "Q5" } }, new CarManufacturer("Volkswagen") { Models = new List { "Golf", "Passat", "Tiguan" } }, new CarManufacturer("Nissan") { Models = new List { "Altima", "Sentra", "Rogue" } }, new CarManufacturer("Hyundai") { Models = new List { "Elantra", "Sonata", "Tucson" } }, new CarManufacturer("SEAT") { Models = new List { "Ibiza", "Leon", "Ateca", "Alhambra" } }, new CarManufacturer("Skoda") { Models = new List { "Octavia", "Fabia", "Kodiaq" } }, }; /// /// Gets the full list of car brands. /// /// A list of strings containing all available car manufacturer names. public static List GetCarBrands() { return carBrands.Select(c => c.Name).ToList(); } /// /// Retrieves the list of car models for a specified brand. /// The search is case-insensitive. /// /// The name of the car brand to search for. /// A list of car models for the specified brand, or an empty list if the brand is not found. public static List GetCarModels(string brand) { var manufacturer = carBrands.FirstOrDefault(c => c.Name.Equals(brand, StringComparison.OrdinalIgnoreCase)); return manufacturer != null ? manufacturer.Models : new List(); } /// /// A predefined list of common car colors for autocompletion purposes. /// Provides standard color options that are frequently used in vehicle descriptions. /// public static List CommonColors = new List { "Black", "White", "Gray", "Silver", "Red", "Blue", "Green", "Yellow", "Brown", "Orange" }; /// /// Reads car completion data from an embedded resource file. /// This method is intended to populate the carBrands list from a JSON resource embedded in the project. /// /// /// The expected resource format is a JSON object where keys are car brand names and values are lists of model names. /// Resource name: CarCompleteData (JSON file format). /// Currently not implemented. /// public static void ReadFromResourceFile() { // Read the json file from the projects resources and populate the carBrands list // Format is a json object with the key being the car brand and the value being a list of models // Get the json string from the resources, Resource name is CarCompleteData, it is a .json file } /// /// Fetches the car data from urls asynchronous and saves it in the users data directory for offline use. /// /// A task representing the asynchronous operation. /// /// This method implements retry throttling using the to prevent excessive network requests. /// It attempts to fetch data from each URL in in order until successful or all URLs are exhausted. /// The fetched data is saved to the local file system for offline access. /// public static async Task FetchCarCompletionDataFromUrlsAsync() { if(DateTime.Now - lastFetchAttempt < fetchRetryInterval) { System.Diagnostics.Debug.WriteLine("Fetch attempt skipped to avoid excessive retries. Last attempt was at " + lastFetchAttempt); Console.WriteLine("Fetch attempt skipped to avoid excessive retries. Last attempt was at " + lastFetchAttempt); return; } foreach (var url in carDataFileUrls) { try { lastFetchAttempt = DateTime.Now; Properties.Settings.Default.LastFetchedAutoCompletions = lastFetchAttempt; Properties.Settings.Default.Save(); using var httpClient = new HttpClient(); var response = await httpClient.GetAsync(url); response.EnsureSuccessStatusCode(); var jsonData = await response.Content.ReadAsStringAsync(); // Saves the json data to a file in the user's data directory for offline use var filePath = getCarCompletionDataFilePath(); await File.WriteAllTextAsync(filePath, jsonData); System.Diagnostics.Debug.WriteLine($"Successfully fetched car data from {url} and saved to {filePath}"); break; // Exit the loop if data is successfully fetched } catch (Exception ex) { // Log the error and try the next URL System.Diagnostics.Debug.WriteLine($"Failed to fetch car data from {url}: {ex.Message}"); Console.WriteLine($"Failed to fetch car data from {url}: {ex.Message}"); } } } /// /// Fetches the car data from urls and saves it in the users data directory for offline use. /// This is a synchronous wrapper that blocks until the asynchronous fetch operation completes. /// /// A task representing the synchronous operation. /// /// This method wraps and runs it synchronously. /// Consider using the async version directly when possible to avoid blocking the thread. /// public static Task FetchCarCompletionDataFromUrls() { // Run synchronously return Task.Run(() => FetchCarCompletionDataFromUrlsAsync().Wait()); } /// /// Loads car manufacturer data from the locally cached file. /// Optionally fetches updated data from remote sources before loading from file. /// /// If true, skips fetching data from remote sources and only reads from the local cache. Default is false. /// A list of objects populated from the cached file, or an empty list if the file doesn't exist or an error occurs. /// /// The expected file format is a JSON object where keys are car brand names and values are lists of model names. /// If the file doesn't exist or cannot be read, an error message is logged and an empty list is returned. /// public static List GetFromFile(bool skipOnlineUpdate = false) { if (!skipOnlineUpdate) FetchCarCompletionDataFromUrls(); var filePath = getCarCompletionDataFilePath(); if (File.Exists(filePath)) { try { var jsonData = File.ReadAllText(filePath); // Parse the json data and populate the carBrands list // Format is a json object with the key being the car brand and the value being a list of models var carData = Newtonsoft.Json.JsonConvert.DeserializeObject>>(jsonData); List _carBrands = carData.Select(kvp => new CarManufacturer(kvp.Key) { Models = kvp.Value }).ToList(); return _carBrands; } catch (Exception ex) { // Silent error. Console.WriteLine($"Failed to read car data from file: {ex.Message}"); } } else { Console.Error.WriteLine("Car data file not found. Please ensure that the data has been fetched successfully at least once."); } return new List(); } /// /// Updates the static list with data from the local cache file. /// Only updates the list if the cached file contains valid data. /// /// If true, skips fetching data from remote sources before updating. Default is false. /// /// This method calls and updates only if the returned list is not empty. /// If no data is retrieved, the existing list remains unchanged. /// public static void UpdateCarCompletionData(bool skipOnlineUpdate = false) { var updatedCarBrands = GetFromFile(skipOnlineUpdate); if (updatedCarBrands.Count > 0) { carBrands = updatedCarBrands; } } /// /// Asynchronously updates the car completion data by fetching from remote sources and updating the local cache. /// This is the recommended method for refreshing car data in the application. /// /// A task representing the asynchronous update operation. /// /// This method first attempts to fetch updated data from remote URLs using , /// then updates the static list from the local cache using . /// public static async Task UpdateCarCompletionDataAsync() { await FetchCarCompletionDataFromUrlsAsync(); System.Diagnostics.Debug.WriteLine("Car completion data fetch attempt completed. Now updating local data."); UpdateCarCompletionData(true); } } }