Files
Tritone/HorseRacingPredictor/HorseRacingPredictor/MainWindow.xaml.cs
T

1258 lines
54 KiB
C#

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<VirtualFootball.VirtualMatch> _vfbResults = new ObservableCollection<VirtualFootball.VirtualMatch>();
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<string> 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<System.Collections.Generic.Dictionary<string, object>>();
foreach (DataRow r in data.Rows)
{
var dict = new System.Collections.Generic.Dictionary<string, object>();
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<string> 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<int>(v => pbFootball.Value = v);
var status = new Progress<string>(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<string, CheckBox> _countryCheckboxes = new Dictionary<string, CheckBox>();
private void BuildCountryCheckboxes()
{
if (pnlRcCountries == null) return;
pnlRcCountries.Children.Clear();
_countryCheckboxes.Clear();
var supported = new HashSet<string>(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<string> 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<string> GetSelectedCountries()
{
return _countryCheckboxes
.Where(kv => kv.Value.IsChecked == true)
.Select(kv => kv.Key)
.ToList();
}
private void SetSelectedCountries(IEnumerable<string> codes)
{
var set = new HashSet<string>(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;
}
}
}
/// <summary>
/// Parses a single CSV line respecting quoted fields (comma delimiter).
/// </summary>
private static string[] ParseCsvLine(string line)
{
var fields = new List<string>();
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();
}
/// <summary>
/// Adds a "No" row-number column as the first column in the DataTable.
/// </summary>
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<int>(v => pbRacing.Value = v);
var status = new Progress<string>(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<string> 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 { }
}
/// <summary>
/// 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.
/// </summary>
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<DateTimeOffset, string> 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}%";
}
}
}
}