from __future__ import annotations

import logging
from datetime import datetime

from textual import events, work
from textual.binding import Binding
from textual.content import Content, Span
from textual.message import Message
from textual.reactive import reactive
from textual.widget import Widget
from textual.widgets import TabPane

from zet import map
from zet.cache import load_map_lines_cached
from zet.database import Point
from zet.map import MapSize, place_vehicle_labels
from zet.realtime import VehiclePosition, get_vehicle_positions


logger = logging.getLogger(__name__)


class MapPane(TabPane):
    def __init__(self) -> None:
        super().__init__("Map", id="map_pane")

    async def on_mount(self):
        await load_map_lines_cached()

    def compose(self):
        yield Map()
        yield StatusBar()

    def on_map_move(self, message: Map.Move):
        self.query_one(StatusBar).center = message.center
        self.query_one(StatusBar).zoom = message.zoom


class StatusBar(Widget):
    DEFAULT_CSS = """
    StatusBar  {
        height: 1;
        content-align: right middle;
    }
    """

    time = reactive(datetime.now())
    center: reactive[Point | None] = reactive(None)
    zoom: reactive[int | None] = reactive(None)

    def render(self):
        time = self.time.strftime("%A, %d. %B %Y %I:%M:%S%p")
        center = f"{self.center.lat:.3f}:{self.center.lon:.3f}" if self.center else None
        zoom = f"{self.zoom}x" if self.zoom else None
        return "  ".join(x for x in [center, zoom, time] if x)

    def on_mount(self):
        self.set_interval(1, self.update_time)

    def update_time(self):
        self.time = datetime.now()


class Map(Widget, can_focus=True):
    DEFAULT_CSS = """
    Map {
        height: 1fr;
    }
    """

    ALLOW_SELECT = False

    BINDINGS = [
        Binding("left", "move_left", "Left"),
        Binding("right", "move_right", "Right"),
        Binding("up", "move_up", "Up"),
        Binding("down", "move_down", "Down"),
        Binding("plus", "zoom_in", "Zoom In"),
        Binding("minus", "zoom_out", "Zoom Out"),
    ]

    class Move(Message):
        def __init__(self, center: Point, zoom: int):
            super().__init__()
            self.center = center
            self.zoom = zoom

    center_lat = reactive(45.8)
    center_lon = reactive(15.98)
    zoom = reactive(map.DEFAULT_ZOOM)
    vehicles: reactive[list[VehiclePosition] | None] = reactive(None)

    def __init__(self):
        super().__init__()
        self.mouse_down_at: tuple[int, int] | None = None
        self.timer = None

    def on_mount(self):
        self.post_move_message()
        self.load_vehicles()
        # Seems like realtime data is updated every 10 seconds
        self.timer = self.set_interval(10, self.load_vehicles)

    @work(exclusive=True)
    async def load_vehicles(self):
        self.vehicles = await get_vehicle_positions()

    def render(self):
        logger.info("Rendering!")
        width = self.size.width
        height = self.size.height

        if width == 0 or height == 0:
            return ""

        center = Point(self.center_lat, self.center_lon)
        bounds = MapSize.new(width, height, center.lat, center.lon, self.zoom)
        image = map.draw_map(bounds)
        labels = place_vehicle_labels(bounds, self.vehicles or [])

        string = ""
        spans = []
        for y in range(bounds.char_height):
            for x in range(bounds.char_width):
                offset = y * bounds.char_width + x
                if (x, y) in labels:
                    string += labels[x, y]
                    # TODO: we could reduce the number of spans by having one per
                    # label instead of one per character
                    spans.append(Span(len(string) - 1, len(string), style="bold"))
                else:
                    string += image[offset]
            string += "\n"

        return Content(string, spans)

    def post_move_message(self):
        self.post_message(self.Move(Point(self.center_lat, self.center_lon), self.zoom))

    def action_move_left(self):
        self.center_lon -= self.move_delta
        self.post_move_message()

    def action_move_right(self):
        self.center_lon += self.move_delta
        self.post_move_message()

    def action_move_up(self):
        self.center_lat += self.move_delta
        self.post_move_message()

    def action_move_down(self):
        self.center_lat -= self.move_delta
        self.post_move_message()

    def action_zoom_in(self):
        if self.zoom < map.MAX_ZOOM:
            self.zoom += 1
        self.post_move_message()

    def action_zoom_out(self):
        if self.zoom > map.MIN_ZOOM:
            self.zoom -= 1
        self.post_move_message()

    @property
    def ratio(self):
        return map.zoom_to_ratio(self.zoom)

    @property
    def move_delta(self):
        return 12 / self.ratio

    def on_mouse_down(self, event: events.MouseDown):
        self.mouse_down_at = (event.x, event.y)

    def on_mouse_up(self, event: events.MouseUp):
        self.mouse_down_at = None

    def on_mouse_move(self, event: events.MouseMove):
        if self.mouse_down_at is not None:
            self.center_lon -= event.delta_x * 2 / self.ratio
            self.center_lat += event.delta_y * 4 / self.ratio
        self.post_move_message()

    def on_mouse_scroll_up(self, event: events.MouseScrollUp):
        self.action_zoom_in()
        self.post_move_message()

    def on_mouse_scroll_down(self, event: events.MouseScrollDown):
        self.action_zoom_out()
        self.post_move_message()
