from __future__ import annotations

import os
from datetime import datetime
from pathlib import Path
from typing import Any, cast

from gi.repository import Gdk, GLib, Gtk

from .file_model import FileItem, FileListModel, FileType
from .reactive import Watcher, update_all_computed
from .thumbnailer import Thumbnailer
from .video_overlay import VideoOverlay
from .video_player import VideoPlayer


class MainWindow(Gtk.ApplicationWindow, Watcher):
    player: VideoPlayer
    overlay: VideoOverlay

    stack: Gtk.Stack

    list_view: Gtk.ListView
    list_model: FileListModel
    selection_model: Gtk.SingleSelection

    video_picture: Gtk.Picture
    thumbnail_picture: Gtk.Picture

    clock: Gtk.Label

    directory_history: list[Path]
    selection_history: dict[str, int]

    last_position_save: float
    now: float

    def __init__(self, *args: Any, thumbnailer: Thumbnailer, **kwargs: Any):
        super().__init__(*args, **kwargs)
        self.thumbnailer = thumbnailer

        # Directory history stack
        self.directory_history = []
        self.selection_history = {}

        # Last time we've saved playback position.
        self.last_position_save = 0.0

        # Make window fullscreen and borderless
        self.set_decorated(False)
        self.fullscreen()

        # Setup key event controller
        key_controller = Gtk.EventControllerKey()
        key_controller.connect("key-pressed", self._on_key_pressed)
        self.add_controller(key_controller)

        # Disable and hide mouse cursor
        self.set_can_target(False)
        display = Gdk.Display.get_default()
        if display:
            cursor = Gdk.Cursor.new_from_name("none")
            if cursor:
                self.set_cursor(cursor)

        # Process frame ticks.
        self.add_tick_callback(self._on_tick, None)

        # Create video widget and overlay
        self.video_picture = Gtk.Picture()
        self.video_picture.set_can_shrink(True)
        self.video_picture.set_keep_aspect_ratio(True)
        self.video_picture.set_vexpand(True)
        self.video_picture.set_hexpand(True)

        # Create main menu clock
        self.clock = Gtk.Label(
            name="main-clock",
            halign=Gtk.Align.START,
            valign=Gtk.Align.START,
        )

        # Create image widget for thumbnail
        self.thumbnail_picture = Gtk.Picture(
            name="thumbnail-picture",
            can_shrink=True,
            keep_aspect_ratio=True,
        )
        self.thumbnail_picture.set_size_request(384, 216)
        self.thumbnail_picture.set_resource(None)

        # Create list & selection model for the file list view.
        self.list_model = FileListModel()
        self.selection_model = Gtk.SingleSelection(model=self.list_model)
        self.selection_model.connect("selection-changed", self._on_selection_changed)

        # Create file list view.
        self.list_view = Gtk.ListView(
            name="file-list",
            vexpand=True,
            model=self.selection_model,
        )
        self.list_view.connect("activate", self._on_activate)

        # Factory for list items
        factory = Gtk.SignalListItemFactory()
        factory.connect("setup", self._setup_list_item)
        factory.connect("bind", self._bind_list_item)
        self.list_view.set_factory(factory)

        # Add list view to a scrolled window
        scrolled = Gtk.ScrolledWindow()
        scrolled.set_child(self.list_view)

        # Setup video player.
        self.player = VideoPlayer(self.video_picture)

        # Setup video overlay using that player.
        self.overlay = VideoOverlay(self.player)
        self.overlay.set_name("overlay")

        # Left third (1/3 width).
        left_box = Gtk.Box(orientation=Gtk.Orientation.VERTICAL)
        left_box.set_valign(Gtk.Align.START)
        left_box.set_halign(Gtk.Align.FILL)
        left_box.append(self.clock)
        left_box.append(self.thumbnail_picture)

        # Right two-thirds (2/3 width).
        right_box = Gtk.Box(orientation=Gtk.Orientation.VERTICAL)
        right_box.set_hexpand(True)
        right_box.append(scrolled)

        # Create a grid to handle the 1:2 ratio we want.
        grid = Gtk.Grid()
        grid.set_column_homogeneous(True)
        grid.set_hexpand(True)
        grid.attach(left_box, 0, 0, 1, 1)
        grid.attach(right_box, 1, 0, 2, 1)

        # Add both main menu and overlay to stack.
        self.stack = Gtk.Stack()
        self.stack.add_named(grid, "menu")
        self.stack.add_named(self.overlay, "player")

        # Start with main menu visible.
        self.stack.set_visible_child_name("menu")

        # Stack is our root.
        self.set_child(self.stack)

        # Enable all watch methods.
        self.watch_all()

        # Populate the list store.
        self._populate_file_list()

    @property
    def selection(self) -> FileItem | None:
        selected_item = self.selection_model.get_selected_item()
        return cast(FileItem, selected_item) if selected_item else None

    def _setup_list_item(self, factory: Gtk.SignalListItemFactory, list_item: Gtk.ListItem):
        # Create horizontal box to hold icon and label
        box = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL)
        box.set_spacing(8)

        # Create icon image
        icon = Gtk.Image()
        icon.set_css_classes(["file-icon"])
        box.append(icon)

        # Create label
        label = Gtk.Label()
        label.set_halign(Gtk.Align.START)
        box.append(label)

        list_item.set_child(box)

    def _bind_list_item(self, factory: Gtk.SignalListItemFactory, list_item: Gtk.ListItem):
        box = cast(Gtk.Box, list_item.get_child())
        icon = cast(Gtk.Image, box.get_first_child())
        label = cast(Gtk.Label, box.get_last_child())
        item = cast(FileItem, list_item.get_item())

        def update_icon():
            if item.file_type == FileType.DIRECTORY:
                icon.set_from_icon_name("folder-symbolic")
                icon.set_css_classes(["file-icon"])
            else:
                position = item.saved_position.value
                duration = item.saved_duration.value

                if position == 0:
                    icon.set_from_icon_name("media-playback-start-symbolic")
                    icon.set_css_classes(["file-icon", "unwatched"])
                elif (position / duration) >= 0.99:
                    icon.set_from_icon_name("object-select-symbolic")
                    icon.set_css_classes(["file-icon", "completed"])
                else:
                    icon.set_from_icon_name("media-playback-pause-symbolic")
                    icon.set_css_classes(["file-icon", "in-progress"])

        label.set_text(item.name)
        item.watch(update_icon)
        update_icon()

    def _on_activate(self, widget: Gtk.ListView, index: int):
        selected_item = self.selection_model.get_item(index)
        if selected_item:
            file_item = cast(FileItem, selected_item)

            if file_item.file_type == FileType.DIRECTORY:
                self._navigate_to(file_item.full_path)
                return

            position = file_item.saved_position.value
            duration = file_item.saved_duration.value

            if (position / duration) >= 0.99:
                position = 0

            # Start playing the video
            self.player.play(
                file_item,
                position,
                file_item.saved_subtitle_track.value,
                file_item.saved_audio_track.value,
            )
            self.last_position_save = self.now

            self.stack.set_visible_child_name("player")
            self.overlay.show_message(f"Playing: {file_item.name}")

    def _restore_selection(self):
        pos = self.selection_history.get(os.getcwd(), 0)
        self.selection_model.set_selected(pos)
        self.list_view.scroll_to(pos, Gtk.ListScrollFlags.SELECT | Gtk.ListScrollFlags.FOCUS, None)

    def _refresh(self):
        self._populate_file_list()
        self._restore_selection()

    def _navigate_to(self, path: Path):
        self.directory_history.append(Path(os.getcwd()))
        os.chdir(path)
        self._populate_file_list()
        self._restore_selection()

    def _navigate_back(self):
        if not self.directory_history:
            return

        prev_dir = self.directory_history.pop()
        os.chdir(prev_dir)
        self._populate_file_list()
        self._restore_selection()

    def _on_selection_changed(
        self,
        selection_model: Gtk.SingleSelection,
        position: int,
        n_items: int,
    ):
        position = selection_model.get_selected()

        if position == Gtk.INVALID_LIST_POSITION:
            return

        self.selection_history[os.getcwd()] = position

        if file_item := self.selection:
            # Update thumbnail if available
            if file_item.thumbnail.value:
                gbytes = GLib.Bytes.new(cast(Any, file_item.thumbnail.value))
                texture = Gdk.Texture.new_from_bytes(gbytes)
                self.thumbnail_picture.set_paintable(texture)
            else:
                self.thumbnailer.generate_thumbnail(file_item)
                self.thumbnail_picture.set_paintable(None)

    def _toggle_play_pause(self) -> None:
        """Toggle between play and pause states"""
        self.player.toggle_play_pause()

    def _on_player_key_pressed(
        self,
        keyval: int,
        keycode: int,
        state: Gdk.ModifierType,
    ) -> bool:
        if keyval == Gdk.keyval_from_name("space"):
            self._toggle_play_pause()
            return True

        elif keyval == Gdk.keyval_from_name("Escape"):
            self._save_position()
            self.player.stop()
            self.stack.set_visible_child_name("menu")
            self.list_view.grab_focus()
            return True

        elif keyval == Gdk.keyval_from_name("Left"):
            self.player.seek_relative(-10)
            return True

        elif keyval == Gdk.keyval_from_name("Right"):
            self.player.seek_relative(10)
            return True

        elif keyval == Gdk.keyval_from_name("Down"):
            self.player.seek_relative(-60)
            return True

        elif keyval == Gdk.keyval_from_name("Up"):
            self.player.seek_relative(60)
            return True

        elif keyval == Gdk.keyval_from_name("Home"):
            self.player.seek_start()
            return True

        elif keyval == Gdk.keyval_from_name("End"):
            self.player.seek_end()
            return True

        elif keyval == Gdk.keyval_from_name("o"):
            self.player.last_user_input.value = self.now
            return True

        elif keyval == Gdk.keyval_from_name("j"):
            has_subs, index, lang = self.player.cycle_subtitles()

            if has_subs:
                if index:
                    self.overlay.show_message(f"Subtitles #{index} ({lang})")
                else:
                    self.overlay.show_message("Subtitles turned off")

                file_item = self.selection
                if file_item is not None:
                    file_item.saved_subtitle_track.value = index - 1
            else:
                self.overlay.show_message("No subtitles available")

            return True

        elif keyval == Gdk.keyval_from_name("a"):
            has_audio, index, lang = self.player.cycle_audio()

            if has_audio:
                self.overlay.show_message(f"Audio #{index} ({lang})")
                file_item = self.selection
                if file_item is not None:
                    file_item.saved_audio_track.value = index - 1
            else:
                self.overlay.show_message("No audio tracks available")

            return True

        return False

    def _toggle_watched_status(self) -> None:
        """Toggle watched status for the selected file"""

        file_item = self.selection

        if file_item is None or file_item.file_type == FileType.DIRECTORY:
            return

        position = file_item.saved_position.value
        duration = file_item.saved_duration.value

        # If position exists and is >= 99% through, clear it
        if position > 0 and (position / duration) >= 0.99:
            file_item.saved_position.value = 0
        else:
            # Otherwise mark as complete
            file_item.saved_position.value = duration

        # Force the list to update the changed item
        self.list_model.items_changed(self.selection_model.get_selected(), 1, 1)

    def _on_menu_key_pressed(
        self,
        keyval: int,
        keycode: int,
        state: Gdk.ModifierType,
    ) -> bool:
        if keyval == Gdk.keyval_from_name("q"):
            self.close()
            return True

        elif keyval == Gdk.keyval_from_name("r"):
            self._refresh()
            return True

        elif keyval == Gdk.keyval_from_name("w"):
            self._toggle_watched_status()
            return True

        elif keyval == Gdk.keyval_from_name("BackSpace"):
            self._navigate_back()
            return True

        return False

    def _on_key_pressed(
        self,
        controller: Gtk.EventControllerKey,
        keyval: int,
        keycode: int,
        state: Gdk.ModifierType,
    ) -> bool:
        # Handle keys differently based on which view is active
        if self.stack.get_visible_child_name() == "player":
            return self._on_player_key_pressed(keyval, keycode, state)
        else:
            return self._on_menu_key_pressed(keyval, keycode, state)

    def _save_position(self) -> None:
        """Save current playback position as xattr"""

        if not self.player.is_playing:
            return

        file_item = self.player.file_item.value

        if file_item is None or file_item.file_type == FileType.DIRECTORY:
            return

        position = self.player.get_position()
        if position is not None:
            file_item.saved_position.value = position

        duration = self.player.get_duration()
        if duration is not None:
            file_item.saved_duration.value = duration

    def _on_tick(self, widget: Gtk.Widget, frame_clock: Gdk.FrameClock, data: Any) -> bool:
        # Update all reactive values for whole application.
        update_all_computed()

        self.clock.set_text(datetime.now().strftime("%H:%M"))

        self.now = frame_clock.get_frame_time() / 1_000_000

        # Save playback position every 60 seconds.
        if self.now - self.last_position_save >= 60.0:
            self._save_position()
            self.last_position_save = self.now

        # Update thumbnail if available
        if file_item := self.selection:
            self.thumbnailer.generate_thumbnail(file_item)

            if file_item.thumbnail.value and not self.thumbnail_picture.get_paintable():
                gbytes = GLib.Bytes.new(cast(Any, file_item.thumbnail.value))
                texture = Gdk.Texture.new_from_bytes(gbytes)
                self.thumbnail_picture.set_paintable(texture)

        if self.thumbnailer._work_queue.empty():
            for i in range(self.list_model.get_n_items()):
                if item := self.list_model.get_item(i):
                    assert isinstance(item, FileItem)
                    if item.file_type != FileType.DIRECTORY and not item.attempted_thumbnail.value:
                        self.thumbnailer.generate_thumbnail(item)
                        break

        return True

    def _populate_file_list(self) -> None:
        items: list[FileItem] = []

        for entry in os.scandir():
            if entry.name.startswith("."):
                continue

            path = Path(entry.name)

            if path.is_dir():
                items.append(FileItem(path.name, FileType.DIRECTORY, path.resolve()))
            elif path.suffix in (".mkv", ".mp4", ".avi"):
                file_item = FileItem(path.name, FileType.VIDEO, path.resolve())
                items.append(file_item)

        # Sort directories first, then files, both alphabetically
        items.sort(key=lambda x: (x.file_type != FileType.DIRECTORY, x.name.lower()))

        self.list_model.set_items(items)