using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Globalization;
using System.IO;
using System.Reflection;
using System.Text;
using UnityEngine;
using Debug = UnityEngine.Debug;

namespace ServiceSim
{
    /// <summary>
    /// 見せるための録画（2026-10-29・依頼 26「LP用に店2の直す前と後を録画する」）。
    ///
    /// 引数に -ssRecord が付いたときだけ立つ。夜の回・段階B・エディタの Play では何もしない。
    /// シーンには置かない（起動したあとに自分で GameObject を1つ作る）。
    ///
    /// 撮り方
    ///   ・店の中の時刻（stops.csv の t_s と同じ。Time.time − 回し始めの時刻）が
    ///     from 〜 to の間、step 秒ごとに1コマ撮る。
    ///     回は1コマ 0.05 秒固定（Time.captureDeltaTime）で進むので、
    ///     撮る重さで店の中の動きは変わらない（実時間が延びるだけ）。
    ///   ・自分で作った真上のカメラ（正投影・ふだんは切）を、撮るコマだけ
    ///     RenderTexture に描いて読み、ffmpeg に生のまま渡して mp4 にする。
    ///     画面の上の文字（OnGUI）は RenderTexture に入らない。
    ///   ・店のもの・人・ロボット・ほかのカメラには触らない。
    ///     見た目は Main Camera の設定（背景色・映すもの）を写して使う。
    ///   ・光：影を落とす Directional Light（Setup の Sun）の高さだけを上げて影を短くする
    ///     （2026-10-29・依頼 28。地面からの角度 52° → 75°。方角・やわらかさ・濃さ・色・明るさは今のまま）。
    ///     撮り始めるときに変える。-ssRecord の無い回（夜の回・確かめの回・エディタの Play）では変わらない。
    ///
    /// 引数
    ///   -ssRecord {名前の頭}     … mp4 の名前の頭（例 20261029_1930_lpdemo_after）
    ///   -ssRecFrom 900 -ssRecTo 1650 -ssRecStep 0.2
    ///   -ssRecFps 30 -ssRecCrf 16
    ///   -ssRecFfmpeg {ffmpeg のパス}
    ///   -ssRecView name,x0,x1,y0,y1,w,h   … 撮る範囲（店の座標 m）と大きさ。何本でも
    ///   -ssRecSunPitch 75                 … 録画のときの光の地面からの角度（既定 75。依頼 28）
    ///   -ssRecStillAt 1500 -ssRecStillPitches 70,75,80
    ///                                     … 静止画だけ書く回（依頼 28）。店の時刻がこの秒に来たコマを、
    ///                                       光の角度ごとに PNG で書いて、回を途中で終える（mp4 は作らない。
    ///                                       台帳には書かれず、記録は {run_stem}_stopped に残る）
    ///
    /// 置き場所：その回の出力フォルダ（HeadlessRun が決めた run_stem）の下の recordings/。
    ///   recordings/ がもうあれば撮らない（上書きしない）。
    /// </summary>
    public class SimTimeRecorder : MonoBehaviour
    {
        const float CamHeight = 60f;
        /// <summary>録画のときの光の地面からの角度（依頼 28。ふだんの Sun は 52°）</summary>
        const float DefaultSunPitch = 75f;

        class View
        {
            public string name;
            public float x0, x1, y0, y1;
            public int w, h;
            public Camera cam;
            public RenderTexture rt;
            public Texture2D tex;
            public Process proc;
            public Stream stdin;
            public string mp4;
            public string err = "";
            public StringBuilder ffLog = new StringBuilder();
        }

        string _prefix = "", _ffmpeg = "ffmpeg";
        float _from = 900f, _to = 1650f, _step = 0.2f;
        int _fps = 30, _crf = 16;
        float _sunPitch = DefaultSunPitch;
        float _stillAt = -1f;                       // 0 以上なら静止画だけ書く回
        readonly List<float> _stillPitches = new List<float>();
        Light _sun;
        readonly List<View> _views = new List<View>();

        ExperimentRunner _runner;
        HeadlessRun _headless;
        FieldInfo _fOffset, _fJobRunning, _fRunDir;

        bool _started, _done, _failed;
        int _next;                // 次に撮るコマの番号（0 から）
        string _dir = "";
        StringWriter _csv;
        int _frames;

        [RuntimeInitializeOnLoadMethod(RuntimeInitializeLoadType.AfterSceneLoad)]
        static void Boot()
        {
            string[] argv;
            try { argv = Environment.GetCommandLineArgs(); }
            catch (Exception) { return; }
            if (Find(argv, "-ssRecord") == null) return;

            GameObject go = new GameObject("SimTimeRecorder");
            SimTimeRecorder r = go.AddComponent<SimTimeRecorder>();
            r.Setup(argv);
        }

        void Setup(string[] argv)
        {
            _prefix = Find(argv, "-ssRecord") ?? "";
            _ffmpeg = Find(argv, "-ssRecFfmpeg") ?? "ffmpeg";
            _from = F(Find(argv, "-ssRecFrom"), _from);
            _to = F(Find(argv, "-ssRecTo"), _to);
            _step = F(Find(argv, "-ssRecStep"), _step);
            _fps = (int)F(Find(argv, "-ssRecFps"), _fps);
            _crf = (int)F(Find(argv, "-ssRecCrf"), _crf);
            _sunPitch = F(Find(argv, "-ssRecSunPitch"), _sunPitch);
            _stillAt = F(Find(argv, "-ssRecStillAt"), -1f);
            string pitches = Find(argv, "-ssRecStillPitches");
            if (pitches != null)
                foreach (string p in pitches.Split(',')) _stillPitches.Add(F(p, _sunPitch));
            if (_stillAt >= 0f)
            {
                if (_stillPitches.Count == 0) _stillPitches.Add(_sunPitch);
                _from = _stillAt; _to = _stillAt + _step;   // 1コマだけ撮る（_to <= _from の確かめに掛からないように）
            }

            for (int i = 0; i < argv.Length - 1; i++)
            {
                if (argv[i] != "-ssRecView") continue;
                string[] p = argv[i + 1].Split(',');
                if (p.Length != 7) { Log("-ssRecView が読めません: " + argv[i + 1]); continue; }
                View v = new View();
                v.name = p[0];
                v.x0 = F(p[1], 0); v.x1 = F(p[2], 0); v.y0 = F(p[3], 0); v.y1 = F(p[4], 0);
                v.w = (int)F(p[5], 0); v.h = (int)F(p[6], 0);
                _views.Add(v);
            }

            _fOffset = typeof(ExperimentRunner).GetField("_timeOffset", BindingFlags.Instance | BindingFlags.NonPublic);
            _fJobRunning = typeof(ExperimentRunner).GetField("_jobRunning", BindingFlags.Instance | BindingFlags.NonPublic);
            _fRunDir = typeof(HeadlessRun).GetField("_runDir", BindingFlags.Instance | BindingFlags.NonPublic);

            if (_prefix.Length == 0 || _views.Count == 0 || _step <= 0f || _to <= _from ||
                _fOffset == null || _fJobRunning == null || _fRunDir == null)
            {
                Log("撮れません（引数か、ExperimentRunner / HeadlessRun の中の名前が合いません）。この回は撮らずに回します");
                FailSetup();
                return;
            }
            if (SystemInfo.graphicsDeviceType == UnityEngine.Rendering.GraphicsDeviceType.Null)
            {
                Log("描画の装置がありません（-nographics では撮れません）。この回は撮らずに回します");
                FailSetup();
                return;
            }
            if (_stillAt >= 0f)
            {
                Log("静止画だけ書きます: 店の時刻 " + _stillAt + " 秒・光の角度 " + string.Join(",", _stillPitches) +
                    "°・" + _views.Count + " 本。書いたら回を途中で終えます");
                return;
            }
            Log("撮ります: 店の時刻 " + _from + "〜" + _to + " 秒・" + _step + " 秒ごと・" + _fps + " コマ/秒・CRF " + _crf +
                "・" + _views.Count + " 本");
        }

        void LateUpdate()
        {
            if (_failed || _done) return;

            if (_runner == null) _runner = FindObjectOfType<ExperimentRunner>();
            if (_headless == null) _headless = FindObjectOfType<HeadlessRun>();
            if (_runner == null || _headless == null) return;
            if (!(bool)_fJobRunning.GetValue(_runner)) return;

            float t = Time.time - (float)_fOffset.GetValue(_runner);
            float target = _from + _next * _step;
            if (target > _to + 1e-3f) { Close(); return; }
            if (t < target - 1e-3f) return;

            if (!_started && !Begin()) { _failed = true; QuitIfStill(); return; }

            if (_stillAt >= 0f) { WriteStills(t); return; }

            for (int i = 0; i < _views.Count; i++) Grab(_views[i]);
            _csv.Write(_frames.ToString(CultureInfo.InvariantCulture));
            _csv.Write(',');
            _csv.Write(t.ToString("0.###", CultureInfo.InvariantCulture));
            _csv.Write('\n');
            _frames++;
            _next++;
            // 1コマ 0.05 秒で進むので、ふつうは1つずつ。万一飛んでも、次の目標へ進める
            while (_from + _next * _step <= t + 1e-3f) _next++;
        }

        bool Begin()
        {
            _started = true;
            string runDir = _fRunDir.GetValue(_headless) as string;
            // 出力フォルダは回の終わりに ExperimentRunner が作る（Directory.CreateDirectory なので、
            // 先にあっても困らない）。無いことは HeadlessRun が回し始める前に確かめてある
            if (string.IsNullOrEmpty(runDir))
            {
                Log("出力フォルダの名前が分かりません。撮りません");
                return false;
            }
            _dir = Path.Combine(runDir, "recordings");
            if (Directory.Exists(_dir))
            {
                Log("recordings/ がもうあります。上書きしないので撮りません: " + _dir);
                return false;
            }
            Directory.CreateDirectory(_dir);

            _sun = FindSun();
            if (_sun == null) Log("影を落とす Directional Light が1つに決まりません。光は変えずに撮ります");
            else SetSunPitch(_sunPitch);

            Camera main = Camera.main;
            for (int i = 0; i < _views.Count; i++)
            {
                View v = _views[i];
                GameObject go = new GameObject("RecCam_" + v.name);
                go.transform.SetParent(transform, false);
                Camera c = go.AddComponent<Camera>();
                if (main != null) c.CopyFrom(main);
                c.enabled = false;
                c.orthographic = true;
                c.orthographicSize = (v.y1 - v.y0) * 0.5f;
                c.aspect = (float)v.w / v.h;
                c.nearClipPlane = 0.1f;
                c.farClipPlane = CamHeight + 20f;
                c.transform.position = new Vector3((v.x0 + v.x1) * 0.5f, CamHeight, (v.y0 + v.y1) * 0.5f);
                c.transform.rotation = Quaternion.Euler(90f, 0f, 0f);
                v.rt = new RenderTexture(v.w, v.h, 24, RenderTextureFormat.ARGB32);
                v.rt.antiAliasing = 8;
                v.rt.Create();
                c.targetTexture = v.rt;
                v.cam = c;
                v.tex = new Texture2D(v.w, v.h, TextureFormat.RGB24, false);
                if (_stillAt >= 0f) continue;   // 静止画だけの回は ffmpeg を立てない

                v.mp4 = Path.Combine(_dir, _prefix + "_" + v.name + ".mp4");
                string args = "-hide_banner -loglevel error -y -f rawvideo -pix_fmt rgb24 -s " + v.w + "x" + v.h +
                              " -r " + _fps + " -i - -vf vflip -c:v libx264 -preset slow -crf " + _crf +
                              " -pix_fmt yuv420p -movflags +faststart \"" + v.mp4 + "\"";
                ProcessStartInfo psi = new ProcessStartInfo(_ffmpeg, args);
                psi.UseShellExecute = false;
                psi.RedirectStandardInput = true;
                psi.RedirectStandardError = true;
                psi.CreateNoWindow = true;
                v.proc = new Process();
                v.proc.StartInfo = psi;
                View vv = v;
                v.proc.ErrorDataReceived += (s, e) => { if (e.Data != null) lock (vv.ffLog) vv.ffLog.AppendLine(e.Data); };
                v.proc.Start();
                v.proc.BeginErrorReadLine();
                v.stdin = v.proc.StandardInput.BaseStream;
            }

            _csv = new StringWriter(CultureInfo.InvariantCulture);
            _csv.Write("frame,t_s\n");
            Log("撮り始めました: " + _dir);
            return true;
        }

        void FailSetup()
        {
            _failed = true;
            QuitIfStill();
        }

        /// <summary>静止画だけの回は、撮れないなら最後まで回さずに終える（台帳に行を残さない）</summary>
        void QuitIfStill()
        {
            if (_stillAt < 0f) return;
            Log("静止画だけの回なので、撮れないまま回さずに終えます");
            Application.Quit(1);
        }

        /// <summary>影を落とす Directional Light（Setup の Sun）。1つに決まらなければ null</summary>
        static Light FindSun()
        {
            Light found = null;
            foreach (Light l in FindObjectsOfType<Light>())
            {
                if (l.type != LightType.Directional || l.shadows == LightShadows.None) continue;
                if (found != null) return null;
                found = l;
            }
            return found;
        }

        /// <summary>地面からの角度だけを変える（方角＝y の回転・z の回転はそのまま）</summary>
        void SetSunPitch(float pitch)
        {
            if (_sun == null) return;
            Vector3 e = _sun.transform.eulerAngles;
            float before = e.x;
            _sun.transform.rotation = Quaternion.Euler(pitch, e.y, e.z);
            Log("光の角度: " + _sun.name + " " + before.ToString("0.#", CultureInfo.InvariantCulture) + "° → " +
                pitch.ToString("0.#", CultureInfo.InvariantCulture) + "°（方角 " +
                e.y.ToString("0.#", CultureInfo.InvariantCulture) + "° のまま）");
        }

        /// <summary>静止画だけの回：光の角度ごとに PNG を書いて、回を途中で終える</summary>
        void WriteStills(float t)
        {
            _done = true;
            StringBuilder note = new StringBuilder();
            for (int k = 0; k < _stillPitches.Count; k++)
            {
                float pitch = _stillPitches[k];
                SetSunPitch(pitch);
                for (int i = 0; i < _views.Count; i++)
                {
                    View v = _views[i];
                    v.cam.Render();
                    RenderTexture prev = RenderTexture.active;
                    RenderTexture.active = v.rt;
                    v.tex.ReadPixels(new Rect(0, 0, v.w, v.h), 0, 0, false);
                    v.tex.Apply(false);
                    RenderTexture.active = prev;
                    string png = Path.Combine(_dir, _prefix + "_" + v.name + "_t" +
                                              t.ToString("0.##", CultureInfo.InvariantCulture) + "_sun" +
                                              pitch.ToString("0.#", CultureInfo.InvariantCulture) + ".png");
                    File.WriteAllBytes(png, v.tex.EncodeToPNG());
                    note.Append("\n  ").Append(png);
                }
            }
            for (int i = 0; i < _views.Count; i++) if (_views[i].rt != null) _views[i].rt.Release();
            Log("静止画を書きました（店の時刻 " + t.ToString("0.###", CultureInfo.InvariantCulture) + " 秒）" + note +
                "\n  回を途中で終えます（台帳には書きません）");
            Application.Quit(0);
        }

        void Grab(View v)
        {
            if (v.stdin == null) return;
            v.cam.Render();
            RenderTexture prev = RenderTexture.active;
            RenderTexture.active = v.rt;
            v.tex.ReadPixels(new Rect(0, 0, v.w, v.h), 0, 0, false);
            RenderTexture.active = prev;
            byte[] raw = v.tex.GetRawTextureData();
            try { v.stdin.Write(raw, 0, raw.Length); }
            catch (Exception e) { v.err = e.Message; v.stdin = null; }
        }

        void Close()
        {
            if (_done) return;
            _done = true;
            if (!_started) return;

            for (int i = 0; i < _views.Count; i++)
            {
                View v = _views[i];
                try { if (v.stdin != null) { v.stdin.Flush(); v.stdin.Close(); } } catch (Exception e) { v.err = e.Message; }
            }
            StringBuilder note = new StringBuilder();
            for (int i = 0; i < _views.Count; i++)
            {
                View v = _views[i];
                try { if (v.proc != null) v.proc.WaitForExit(); } catch (Exception) { }
                int code = -1;
                try { code = v.proc.ExitCode; } catch (Exception) { }
                File.WriteAllText(Path.Combine(_dir, _prefix + "_" + v.name + "_frames.csv"), _csv.ToString(),
                                  new UTF8Encoding(false));
                note.Append("\n  ").Append(v.mp4).Append("（").Append(v.w).Append('×').Append(v.h)
                    .Append("・ffmpeg 終了コード ").Append(code)
                    .Append(v.err.Length > 0 ? "・" + v.err : "")
                    .Append(v.ffLog.Length > 0 ? "・" + v.ffLog.ToString().Trim() : "").Append('）');
                if (v.rt != null) v.rt.Release();
            }
            Log("撮り終わりました: " + _frames + " コマ" + note);
        }

        void OnApplicationQuit() { Close(); }
        void OnDestroy() { Close(); }

        static string Find(string[] argv, string name)
        {
            for (int i = 0; i < argv.Length - 1; i++)
                if (argv[i] == name) return argv[i + 1];
            return null;
        }

        static float F(string s, float def)
        {
            float v;
            return s != null && float.TryParse(s, NumberStyles.Float, CultureInfo.InvariantCulture, out v) ? v : def;
        }

        static void Log(string msg) { Debug.Log("[ServiceSim/rec] " + msg); }
    }
}
