using System;
using System.Collections.Generic;
using System.IO;
using UnityEngine;
using UnityEngine.UI;
using UnityEngine.EventSystems;


/// <summary>
/// Controller for ProgrammedMovements view: timeline banner at bottom.
/// 3 tracks, 0-10 seconds, movement blocks, Run button.
/// </summary>
public class ProgrammedMovementsController : MonoBehaviour
{
    public const int TracksCount = 4;
    public const int TimelineDurationSeconds = 10;
    public static readonly Color HighlightBlue = new Color(0.2f, 0.5f, 1f, 1f);

    [Header("References")]
    public Camera mainCamera;
    public RectTransform timelineRoot;
    public Button runButton;
    public Button saveButton;
    public Button loadButton;
    public GameObject filePickerPanel;
    public RectTransform filePickerContent;

    private RectTransform _bannerRoot;
    private Transform _timelineView;
    private Transform _configView;
    private readonly List<MovementBlock> _blocks = new List<MovementBlock>();
    private readonly Dictionary<int, List<MovementBlock>> _blocksBySecond = new Dictionary<int, List<MovementBlock>>();
    private bool _isRunning;
    private float _runStartTime;
    private int _lastExecutedSecond = -1;
    private float _lastRewireTime = -1f;
    private ModuleBase _highlightedModule;
    private readonly Dictionary<Renderer, Material[]> _originalMaterialsByRenderer = new Dictionary<Renderer, Material[]>();
    private MovementBlock _pickModeBlock;
    private MovementBlock _editingBlock;
    private ModuleBase _pendingModule;

    void OnEnable()
    {
        ResolveReferences();
        if (_bannerRoot == null)
        {
            Debug.LogError("[ProgrammedMovements] timelineRoot not set. Assign it in the Inspector.");
            return;
        }
        EnsureTimelineViewShown();
        WireUpButtons();
        WireSaveLoadButtons();
        RefreshRunButton();
        // Force layout rebuild so block positions align with timeline after view switch
        Canvas.ForceUpdateCanvases();
        LayoutRebuilder.ForceRebuildLayoutImmediate(_bannerRoot);
    }

    void EnsureTimelineViewShown()
    {
        if (_timelineView != null) _timelineView.gameObject.SetActive(true);
        if (_configView != null) _configView.gameObject.SetActive(false);
    }

    void ResolveReferences()
    {
        if (timelineRoot != null)
        {
            _bannerRoot = timelineRoot;
        }
        else
        {
            var pmView = transform.parent;
            if (pmView == null) return;
            var canvas = pmView.Find("ProgrammedMovementsCanvas");
            if (canvas == null) return;
            var banner = canvas.Find("TimelineBanner");
            if (banner != null)
            {
                _bannerRoot = banner.GetComponent<RectTransform>();
                if (timelineRoot == null) timelineRoot = _bannerRoot;
            }
        }
        if (_bannerRoot != null)
        {
            _timelineView = _bannerRoot.Find("TimelineView");
            _configView = _bannerRoot.Find("ConfigView");
            if (_timelineView == null)
            {
                _timelineView = _bannerRoot;
            }
        }
        if (runButton == null && _bannerRoot != null)
        {
            var canvas = _bannerRoot.parent;
            if (canvas != null)
            {
                var runGO = canvas.Find("RunButton");
                if (runGO != null) runButton = runGO.GetComponent<Button>();
            }
        }
    }

    private bool _saveLoadWired;

    void WireSaveLoadButtons()
    {
        if (_saveLoadWired) return;
        _saveLoadWired = true;
        if (saveButton != null)
            saveButton.onClick.AddListener(SaveTimelineToFile);
        if (loadButton != null)
            loadButton.onClick.AddListener(LoadTimelineFromFile);
    }

    void WireUpButtons()
    {
        WireUpRunButton();
        var tracks = _timelineView != null ? _timelineView.Find("TracksContainer") : null;
        if (tracks != null)
        {
            for (int t = 0; t < TracksCount; t++)
            {
                var track = tracks.GetChild(t);
                var addBtn = track.Find("AddButton")?.GetComponent<Button>();
                if (addBtn != null)
                {
                    int trackIndex = t;
                    addBtn.onClick.RemoveAllListeners();
                    addBtn.onClick.AddListener(() => AddBlock(trackIndex, 0));
                }
                var blocks = track.Find("Blocks");
                var clickHandler = blocks?.GetComponent<TrackClickHandler>();
                if (clickHandler != null)
                {
                    int trackIndex = t;
                    clickHandler.OnTrackClicked = (tr, sec) => AddBlock(tr, sec);
                }
            }
        }
        var config = _configView?.Find("ConfigContent");
        if (config != null)
        {
            var row1 = config.Find("ConfigRow1");
            var row2 = config.Find("ConfigRow2");
            var selectBtn = (row1 != null ? row1.Find("SelectButton") : config.Find("SelectButton"))?.GetComponent<Button>();
            if (selectBtn != null)
            {
                selectBtn.onClick.RemoveAllListeners();
                selectBtn.onClick.AddListener(OnConfigSelectClicked);
            }
            var setBtn = (row1 != null ? row1.Find("SetButton") : null)?.GetComponent<Button>();
            if (setBtn != null)
            {
                setBtn.onClick.RemoveAllListeners();
                setBtn.onClick.AddListener(OnConfigSetClicked);
            }
            var saveBtn = (row2 != null ? row2.Find("SaveButton") : config.Find("SaveButton"))?.GetComponent<Button>();
            if (saveBtn != null)
            {
                saveBtn.onClick.RemoveAllListeners();
                saveBtn.onClick.AddListener(OnConfigSaveClicked);
            }
            var backBtn = (row2 != null ? row2.Find("BackButton") : null)?.GetComponent<Button>();
            if (backBtn != null)
            {
                backBtn.onClick.RemoveAllListeners();
                backBtn.onClick.AddListener(OnConfigBackClicked);
            }
            var deleteBtn = (row2 != null ? row2.Find("DeleteButton") : config.Find("DeleteButton"))?.GetComponent<Button>();
            if (deleteBtn != null)
            {
                deleteBtn.onClick.RemoveAllListeners();
                deleteBtn.onClick.AddListener(OnConfigDeleteClicked);
            }
            var dirButtons = (row2 != null ? row2.Find("DirectionSection/DirectionButtons") : config.Find("DirectionSection/DirectionButtons"));
            var fwdBtn = dirButtons?.Find("ForwardButton")?.GetComponent<Button>();
            var bwdBtn = dirButtons?.Find("BackwardButton")?.GetComponent<Button>();
            if (fwdBtn != null)
            {
                fwdBtn.onClick.RemoveAllListeners();
                fwdBtn.onClick.AddListener(OnDirectionForwardClicked);
            }
            if (bwdBtn != null)
            {
                bwdBtn.onClick.RemoveAllListeners();
                bwdBtn.onClick.AddListener(OnDirectionBackwardClicked);
            }
        }
    }

    void OnDirectionForwardClicked()
    {
        if (_editingBlock == null) return;
        _editingBlock.dcDirection = 1;
        var config = _configView?.Find("ConfigContent");
        var dirButtons = config?.Find("ConfigRow2/DirectionSection/DirectionButtons") ?? config?.Find("DirectionSection/DirectionButtons");
        if (dirButtons != null) UpdateDirectionButtonsVisual(dirButtons, 1);
    }

    void OnDirectionBackwardClicked()
    {
        if (_editingBlock == null) return;
        _editingBlock.dcDirection = -1;
        var config = _configView?.Find("ConfigContent");
        var dirButtons = config?.Find("ConfigRow2/DirectionSection/DirectionButtons") ?? config?.Find("DirectionSection/DirectionButtons");
        if (dirButtons != null) UpdateDirectionButtonsVisual(dirButtons, -1);
    }

    void UpdateDirectionButtonsVisual(Transform dirButtons, int dcDirection)
    {
        var fwdImg = dirButtons.Find("ForwardButton")?.GetComponent<Image>();
        var bwdImg = dirButtons.Find("BackwardButton")?.GetComponent<Image>();
        var selected = new Color(0.2f, 0.6f, 0.4f, 1f);
        var normal = new Color(0.3f, 0.5f, 0.8f, 1f);
        if (fwdImg != null) fwdImg.color = dcDirection >= 0 ? selected : normal;
        if (bwdImg != null) bwdImg.color = dcDirection < 0 ? selected : normal;
    }

    void OnDisable()
    {
        _isRunning = false;
        ClearModuleHighlight();
    }

    void Update()
    {
        if (_isRunning)
        {
            float elapsed = Time.time - _runStartTime;
            if (elapsed >= TimelineDurationSeconds)
            {
                _isRunning = false;
                _lastExecutedSecond = -1;
                RefreshRunButton();
                Invoke(nameof(WireUpRunButton), 0.05f); // Delayed re-wire so Run works again
                return;
            }
            int currentSecond = Mathf.FloorToInt(elapsed);
            if (currentSecond != _lastExecutedSecond && _blocksBySecond.TryGetValue(currentSecond, out var list))
            {
                _lastExecutedSecond = currentSecond;
                foreach (var b in list)
                {
                    if (b.moduleServo != null)
                        b.moduleServo.SetAngleAndSendControlLibrary(b.targetAngle, 0f);
                    else if (b.moduleDC != null)
                        b.moduleDC.Rotate(b.targetAngle, b.dcDirection);
                }
            }
        }
        else
        {
            // Periodically re-wire Run button when idle (every 2s) so it stays clickable
            if (Time.time - _lastRewireTime > 2f)
            {
                _lastRewireTime = Time.time;
                WireUpRunButton();
            }
        }

        if (_pickModeBlock != null && Input.GetMouseButtonDown(0) && !EventSystem.current.IsPointerOverGameObject())
        {
            var cam = mainCamera != null ? mainCamera : Camera.main;
            if (cam != null && Physics.Raycast(cam.ScreenPointToRay(Input.mousePosition), out var hit))
            {
                var servo = hit.collider.GetComponentInParent<ServoMotorModule>();
                var dc = hit.collider.GetComponentInParent<DCMotorModule>();
                // Prefer DC when both exist (e.g. DC attached to servo arm) - user clicked the DC
                var module = (ModuleBase)dc ?? servo;
                if (module != null && IsInGeneratedTopology(module.transform))
                {
                    _pendingModule = module;
                    SetModuleHighlight(module);
                    UpdateConfigModuleLabel(GetModuleTypeName(module));
                }
                else
                {
                    _pendingModule = null;
                    ClearModuleHighlight();
                    UpdateConfigModuleLabel("None");
                }
            }
        }
    }

    GameObject CreateText(Transform parent, string text, int fontSize)
    {
        var go = new GameObject();
        go.transform.SetParent(parent, false);
        var txt = go.AddComponent<Text>();
        txt.font = Resources.GetBuiltinResource<Font>("LegacyRuntime.ttf");
        txt.text = text;
        txt.fontSize = fontSize;
        txt.color = Color.white;
        txt.alignment = TextAnchor.MiddleCenter;
        go.AddComponent<RectTransform>();
        return go;
    }

    GameObject CreateButton(Transform parent, string label, Action onClick)
    {
        var go = new GameObject();
        go.transform.SetParent(parent, false);
        var img = go.AddComponent<Image>();
        img.color = new Color(0.3f, 0.5f, 0.8f, 1f);
        var btn = go.AddComponent<Button>();
        btn.onClick.AddListener(() => onClick?.Invoke());
        var textGO = CreateText(go.transform, label, 14);
        var textRect = textGO.GetComponent<RectTransform>();
        textRect.anchorMin = Vector2.zero;
        textRect.anchorMax = Vector2.one;
        textRect.offsetMin = Vector2.zero;
        textRect.offsetMax = Vector2.zero;
        return go;
    }

    void AddBlock(int trackIndex, float second)
    {
        if (_bannerRoot == null) return;

        int sec = Mathf.Clamp(Mathf.FloorToInt(second), 0, TimelineDurationSeconds - 1);
        var block = new MovementBlock { trackIndex = trackIndex, second = sec, targetAngle = 90f };
        _blocks.Add(block);
        IndexBlock(block);

        var tracksRect = _timelineView?.Find("TracksContainer");
        if (tracksRect == null) return;
        var track = tracksRect.GetChild(trackIndex);
        var blocksHolder = track.Find("Blocks");
        if (blocksHolder == null) return;

        CreateMovementBlockUI(blocksHolder, block);
        OpenConfigPanel(block);
    }

    GameObject CreateMovementBlockUI(Transform parent, MovementBlock block)
    {
        var go = new GameObject($"Block_{block.second}s_T{block.trackIndex}");
        go.transform.SetParent(parent, false);

        var rect = go.AddComponent<RectTransform>();
        float x = (float)block.second / TimelineDurationSeconds;
        float w = 1f / TimelineDurationSeconds; // 1 second width to align with timeline
        rect.anchorMin = new Vector2(x, 0);
        rect.anchorMax = new Vector2(x + w, 1);
        rect.offsetMin = new Vector2(2, 2);
        rect.offsetMax = new Vector2(-2, -2);

        var bg = go.AddComponent<Image>();
        bg.color = new Color(0.35f, 0.5f, 0.7f, 0.95f);

        var btn = go.AddComponent<Button>();
        btn.onClick.AddListener(() => OpenConfigPanel(block));

        block.uiRoot = go;

        return go;
    }

    void OpenConfigPanel(MovementBlock block)
    {
        _editingBlock = block;
        _pendingModule = (ModuleBase)block.moduleServo ?? block.moduleDC;
        _pickModeBlock = null;
        if (_configView != null)
        {
            if (_timelineView != null) _timelineView.gameObject.SetActive(false);
            _configView.gameObject.SetActive(true);
            PopulateConfigPanel(block);
        }
        var mod = (ModuleBase)block.moduleServo ?? block.moduleDC;
        if (mod != null) SetModuleHighlight(mod);
        else ClearModuleHighlight();
    }

    void CloseConfigPanel()
    {
        _editingBlock = null;
        _pickModeBlock = null;
        _pendingModule = null;
        ClearModuleHighlight();
        if (_timelineView != null) _timelineView.gameObject.SetActive(true);
        if (_configView != null) _configView.gameObject.SetActive(false);
    }

    void PopulateConfigPanel(MovementBlock block)
    {
        var config = _configView?.Find("ConfigContent");
        if (config == null) return;

        var mod = (ModuleBase)block.moduleServo ?? block.moduleDC;
        var moduleLabel = config.Find("ConfigRow1/ModuleLabel") ?? config.Find("ModuleLabel");
        var ml = moduleLabel?.GetComponent<Text>();
        if (ml != null) ml.text = mod != null ? GetModuleTypeName(mod) : "None";

        var row2 = config.Find("ConfigRow2");
        var angleSection = row2?.Find("AngleSection") ?? config.Find("AngleSection");
        var angleInput = angleSection?.Find("AngleInput") ?? row2?.Find("AngleInput") ?? config.Find("AngleInput");
        var directionSection = row2?.Find("DirectionSection") ?? config.Find("DirectionSection");
        var dirButtons = directionSection?.Find("DirectionButtons");
        var dcDegreesInput = directionSection?.Find("DCDegreesInput");

        bool isDC = block.moduleDC != null;
        if (angleSection != null) angleSection.gameObject.SetActive(!isDC);
        if (directionSection != null) directionSection.gameObject.SetActive(isDC);

        if (!isDC)
        {
            var ai = angleInput?.GetComponent<InputField>();
            if (ai != null) ai.text = block.targetAngle.ToString("F0");
        }
        else
        {
            if (dirButtons != null) UpdateDirectionButtonsVisual(dirButtons, block.dcDirection);
            var dcd = dcDegreesInput?.GetComponent<InputField>();
            if (dcd != null) dcd.text = block.targetAngle.ToString("F0");
        }

        var secondInput = config.Find("ConfigRow2/SecondInput") ?? config.Find("SecondInput");
        var si = secondInput?.GetComponent<InputField>();
        if (si != null) si.text = block.second.ToString();
    }

    void UpdateConfigModuleLabel(string text)
    {
        var config = _configView?.Find("ConfigContent");
        var moduleLabel = config?.Find("ConfigRow1/ModuleLabel") ?? config?.Find("ModuleLabel");
        var ml = moduleLabel?.GetComponent<Text>();
        if (ml != null) ml.text = text;
    }

    static string GetModuleTypeName(ModuleBase m)
    {
        if (m == null) return "None";
        var t = m.GetType();
        if (t.Name == "ServoBendModule") return "Servo Bend";
        if (t.Name == "ServoStraightModule") return "Servo Straight";
        if (t.Name == "DCMotorModule") return "DC Motor";
        return t.Name.Replace("Module", "");
    }

    void OnConfigSelectClicked()
    {
        if (_editingBlock == null) return;
        _pickModeBlock = _editingBlock;
        _pendingModule = (ModuleBase)_editingBlock.moduleServo ?? _editingBlock.moduleDC;
        if (_pendingModule != null)
        {
            SetModuleHighlight(_pendingModule);
            UpdateConfigModuleLabel(GetModuleTypeName(_pendingModule));
        }
        else
        {
            ClearModuleHighlight();
            UpdateConfigModuleLabel("Click a module, then Set");
        }
    }

    void OnConfigSetClicked()
    {
        if (_editingBlock == null) return;
        var block = _editingBlock;
        block.moduleServo = null;
        block.moduleDC = null;
        if (_pendingModule is ServoMotorModule sm)
            block.moduleServo = sm;
        else if (_pendingModule is DCMotorModule dm)
            block.moduleDC = dm;
        _pickModeBlock = null;
        if (_pendingModule != null)
        {
            SetModuleHighlight(_pendingModule);
            UpdateConfigModuleLabel(GetModuleTypeName(_pendingModule));
        }
        else
        {
            ClearModuleHighlight();
            UpdateConfigModuleLabel("None");
        }
        PopulateConfigPanel(block);
    }

    void OnConfigBackClicked()
    {
        CloseConfigPanel();
    }

    void OnConfigSaveClicked()
    {
        if (_editingBlock == null) return;
        ApplyConfigToBlock();
        CloseConfigPanel();
    }

    void OnConfigDeleteClicked()
    {
        if (_editingBlock == null) return;
        var block = _editingBlock;
        UnindexBlock(block);
        _blocks.Remove(block);
        if (block.uiRoot != null) Destroy(block.uiRoot);
        CloseConfigPanel();
    }

    void ApplyConfigToBlock()
    {
        if (_editingBlock == null) return;
        var config = _configView?.Find("ConfigContent");
        if (config == null) return;

        var block = _editingBlock;
        var directionSection = config.Find("ConfigRow2/DirectionSection") ?? config.Find("DirectionSection");
        if (block.moduleDC == null)
        {
            var angleInput = config.Find("ConfigRow2/AngleSection/AngleInput") ?? config.Find("ConfigRow2/AngleInput") ?? config.Find("AngleInput");
            if (angleInput != null && float.TryParse(angleInput.GetComponent<InputField>()?.text, out float angle))
                block.targetAngle = Mathf.Clamp(angle, 0f, 180f);
        }
        else
        {
            var dcDegreesInput = directionSection?.Find("DCDegreesInput");
            if (dcDegreesInput != null && float.TryParse(dcDegreesInput.GetComponent<InputField>()?.text, out float degrees))
                block.targetAngle = Mathf.Clamp(degrees, 1f, 3600f);
        }

        var secondInput = config.Find("ConfigRow2/SecondInput") ?? config.Find("SecondInput");
        if (secondInput != null && int.TryParse(secondInput.GetComponent<InputField>()?.text, out int sec))
        {
            UnindexBlock(_editingBlock);
            _editingBlock.second = Mathf.Clamp(sec, 0, TimelineDurationSeconds - 1);
            IndexBlock(_editingBlock);
            UpdateBlockPosition(_editingBlock);
        }
    }

    GameObject CreateSimpleInput(Transform parent, string initial, Action<string> onEndEdit)
    {
        var go = new GameObject("Input");
        go.transform.SetParent(parent, false);
        var img = go.AddComponent<Image>();
        img.color = new Color(0.2f, 0.2f, 0.25f, 1f);
        var input = go.AddComponent<InputField>();
        var textGO = new GameObject("Text");
        textGO.transform.SetParent(go.transform, false);
        var text = textGO.AddComponent<Text>();
        text.font = Resources.GetBuiltinResource<Font>("LegacyRuntime.ttf");
        text.text = initial;
        text.fontSize = 11;
        text.color = Color.white;
        text.alignment = TextAnchor.MiddleCenter;
        text.horizontalOverflow = HorizontalWrapMode.Overflow;
        var textRect = textGO.GetComponent<RectTransform>();
        textRect.anchorMin = Vector2.zero;
        textRect.anchorMax = Vector2.one;
        textRect.offsetMin = new Vector2(4, 2);
        textRect.offsetMax = new Vector2(-4, -2);
        input.textComponent = text;
        input.text = initial;
        input.onEndEdit.AddListener((s) => onEndEdit?.Invoke(s));
        return go;
    }

    bool IsInGeneratedTopology(Transform t)
    {
        while (t != null)
        {
            if (t.name == "GeneratedTopology") return true;
            t = t.parent;
        }
        return false;
    }

    List<ModuleBase> GetModulesInTopology()
    {
        var list = new List<ModuleBase>();
        var root = GameObject.Find("GeneratedTopology")?.transform;
        if (root == null) return list;

        foreach (var s in root.GetComponentsInChildren<ServoMotorModule>(true))
            list.Add(s);
        foreach (var d in root.GetComponentsInChildren<DCMotorModule>(true))
            list.Add(d);
        return list;
    }

    void IndexBlock(MovementBlock b)
    {
        if (!_blocksBySecond.ContainsKey(b.second))
            _blocksBySecond[b.second] = new List<MovementBlock>();
        _blocksBySecond[b.second].Add(b);
    }

    void UnindexBlock(MovementBlock b)
    {
        if (_blocksBySecond.TryGetValue(b.second, out var list))
        {
            list.Remove(b);
            if (list.Count == 0) _blocksBySecond.Remove(b.second);
        }
    }

    void UpdateBlockPosition(MovementBlock b)
    {
        if (b.uiRoot == null) return;
        var rect = b.uiRoot.GetComponent<RectTransform>();
        if (rect == null) return;
        float x = (float)b.second / TimelineDurationSeconds;
        float w = 1f / TimelineDurationSeconds;
        rect.anchorMin = new Vector2(x, 0);
        rect.anchorMax = new Vector2(x + w, 1);
        b.uiRoot.name = $"Block_{b.second}s_T{b.trackIndex}";
    }

    void WireUpRunButton()
    {
        if (runButton != null)
        {
            runButton.onClick.RemoveAllListeners();
            runButton.onClick.AddListener(OnRunClicked);
        }
    }

    void OnRunClicked()
    {
        CancelInvoke(nameof(WireUpRunButton)); // Cancel any pending delayed re-wire
        _isRunning = true;
        _runStartTime = Time.time;
        _lastExecutedSecond = -1;
        RefreshRunButton();
    }

    void RefreshRunButton()
    {
        if (runButton != null)
        {
            runButton.interactable = true;
            var txt = runButton.GetComponentInChildren<Text>();
            if (txt != null) txt.text = _isRunning ? "Running" : "Run";
        }
    }

    void SetModuleHighlight(ModuleBase module)
    {
        ClearModuleHighlight();
        _highlightedModule = module;
        if (module == null) return;

        foreach (var r in GetRenderersOwnedByModule(module))
        {
            if (!_originalMaterialsByRenderer.ContainsKey(r))
                _originalMaterialsByRenderer[r] = r.sharedMaterials;
            var mats = r.materials;
            for (int i = 0; i < mats.Length; i++)
            {
                if (mats[i].HasProperty("_BaseColor")) mats[i].SetColor("_BaseColor", HighlightBlue);
                if (mats[i].HasProperty("_Color")) mats[i].SetColor("_Color", HighlightBlue);
            }
        }
    }

    void ClearModuleHighlight()
    {
        if (_highlightedModule == null) return;
        foreach (var kv in _originalMaterialsByRenderer)
        {
            if (kv.Key != null && kv.Value != null)
                kv.Key.sharedMaterials = kv.Value;
        }
        _originalMaterialsByRenderer.Clear();
        _highlightedModule = null;
    }

    static IEnumerable<Renderer> GetRenderersOwnedByModule(ModuleBase rootModule)
    {
        if (rootModule == null) yield break;
        var root = rootModule.transform;
        var stack = new Stack<Transform>();
        stack.Push(root);
        while (stack.Count > 0)
        {
            var t = stack.Pop();
            if (t != root)
            {
                var other = t.GetComponent<ModuleBase>();
                if (other != null && other != rootModule) continue;
            }
            foreach (var r in t.GetComponents<Renderer>())
                if (r != null) yield return r;
            for (int i = 0; i < t.childCount; i++)
                stack.Push(t.GetChild(i));
        }
    }

    [Serializable]
    public class MovementBlock
    {
        public int trackIndex;
        public int second;
        public float targetAngle;
        public ServoMotorModule moduleServo;
        public DCMotorModule moduleDC;
        public int dcDirection = 1; // 1 = forward, -1 = backward
        public GameObject uiRoot;
    }

    [Serializable]
    private class SavedMovement
    {
        public string ModuleId;
        public string ModuleType;
        public float Degree;
        public int Direction;
        public int Track;
    }

    [Serializable]
    private class SavedSecond
    {
        public int Second;
        public List<SavedMovement> Movements = new List<SavedMovement>();
    }

    [Serializable]
    private class SavedTimeline
    {
        public List<SavedSecond> Timeline = new List<SavedSecond>();
    }

    public void SaveTimelineToFile()
    {
        var bySecond = new Dictionary<int, List<MovementBlock>>();
        foreach (var b in _blocks)
        {
            if (!bySecond.ContainsKey(b.second))
                bySecond[b.second] = new List<MovementBlock>();
            bySecond[b.second].Add(b);
        }

        var saved = new SavedTimeline();
        var sortedSeconds = new List<int>(bySecond.Keys);
        sortedSeconds.Sort();
        foreach (int sec in sortedSeconds)
        {
            var entry = new SavedSecond { Second = sec };
            foreach (var b in bySecond[sec])
            {
                var m = new SavedMovement
                {
                    Degree = b.targetAngle,
                    Direction = b.dcDirection,
                    Track = b.trackIndex
                };
                if (b.moduleServo != null)
                {
                    m.ModuleId = b.moduleServo.moduleID;
                    m.ModuleType = "Servo";
                }
                else if (b.moduleDC != null)
                {
                    m.ModuleId = b.moduleDC.moduleID;
                    m.ModuleType = "DC";
                }
                entry.Movements.Add(m);
            }
            saved.Timeline.Add(entry);
        }

        string json = JsonUtility.ToJson(saved, true);
        string dir = Path.Combine(Application.dataPath, "ProgrammedMovements", "SavedTimelines");
        if (!Directory.Exists(dir)) Directory.CreateDirectory(dir);
        string timestamp = DateTime.Now.ToString("yyyy-MM-dd_HH-mm-ss");
        string filename = $"timeline_{timestamp}.json";
        string path = Path.Combine(dir, filename);
        File.WriteAllText(path, json);
        Debug.Log($"[ProgrammedMovements] Saved {_blocks.Count} movements to {path}");
    }

    public void LoadTimelineFromFile()
    {
        string dir = Path.Combine(Application.dataPath, "ProgrammedMovements", "SavedTimelines");
        if (!Directory.Exists(dir))
        {
            Debug.LogWarning($"[ProgrammedMovements] No saved timelines directory: {dir}");
            return;
        }
        var files = Directory.GetFiles(dir, "timeline_*.json");
        if (files.Length == 0)
        {
            Debug.LogWarning($"[ProgrammedMovements] No saved timelines found in {dir}");
            return;
        }
        Array.Sort(files);
        Array.Reverse(files);
        ShowFilePicker(files);
    }

    void ShowFilePicker(string[] filePaths)
    {
        if (filePickerPanel == null || filePickerContent == null) return;

        ClearFilePickerContent();
        EnsureScrollRect();

        float itemHeight = 32f;
        float gap = 4f;
        float totalHeight = filePaths.Length * (itemHeight + gap);
        filePickerContent.sizeDelta = new Vector2(0, totalHeight);

        for (int i = 0; i < filePaths.Length; i++)
        {
            string filePath = filePaths[i];
            string fileName = Path.GetFileNameWithoutExtension(filePath);

            var itemGo = new GameObject("Item_" + i);
            itemGo.transform.SetParent(filePickerContent, false);
            var itemRect = itemGo.AddComponent<RectTransform>();
            itemRect.anchorMin = new Vector2(0, 1);
            itemRect.anchorMax = new Vector2(1, 1);
            itemRect.pivot = new Vector2(0.5f, 1);
            itemRect.anchoredPosition = new Vector2(0, -(i * (itemHeight + gap)));
            itemRect.sizeDelta = new Vector2(-16, itemHeight);
            var itemImg = itemGo.AddComponent<Image>();
            itemImg.color = new Color(0.15f, 0.15f, 0.18f, 1f);
            var itemBtn = itemGo.AddComponent<Button>();
            itemBtn.targetGraphic = itemImg;
            var colors = itemBtn.colors;
            colors.highlightedColor = new Color(0.35f, 0.45f, 0.7f, 1f);
            colors.pressedColor = new Color(0.2f, 0.3f, 0.6f, 1f);
            itemBtn.colors = colors;
            itemBtn.onClick.AddListener(() => OnFileSelected(filePath));

            var labelGo = new GameObject("Label");
            labelGo.transform.SetParent(itemGo.transform, false);
            var labelRect = labelGo.AddComponent<RectTransform>();
            labelRect.anchorMin = Vector2.zero;
            labelRect.anchorMax = Vector2.one;
            labelRect.offsetMin = new Vector2(10, 0);
            labelRect.offsetMax = new Vector2(-10, 0);
            var labelTxt = labelGo.AddComponent<Text>();
            labelTxt.font = Resources.GetBuiltinResource<Font>("LegacyRuntime.ttf");
            labelTxt.text = fileName;
            labelTxt.fontSize = 13;
            labelTxt.alignment = TextAnchor.MiddleLeft;
            labelTxt.color = Color.white;
            labelTxt.horizontalOverflow = HorizontalWrapMode.Overflow;
        }

        filePickerPanel.SetActive(true);
    }

    void EnsureScrollRect()
    {
        var scrollArea = filePickerContent.parent;
        if (scrollArea == null) return;
        var scrollGo = scrollArea.gameObject;
        if (scrollGo.GetComponent<Mask>() == null)
            scrollGo.AddComponent<Mask>().showMaskGraphic = false;
        var sr = scrollGo.GetComponent<ScrollRect>();
        if (sr == null)
        {
            sr = scrollGo.AddComponent<ScrollRect>();
            sr.horizontal = false;
            sr.vertical = true;
            sr.content = filePickerContent;
            sr.movementType = ScrollRect.MovementType.Clamped;
        }
    }

    void ClearFilePickerContent()
    {
        if (filePickerContent == null) return;
        for (int i = filePickerContent.childCount - 1; i >= 0; i--)
            Destroy(filePickerContent.GetChild(i).gameObject);
    }

    void OnFileSelected(string filePath)
    {
        DismissFilePicker();
        LoadTimelineFromPath(filePath);
    }

    public void DismissFilePicker()
    {
        ClearFilePickerContent();
        if (filePickerPanel != null)
            filePickerPanel.SetActive(false);
    }

    void LoadTimelineFromPath(string path)
    {
        string json = File.ReadAllText(path);
        Debug.Log($"[ProgrammedMovements] Loading from: {path}");

        var saved = JsonUtility.FromJson<SavedTimeline>(json);
        if (saved == null || saved.Timeline == null || saved.Timeline.Count == 0)
        {
            Debug.LogWarning("[ProgrammedMovements] Save file is empty or invalid.");
            return;
        }

        foreach (var b in _blocks)
            if (b.uiRoot != null) Destroy(b.uiRoot);
        _blocks.Clear();
        _blocksBySecond.Clear();

        ResolveReferences();
        EnsureTimelineViewShown();

        var tracksRect = _timelineView?.Find("TracksContainer");
        if (tracksRect == null)
        {
            Debug.LogError("[ProgrammedMovements] TracksContainer not found, cannot load.");
            return;
        }

        int loaded = 0;
        foreach (var entry in saved.Timeline)
        {
            foreach (var m in entry.Movements)
            {
                int trackIndex = Mathf.Clamp(m.Track, 0, TracksCount - 1);

                var block = new MovementBlock
                {
                    trackIndex = trackIndex,
                    second = Mathf.Clamp(entry.Second, 0, TimelineDurationSeconds - 1),
                    targetAngle = m.Degree,
                    dcDirection = m.Direction
                };

                if (!string.IsNullOrEmpty(m.ModuleId) && TopologyBuilder.idToInstance.TryGetValue(m.ModuleId, out var go))
                {
                    if (m.ModuleType == "DC")
                        block.moduleDC = go.GetComponent<DCMotorModule>();
                    else
                        block.moduleServo = go.GetComponent<ServoMotorModule>();
                }
                else if (!string.IsNullOrEmpty(m.ModuleId))
                {
                    Debug.LogWarning($"[ProgrammedMovements] Module '{m.ModuleId}' not found in topology.");
                }

                _blocks.Add(block);
                IndexBlock(block);

                var track = tracksRect.GetChild(trackIndex);
                var blocksHolder = track.Find("Blocks");
                if (blocksHolder == null) continue;
                CreateMovementBlockUI(blocksHolder, block);
                loaded++;
            }
        }

        Debug.Log($"[ProgrammedMovements] Loaded {loaded} movements from {Path.GetFileName(path)}");
    }
}
