using System; using System.Collections.Generic; using System.Data; using System.IO; using System.Linq; using System.Text; using System.Text.RegularExpressions; using System.Threading; using System.Threading.Tasks; using System.Windows; using System.Collections.ObjectModel; using System.Windows.Controls; using Microsoft.Web.WebView2.Core; namespace HorseRacingPredictor { public partial class MainWindow : Window { private readonly Football.Main _footballManager; private HorseRacing.Main _racingManager; private DataTable _footballData; private DataTable _racingData; private CancellationTokenSource _racingCts; // Virtual Football private readonly ObservableCollection _vfbResults = new ObservableCollection(); private const string DefaultRacingApiKey = ""; public MainWindow() { InitializeComponent(); _footballManager = new Football.Main(); _racingManager = new HorseRacing.Main(DefaultRacingApiKey); BuildCountryCheckboxes(); // Wire preview update events txtFbPrefix.TextChanged += (s, e) => UpdateFbPreview(); txtFbSuffix.TextChanged += (s, e) => UpdateFbPreview(); chkFbIncludeDate.Checked += (s, e) => UpdateFbPreview(); chkFbIncludeDate.Unchecked += (s, e) => UpdateFbPreview(); cmbFbDateFormat.SelectionChanged += (s, e) => UpdateFbPreview(); cmbFbFormat.SelectionChanged += (s, e) => UpdateFbPreview(); dpFootball.SelectedDateChanged += (s, e) => UpdateFbPreview(); txtRcPrefix.TextChanged += (s, e) => UpdateRcPreview(); txtRcSuffix.TextChanged += (s, e) => UpdateRcPreview(); chkRcIncludeDate.Checked += (s, e) => UpdateRcPreview(); chkRcIncludeDate.Unchecked += (s, e) => UpdateRcPreview(); cmbRcDateFormat.SelectionChanged += (s, e) => UpdateRcPreview(); cmbRcFormat.SelectionChanged += (s, e) => UpdateRcPreview(); } private void dgRacing_AutoGeneratingColumn(object sender, DataGridAutoGeneratingColumnEventArgs e) { // Hide the "Inizio" column in the racing grid if it appears if (e.PropertyName.Equals("Inizio", StringComparison.OrdinalIgnoreCase) || e.Column.Header?.ToString() == "Inizio") { e.Cancel = true; return; } // Ensure the row number 'No' column is visible and placed first if (e.PropertyName.Equals("No", StringComparison.OrdinalIgnoreCase)) { // make header readable e.Column.Header = "No"; // set width small e.Column.Width = 60; } } private void ExportToJson(DataTable data, string folder, string defaultName, Action setStatus) { if (data == null || data.Rows.Count == 0) { MessageBox.Show("Nessun dato da esportare.", "Nessun dato", MessageBoxButton.OK, MessageBoxImage.Warning); return; } // ensure file name has .json defaultName = EnsureFileExtension(string.IsNullOrWhiteSpace(defaultName) ? "export.json" : defaultName, ".json"); string filePath; if (!string.IsNullOrEmpty(folder) && Directory.Exists(folder)) filePath = Path.Combine(folder, defaultName); else { var dlg = new Microsoft.Win32.SaveFileDialog { Filter = "File JSON|*.json", FileName = defaultName, AddExtension = true }; if (dlg.ShowDialog() != true) return; filePath = dlg.FileName; } try { var list = new System.Collections.Generic.List>(); foreach (DataRow r in data.Rows) { var dict = new System.Collections.Generic.Dictionary(); foreach (DataColumn c in data.Columns) dict[c.ColumnName] = r[c] == DBNull.Value ? null : r[c]; list.Add(dict); } var json = System.Text.Json.JsonSerializer.Serialize(list, new System.Text.Json.JsonSerializerOptions { WriteIndented = true }); File.WriteAllText(filePath, json, Encoding.UTF8); setStatus?.Invoke($"JSON esportato: {Path.GetFileName(filePath)}"); MessageBox.Show($"Esportate {data.Rows.Count} righe in:\n{filePath}", "Esportazione completata", MessageBoxButton.OK, MessageBoxImage.Information); } catch (Exception ex) { MessageBox.Show($"Errore durante l'esportazione JSON:\n{ex.Message}", "Errore", MessageBoxButton.OK, MessageBoxImage.Error); } finally { // display count somewhere: update status label if provided setStatus?.Invoke($"Esportate {data.Rows.Count} righe"); } } private void ExportToXml(DataTable data, string folder, string defaultName, Action setStatus) { if (data == null || data.Rows.Count == 0) { MessageBox.Show("Nessun dato da esportare.", "Nessun dato", MessageBoxButton.OK, MessageBoxImage.Warning); return; } // ensure file name has .xml defaultName = EnsureFileExtension(string.IsNullOrWhiteSpace(defaultName) ? "export.xml" : defaultName, ".xml"); string filePath; if (!string.IsNullOrEmpty(folder) && Directory.Exists(folder)) filePath = Path.Combine(folder, defaultName); else { var dlg = new Microsoft.Win32.SaveFileDialog { Filter = "File XML|*.xml", FileName = defaultName, AddExtension = true }; if (dlg.ShowDialog() != true) return; filePath = dlg.FileName; } try { // Build a simple XML without schema to avoid assembly/type resolution issues var doc = new System.Xml.Linq.XDocument(); var root = new System.Xml.Linq.XElement("Rows"); foreach (DataRow r in data.Rows) { var rowEl = new System.Xml.Linq.XElement("Row"); foreach (DataColumn c in data.Columns) { var val = r[c] == DBNull.Value ? string.Empty : r[c].ToString(); rowEl.Add(new System.Xml.Linq.XElement(XmlConvertName(c.ColumnName), val)); } root.Add(rowEl); } doc.Add(root); doc.Save(filePath); setStatus?.Invoke($"XML esportato: {Path.GetFileName(filePath)}"); MessageBox.Show($"Esportate {data.Rows.Count} righe in:\n{filePath}", "Esportazione completata", MessageBoxButton.OK, MessageBoxImage.Information); } catch (Exception ex) { MessageBox.Show($"Errore durante l'esportazione XML:\n{ex.Message}", "Errore", MessageBoxButton.OK, MessageBoxImage.Error); } finally { setStatus?.Invoke($"Esportate {data.Rows.Count} righe"); } } private string XmlConvertName(string name) { // Ensure XML element name is valid: replace spaces and illegal chars with underscore if (string.IsNullOrEmpty(name)) return "Column"; var sb = new StringBuilder(); foreach (var ch in name) { if (char.IsLetterOrDigit(ch) || ch == '_' || ch == '-') sb.Append(ch); else sb.Append('_'); } // cannot start with digit if (char.IsDigit(sb[0])) return "C_" + sb.ToString(); return sb.ToString(); } // ???????????????????? LIFECYCLE ???????????????????? private void Window_Loaded(object sender, RoutedEventArgs e) { dpFootball.SelectedDate = DateTime.Today; dpRacing.SelectedDate = DateTime.Today; LoadSettings(); } // ???????????????????? NAVIGATION ???????????????????? private void ShowPage(string name) { // Guard against UI elements not being initialized (possible when called early) if (pageFootball != null) pageFootball.Visibility = name == "football" ? Visibility.Visible : Visibility.Collapsed; if (pageRacing != null) pageRacing.Visibility = name == "racing" ? Visibility.Visible : Visibility.Collapsed; if (pageSettings != null) pageSettings.Visibility = name == "settings" ? Visibility.Visible : Visibility.Collapsed; if (pageVirtualFb != null) pageVirtualFb.Visibility = name == "virtualfb" ? Visibility.Visible : Visibility.Collapsed; // Update title if available if (lblTitle != null) { switch (name) { case "football": lblTitle.Text = "Calcio"; break; case "racing": lblTitle.Text = "Corse Cavalli"; break; case "settings": lblTitle.Text = "Impostazioni"; break; case "virtualfb": lblTitle.Text = "Calcio Virtuale"; break; } } } private void navFootball_Checked(object sender, RoutedEventArgs e) => ShowPage("football"); private void navRacing_Checked(object sender, RoutedEventArgs e) => ShowPage("racing"); private void navSettings_Checked(object sender, RoutedEventArgs e) => ShowPage("settings"); private bool _vfbInitialized; private async void navVirtualFb_Checked(object sender, RoutedEventArgs e) { ShowPage("virtualfb"); // Bind results list once if (lbVfbResults.ItemsSource == null) lbVfbResults.ItemsSource = _vfbResults; if (!_vfbInitialized) { _vfbInitialized = true; try { // Persistent user-data folder so cookies, localStorage and session survive var userDataFolder = Path.Combine( Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), "BettingPredictor", "WebView2Data"); var env = await CoreWebView2Environment.CreateAsync( browserExecutableFolder: null, userDataFolder: userDataFolder, options: new CoreWebView2EnvironmentOptions()); await wbVirtualFb.EnsureCoreWebView2Async(env); // Match the real Chrome User-Agent from the HAR capture wbVirtualFb.CoreWebView2.Settings.UserAgent = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 " + "(KHTML, like Gecko) Chrome/145.0.0.0 Safari/537.36"; // Allow everything the SPA needs wbVirtualFb.CoreWebView2.Settings.IsScriptEnabled = true; wbVirtualFb.CoreWebView2.Settings.IsWebMessageEnabled = true; wbVirtualFb.CoreWebView2.Settings.AreDefaultScriptDialogsEnabled = true; wbVirtualFb.CoreWebView2.Settings.IsStatusBarEnabled = false; wbVirtualFb.CoreWebView2.Navigate(txtVfbUrl.Text); } catch (Exception ex) { System.Diagnostics.Debug.WriteLine($"[VFB] WebView2 init error: {ex.Message}"); MessageBox.Show( $"Impossibile inizializzare WebView2.\n\n" + $"Assicurati che il Microsoft Edge WebView2 Runtime sia installato.\n\n{ex.Message}", "Errore WebView2", MessageBoxButton.OK, MessageBoxImage.Warning); _vfbInitialized = false; } } } // ???????????????????? FOOTBALL ???????????????????? private async void btnDownloadFb_Click(object sender, RoutedEventArgs e) { var date = dpFootball.SelectedDate ?? DateTime.Today; await DownloadFootballAsync(date); } private async Task DownloadFootballAsync(DateTime date) { try { pbFootball.Value = 0; lblStatusFb.Text = "Scaricamento elenco partite…"; btnDownloadFb.IsEnabled = false; dpFootball.IsEnabled = false; btnExportFbCsv.IsEnabled = false; var progress = new Progress(v => pbFootball.Value = v); var status = new Progress(s => lblStatusFb.Text = s); var table = await Task.Run(() => _footballManager.GetTodayFixtures(date, progress, status)); _footballData = table; // Ensure the start time column exists and populate it (no timezone label) InjectRomeStartTimeColumn(_footballData, "Inizio"); dgFootball.ItemsSource = _footballData?.DefaultView; if (_footballData != null && _footballData.Rows.Count > 0) { btnExportFbCsv.IsEnabled = true; lblStatusFb.Text = $"Scaricate {_footballData.Rows.Count} partite"; } else { lblStatusFb.Text = "Nessuna partita trovata per la data selezionata"; } } catch (Exception ex) { MessageBox.Show($"Errore durante lo scaricamento:\n{ex.Message}", "Errore", MessageBoxButton.OK, MessageBoxImage.Error); lblStatusFb.Text = "Errore nello scaricamento"; pbFootball.Value = 0; } finally { btnDownloadFb.IsEnabled = true; dpFootball.IsEnabled = true; } } private void btnExportFbCsv_Click(object sender, RoutedEventArgs e) { var format = (cmbFbFormat?.SelectedItem as ComboBoxItem)?.Content?.ToString() ?? "CSV"; var filename = BuildFilename(txtFbPrefix?.Text, chkFbIncludeDate?.IsChecked == true ? GetSelectedDateString(cmbFbDateFormat, dpFootball.SelectedDate ?? DateTime.Today) : null, txtFbSuffix?.Text, null, $"Partite_{dpFootball.SelectedDate:yyyy-MM-dd}.{format.ToLower()}"); filename = EnsureFileExtension(SanitizeFileName(filename), "." + format.ToLower()); switch (format.ToUpper()) { case "CSV": ExportToCsv(_footballData, txtFbExportPath.Text, filename, s => lblStatusFb.Text = s); break; case "JSON": ExportToJson(_footballData, txtFbExportPath.Text, filename, s => lblStatusFb.Text = s); break; case "XML": ExportToXml(_footballData, txtFbExportPath.Text, filename, s => lblStatusFb.Text = s); break; default: ExportToCsv(_footballData, txtFbExportPath.Text, filename, s => lblStatusFb.Text = s); break; } // show total rows extracted lblStatusFb.Text = _footballData == null ? "Nessuna riga" : $"Righe estratte: {_footballData.Rows.Count}"; } // ???????????????????? HORSE RACING ???????????????????? private readonly Dictionary _countryCheckboxes = new Dictionary(); private void BuildCountryCheckboxes() { if (pnlRcCountries == null) return; pnlRcCountries.Children.Clear(); _countryCheckboxes.Clear(); var supported = new HashSet(HorseRacing.Main.SupportedCountries); // Header: nazioni con dati pnlRcCountries.Children.Add(new TextBlock { Text = "Con dati disponibili", FontSize = 10, FontFamily = new System.Windows.Media.FontFamily("Segoe UI Semibold"), Foreground = FindResource("BrBlue") as System.Windows.Media.Brush, Margin = new Thickness(6, 2, 0, 4) }); foreach (var code in HorseRacing.Main.SupportedCountries) AddCountryCheckbox(code, supported, true); // Separator pnlRcCountries.Children.Add(new Border { Height = 1, Background = FindResource("BrBorder") as System.Windows.Media.Brush, Margin = new Thickness(4, 6, 4, 6) }); // Header: catalogo pnlRcCountries.Children.Add(new TextBlock { Text = "Solo catalogo (nessun dato di forma)", FontSize = 10, Foreground = FindResource("BrOverlay0") as System.Windows.Media.Brush, Margin = new Thickness(6, 2, 0, 4) }); foreach (var code in HorseRacing.Main.AllCountries) { if (supported.Contains(code)) continue; AddCountryCheckbox(code, supported, false); } } private void AddCountryCheckbox(string code, HashSet supported, bool isSupported) { string label = HorseRacing.Main.CountryNames.TryGetValue(code, out var name) ? $"{name} ({code.ToUpper()})" : code.ToUpper(); var cb = new CheckBox { Content = label, Tag = code, IsChecked = false, Margin = new Thickness(4, 2, 4, 2), FontSize = 12, Foreground = isSupported ? FindResource("BrText") as System.Windows.Media.Brush : FindResource("BrOverlay0") as System.Windows.Media.Brush, Opacity = isSupported ? 1.0 : 0.7 }; cb.Checked += (s, e) => UpdateCountriesSummary(); cb.Unchecked += (s, e) => UpdateCountriesSummary(); _countryCheckboxes[code] = cb; pnlRcCountries.Children.Add(cb); } private List GetSelectedCountries() { return _countryCheckboxes .Where(kv => kv.Value.IsChecked == true) .Select(kv => kv.Key) .ToList(); } private void SetSelectedCountries(IEnumerable codes) { var set = new HashSet(codes.Select(c => c.Trim().ToLowerInvariant())); foreach (var kv in _countryCheckboxes) kv.Value.IsChecked = set.Contains(kv.Key); UpdateCountriesSummary(); } private void UpdateCountriesSummary() { var selected = GetSelectedCountries(); if (lblRcCountriesSummary != null) { lblRcCountriesSummary.Text = selected.Count > 0 ? string.Join(", ", selected.Select(c => c.ToUpper())) : "Nessuna"; } } private void rbRcSource_Checked(object sender, RoutedEventArgs e) { // Toggle visibility of API vs CSV controls if (dpRacing == null || btnDownloadRc == null || btnBrowseCsvRc == null) return; bool isApi = rbRcApi.IsChecked == true; dpRacing.Visibility = isApi ? Visibility.Visible : Visibility.Collapsed; btnDownloadRc.Visibility = isApi ? Visibility.Visible : Visibility.Collapsed; btnBrowseCsvRc.Visibility = isApi ? Visibility.Collapsed : Visibility.Visible; } private async void btnDownloadRc_Click(object sender, RoutedEventArgs e) { await DownloadRacecardsAsync(); } private void btnBrowseCsvRc_Click(object sender, RoutedEventArgs e) { using (var dlg = new System.Windows.Forms.FolderBrowserDialog()) { dlg.Description = "Seleziona la cartella con i file CSV Punters"; if (dlg.ShowDialog() != System.Windows.Forms.DialogResult.OK) return; try { lblStatusRc.Text = "Caricamento file CSV…"; pbRacing.Value = 0; btnExportRcCsv.IsEnabled = false; var csvFiles = Directory.GetFiles(dlg.SelectedPath, "*.csv", SearchOption.AllDirectories) .OrderBy(f => f) .ToList(); if (csvFiles.Count == 0) { lblStatusRc.Text = "Nessun file CSV trovato nella cartella selezionata"; return; } // Merge all CSV files into a single DataTable preserving all original columns var table = new DataTable(); table.Columns.Add("Meeting", typeof(string)); table.Columns.Add("Race", typeof(int)); int processed = 0; foreach (var file in csvFiles) { try { // Extract meeting name and race number from filename pattern YYYYMMDD-meeting-rXX.csv string fileName = Path.GetFileNameWithoutExtension(file); string meetingName = fileName; int raceNumber = 0; var m = Regex.Match(Path.GetFileName(file), @"^\d{8}-(.+)-r(\d+)\.csv$", RegexOptions.IgnoreCase); if (m.Success) { meetingName = string.Join(" ", m.Groups[1].Value.Split('-') .Select(s => s.Length > 0 ? char.ToUpper(s[0]) + s.Substring(1).ToLower() : s)); int.TryParse(m.Groups[2].Value, out raceNumber); } // Read CSV with simple parser (comma-delimited, first row = header) var lines = File.ReadAllLines(file, Encoding.UTF8); if (lines.Length < 2) { processed++; continue; } var headers = ParseCsvLine(lines[0]); // Ensure all columns exist in the merged DataTable foreach (var h in headers) { string colName = h.Trim(); if (string.IsNullOrWhiteSpace(colName)) continue; if (!table.Columns.Contains(colName)) table.Columns.Add(colName, typeof(string)); } // Parse data rows for (int i = 1; i < lines.Length; i++) { if (string.IsNullOrWhiteSpace(lines[i])) continue; var values = ParseCsvLine(lines[i]); var row = table.NewRow(); row["Meeting"] = meetingName; row["Race"] = raceNumber; for (int c = 0; c < headers.Length && c < values.Length; c++) { string colName = headers[c].Trim(); if (string.IsNullOrWhiteSpace(colName)) continue; if (table.Columns.Contains(colName)) row[colName] = values[c]?.Trim() ?? ""; } table.Rows.Add(row); } } catch (Exception ex) { System.Diagnostics.Debug.WriteLine($"Errore CSV {file}: {ex.Message}"); } processed++; pbRacing.Value = (int)((double)processed / csvFiles.Count * 100); } // Add row numbers InjectRowNumbers(table); _racingData = table; dgRacing.ItemsSource = _racingData?.DefaultView; if (_racingData.Rows.Count > 0) { btnExportRcCsv.IsEnabled = true; lblStatusRc.Text = $"Caricati {_racingData.Rows.Count} cavalli da {csvFiles.Count} file CSV"; } else { lblStatusRc.Text = "Nessun cavallo trovato nei file CSV"; } } catch (Exception ex) { MessageBox.Show($"Errore durante il caricamento CSV:\n{ex.Message}", "Errore", MessageBoxButton.OK, MessageBoxImage.Error); lblStatusRc.Text = "Errore nel caricamento CSV"; pbRacing.Value = 0; } } } /// /// Parses a single CSV line respecting quoted fields (comma delimiter). /// private static string[] ParseCsvLine(string line) { var fields = new List(); bool inQuotes = false; var sb = new StringBuilder(); for (int i = 0; i < line.Length; i++) { char c = line[i]; if (inQuotes) { if (c == '"') { if (i + 1 < line.Length && line[i + 1] == '"') { sb.Append('"'); i++; // skip escaped quote } else { inQuotes = false; } } else { sb.Append(c); } } else { if (c == '"') { inQuotes = true; } else if (c == ',') { fields.Add(sb.ToString()); sb.Clear(); } else { sb.Append(c); } } } fields.Add(sb.ToString()); return fields.ToArray(); } /// /// Adds a "No" row-number column as the first column in the DataTable. /// private static void InjectRowNumbers(DataTable table) { if (table == null || table.Rows.Count == 0) return; if (table.Columns.Contains("No")) table.Columns.Remove("No"); var col = new DataColumn("No", typeof(int)); table.Columns.Add(col); col.SetOrdinal(0); int n = 1; foreach (DataRow r in table.Rows) r[col] = n++; } private async Task DownloadRacecardsAsync() { // Se e' gia' in corso, annulla if (_racingCts != null) { _racingCts.Cancel(); _racingCts = null; btnDownloadRc.Content = "Scarica Corse"; lblStatusRc.Text = "Annullato"; return; } _racingCts = new CancellationTokenSource(); var ct = _racingCts.Token; try { pbRacing.Value = 0; lblStatusRc.Text = "Scaricamento corse da FormFav..."; btnDownloadRc.Content = "Annulla"; dpRacing.IsEnabled = false; btnExportRcCsv.IsEnabled = false; // Applica impostazioni correnti al manager ApplyRacingSettings(); var progress = new Progress(v => pbRacing.Value = v); var status = new Progress(s => lblStatusRc.Text = s); var date = dpRacing.SelectedDate ?? DateTime.Today; var table = await Task.Run(() => _racingManager.GetAllRacesForDate(date, progress, status, ct), ct); _racingData = table; // Add row numbers InjectRowNumbers(_racingData); dgRacing.ItemsSource = _racingData?.DefaultView; if (_racingData != null && _racingData.Rows.Count > 0) { btnExportRcCsv.IsEnabled = true; lblStatusRc.Text = $"Trovati {_racingData.Rows.Count} corridori"; } else { lblStatusRc.Text = "Nessuna corsa trovata per la data selezionata"; } } catch (OperationCanceledException) { lblStatusRc.Text = "Scaricamento annullato"; pbRacing.Value = 0; } catch (Exception ex) { MessageBox.Show($"Errore durante lo scaricamento:\n{ex.Message}", "Errore", MessageBoxButton.OK, MessageBoxImage.Error); lblStatusRc.Text = "Errore nello scaricamento"; pbRacing.Value = 0; } finally { _racingCts = null; btnDownloadRc.Content = "Scarica Corse"; dpRacing.IsEnabled = true; } } private void ApplyRacingSettings() { _racingManager.UpdateApiKey(txtRacingApiKey.Text.Trim()); var tz = txtRcTimezone?.Text?.Trim(); if (!string.IsNullOrEmpty(tz)) _racingManager.Timezone = tz; var selected = GetSelectedCountries(); if (selected.Count > 0) _racingManager.Countries = selected; } private void btnExportRcCsv_Click(object sender, RoutedEventArgs e) { var rcDate = dpRacing.SelectedDate ?? DateTime.Today; var format = (cmbRcFormat?.SelectedItem as ComboBoxItem)?.Content?.ToString() ?? "CSV"; var defaultName = $"Corse_{rcDate:yyyy-MM-dd}.{format.ToLower()}"; var filename = BuildFilename(txtRcPrefix?.Text, chkRcIncludeDate?.IsChecked == true ? GetSelectedDateString(cmbRcDateFormat, rcDate) : null, txtRcSuffix?.Text, null, defaultName); filename = EnsureFileExtension(SanitizeFileName(filename), "." + format.ToLower()); switch (format.ToUpper()) { case "CSV": ExportToCsv(_racingData, txtRcExportPath.Text, filename, s => lblStatusRc.Text = s); break; case "JSON": ExportToJson(_racingData, txtRcExportPath.Text, filename, s => lblStatusRc.Text = s); break; case "XML": ExportToXml(_racingData, txtRcExportPath.Text, filename, s => lblStatusRc.Text = s); break; default: ExportToCsv(_racingData, txtRcExportPath.Text, filename, s => lblStatusRc.Text = s); break; } lblStatusRc.Text = _racingData == null ? "Nessuna riga" : $"Righe estratte: {_racingData.Rows.Count}"; } // ???????????????????? SHARED CSV EXPORT ???????????????????? private void ExportToCsv(DataTable data, string folder, string defaultName, Action setStatus) { if (data == null || data.Rows.Count == 0) { MessageBox.Show("Nessun dato da esportare.", "Nessun dato", MessageBoxButton.OK, MessageBoxImage.Warning); return; } // Ensure filename has .csv extension defaultName = EnsureFileExtension(defaultName, ".csv"); string filePath; if (!string.IsNullOrEmpty(folder) && Directory.Exists(folder)) { filePath = Path.Combine(folder, defaultName); } else { var dlg = new Microsoft.Win32.SaveFileDialog { Filter = "File CSV|*.csv", FileName = defaultName, AddExtension = true }; if (dlg.ShowDialog() != true) return; filePath = dlg.FileName; } try { var sb = new StringBuilder(); var headers = new string[data.Columns.Count]; for (int i = 0; i < data.Columns.Count; i++) headers[i] = data.Columns[i].ColumnName; sb.AppendLine(string.Join(";", headers)); foreach (DataRow row in data.Rows) { var vals = new string[data.Columns.Count]; for (int i = 0; i < data.Columns.Count; i++) { var v = row[i]?.ToString() ?? ""; if (v.Contains(";") || v.Contains("\"")) v = "\"" + v.Replace("\"", "\"\"") + "\""; vals[i] = v; } sb.AppendLine(string.Join(";", vals)); } File.WriteAllText(filePath, sb.ToString(), Encoding.UTF8); setStatus?.Invoke($"CSV esportato: {Path.GetFileName(filePath)}"); MessageBox.Show($"Esportate {data.Rows.Count} righe in:\n{filePath}", "Esportazione completata", MessageBoxButton.OK, MessageBoxImage.Information); } catch (Exception ex) { MessageBox.Show($"Errore durante l'esportazione CSV:\n{ex.Message}", "Errore", MessageBoxButton.OK, MessageBoxImage.Error); } finally { setStatus?.Invoke($"Esportate {data.Rows.Count} righe"); } } // ???????????????????? FOLDER BROWSE ???????????????????? private void btnBrowseFbExport_Click(object sender, RoutedEventArgs e) { var path = BrowseFolder("Seleziona la cartella di esportazione per Calcio"); if (path != null) txtFbExportPath.Text = path; UpdateFbPreview(); } private void btnBrowseRcExport_Click(object sender, RoutedEventArgs e) { var path = BrowseFolder("Seleziona la cartella di esportazione per Corse Cavalli"); if (path != null) txtRcExportPath.Text = path; UpdateRcPreview(); } private static string BrowseFolder(string description) { using (var dlg = new System.Windows.Forms.FolderBrowserDialog()) { dlg.Description = description; if (dlg.ShowDialog() == System.Windows.Forms.DialogResult.OK) return dlg.SelectedPath; } return null; } // ???????????????????? SETTINGS ???????????????????? private string SettingsFilePath => Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "settings.ini"); private void LoadSettings() { try { txtRacingApiKey.Text = DefaultRacingApiKey; // Default countries SetSelectedCountries(new[] { "au", "nz" }); if (!File.Exists(SettingsFilePath)) { ApplyRacingSettings(); return; } foreach (var line in File.ReadAllLines(SettingsFilePath)) { var idx = line.IndexOf('='); if (idx < 0) continue; var key = line.Substring(0, idx).Trim(); var val = line.Substring(idx + 1).Trim(); if (key == "ApiKey") txtApiKey.Text = val; else if (key == "FbExportPath") txtFbExportPath.Text = val; else if (key == "FbPrefix") txtFbPrefix.Text = val; else if (key == "FbSuffix") txtFbSuffix.Text = val; else if (key == "FbIncludeDate") chkFbIncludeDate.IsChecked = val == "1" || val.Equals("true", StringComparison.OrdinalIgnoreCase); else if (key == "FbDateFormat") { try { SetComboBoxSelectionByContent(cmbFbDateFormat, val); } catch { } } else if (key == "FbFormat") { try { SetComboBoxSelectionByContent(cmbFbFormat, val); } catch { } } else if (key == "RcExportPath") txtRcExportPath.Text = val; else if (key == "RcPrefix") txtRcPrefix.Text = val; else if (key == "RcSuffix") txtRcSuffix.Text = val; else if (key == "RcIncludeDate") chkRcIncludeDate.IsChecked = val == "1" || val.Equals("true", StringComparison.OrdinalIgnoreCase); else if (key == "RcDateFormat") { try { SetComboBoxSelectionByContent(cmbRcDateFormat, val); } catch { } } else if (key == "RcFormat") { try { SetComboBoxSelectionByContent(cmbRcFormat, val); } catch { } } else if (key == "RacingApiKey") txtRacingApiKey.Text = val; else if (key == "RcTimezone" && txtRcTimezone != null) txtRcTimezone.Text = val; else if (key == "RcCountries") { var codes = val.Split(new[] { ',', ';', ' ' }, StringSplitOptions.RemoveEmptyEntries); SetSelectedCountries(codes); } } // Update preview UI after loading values UpdateFbPreview(); UpdateRcPreview(); ApplyRacingSettings(); } catch { } } private void SetComboBoxSelectionByContent(ComboBox combo, string content) { if (combo == null) return; for (int i = 0; i < combo.Items.Count; i++) { var item = combo.Items[i] as ComboBoxItem; if (item != null && string.Equals(item.Content?.ToString(), content, StringComparison.OrdinalIgnoreCase)) { combo.SelectedIndex = i; return; } } } private static string EnsureFileExtension(string fileName, string extension) { if (string.IsNullOrWhiteSpace(fileName)) return ""; if (!extension.StartsWith(".")) extension = "." + extension; if (fileName.EndsWith(extension, StringComparison.OrdinalIgnoreCase)) return fileName; return fileName + extension; } private static string BuildFilename(string prefix, string datePart, string suffix, string explicitName, string fallback) { // If user provided explicit full filename, use it if (!string.IsNullOrWhiteSpace(explicitName)) return SanitizeFileName(explicitName.Trim()); prefix = prefix ?? ""; suffix = suffix ?? ""; // The user may include underscores in prefix/suffix as desired string middle = string.IsNullOrWhiteSpace(datePart) ? "" : datePart; string name = (prefix ?? string.Empty) + middle + (suffix ?? string.Empty); if (string.IsNullOrWhiteSpace(name)) return fallback; return SanitizeFileName(name); } private static string GetSelectedDateString(ComboBox combo, DateTime? date) { if (date == null) return null; var fmt = (combo?.SelectedItem as ComboBoxItem)?.Content?.ToString(); if (string.IsNullOrWhiteSpace(fmt)) fmt = "yyyy-MM-dd"; try { return date.Value.ToString(fmt); } catch { return date.Value.ToString("yyyy-MM-dd"); } } private static string SanitizeFileName(string name) { if (string.IsNullOrWhiteSpace(name)) return name; var invalid = Path.GetInvalidFileNameChars(); var sb = new StringBuilder(); foreach (var ch in name) { if (Array.IndexOf(invalid, ch) >= 0) continue; // remove invalid characters sb.Append(ch); } // trim whitespace return sb.ToString().Trim(); } private void UpdateFbPreview() { try { var format = (cmbFbFormat?.SelectedItem as ComboBoxItem)?.Content?.ToString() ?? "CSV"; var datePart = chkFbIncludeDate?.IsChecked == true ? GetSelectedDateString(cmbFbDateFormat, dpFootball.SelectedDate ?? DateTime.Today) : null; var name = BuildFilename(txtFbPrefix?.Text, datePart, txtFbSuffix?.Text, null, $"Partite_{(dpFootball.SelectedDate ?? DateTime.Today):yyyy-MM-dd}.{format.ToLower()}"); name = SanitizeFileName(name); name = EnsureFileExtension(name, "." + format.ToLower()); if (txtFbPreview != null) txtFbPreview.Text = name; } catch { } } private void UpdateRcPreview() { try { var format = (cmbRcFormat?.SelectedItem as ComboBoxItem)?.Content?.ToString() ?? "CSV"; var rcDate = dpRacing?.SelectedDate ?? DateTime.Today; var datePart = chkRcIncludeDate?.IsChecked == true ? GetSelectedDateString(cmbRcDateFormat, rcDate) : null; var defaultName = $"Corse_{rcDate:yyyy-MM-dd}.{format.ToLower()}"; var name = BuildFilename(txtRcPrefix?.Text, datePart, txtRcSuffix?.Text, null, defaultName); name = SanitizeFileName(name); name = EnsureFileExtension(name, "." + format.ToLower()); if (txtRcPreview != null) txtRcPreview.Text = name; } catch { } } /// /// Ensure a visible column with the fixture/race start time converted to Rome timezone exists in the DataTable. /// It attempts to use a unix timestamp column (unix_ts) or a date column (Data / Ora / date) and adds a formatted column. /// private void InjectRomeStartTimeColumn(DataTable table, string columnName) { if (table == null) return; try { // If no columnName specified, only add/populate row number column and return if (string.IsNullOrWhiteSpace(columnName)) { if (table.Columns.Contains("No")) table.Columns.Remove("No"); var rowOnly = new DataColumn("No", typeof(int)); table.Columns.Add(rowOnly); rowOnly.SetOrdinal(0); int rnOnly = 1; foreach (DataRow r in table.Rows) { try { r[rowOnly] = rnOnly++; } catch { } } return; } // Remove any existing columns with the same names to avoid duplicates if (table.Columns.Contains(columnName)) table.Columns.Remove(columnName); if (table.Columns.Contains("No")) table.Columns.Remove("No"); // Add row number column as first column var rowNoCol = new DataColumn("No", typeof(int)); table.Columns.Add(rowNoCol); rowNoCol.SetOrdinal(0); // Add new column as string for formatted display and place it after 'No' var col = new DataColumn(columnName, typeof(string)); table.Columns.Add(col); col.SetOrdinal(1); // Populate row numbers immediately int rn = 1; foreach (DataRow r in table.Rows) { try { r[rowNoCol] = rn++; } catch { } } // Determine timezone for Rome (Windows TZ id). Fallback to UTC+1 offset if not found. TimeZoneInfo romeTz = null; try { romeTz = TimeZoneInfo.FindSystemTimeZoneById("W. Europe Standard Time"); } catch { try { romeTz = TimeZoneInfo.FindSystemTimeZoneById("Central Europe Standard Time"); } catch { romeTz = null; } } // Helper to convert DateTimeOffset to Rome local time and format Func fmt = dto => { DateTimeOffset rome; if (romeTz != null) rome = TimeZoneInfo.ConvertTime(dto, romeTz); else rome = dto.ToOffset(TimeSpan.FromHours(1)); // fallback UTC+1 return rome.ToString("yyyy-MM-dd HH:mm"); }; // Try unix timestamp first if (table.Columns.Contains("unix_ts")) { foreach (DataRow r in table.Rows) { try { var obj = r["unix_ts"]; if (obj == DBNull.Value) { r[col] = string.Empty; continue; } long ts = 0; if (obj is long) ts = (long)obj; else if (obj is int) ts = Convert.ToInt64(obj); else ts = Convert.ToInt64(obj); var dto = DateTimeOffset.FromUnixTimeSeconds(ts); r[col] = fmt(dto); } catch { r[col] = string.Empty; } } return; } // Otherwise try common date columns string[] candidates = new[] { "Data / Ora", "date", "Date", "start", "kickoff" }; foreach (var c in candidates) { if (!table.Columns.Contains(c)) continue; foreach (DataRow r in table.Rows) { try { var v = r[c]; if (v == DBNull.Value) { r[col] = string.Empty; continue; } DateTimeOffset dto; if (v is DateTime dt) { // treat as UTC if unspecified dto = new DateTimeOffset(DateTime.SpecifyKind(dt, DateTimeKind.Utc)); } else { // parse string including offset dto = DateTimeOffset.Parse(v.ToString()); } r[col] = fmt(dto); } catch { r[col] = string.Empty; } } return; } // If no source found, leave column empty } catch (Exception ex) { // Non-fatal: log to debug output System.Diagnostics.Debug.WriteLine("InjectRomeStartTimeColumn error: " + ex.Message); } } private void btnSaveSettings_Click(object sender, RoutedEventArgs e) { try { var sb = new StringBuilder(); sb.AppendLine($"ApiKey={txtApiKey.Text.Trim()}"); sb.AppendLine($"FbExportPath={txtFbExportPath.Text.Trim()}"); sb.AppendLine($"FbPrefix={txtFbPrefix.Text.Trim()}"); sb.AppendLine($"FbSuffix={txtFbSuffix.Text.Trim()}"); sb.AppendLine($"FbIncludeDate={(chkFbIncludeDate.IsChecked==true?"1":"0")}"); sb.AppendLine($"FbDateFormat={(cmbFbDateFormat?.SelectedItem as ComboBoxItem)?.Content?.ToString() ?? "yyyy-MM-dd"}"); sb.AppendLine($"FbFormat={(cmbFbFormat?.SelectedItem as ComboBoxItem)?.Content?.ToString() ?? "CSV"}"); sb.AppendLine($"RcExportPath={txtRcExportPath.Text.Trim()}"); sb.AppendLine($"RcPrefix={txtRcPrefix.Text.Trim()}"); sb.AppendLine($"RcSuffix={txtRcSuffix.Text.Trim()}"); sb.AppendLine($"RcIncludeDate={(chkRcIncludeDate.IsChecked==true?"1":"0")}"); sb.AppendLine($"RcDateFormat={(cmbRcDateFormat?.SelectedItem as ComboBoxItem)?.Content?.ToString() ?? "yyyy-MM-dd"}"); sb.AppendLine($"RcFormat={(cmbRcFormat?.SelectedItem as ComboBoxItem)?.Content?.ToString() ?? "CSV"}"); sb.AppendLine($"RacingApiKey={txtRacingApiKey.Text.Trim()}"); sb.AppendLine($"RcTimezone={txtRcTimezone?.Text?.Trim() ?? "Australia/Sydney"}"); sb.AppendLine($"RcCountries={string.Join(",", GetSelectedCountries())}"); File.WriteAllText(SettingsFilePath, sb.ToString(), Encoding.UTF8); // update previews after save UpdateFbPreview(); UpdateRcPreview(); ApplyRacingSettings(); MessageBox.Show("Impostazioni salvate con successo.", "Salvato", MessageBoxButton.OK, MessageBoxImage.Information); } catch (Exception ex) { MessageBox.Show($"Errore nel salvataggio:\n{ex.Message}", "Errore", MessageBoxButton.OK, MessageBoxImage.Error); } } // ???????????????????????? VIRTUAL FOOTBALL ???????????????????????? private void btnVfbNavigate_Click(object sender, RoutedEventArgs e) { try { var url = txtVfbUrl.Text?.Trim(); if (!string.IsNullOrEmpty(url) && wbVirtualFb.CoreWebView2 != null) wbVirtualFb.CoreWebView2.Navigate(url); } catch (Exception ex) { System.Diagnostics.Debug.WriteLine($"[VFB] Navigate error: {ex.Message}"); } } private void btnVfbRefresh_Click(object sender, RoutedEventArgs e) { try { wbVirtualFb.CoreWebView2?.Reload(); } catch (Exception ex) { System.Diagnostics.Debug.WriteLine($"[VFB] Refresh error: {ex.Message}"); } } private void btnVfbAddResult_Click(object sender, RoutedEventArgs e) { if (!int.TryParse(txtVfbHomeGoals.Text, out int hg)) hg = 0; if (!int.TryParse(txtVfbAwayGoals.Text, out int ag)) ag = 0; var match = new VirtualFootball.VirtualMatch { Time = DateTime.Now.ToString("HH:mm"), Home = string.IsNullOrWhiteSpace(txtVfbHome.Text) ? "Casa" : txtVfbHome.Text.Trim(), HomeGoals = hg, AwayGoals = ag, Away = string.IsNullOrWhiteSpace(txtVfbAway.Text) ? "Ospite" : txtVfbAway.Text.Trim() }; _vfbResults.Insert(0, match); // Reset input txtVfbHomeGoals.Text = "0"; txtVfbAwayGoals.Text = "0"; UpdateVfbStats(); UpdateVfbSuggestion(); } private void UpdateVfbStats() { if (_vfbResults.Count == 0) { lblVfbStats.Text = "Nessun dato"; return; } int total = _vfbResults.Count; int draws = _vfbResults.Count(m => m.Outcome == "X"); int home = _vfbResults.Count(m => m.Outcome == "1"); int away = _vfbResults.Count(m => m.Outcome == "2"); double drawPct = (double)draws / total * 100; double homePct = (double)home / total * 100; double awayPct = (double)away / total * 100; int totalGoals = _vfbResults.Sum(m => m.HomeGoals + m.AwayGoals); double avgGoals = (double)totalGoals / total; int over25 = _vfbResults.Count(m => m.HomeGoals + m.AwayGoals > 2); lblVfbStats.Text = $"Partite: {total}\n" + $"1: {home} ({homePct:F1}%) X: {draws} ({drawPct:F1}%) 2: {away} ({awayPct:F1}%)\n" + $"Media gol: {avgGoals:F1} Over 2.5: {over25} ({(double)over25 / total * 100:F1}%)"; } private void UpdateVfbSuggestion() { if (_vfbResults.Count < 5) { lblVfbSuggestion.Text = "Inserisci almeno 5 risultati"; return; } // Count consecutive non-draw results (most recent first) int streak = 0; foreach (var m in _vfbResults) { if (m.Outcome != "X") streak++; else break; } // Simple strategy: after a long streak without draws, suggest betting on draw if (streak >= 8) { lblVfbSuggestion.Text = $"\u26A0 PUNTA X (pareggio) \u2014 {streak} partite consecutive senza pareggio!\n" + "Puntata alta consigliata."; } else if (streak >= 5) { lblVfbSuggestion.Text = $"\u2705 Punta X (pareggio) \u2014 {streak} partite senza pareggio.\n" + "Puntata media consigliata."; } else if (streak >= 3) { lblVfbSuggestion.Text = $"\u23F3 Possibile X \u2014 {streak} partite senza pareggio.\n" + "Puntata bassa o attendi."; } else { double drawPct = (double)_vfbResults.Count(m => m.Outcome == "X") / _vfbResults.Count * 100; lblVfbSuggestion.Text = $"Pareggio recente \u2014 attendi una serie senza X.\n" + $"Frequenza X attuale: {drawPct:F1}%"; } } } }