using System;
using System.Collections.Generic;
using System.Globalization;
using System.IO;
/// One chip position. Mirrors Form2.Circle.
public class Circle
{
public float Number { get; set; }
public int X { get; set; } // screen-space: OriginalX * 50 + MarginX
public int Y { get; set; }
public float OriginalX { get; set; }
public float OriginalY { get; set; }
public float OriginalZ { get; set; }
public bool Inner { get; set; } // non-integer Number
}
/// Loads the 8 CSVs from JacketData/ — front|back|right|left x L|XL.
public class JacketData
{
public static string AppFile(string relative) { return relative; }
public const int CoordScale = 50; // Form2: originalX * 50
public const int MarginX = 30;
public const int MarginY = 30;
private const string ConfigFile = "config.txt";
private const string DefaultDir = "JacketData";
private static readonly string[] Views = { "front", "back", "right", "left" };
private static readonly string[] Sizes = { "L", "XL" };
private readonly Dictionary<string, List<Circle>> _l = new Dictionary<string, List<Circle>>();
private readonly Dictionary<string, List<Circle>> _xl = new Dictionary<string, List<Circle>>();
public string Folder { get; private set; }
public bool Loaded { get; private set; }
/// Form2.GetCurrentCSVData()
public List<Circle> Get(string size, string view)
{
var dict = (size == "XL") ? _xl : _l;
List<Circle> list;
return dict.TryGetValue(view, out list) ? list : new List<Circle>();
}
public bool Has(string size, string view)
{
var dict = (size == "XL") ? _xl : _l;
return dict.ContainsKey(view) && dict[view].Count > 0;
}
// ── folder resolution: config.txt, else JacketData/ next to the exe ──
public static string ResolveFolder()
{
string cfg = ConfigFile;
if (File.Exists(cfg))
{
try
{
string saved = File.ReadAllText(cfg).Trim();
if (Directory.Exists(saved) && Validate(saved))
{
Logger.Write("jacket", "folder from config.txt: " + saved);
return saved;
}
Logger.Write("jacket", "config.txt path invalid, ignoring: " + saved);
}
catch (Exception ex)
{
Logger.Write("jacket", "config.txt unreadable: " + ex.Message);
}
}
// return SoundWindow.AppFile(DefaultDir);
return DefaultDir;
}
public static void SaveFolder(string folder)
{
try { File.WriteAllText(SoundWindow.AppFile(ConfigFile), folder); }
catch (Exception ex) { Logger.Write("jacket", "could not save config.txt: " + ex.Message); }
}
/// Form1.ValidateCSVFiles — all 8 must be present.
public static bool Validate(string folder)
{
foreach (string v in Views)
foreach (string s in Sizes)
if (!File.Exists(Path.Combine(folder, v + "_" + s + ".csv")))
return false;
return true;
}
public static string[] MissingFiles(string folder)
{
var missing = new List<string>();
foreach (string v in Views)
foreach (string s in Sizes)
{
string name = v + "_" + s + ".csv";
if (!File.Exists(Path.Combine(folder, name))) missing.Add(name);
}
return missing.ToArray();
}
// ── loading ─────────────────────────────────────────────────────────
public static JacketData Load()
{
return Load(ResolveFolder());
}
public static JacketData Load(string folder)
{
var data = new JacketData();
data.Folder = folder;
if (!Directory.Exists(folder))
{
Logger.Write("jacket", "folder not found: " + folder);
return data;
}
string[] missing = MissingFiles(folder);
if (missing.Length > 0)
Logger.Write("jacket", "missing " + missing.Length + " file(s): " + string.Join(", ", missing));
foreach (string view in Views)
{
foreach (string size in Sizes)
{
string path = Path.Combine(folder, view + "_" + size + ".csv");
var circles = ReadCsv(path);
if (circles.Count == 0) continue;
if (size == "XL") data._xl[view] = circles;
else data._l[view] = circles;
Logger.Write("jacket", "loaded " + circles.Count + " chips from " + view + "_" + size + ".csv");
}
}
data.Loaded = (data._l.Count > 0 || data._xl.Count > 0);
if (!data.Loaded) Logger.Write("jacket", "NO chip data loaded from " + folder);
return data;
}
/// Columns: Number, OriginalX, OriginalY, OriginalZ. Header optional.
private static List<Circle> ReadCsv(string path)
{
var circles = new List<Circle>();
if (!File.Exists(path))
{
Logger.Write("jacket", "file not found: " + path);
return circles;
}
string[] lines;
try { lines = File.ReadAllLines(path); }
catch (Exception ex)
{
Logger.Write("jacket", "read failed " + path + ": " + ex.Message);
return circles;
}
int start = 0;
if (lines.Length > 0)
{
string h = lines[0].ToLower();
if (h.Contains("number") || h.Contains("originalx")) start = 1;
}
for (int i = start; i < lines.Length; i++)
{
string line = lines[i].Trim();
if (line.Length == 0) continue;
string[] p = line.Split(',');
if (p.Length < 4)
{
Logger.Write("jacket", path + " line " + (i + 1) + ": only " + p.Length + " columns");
continue;
}
float number, ox, oy, oz;
var inv = CultureInfo.InvariantCulture;
// InvariantCulture matters — under ja_JP/de_DE, "1.5" can fail to parse.
if (!float.TryParse(p[0].Trim(), NumberStyles.Float, inv, out number) ||
!float.TryParse(p[1].Trim(), NumberStyles.Float, inv, out ox) ||
!float.TryParse(p[2].Trim(), NumberStyles.Float, inv, out oy) ||
!float.TryParse(p[3].Trim(), NumberStyles.Float, inv, out oz))
{
Logger.Write("jacket", path + " line " + (i + 1) + ": bad number format");
continue;
}
circles.Add(new Circle
{
Number = number,
X = (int)(ox * CoordScale) + MarginX,
Y = (int)(oy * CoordScale) + MarginY,
OriginalX = ox,
OriginalY = oy,
OriginalZ = oz,
Inner = (number % 1 != 0)
});
}
return circles;
}
}