using System;
using System.Collections.Generic;
using System.Globalization;
using System.IO;
using System.Text;
using Gtk;
using UI = Gtk.Builder.ObjectAttribute;
using IOPath = System.IO.Path;

/// Standalone tuning tool: circle cluster scaling, sound-map zoom,
/// and sound-map position over the jacket. Writes values you paste
/// back into MapWindow.cs.
public class CalibrationWindow : Window
{
    [UI] private Box         ControlBox = null;
    [UI] private DrawingArea CalibArea  = null;
    [UI] private TextView    OutputView = null;
    [UI] private Button      DumpButton = null;
    [UI] private Button      SaveButton = null;
    [UI] private Button      ResetButton = null;

    // ── per (size, view) tunables ──
    private class Tune
    {
        public double OffsetX     = 25.0;   // circle cluster shift (subtracted)
        public double OffsetY     = 50.0;
        public double SizeScale   = 1.0;    // circle diameter multiplier
        public double Cluster     = 1.0;    // 1.0 = no clustering
        public double MapZoom     = 1.0;    // fraction of body the map fills
        public double MapX        = 0.0;    // map nudge, body pixels
        public double MapY        = 0.0;
        public double MapAlpha    = 0.7;
        public double FitMargin   = 40.0;   // Form2: panel - 40 (front: -100)

        public Tune Clone() { return (Tune)MemberwiseClone(); }
    }

    private readonly Dictionary<string, Tune> _tunes = new Dictionary<string, Tune>();
    private readonly Dictionary<string, SpinButton> _spins = new Dictionary<string, SpinButton>();

    private ComboBoxText _viewCombo, _sizeCombo, _bodyCombo, _caseCombo;
    private CheckButton  _showCircles, _showMap, _showBody, _showGuides;

    private JacketData _jacket;
    private List<CaseDefinition> _cases;

    private Gdk.Pixbuf _body, _map;
    private string _view = "front";
    private string _size = "L";
    private bool   _skeleton = true;
    private bool   _building;   // suppress handlers while repopulating spins

    private const int CircleDiameter = 50;
    private const string CalibFile = "calib.txt";

    private string Key { get { return _size + ":" + _view; } }
    private bool _listed;
    private Tune T
    {
        get
        {
            if (!_tunes.ContainsKey(Key)) _tunes[Key] = DefaultFor(_view);
            return _tunes[Key];
        }
    }

    private static Tune DefaultFor(string view)
    {
        var t = new Tune();
        switch (view)
        {
            case "back":  t.OffsetX = 20;  t.OffsetY = 20;   t.SizeScale = 0.5; break;
            case "right": t.OffsetX = 10;  t.OffsetY = -150; t.Cluster = 0.5;   break;
            case "left":  t.OffsetX = -50; t.OffsetY = -150; t.Cluster = 0.4;   break;
            default:      t.OffsetX = 25;  t.OffsetY = 50;   t.FitMargin = 100; break;
        }
        return t;
    }

    // ── construction ───────────────────────────────────────────────────

    public CalibrationWindow() : this(CreateBuilder()) { }

    private static Builder CreateBuilder()
    {
        var b = new Builder();
        b.AddFromFile("calibrate.glade");
        return b;
    }

    private CalibrationWindow(Builder builder) : base(builder.GetObject("CalibRoot").Handle)
    {
        builder.Autoconnect(this);

        _jacket = JacketData.Load();
        _cases  = CaseDefinition.LoadCasesFromCsv("cases.csv");

        BuildControls();
        LoadCalib();

        CalibArea.Drawn   += OnDraw;
        DumpButton.Clicked  += (s, e) => Emit(DumpConstants());
        SaveButton.Clicked  += (s, e) => SaveCalib();
        ResetButton.Clicked += (s, e) => { _tunes[Key] = DefaultFor(_view); SyncSpins(); Redraw(); };

        DeleteEvent += (o, a) => Application.Quit();

        ReloadImages();
        SyncSpins();
    }

    private void BuildControls()
    {
        ControlBox.PackStart(Header("Source"), false, false, 0);

        _viewCombo = Combo(new[] { "front", "back", "right", "left" }, 0, v =>
        {
            _view = v; ReloadImages(); SyncSpins(); Redraw();
        });
        ControlBox.PackStart(Row("View", _viewCombo), false, false, 0);

        _sizeCombo = Combo(new[] { "L", "XL" }, 0, v =>
        {
            _size = v; SyncSpins(); Redraw();
        });
        ControlBox.PackStart(Row("Jacket size", _sizeCombo), false, false, 0);

        _bodyCombo = Combo(new[] { "Skeleton", "Jacket" }, 0, v =>
        {
            _skeleton = (v == "Skeleton"); ReloadImages(); Redraw();
        });
        ControlBox.PackStart(Row("Body", _bodyCombo), false, false, 0);

        _caseCombo = new ComboBoxText();
        foreach (var c in _cases)
            _caseCombo.AppendText(c.Number + " " + string.Join(":", c.TreePath));
        if (_cases.Count > 0) _caseCombo.Active = 0;
        _caseCombo.Changed += (s, e) => { ReloadImages(); Redraw(); };
        ControlBox.PackStart(Row("Case", _caseCombo), false, false, 0);

        ControlBox.PackStart(Header("Sound map"), false, false, 0);
        AddSpin("MapZoom",  "Zoom (x body)", 0.10, 3.00, 0.01, 2);
        AddSpin("MapX",     "Offset X (px)", -600, 600, 1, 0);
        AddSpin("MapY",     "Offset Y (px)", -600, 600, 1, 0);
        AddSpin("MapAlpha", "Opacity",       0.00, 1.00, 0.05, 2);

        ControlBox.PackStart(Header("Circle cluster"), false, false, 0);
        AddSpin("OffsetX",   "Offset X (px)",  -600, 600, 1, 0);
        AddSpin("OffsetY",   "Offset Y (px)",  -600, 600, 1, 0);
        AddSpin("SizeScale", "Circle size",    0.05, 4.00, 0.05, 2);
        AddSpin("Cluster",   "Cluster factor", 0.05, 2.00, 0.05, 2);
        AddSpin("FitMargin", "Fit margin (px)", 0, 400, 5, 0);

        ControlBox.PackStart(Header("Display"), false, false, 0);
        _showBody    = Check("Show body",     true);
        _showMap     = Check("Show map",      true);
        _showCircles = Check("Show circles",  true);
        _showGuides  = Check("Show centre guides", true);
    }

    // ── widget helpers ─────────────────────────────────────────────────

    private Label Header(string text)
    {
        var l = new Label();
        l.Markup = "<b>" + text + "</b>";
        l.Xalign = 0;
        l.MarginTop = 8;
        return l;
    }

    private Box Row(string label, Widget w)
    {
        var box = new Box(Orientation.Horizontal, 6);
        var l = new Label(label);
        l.Xalign = 0;
        l.WidthRequest = 130;
        box.PackStart(l, false, false, 0);
        box.PackStart(w, true, true, 0);
        return box;
    }

    private ComboBoxText Combo(string[] items, int active, Action<string> onChange)
    {
        var c = new ComboBoxText();
        foreach (string s in items) c.AppendText(s);
        c.Active = active;
        c.Changed += (s, e) => { if (!_building && c.ActiveText != null) onChange(c.ActiveText); };
        return c;
    }

    private CheckButton Check(string label, bool active)
    {
        var c = new CheckButton(label);
        c.Active = active;
        c.Toggled += (s, e) => Redraw();
        ControlBox.PackStart(c, false, false, 0);
        return c;
    }

    private void AddSpin(string field, string label, double min, double max, double step, uint digits)
    {
        var sb = new SpinButton(min, max, step);
        sb.Digits = digits;
        sb.Numeric = true;
        sb.ValueChanged += (s, e) =>
        {
            if (_building) return;
            Set(field, sb.Value);
            Redraw();
        };
        _spins[field] = sb;
        ControlBox.PackStart(Row(label, sb), false, false, 0);
    }

    private void Set(string field, double v)
    {
        var t = T;
        switch (field)
        {
            case "OffsetX":   t.OffsetX   = v; break;
            case "OffsetY":   t.OffsetY   = v; break;
            case "SizeScale": t.SizeScale = v; break;
            case "Cluster":   t.Cluster   = v; break;
            case "MapZoom":   t.MapZoom   = v; break;
            case "MapX":      t.MapX      = v; break;
            case "MapY":      t.MapY      = v; break;
            case "MapAlpha":  t.MapAlpha  = v; break;
            case "FitMargin": t.FitMargin = v; break;
        }
    }

    private double Get(string field)
    {
        var t = T;
        switch (field)
        {
            case "OffsetX":   return t.OffsetX;
            case "OffsetY":   return t.OffsetY;
            case "SizeScale": return t.SizeScale;
            case "Cluster":   return t.Cluster;
            case "MapZoom":   return t.MapZoom;
            case "MapX":      return t.MapX;
            case "MapY":      return t.MapY;
            case "MapAlpha":  return t.MapAlpha;
            case "FitMargin": return t.FitMargin;
        }
        return 0;
    }

    private void SyncSpins()
    {
        _building = true;
        foreach (var kv in _spins) kv.Value.Value = Get(kv.Key);
        _building = false;
    }

    private void Redraw() { CalibArea.QueueDraw(); }

    // ── images ─────────────────────────────────────────────────────────

    private void ReloadImages()
    {
        if (_body != null) { _body.Dispose(); _body = null; }
        if (_map  != null) { _map.Dispose();  _map  = null; }

        _body = LoadBody();
        _map  = LoadMap();
    }

    private Gdk.Pixbuf LoadBody()
    {
        string stem;
        if (_skeleton)
        {
            switch (_view)
            {
                case "back":  stem = "jacketBackBody";  break;
                case "right": stem = "jacketRightBody"; break;
                case "left":  stem = "jacketLeftBody";  break;
                default:      stem = "jacketFrontBody"; break;
            }
        }
        else
        {
            switch (_view)
            {
                case "back":  stem = "jacketBack";  break;
                case "right": stem = "jacketRight"; break;
                case "left":  stem = "jacketLeft";  break;
                default:      stem = "jacketFront"; break;
            }
        }

        foreach (string ext in new[] { ".png", ".jpg", ".bmp" })
        {
            string p = IOPath.Combine("map", stem + ext);
            if (File.Exists(p)) return Safe(p);
        }

        Emit("body not found: map/" + stem + ".*");
        ListMapFolder();
        return null;
    }

    private void ListMapFolder()
    {
        if (_listed) return;
        _listed = true;

        Emit("cwd: " + Directory.GetCurrentDirectory());

        if (!Directory.Exists("map")) { Emit("map/ does not exist"); return; }

        var sb = new StringBuilder();
        sb.AppendLine("map/ contains:");
        foreach (string f in Directory.GetFiles("map"))
            sb.AppendLine("  " + IOPath.GetFileName(f));
        Emit(sb.ToString());
    }

    private Gdk.Pixbuf LoadMap()
    {
        if (_caseCombo == null || _caseCombo.Active < 0 || _caseCombo.Active >= _cases.Count)
            return null;

        var c = _cases[_caseCombo.Active];
        string rel;
        switch (_view)
        {
            case "back":  rel = c.MapBack;  break;
            case "right": rel = c.MapRight; break;
            case "left":  rel = c.MapLeft;  break;
            default:      rel = c.MapFront; break;
        }
        if (string.IsNullOrEmpty(rel)) rel = c.MapFront;
        if (string.IsNullOrEmpty(rel)) return null;

        string full = rel;
        if (!File.Exists(full)) { Emit("map not found: " + rel); return null; }

        var raw = Safe(full);
        if (raw == null) return null;

        var keyed = raw.AddAlpha(true, 0, 0, 0);
        raw.Dispose();
        return keyed;
    }

    private Gdk.Pixbuf Safe(string path)
    {
        try { return new Gdk.Pixbuf(path); }
        catch (Exception ex) { Emit("load failed " + path + ": " + ex.Message); return null; }
    }

    // ── the draw ───────────────────────────────────────────────────────

    private void OnDraw(object o, DrawnArgs args)
    {
        var cr = args.Cr;
        int w = CalibArea.AllocatedWidth, h = CalibArea.AllocatedHeight;

        cr.SetSourceRGB(1, 1, 1);
        cr.Rectangle(0, 0, w, h);
        cr.Fill();

        Gdk.Pixbuf refPb = _body ?? _map;
        if (refPb == null) { args.RetVal = true; return; }

        double s  = Math.Min((double)w / refPb.Width, (double)h / refPb.Height);
        double ox = (w - refPb.Width  * s) / 2.0;
        double oy = (h - refPb.Height * s) / 2.0;

        cr.Save();
        cr.Translate(ox, oy);
        cr.Scale(s, s);                      // body-pixel space

        if (_body != null && _showBody.Active)
        {
            Gdk.CairoHelper.SetSourcePixbuf(cr, _body, 0, 0);
            cr.Paint();
        }

        if (_map != null && _showMap.Active)
        {
            double ms = Math.Min((double)refPb.Width  / _map.Width,
                                 (double)refPb.Height / _map.Height) * T.MapZoom;
            double mw = _map.Width * ms, mh = _map.Height * ms;
            double mx = (refPb.Width  - mw) / 2.0 + T.MapX;
            double my = (refPb.Height - mh) / 2.0 + T.MapY;

            cr.Save();
            cr.Translate(mx, my);
            cr.Scale(ms, ms);
            Gdk.CairoHelper.SetSourcePixbuf(cr, _map, 0, 0);
            cr.PaintWithAlpha(T.MapAlpha);
            cr.Restore();

            if (_showGuides.Active)
            {
                cr.NewPath();
                cr.SetSourceRGBA(1, 0, 0, 0.6);
                cr.LineWidth = 1.5;
                cr.Rectangle(mx, my, mw, mh);
                cr.Stroke();
            }
        }

        if (_showCircles.Active) DrawCircles(cr, refPb);

        if (_showGuides.Active)
        {
            cr.NewPath();
            cr.SetSourceRGBA(0, 0, 1, 0.35);
            cr.LineWidth = 1.0;
            cr.MoveTo(refPb.Width / 2.0, 0);
            cr.LineTo(refPb.Width / 2.0, refPb.Height);
            cr.MoveTo(0, refPb.Height / 2.0);
            cr.LineTo(refPb.Width, refPb.Height / 2.0);
            cr.Stroke();
        }

        cr.Restore();
        args.RetVal = true;
    }

    private void DrawCircles(Cairo.Context cr, Gdk.Pixbuf refPb)
    {
        var circles = _jacket.Get(_size, _view);
        if (circles.Count == 0) return;

        int panelW = refPb.Width, panelH = refPb.Height;

        float minX = float.MaxValue, maxX = float.MinValue;
        float minY = float.MaxValue, maxY = float.MinValue;
        foreach (var c in circles)
        {
            if (c.X < minX) minX = c.X;
            if (c.X > maxX) maxX = c.X;
            if (c.Y < minY) minY = c.Y;
            if (c.Y > maxY) maxY = c.Y;
        }

        double availW = panelW - T.FitMargin, availH = panelH - T.FitMargin;
        const double circleRadius = 2.1 / 2;
        double dataW = (maxX - minX) + circleRadius * 2;
        double dataH = (maxY - minY) + circleRadius * 2;
        if (dataW <= 0 || dataH <= 0) return;

        double scale = Math.Min(availW / dataW, availH / dataH) * 0.9;

        double offX = (panelW - (maxX - minX) * scale) / 2 - minX * scale;
        double offY = (panelH - (maxY - minY) * scale) / 2 - minY * scale;

        // cluster centroid, in already-offset space
        double avgX = 0, avgY = 0;
        foreach (var c in circles)
        {
            avgX += c.X * scale + offX - T.OffsetX;
            avgY += c.Y * scale + offY - T.OffsetY;
        }
        avgX /= circles.Count;
        avgY /= circles.Count;

        cr.SelectFontFace("Sans", Cairo.FontSlant.Normal, Cairo.FontWeight.Bold);

        foreach (var c in circles)
        {
            double x = c.X * scale + offX - T.OffsetX;
            double y = c.Y * scale + offY - T.OffsetY;

            if (Math.Abs(T.Cluster - 1.0) > 0.001)
            {
                x = avgX + (x - avgX) * T.Cluster;
                y = avgY + (y - avgY) * T.Cluster;
            }

            double d = CircleDiameter * scale * T.SizeScale;
            double r = d / 2;

            cr.NewPath();
            cr.Arc(x, y, r, 0, 2 * Math.PI);
            cr.SetSourceRGBA(0.5, 0.5, 0.5, 1.0);
            cr.LineWidth = Math.Max(1.0, scale);
            cr.Stroke();

            string text = (c.Number % 1 == 0)
                ? ((int)c.Number).ToString()
                : c.Number.ToString("F1", CultureInfo.InvariantCulture);

            cr.SetFontSize(Math.Max(6.0, d / (c.Inner ? 3 : 4)));
            var ext = cr.TextExtents(text);
            cr.MoveTo(x - ext.Width / 2 - ext.XBearing, y + ext.Height / 2);
            cr.SetSourceRGBA(0.2, 0.2, 0.2, 1.0);
            cr.ShowText(text);
            cr.NewPath();
        }
    }

    // ── output ─────────────────────────────────────────────────────────

    private string DumpConstants()
    {
        var inv = CultureInfo.InvariantCulture;
        var sb = new StringBuilder();

        sb.AppendLine("// generated by CalibrationWindow " + DateTime.Now.ToString("yyyy-MM-dd HH:mm"));

        foreach (string size in new[] { "L", "XL" })
        {
            sb.AppendLine("// ---- " + size + " ----");
            foreach (string view in new[] { "front", "back", "right", "left" })
            {
                string k = size + ":" + view;
                if (!_tunes.ContainsKey(k)) continue;

                var t   = _tunes[k];
                string p = (size == "XL" ? "XL_" : "") + view.ToUpper();

                sb.AppendLine("private const float " + p + "_OFFSET_X   = " + t.OffsetX.ToString("F1", inv) + "f;");
                sb.AppendLine("private const float " + p + "_OFFSET_Y   = " + t.OffsetY.ToString("F1", inv) + "f;");
                sb.AppendLine("private const float " + p + "_SIZE       = " + t.SizeScale.ToString("F2", inv) + "f;");
                sb.AppendLine("private const float " + p + "_CLUSTER    = " + t.Cluster.ToString("F2", inv) + "f;");
                sb.AppendLine("private const float " + p + "_FIT_MARGIN = " + t.FitMargin.ToString("F0", inv) + "f;");
                sb.AppendLine("private const double " + p + "_MAP_ZOOM  = " + t.MapZoom.ToString("F3", inv) + ";");
                sb.AppendLine("private const double " + p + "_MAP_X     = " + t.MapX.ToString("F1", inv) + ";");
                sb.AppendLine("private const double " + p + "_MAP_Y     = " + t.MapY.ToString("F1", inv) + ";");
                sb.AppendLine("private const double " + p + "_MAP_ALPHA = " + t.MapAlpha.ToString("F2", inv) + ";");
                sb.AppendLine();
            }
        }

        return sb.ToString();
    }

    private void SaveCalib()
    {
        var inv = CultureInfo.InvariantCulture;
        var sb = new StringBuilder();

        sb.AppendLine("# size:view offsetX offsetY sizeScale cluster mapZoom mapX mapY mapAlpha fitMargin");
        foreach (var kv in _tunes)
        {
            var t = kv.Value;
            sb.AppendLine(string.Join("\t", new[]
            {
                kv.Key,
                t.OffsetX.ToString(inv),   t.OffsetY.ToString(inv),
                t.SizeScale.ToString(inv), t.Cluster.ToString(inv),
                t.MapZoom.ToString(inv),   t.MapX.ToString(inv),
                t.MapY.ToString(inv),      t.MapAlpha.ToString(inv),
                t.FitMargin.ToString(inv)
            }));
        }

        try
        {
            File.WriteAllText(CalibFile, sb.ToString());
            Emit("saved " + CalibFile);
        }
        catch (Exception ex) { Emit("save failed: " + ex.Message); }
    }

    private void LoadCalib()
    {
        string p = CalibFile;
        if (!File.Exists(p)) return;

        var inv = CultureInfo.InvariantCulture;
        foreach (string line in File.ReadAllLines(p))
        {
            if (line.StartsWith("#") || line.Trim().Length == 0) continue;
            string[] f = line.Split('\t');
            if (f.Length < 10) continue;

            var t = new Tune();
            try
            {
                t.OffsetX   = double.Parse(f[1], inv);
                t.OffsetY   = double.Parse(f[2], inv);
                t.SizeScale = double.Parse(f[3], inv);
                t.Cluster   = double.Parse(f[4], inv);
                t.MapZoom   = double.Parse(f[5], inv);
                t.MapX      = double.Parse(f[6], inv);
                t.MapY      = double.Parse(f[7], inv);
                t.MapAlpha  = double.Parse(f[8], inv);
                t.FitMargin = double.Parse(f[9], inv);
                _tunes[f[0]] = t;
            }
            catch { }
        }

        Emit("loaded " + CalibFile);
    }

    private void Emit(string msg)
    {
        OutputView.Buffer.Text = msg + "\n\n" + OutputView.Buffer.Text;
    }
}