using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text.RegularExpressions;
using TMPro;
using UnityEditor;
using UnityEditor.SceneManagement;
using UnityEngine;
using UnityEngine.Playables;
using UnityEngine.Timeline;

namespace Streamingle.Subtitles.Editor
{
    public static class SrtSubtitleInstaller
    {
        public const string GeneratedTrackName = "Subtitles [Generated]";

        private static readonly Regex TimestampPattern = new Regex(
            @"^(?<sh>\d+):(?<sm>\d{2}):(?<ss>\d{2}),(?<sms>\d{3})\s*-->\s*(?<eh>\d+):(?<em>\d{2}):(?<es>\d{2}),(?<ems>\d{3})$",
            RegexOptions.Compiled | RegexOptions.CultureInvariant);

        public static void Install(
            PlayableDirector director,
            TMP_Text target,
            string srtAssetPath,
            double offsetSeconds)
        {
            if (director == null)
                throw new ArgumentNullException(nameof(director));
            if (target == null)
                throw new ArgumentNullException(nameof(target));
            if (director.playableAsset is not TimelineAsset timeline)
                throw new InvalidOperationException("The PlayableDirector must reference a TimelineAsset.");
            if (double.IsNaN(offsetSeconds) || double.IsInfinity(offsetSeconds))
                throw new ArgumentOutOfRangeException(nameof(offsetSeconds));

            var fullSrtPath = ResolveProjectPath(srtAssetPath);
            if (!File.Exists(fullSrtPath))
                throw new FileNotFoundException("SRT file was not found.", fullSrtPath);

            var cues = Parse(File.ReadAllText(fullSrtPath));
            if (cues.Count == 0)
                throw new InvalidDataException("The SRT file contains no subtitle cues.");

            var undoGroup = Undo.GetCurrentGroup();
            Undo.SetCurrentGroupName("Install generated subtitle track");

            try
            {
                Undo.RegisterCompleteObjectUndo(timeline, "Replace generated subtitle track");
                Undo.RecordObject(director, "Bind generated subtitle track");

                RemoveGeneratedTracks(timeline, director);
                var track = timeline.CreateTrack<SubtitleTrack>(null, GeneratedTrackName);

                foreach (var cue in cues)
                {
                    var start = Math.Max(0d, cue.StartSeconds + offsetSeconds);
                    var clip = track.CreateClip<SubtitleClip>();
                    clip.start = start;
                    clip.duration = cue.EndSeconds - cue.StartSeconds;
                    clip.displayName = CreateDisplayName(cue.Text);

                    if (clip.asset is SubtitleClip subtitleAsset)
                    {
                        subtitleAsset.Text = cue.Text;
                        EditorUtility.SetDirty(subtitleAsset);
                    }
                }

                director.SetGenericBinding(track, target);

                EditorUtility.SetDirty(track);
                EditorUtility.SetDirty(timeline);
                EditorUtility.SetDirty(director);
                EditorSceneManager.MarkSceneDirty(director.gameObject.scene);

                AssetDatabase.SaveAssetIfDirty(timeline);
                director.RebuildGraph();
                director.Evaluate();
                SceneView.RepaintAll();
            }
            finally
            {
                Undo.CollapseUndoOperations(undoGroup);
            }
        }

        public static IReadOnlyList<SrtCue> Parse(string srtContents)
        {
            if (string.IsNullOrWhiteSpace(srtContents))
                return Array.Empty<SrtCue>();

            var normalized = srtContents.Replace("\r\n", "\n").Replace('\r', '\n').Trim();
            var blocks = Regex.Split(normalized, @"\n{2,}");
            var cues = new List<SrtCue>(blocks.Length);

            for (var blockIndex = 0; blockIndex < blocks.Length; blockIndex++)
            {
                var lines = blocks[blockIndex]
                    .Split('\n')
                    .Select(line => line.TrimEnd())
                    .ToArray();

                var timestampLineIndex = Array.FindIndex(lines, line => line.Contains("-->"));
                if (timestampLineIndex < 0 || timestampLineIndex + 1 >= lines.Length)
                    throw new FormatException($"Invalid SRT cue at block {blockIndex + 1}.");

                var match = TimestampPattern.Match(lines[timestampLineIndex].Trim());
                if (!match.Success)
                    throw new FormatException($"Invalid SRT timestamp at block {blockIndex + 1}.");

                var start = ParseTimestamp(match, "s");
                var end = ParseTimestamp(match, "e");
                if (end <= start)
                    throw new FormatException($"SRT cue {blockIndex + 1} must end after it starts.");

                var text = string.Join("\n", lines.Skip(timestampLineIndex + 1)).Trim();
                if (text.Length == 0)
                    continue;

                if (cues.Count > 0 && start < cues[cues.Count - 1].EndSeconds)
                    throw new FormatException($"SRT cue {blockIndex + 1} overlaps the previous cue.");

                cues.Add(new SrtCue(start, end, text));
            }

            return cues;
        }

        private static void RemoveGeneratedTracks(TimelineAsset timeline, PlayableDirector director)
        {
            var generatedTracks = timeline.GetRootTracks()
                .OfType<SubtitleTrack>()
                .Where(track => track.name == GeneratedTrackName)
                .ToArray();

            foreach (var track in generatedTracks)
            {
                director.ClearGenericBinding(track);
                timeline.DeleteTrack(track);
            }
        }

        private static string ResolveProjectPath(string path)
        {
            if (string.IsNullOrWhiteSpace(path))
                throw new ArgumentException("An SRT asset path is required.", nameof(path));

            if (Path.IsPathRooted(path))
                return Path.GetFullPath(path);

            var projectRoot = Directory.GetParent(Application.dataPath)?.FullName
                ?? throw new InvalidOperationException("Could not resolve the Unity project root.");
            return Path.GetFullPath(Path.Combine(projectRoot, path));
        }

        private static double ParseTimestamp(Match match, string prefix)
        {
            var hours = int.Parse(match.Groups[prefix + "h"].Value);
            var minutes = int.Parse(match.Groups[prefix + "m"].Value);
            var seconds = int.Parse(match.Groups[prefix + "s"].Value);
            var milliseconds = int.Parse(match.Groups[prefix + "ms"].Value);
            return hours * 3600d + minutes * 60d + seconds + milliseconds / 1000d;
        }

        private static string CreateDisplayName(string text)
        {
            var singleLine = Regex.Replace(text ?? string.Empty, @"\s+", " ").Trim();
            return singleLine.Length <= 36 ? singleLine : singleLine.Substring(0, 35) + "…";
        }

        public readonly struct SrtCue
        {
            public SrtCue(double startSeconds, double endSeconds, string text)
            {
                StartSeconds = startSeconds;
                EndSeconds = endSeconds;
                Text = text;
            }

            public double StartSeconds { get; }
            public double EndSeconds { get; }
            public string Text { get; }
        }
    }
}
