import os
import json
import threading
import shutil
import subprocess
import wx
import wx.media

AUDIO_FORMATS = ["mp3", "wav", "aac", "flac", "ogg", "m4a"]
VIDEO_FORMATS = ["mp4", "mkv", "mov", "webm"]


class ConverterFrame(wx.Frame):
    def __init__(self):
        super().__init__(parent=None, title="Audio/Video Converter", size=(640, 380))
        panel = wx.Panel(self)
        label_color = wx.SystemSettings.GetColour(wx.SYS_COLOUR_WINDOWTEXT)

        self.input_picker = wx.FilePickerCtrl(
            panel,
            message="Select input file",
            style=wx.FLP_OPEN | wx.FLP_FILE_MUST_EXIST | wx.FLP_USE_TEXTCTRL,
        )
        self.mode_choice = wx.Choice(panel, choices=["Audio", "Video"])
        self.mode_choice.SetSelection(0)
        self.format_choice = wx.Choice(panel, choices=AUDIO_FORMATS)
        self.format_choice.SetSelection(0)
        self.output_picker = wx.FilePickerCtrl(
            panel,
            message="Select output file",
            style=wx.FLP_SAVE | wx.FLP_OVERWRITE_PROMPT | wx.FLP_USE_TEXTCTRL,
        )
        self.audio_bitrate = wx.TextCtrl(panel, value="", size=(120, -1))
        self.video_bitrate = wx.TextCtrl(panel, value="", size=(120, -1))
        self.activation_bytes = wx.TextCtrl(panel, value="", size=(120, -1))
        self.copy_streams = wx.CheckBox(panel, label="Co&py streams (fast, no re-encode)")
        self.copy_streams.SetForegroundColour(label_color)
        self.split_mp3_choice = wx.Choice(panel, choices=["No", "Yes"])
        self.split_mp3_choice.SetSelection(0)
        self.start_btn = wx.Button(panel, label="&Convert")
        self.log_ctrl = wx.TextCtrl(panel, style=wx.TE_MULTILINE | wx.TE_READONLY)
        self.status = wx.StaticText(panel, label="Ready")
        self.status.SetForegroundColour(label_color)

        self.media = wx.media.MediaCtrl(panel, style=wx.SIMPLE_BORDER)
        self.play_btn = wx.Button(panel, label="&Play/Pause")
        self.stop_btn = wx.Button(panel, label="S&top")
        self.position_slider = wx.Slider(panel, minValue=0, maxValue=1000, style=wx.SL_HORIZONTAL)
        self.time_label = wx.StaticText(panel, label="00:00 / 00:00")
        self.time_label.SetForegroundColour(label_color)
        self.media_length_ms = 0
        self.dragging_slider = False
        self.pending_play = False
        self.media_loaded = False
        self.loaded_media_path = ""
        self.player_timer = wx.Timer(self)

        self.audio_bitrate.SetHint("e.g. 192k")
        self.video_bitrate.SetHint("e.g. 2000k")
        self.activation_bytes.SetHint("e.g. 1")
        self.audio_bitrate.SetName("Audio bitrate")
        self.video_bitrate.SetName("Video bitrate")
        self.activation_bytes.SetName("Activation bytes")
        self.configure_file_picker_accessibility(self.input_picker, "Input file")
        self.mode_choice.SetName("Conversion mode")
        self.format_choice.SetName("Output format")
        self.configure_file_picker_accessibility(self.output_picker, "Output file")
        self.copy_streams.SetName("Copy streams")
        self.split_mp3_choice.SetName("Split MP3 into captures")
        self.start_btn.SetName("Convert")
        self.play_btn.SetName("Play or pause")
        self.stop_btn.SetName("Stop playback")
        self.input_picker.SetHelpText("Input file")
        self.mode_choice.SetHelpText("Conversion mode")
        self.format_choice.SetHelpText("Output format")
        self.output_picker.SetHelpText("Output file")
        self.audio_bitrate.SetHelpText("Audio bitrate")
        self.video_bitrate.SetHelpText("Video bitrate")
        self.activation_bytes.SetHelpText("Activation bytes")
        self.copy_streams.SetHelpText("Copy streams")
        self.split_mp3_choice.SetHelpText("Split MP3 output into captures based on chapters")
        self.start_btn.SetHelpText("Convert")
        self.play_btn.SetHelpText("Play or pause")
        self.stop_btn.SetHelpText("Stop playback")
        self.media.SetMinSize((200, 36))
        if hasattr(self.media, "SetVolume"):
            self.media.SetVolume(1.0)

        form = wx.BoxSizer(wx.VERTICAL)
        self.add_labeled_control(panel, form, "&Input file", self.input_picker, "Input file")
        self.add_labeled_control(panel, form, "&Mode", self.mode_choice, "Conversion mode")
        self.add_labeled_control(panel, form, "F&ormat", self.format_choice, "Output format")
        self.add_labeled_control(panel, form, "O&utput file", self.output_picker, "Output file")
        self.add_labeled_control(panel, form, "Audio &bitrate", self.audio_bitrate, "Audio bitrate")
        self.add_labeled_control(panel, form, "Video b&itrate", self.video_bitrate, "Video bitrate")
        self.add_labeled_control(panel, form, "A&ctivation bytes", self.activation_bytes, "Activation bytes")
        form.Add(self.copy_streams, 0, wx.EXPAND | wx.BOTTOM, 6)
        self.add_labeled_control(panel, form, "Split MP3 into &captures", self.split_mp3_choice, "Split MP3 into captures")

        main = wx.BoxSizer(wx.VERTICAL)
        main.Add(form, 0, wx.EXPAND | wx.ALL, 10)
        main.Add(self.start_btn, 0, wx.LEFT | wx.RIGHT | wx.BOTTOM, 10)
        main.Add(self.build_player(panel), 0, wx.EXPAND | wx.LEFT | wx.RIGHT | wx.BOTTOM, 10)
        main.Add(self.log_ctrl, 1, wx.EXPAND | wx.LEFT | wx.RIGHT, 10)
        main.Add(self.status, 0, wx.LEFT | wx.RIGHT | wx.BOTTOM, 10)

        panel.SetSizer(main)

        self.mode_choice.Bind(wx.EVT_CHOICE, self.on_mode_change)
        self.format_choice.Bind(wx.EVT_CHOICE, self.on_format_change)
        self.input_picker.Bind(wx.EVT_FILEPICKER_CHANGED, self.on_input_change)
        self.start_btn.Bind(wx.EVT_BUTTON, self.on_start)

        self.play_btn.Bind(wx.EVT_BUTTON, self.on_play_pause)
        self.stop_btn.Bind(wx.EVT_BUTTON, self.on_stop)
        self.position_slider.Bind(wx.EVT_SLIDER, self.on_seek)
        self.position_slider.Bind(wx.EVT_SCROLL_THUMBRELEASE, self.on_seek_release)
        self.Bind(wx.media.EVT_MEDIA_LOADED, self.on_media_loaded, self.media)
        self.Bind(wx.media.EVT_MEDIA_FINISHED, self.on_media_finished, self.media)
        self.Bind(wx.EVT_TIMER, self.on_player_timer, self.player_timer)

        self.setup_hotkeys()

        self.update_format_choices()
        self.Center()
        self.player_timer.Start(250)

    def on_mode_change(self, _event):
        self.update_format_choices()
        self.suggest_output_path()

    def on_format_change(self, _event):
        self.suggest_output_path()

    def on_input_change(self, _event):
        self.suggest_output_path()
        self.load_media_from_input()

    def update_format_choices(self):
        mode = self.mode_choice.GetStringSelection()
        if mode == "Audio":
            choices = AUDIO_FORMATS
        else:
            choices = VIDEO_FORMATS
        self.format_choice.Set(choices)
        self.format_choice.SetSelection(0)
        self.update_split_option_state()

    def suggest_output_path(self):
        in_path = self.input_picker.GetPath()
        if not in_path:
            return
        out_ext = self.format_choice.GetStringSelection()
        base, _ = os.path.splitext(in_path)
        suggested = base + "." + out_ext
        self.output_picker.SetPath(suggested)
        self.update_split_option_state()

    def update_split_option_state(self):
        is_mp3 = self.format_choice.GetStringSelection().lower() == "mp3"
        self.split_mp3_choice.Enable(is_mp3)
        if not is_mp3:
            self.split_mp3_choice.SetSelection(0)

    def build_player(self, panel):
        player = wx.BoxSizer(wx.VERTICAL)

        controls = wx.BoxSizer(wx.HORIZONTAL)
        controls.Add(self.play_btn, 0, wx.RIGHT, 8)
        controls.Add(self.stop_btn, 0, wx.RIGHT, 8)
        controls.Add(self.time_label, 0, wx.ALIGN_CENTER_VERTICAL)

        player.Add(self.media, 0, wx.EXPAND | wx.BOTTOM, 6)
        player.Add(self.position_slider, 0, wx.EXPAND | wx.BOTTOM, 6)
        player.Add(controls, 0, wx.EXPAND | wx.BOTTOM, 4)

        return player

    def add_labeled_control(self, panel, parent_sizer, label, control, accessible_name=None):
        label_ctrl = wx.StaticText(panel, label=label)
        label_ctrl.SetForegroundColour(wx.SystemSettings.GetColour(wx.SYS_COLOUR_WINDOWTEXT))
        clean_label = label.replace("&", "")
        if accessible_name is None:
            accessible_name = clean_label
        # Keep a stable, human-readable name/description for assistive technologies.
        control.SetName(accessible_name)
        control.SetHelpText(accessible_name)
        label_ctrl.SetName(clean_label)
        # Keep label and field adjacent in tab order for native AT label lookup.
        if hasattr(label_ctrl, "MoveBeforeInTabOrder"):
            label_ctrl.MoveBeforeInTabOrder(control)
        field_sizer = wx.BoxSizer(wx.VERTICAL)
        field_sizer.Add(label_ctrl, 0, wx.BOTTOM, 2)
        field_sizer.Add(control, 0, wx.EXPAND)
        parent_sizer.Add(field_sizer, 0, wx.EXPAND | wx.BOTTOM, 6)

    def configure_file_picker_accessibility(self, picker, label):
        picker.SetName(label)
        picker.SetHelpText(label)

        text_ctrl = picker.GetTextCtrl()
        if text_ctrl:
            text_ctrl.SetName(f"{label} path")
            text_ctrl.SetHelpText(label)

        button = picker.GetPickerCtrl()
        if button:
            button.SetName(f"Browse {label}")
            button.SetHelpText(f"Browse {label}")

    def setup_hotkeys(self):
        self.ID_PLAYPAUSE = wx.NewIdRef()
        self.ID_STOP = wx.NewIdRef()
        self.ID_CONVERT = wx.NewIdRef()
        self.ID_SEEK_BACK = wx.NewIdRef()
        self.ID_SEEK_FWD = wx.NewIdRef()

        entries = [
            wx.AcceleratorEntry(wx.ACCEL_NORMAL, wx.WXK_SPACE, self.ID_PLAYPAUSE),
            wx.AcceleratorEntry(wx.ACCEL_CTRL, ord("S"), self.ID_STOP),
            wx.AcceleratorEntry(wx.ACCEL_CTRL, wx.WXK_RETURN, self.ID_CONVERT),
            wx.AcceleratorEntry(wx.ACCEL_ALT, wx.WXK_LEFT, self.ID_SEEK_BACK),
            wx.AcceleratorEntry(wx.ACCEL_ALT, wx.WXK_RIGHT, self.ID_SEEK_FWD),
        ]

        self.SetAcceleratorTable(wx.AcceleratorTable(entries))
        self.Bind(wx.EVT_MENU, self.on_play_pause, id=self.ID_PLAYPAUSE)
        self.Bind(wx.EVT_MENU, self.on_stop, id=self.ID_STOP)
        self.Bind(wx.EVT_MENU, self.on_start, id=self.ID_CONVERT)
        self.Bind(wx.EVT_MENU, self.on_seek_back, id=self.ID_SEEK_BACK)
        self.Bind(wx.EVT_MENU, self.on_seek_forward, id=self.ID_SEEK_FWD)

    def load_media_from_input(self):
        path = self.input_picker.GetPath()
        if not path:
            return
        self.load_media(path)

    def load_media(self, path):
        if not os.path.isfile(path):
            return
        self.media_length_ms = 0
        self.media_loaded = False
        self.loaded_media_path = path
        self.position_slider.SetRange(0, 1000)
        self.position_slider.SetValue(0)
        self.update_time_label(0)
        if not self.media.Load(path):
            self.pending_play = False
            self.append_log("Unable to load media: " + path)
        else:
            self.media_loaded = True

    def on_media_loaded(self, _event):
        self.media_loaded = True
        length = self.media.Length()
        if length and length > 0:
            self.media_length_ms = length
            self.position_slider.SetRange(0, length)
            self.update_time_label(0)
        if self.pending_play:
            self.pending_play = False
            self.media.Play()

    def on_media_finished(self, _event):
        self.update_time_label(self.media_length_ms)
        if self.media_length_ms > 0:
            self.position_slider.SetValue(self.media_length_ms)

    def on_player_timer(self, _event):
        if self.dragging_slider or self.media_length_ms <= 0:
            return
        if self.media.GetState() == wx.media.MEDIASTATE_PLAYING:
            position = self.media.Tell()
            if position >= 0:
                self.position_slider.SetValue(position)
                self.update_time_label(position)

    def on_play_pause(self, _event):
        path = self.input_picker.GetPath()
        if not path:
            return

        state = self.media.GetState()
        if state == wx.media.MEDIASTATE_PLAYING:
            self.pending_play = False
            self.media.Pause()
            return

        if (not self.media_loaded) or (self.loaded_media_path != path):
            self.load_media(path)

        if self.media.Play():
            self.pending_play = False
        else:
            self.pending_play = True

    def on_stop(self, _event):
        self.pending_play = False
        self.media.Stop()
        self.position_slider.SetValue(0)
        self.update_time_label(0)

    def on_seek(self, _event):
        self.dragging_slider = True
        if self.media_length_ms <= 0:
            return
        self.update_time_label(self.position_slider.GetValue())

    def on_seek_release(self, _event):
        self.dragging_slider = False
        if self.media_length_ms <= 0:
            return
        self.media.Seek(self.position_slider.GetValue())

    def on_seek_back(self, _event):
        self.seek_relative(-5000)

    def on_seek_forward(self, _event):
        self.seek_relative(5000)

    def seek_relative(self, delta_ms):
        if self.media_length_ms <= 0:
            return
        position = self.media.Tell()
        if position < 0:
            position = 0
        new_pos = max(0, min(self.media_length_ms, position + delta_ms))
        self.media.Seek(new_pos)
        self.position_slider.SetValue(new_pos)
        self.update_time_label(new_pos)

    def update_time_label(self, position_ms):
        total = self.media_length_ms
        self.time_label.SetLabel(
            f"{self.format_time(position_ms)} / {self.format_time(total)}"
        )

    def format_time(self, millis):
        seconds = max(0, int(millis / 1000))
        minutes = seconds // 60
        seconds = seconds % 60
        return f"{minutes:02d}:{seconds:02d}"

    def on_start(self, _event):
        if not shutil.which("ffmpeg"):
            wx.MessageBox("ffmpeg not found on PATH.", "Error", wx.ICON_ERROR)
            return

        in_path = self.input_picker.GetPath()
        out_path = self.output_picker.GetPath()
        if not in_path or not out_path:
            wx.MessageBox("Please choose input and output files.", "Error", wx.ICON_ERROR)
            return

        self.log_ctrl.Clear()
        self.status.SetLabel("Running...")
        self.start_btn.Disable()

        thread = threading.Thread(target=self.run_ffmpeg, args=(in_path, out_path), daemon=True)
        thread.start()

    def run_ffmpeg(self, in_path, out_path):
        mode = self.mode_choice.GetStringSelection()
        a_bitrate = self.audio_bitrate.GetValue().strip()
        v_bitrate = self.video_bitrate.GetValue().strip()
        activation_bytes = self.activation_bytes.GetValue().strip()
        copy = self.copy_streams.GetValue()
        split_mp3 = self.split_mp3_choice.GetStringSelection().lower() == "yes"
        output_format = self.format_choice.GetStringSelection().lower()
        should_split_mp3 = split_mp3 and output_format == "mp3"

        cmd = ["ffmpeg", "-y"]
        if activation_bytes:
            cmd += ["-activation_bytes", activation_bytes]
        cmd += ["-i", in_path]

        if mode == "Audio":
            cmd.append("-vn")
            if copy:
                cmd += ["-c:a", "copy"]
            elif a_bitrate:
                cmd += ["-b:a", a_bitrate]
        else:
            if copy:
                cmd += ["-c:v", "copy", "-c:a", "copy"]
            else:
                if v_bitrate:
                    cmd += ["-b:v", v_bitrate]
                if a_bitrate:
                    cmd += ["-b:a", a_bitrate]

        if should_split_mp3:
            chapter_times = self.get_chapter_end_times(in_path)
            if chapter_times:
                output_pattern = self.build_capture_output_pattern(out_path)
                cmd += [
                    "-f",
                    "segment",
                    "-segment_times",
                    ",".join(chapter_times),
                    "-reset_timestamps",
                    "1",
                    "-segment_format",
                    "mp3",
                    output_pattern,
                ]
            else:
                self.append_log("No chapter markers found; writing a single MP3 file.")
                cmd.append(out_path)
        else:
            cmd.append(out_path)

        self.append_log("Command: " + " ".join(cmd))

        try:
            proc = subprocess.Popen(
                cmd,
                stdout=subprocess.PIPE,
                stderr=subprocess.STDOUT,
                text=True,
                bufsize=1,
                universal_newlines=True,
            )
            for line in proc.stdout:
                self.append_log(line.rstrip())
            proc.wait()
            if proc.returncode == 0:
                self.finish("Done")
            else:
                self.finish("Failed")
        except Exception as exc:
            self.append_log(f"Error: {exc}")
            self.finish("Failed")

    def append_log(self, text):
        wx.CallAfter(self.log_ctrl.AppendText, text + "\n")

    def finish(self, status_text):
        wx.CallAfter(self.status.SetLabel, status_text)
        wx.CallAfter(self.start_btn.Enable)

    def build_capture_output_pattern(self, out_path):
        base, ext = os.path.splitext(out_path)
        return f"{base}_capture_%03d{ext}"

    def get_chapter_end_times(self, in_path):
        if not shutil.which("ffprobe"):
            self.append_log("ffprobe not found on PATH; cannot split by chapters.")
            return []
        cmd = [
            "ffprobe",
            "-v",
            "error",
            "-print_format",
            "json",
            "-show_chapters",
            "-i",
            in_path,
        ]
        try:
            proc = subprocess.run(
                cmd,
                stdout=subprocess.PIPE,
                stderr=subprocess.STDOUT,
                text=True,
                check=False,
            )
            if proc.returncode != 0:
                self.append_log("ffprobe failed; cannot split by chapters.")
                return []
            data = json.loads(proc.stdout or "{}")
        except Exception as exc:
            self.append_log(f"ffprobe error: {exc}")
            return []

        chapters = data.get("chapters", [])
        end_times = []
        for chapter in chapters:
            end_time = chapter.get("end_time")
            if end_time is None:
                continue
            try:
                end_times.append(float(end_time))
            except (TypeError, ValueError):
                continue
        if len(end_times) <= 1:
            return []
        end_times = end_times[:-1]
        formatted = []
        for value in end_times:
            formatted.append(f"{value:.3f}".rstrip("0").rstrip("."))
        return formatted


class App(wx.App):
    def OnInit(self):
        frame = ConverterFrame()
        frame.Show()
        return True


if __name__ == "__main__":
    app = App()
    app.MainLoop()
