# lyrics_page.py

from gi.repository import Gtk, GObject, GLib, Gio, Pango, Gst
from ...integrations import get_current_integration
from ..lyrics.helpers import get_lyrics
from ...constants import DATA_DIR, CONTEXT_LYRICS
from ..containers import ContextContainer
import threading, os, re

class LyricRow(Gtk.ListBoxRow):
    __gtype_name__ = 'NocturneLyricRow'

    ms = GObject.Property(type=int)

    def __init__(self, content:str, ms:int, duration_ms:int):
        super().__init__(
            ms=ms,
            child=Gtk.Label(
                wrap=True,
                wrap_mode=Pango.WrapMode.WORD_CHAR,
                justify=Gtk.Justification.CENTER,
                halign=Gtk.Align.CENTER,
                valign=Gtk.Align.CENTER
            )
        )

        pattern = r"<(\d{2}):(\d{2})\.(\d{2,3})>\s*([^<]*)"
        matches = re.findall(pattern, content)
        self.cues = {}
        if len(matches) > 0:
            for m, s, ms_part, text in matches:
                if text:
                    minutes = int(m)
                    seconds = int(s)
                    fraction = int(ms_part)
                    if len(ms_part) == 2:
                        fraction *= 10
                    total_ms = (minutes * 60000) + (seconds * 1000) + fraction
                    self.cues[total_ms] = text
        if len(self.cues) == 0:
            if content:
                if duration_ms == 0:
                    self.cues[ms] = content
                else:
                    words = content.split(' ')
                    weights = []
                    for word in words:
                        word = word.replace('.', '...') # Add more time
                        word = word.replace(',', '..') # Add more time
                        word = ''.join(c for c in word if c not in '¡!¿?\'"') # Ignore characters
                        weights.append(len(word))

                    ms_per_letter = int(duration_ms / sum(weights))
                    last_timestamp = 0
                    for i, word in enumerate(words):
                        if i == 0:
                            timestamp = ms
                        else:
                            timestamp = int(last_timestamp + weights[i-1] * ms_per_letter)
                        last_timestamp = timestamp
                        self.cues[timestamp] = word + ' '
            else:
                self.cues[ms] = "🎵" if duration_ms > 1000 else ""

        self.sorted_ts = sorted(self.cues.keys())
        self.alphas = {ts: 0.3 for ts in self.sorted_ts}
        self.last_ms = 0
        self.add_tick_callback(self.on_frame_tick)
        self.on_frame_tick(self, None, True)

    def on_frame_tick(self, widget, frame_clock, force:bool=False):
        lerp_speed = 0.1
        changed = False
        parts = []

        for ts in self.sorted_ts:
            word = GLib.markup_escape_text(self.cues[ts])
            target = 1.0 if self.last_ms >= ts else 0.3
            current = self.alphas[ts]

            if abs(current - target) > 0.001:
                self.alphas[ts] = current + (target - current) * lerp_speed
                changed = True

            alpha_val = int(self.alphas[ts] * 65536) # Pango uses that number as max for some reason
            parts.append(f"<span fgalpha='{alpha_val}'>{word}</span>")

        if changed or force:
            self.get_child().set_markup("".join(parts))
        return self.get_root()


@Gtk.Template(resource_path='/com/jeffser/Nocturne/playing/lyrics_page.ui')
class PlayingLyricsPage(Gtk.Stack):
    __gtype_name__ = 'NocturnePlayingLyricsPage'

    plain_label_el = Gtk.Template.Child()
    lrc_list_el = Gtk.Template.Child()
    scrolledwindow = Gtk.Template.Child()
    lrc_menu_popover = Gtk.Template.Child()
    code_is_selecting = False # used so that `on_lrc_selection` is only executed when manually selecting
    last_best_match = None

    def __init__(self):
        super().__init__()
        context = CONTEXT_LYRICS.copy()
        context["remove"]["connection"] = self.go_to_main
        self.lrc_menu_popover.set_child(ContextContainer(context))

    def setup(self):
        # Called after login
        integration = get_current_integration()
        integration.connect_to_model('currentSong', 'songId', self.song_changed)
        integration.connect_to_model('currentSong', 'positionSeconds', self.position_changed)

    def song_changed(self, song_id:str, lrclib_download:bool=False):
        self.set_visible_child_name('loading')
        def update_lyrics():
            lyrics = get_lyrics(song_id, lrclib_download)
            GLib.idle_add(self.set_visible_child_name, lyrics.get('type'))

            if lyrics.get('type') == 'plain':
                GLib.idle_add(self.plain_label_el.set_label, lyrics.get('content'))
            elif lyrics.get('type') == 'lrc':
                GLib.idle_add(self.lrc_list_el.remove_all)
                if lyrics.get('content')[0].get('content'):
                    lyrics['content'].insert(0, {'content': '', 'ms': 0})
                settings = Gio.Settings(schema_id="com.jeffser.Nocturne")
                simulate_wbwl = settings.get_value('simulate-word-by-word-lyrics').unpack()
                content = lyrics.get('content')
                for i, line in enumerate(content):
                    if simulate_wbwl:
                        duration_ms = 1000
                        if i + 1 < len(content):
                            duration_ms = content[i+1].get('ms') - line.get('ms')
                        else:
                            integration = get_current_integration()
                            if model := integration.loaded_models.get(song_id):
                                duration_ms = model.get_property('duration') * 1000 - line.get('ms')
                    else:
                        duration_ms = 0
                    row = LyricRow(
                        ms=line.get('ms'),
                        content=line.get('content', ''),
                        duration_ms=duration_ms
                    )
                    GLib.idle_add(self.lrc_list_el.append, row)

        threading.Thread(target=update_lyrics, daemon=True).start()

    def position_changed(self, position_seconds:float):
        if self.get_visible_child_name() == 'lrc':
            ms = int(position_seconds * 1000)+100
            best_match = None
            for row in list(self.lrc_list_el):
                if row.ms <= ms:
                    best_match = row
                else:
                    break
            if best_match:
                if best_match == self.lrc_list_el.get_selected_row():
                    best_match.last_ms = ms
                else:
                    self.code_is_selecting = True
                    self.lrc_list_el.select_row(best_match)
                    self.code_is_selecting = False

    @Gtk.Template.Callback()
    def on_lrc_selection(self, list_el, position):
        row = list_el.get_selected_row()
        if self.last_best_match:
            self.last_best_match.last_ms = -1
        self.last_best_match = row
        if row:
            if not self.code_is_selecting:
                self.get_root().get_application().player.gst.seek_simple(
                    Gst.Format.TIME,
                    Gst.SeekFlags.FLUSH | Gst.SeekFlags.KEY_UNIT,
                    int(row.ms/1000 * Gst.SECOND)
                )
            def do_scroll():
                vadj = self.scrolledwindow.get_vadjustment()
                row_alloc = row.get_allocation()
                viewport_height = self.scrolledwindow.get_height()
                row_y = row_alloc.y
                row_height = row_alloc.height
                target = row_y - (viewport_height / 2) + (row_height / 2)
                lower = vadj.get_lower()
                upper = vadj.get_upper() - vadj.get_page_size()
                target = max(lower, min(target, upper))
                vadj.set_value(target)

            GLib.idle_add(do_scroll)

    @Gtk.Template.Callback()
    def lyric_download_requested(self, button):
        integration = get_current_integration()
        self.song_changed(integration.loaded_models.get('currentSong').get_property('songId'), True)

    def get_lrc_path(self) -> str:
        integration = get_current_integration()
        song_id = integration.loaded_models.get('currentSong').get_property('songId')
        model = integration.loaded_models.get(song_id)
        lyrics_dir = os.path.join(DATA_DIR, 'lyrics')
        os.makedirs(lyrics_dir, exist_ok=True)
        file_name_without_ext = '{}|{}|{}|{}'.format(
            model.get_property('title'),
            model.get_property('artist'),
            model.get_property('album') or model.get_property('title'),
            model.get_property('duration')
        )
        return os.path.join(lyrics_dir, file_name_without_ext+'.lrc')

    def copy_lyrics_lrc(self, dialog, task):
        if source_file := dialog.open_finish(task):
            lrc_path = self.get_lrc_path()
            destination_file = Gio.File.new_for_path(lrc_path)
            integration = get_current_integration()
            song_id = integration.loaded_models.get('currentSong').get_property('songId')
            source_file.copy_async(
                destination_file,
                Gio.FileCopyFlags.OVERWRITE,
                GLib.PRIORITY_DEFAULT,
                None,
                None,
                lambda *_: self.song_changed(song_id, False)
            )


    @Gtk.Template.Callback()
    def lyric_load_requested(self, button):
        integration = get_current_integration()
        file_filter = Gtk.FileFilter(
            name=_("LRC File")
        )
        file_filter.add_mime_type('text/x-lrc')
        file_filter.add_mime_type('application/x-lrc')
        file_filter.add_suffix('lrc')
        file_filter_list = Gio.ListStore.new(Gtk.FileFilter)
        file_filter_list.append(file_filter)

        Gtk.FileDialog(
            filters=file_filter_list
        ).open(self.get_root(), None, self.copy_lyrics_lrc)

    @Gtk.Template.Callback()
    def go_to_main(self, button=None):
        self.set_visible_child_name('not-found-locally')
        if existing_path := self.get_lrc_path():
            if os.path.isfile(existing_path):
                os.remove(existing_path)
