from copy import copy
from typing import Generator, List, Union
from PySide6.QtCore import Qt

from PySide6.QtGui import QGuiApplication

import fusion
from fusion.util import Point2D, Rectangle
from fusion.view import View
import pamet

from pamet.constants import ARROW_EDGE_RAIDUS, RESIZE_CIRCLE_RADIUS, ARROW_SELECTION_RADIUS
from pamet.desktop_app.util import control_is_pressed, shift_is_pressed

from pamet.actions import note as note_actions
from pamet.actions import map_page as map_page_actions
from pamet.actions import tab as tab_actions
from pamet.model.arrow import ArrowAnchorType
from pamet.model.text_note import TextNote
from pamet.views.arrow.widget import ArrowView
from pamet.views.map_page.state import MapPageViewState, MapPageMode
from pamet.views.note.base.view import NoteView

log = fusion.get_logger(__name__)


class MapPageView(View):

    def __init__(self, parent, initial_state):
        super().__init__(initial_state=initial_state)
        self.parent_tab = parent

        self._state = initial_state
        self._mouse_position_on_left_press = Point2D(0, 0)
        self._note_views_under_mouse = []

    @property
    def _left_mouse_is_pressed(self):
        return QGuiApplication.mouseButtons() & Qt.LeftButton

    def note_views(self) -> List[NoteView]:
        raise NotImplementedError

    def arrow_views(self) -> List[ArrowView]:
        raise NotImplementedError

    @property
    def page(self):
        return self.state().page.copy()

    @property
    def viewport(self):
        return self.state().viewport

    def get_note_view_state_for_note_gid(self, note_gid: str):
        for nv_state in self.state().note_view_states:
            if nv_state.note_gid == note_gid:
                return nv_state
        return None

    def get_note_views_in_area(self, rect: Rectangle):
        unprojected_rect = self.state().unproject_rect(rect)
        intersecting = []

        for child in self.note_views():
            if child.state().rect().intersects(unprojected_rect):
                intersecting.append(child)

        return intersecting

    def get_note_views_at(
            self,
            position: Point2D,
            radius: float = 0) -> Generator[NoteView, None, None]:
        unprojected_mouse_pos = self.state().unproject_point(position)

        for note_view in self.note_views():
            nv_rect = note_view.state().rect()
            if radius:
                # Instead of implementing a Rectangle.intersects(circle)
                # we expand the rectangle to do the same inference
                nv_rect: Rectangle = copy(nv_rect)
                nv_rect.set_size(nv_rect.size() +
                                 Point2D(radius * 2, radius * 2))
                nv_rect.set_top_left(nv_rect.top_left() -
                                     Point2D(radius, radius))

            if nv_rect.contains(unprojected_mouse_pos):
                yield note_view

    def get_note_view_at(self,
                         position: Point2D,
                         radius: float = 0) -> Union[NoteView, None]:
        for note_view in self.get_note_views_at(position, radius):
            return note_view
        return None

    def get_arrow_views_at(
            self, position: Point2D) -> Generator[ArrowView, None, None]:
        unprojected_pos = self.state().unproject_point(position)
        for arrow_view in self.arrow_views():
            if arrow_view.intersects_circle(unprojected_pos,
                                            ARROW_SELECTION_RADIUS):
                yield arrow_view

    def get_arrow_view_at(self, position: Point2D) -> Union[ArrowView, None]:
        for arrow_view in self.get_arrow_views_at(position):
            return arrow_view
        return None

    def resize_circle_intersect(self, position: Point2D):
        for nc in self.note_views():
            unprojected_pos = self.state().unproject_point(position)
            resize_circle_center = nc.state().rect().bottom_right()

            distance = resize_circle_center.distance_to(unprojected_pos)
            if distance <= RESIZE_CIRCLE_RADIUS:
                return nc

    def arrow_anchor_at(self, position: Point2D):
        """Returns a tuple (note_id, anchor_type) for the closest anchor"""
        unprojected_pos = self.state().unproject_point(position)

        # Get the note views close enough to the mouse to show anchors
        adjacent_note_views = self.get_note_views_at(position,
                                                     ARROW_EDGE_RAIDUS)

        # Get the note, anchor, distance tuples of the intersections
        intersections = []
        for note_view in adjacent_note_views:
            for anchor_type in ArrowAnchorType.real_types():
                note_vs = note_view.state()
                anchor_center = note_vs.arrow_anchor(anchor_type)
                distance = unprojected_pos.distance_to(anchor_center)
                intersections.append((note_view, anchor_type, distance))

        if not intersections:
            return None

        # Sort them and return the first
        intersections = sorted(intersections, key=lambda i: i[2])
        note_view, anchor_type = intersections[0][:2]
        return note_view.state().get_note().own_id, anchor_type

    def handle_delete_shortcut(self):
        map_page_actions.delete_selected_children(self.state())

    def handle_left_mouse_long_press(self, mouse_pos: Point2D):
        if self.state().mode() != MapPageMode.NONE:
            return

        nc_under_mouse = self.get_note_view_at(mouse_pos)
        arrow_view_under_mouse = self.get_arrow_view_at(mouse_pos)
        child_under_mouse = nc_under_mouse

        # If there's a note - prefer ignore the arrow
        if arrow_view_under_mouse and not nc_under_mouse:
            # Ignore  arrows that are not free floating on both sides
            av_state = arrow_view_under_mouse.state()
            if av_state.tail_coords and av_state.head_coords:
                child_under_mouse = arrow_view_under_mouse

        if child_under_mouse:
            state = self.state()
            if child_under_mouse.state() not in state.selected_children:
                map_page_actions.update_child_selections(
                    state, {child_under_mouse.state(): True})
            map_page_actions.start_child_move(self.state(), mouse_pos)

    def handle_left_mouse_press(self, mouse_pos: Point2D):
        self._mouse_position_on_left_press = copy(mouse_pos)
        ctrl_pressed = control_is_pressed()
        shift_pressed = shift_is_pressed()
        state = self.state()
        unproj_mouse_pos = state.unproject_point(mouse_pos)

        child_under_mouse = self.get_arrow_view_at(mouse_pos)
        nv_under_mouse = self.get_note_view_at(mouse_pos)
        if nv_under_mouse:
            child_under_mouse = nv_under_mouse
        resize_nv = self.resize_circle_intersect(mouse_pos)

        if state.mode() == MapPageMode.CREATE_ARROW:
            anchor_props = self.arrow_anchor_at(mouse_pos)
            note_id, anchor_type = anchor_props or (None, ArrowAnchorType.NONE)
            map_page_actions.arrow_creation_click(state,
                                                  fixed_pos=unproj_mouse_pos,
                                                  anchor_note_id=note_id,
                                                  anchor_type=anchor_type)
            return

        if ctrl_pressed and shift_pressed:
            map_page_actions.start_drag_select(state, mouse_pos)
            return

        if ctrl_pressed and not shift_pressed:
            if child_under_mouse:
                nc_selected = child_under_mouse.state() in self.state(
                ).selected_children
                map_page_actions.update_child_selections(
                    state, {child_under_mouse.state(): not nc_selected})
        elif shift_pressed or (ctrl_pressed and shift_pressed):
            # Select the note under the mouse (if it's not already so)
            if child_under_mouse:
                nc_selected = child_under_mouse.state() in self.state(
                ).selected_children
                if not nc_selected:
                    map_page_actions.update_child_selections(
                        state, {child_under_mouse.state(): True})

        # Clear selection (or reduce it to the note under the mouse)
        if not ctrl_pressed and not shift_pressed:
            if resize_nv:
                child_under_mouse = resize_nv

            map_page_actions.clear_child_selection(state)

            if child_under_mouse:
                map_page_actions.update_child_selections(
                    state, {child_under_mouse.state(): True})

        # Check for resize initiation
        if resize_nv:
            resize_nv_state = resize_nv.state()
            if resize_nv_state not in state.selected_children:
                map_page_actions.update_child_selections(
                    state, {resize_nv_state: True})

            resize_circle_center = resize_nv_state.rect().bottom_right()
            rcc_projected = state.project_point(resize_circle_center)
            map_page_actions.start_notes_resize(state,
                                                resize_nv_state.get_note(),
                                                mouse_pos, rcc_projected)

    def handle_left_mouse_release(self, mouse_pos: Point2D):
        state: MapPageViewState = self.state()
        mode = state.mode()
        unproj_mouse_pos = state.unproject_point(mouse_pos)

        if mode == MapPageMode.DRAG_SELECT:
            same_pos = state.mouse_position_on_drag_select_start == mouse_pos
            map_page_actions.stop_drag_select(state, mouse_pos)
            # If no drag select has happened, but it was just a click - add
            # the note under the mouse to the selection.
            if same_pos:
                child_under_mouse = self.get_arrow_view_at(mouse_pos)
                nv_under_mouse = self.get_note_view_at(mouse_pos)
                if nv_under_mouse:
                    child_under_mouse = nv_under_mouse
                map_page_actions.update_child_selections(
                    state, {child_under_mouse.state(): True})

        elif mode == MapPageMode.NOTE_RESIZE:
            new_size = self._new_note_size_on_resize(mouse_pos)
            map_page_actions.finish_notes_resize(self.state(), new_size,
                                                 mouse_pos)

        elif mode == MapPageMode.CHILD_MOVE:
            pos_delta = mouse_pos - state.mouse_position_on_note_drag_start
            pos_delta /= self.state().height_scale_factor()
            map_page_actions.finish_child_move(self.state(), pos_delta)

        elif mode == MapPageMode.DRAG_NAVIGATION:
            map_page_actions.finish_mouse_drag_navigation(self.state())
            tab_actions.update_current_url(self.parent_tab.state())

        elif mode == MapPageMode.ARROW_EDGE_DRAG:
            anchor_props = self.arrow_anchor_at(mouse_pos)
            note_id, anchor_type = anchor_props or (None, ArrowAnchorType.NONE)
            map_page_actions.finish_arrow_edge_drag(state,
                                                    fixed_pos=unproj_mouse_pos,
                                                    anchor_note_id=note_id,
                                                    anchor_type=anchor_type)

    def _new_note_size_on_resize(self, new_mouse_pos: Point2D) -> Point2D:
        mouse_delta = new_mouse_pos - self.state().note_resize_click_position
        size_delta = mouse_delta - self.state(
        ).note_resize_delta_from_note_edge

        size_delta = size_delta / self.state().height_scale_factor()
        new_size = self.state().note_resize_main_note.size() + size_delta

        return new_size

    def _handle_move_on_drag_select(self, mouse_pos: Point2D):
        selection_rect = Rectangle.from_points(
            self.state().mouse_position_on_drag_select_start, mouse_pos)
        unprojected_rect = self.state().unproject_rect(selection_rect)

        nvs_in_selection = self.get_note_views_in_area(selection_rect)
        arrow_views_in_selection = [
            av for av in self.arrow_views()
            if av.intersects_rect(unprojected_rect)
        ]
        drag_selected_children = set([
            child.state()
            for child in nvs_in_selection + arrow_views_in_selection
        ])

        map_page_actions.update_drag_select(self.state(),
                                            selection_rect.as_tuple(),
                                            drag_selected_children)

    def handle_mouse_move(self, mouse_pos: Point2D):
        state = self.state()
        mode = state.mode()
        delta = self._mouse_position_on_left_press - mouse_pos
        unproj_mouse_pos = state.unproject_point(mouse_pos)
        self._note_views_under_mouse = list(self.get_note_views_at(mouse_pos))

        if mode == MapPageMode.NONE:
            if self._left_mouse_is_pressed:
                map_page_actions.start_mouse_drag_navigation(
                    state, self._mouse_position_on_left_press, delta)

        elif mode == MapPageMode.DRAG_SELECT:
            self._handle_move_on_drag_select(mouse_pos)

        elif mode == MapPageMode.NOTE_RESIZE:
            new_size = self._new_note_size_on_resize(mouse_pos)
            map_page_actions.resize_note_views(state, new_size)

        elif mode == MapPageMode.CHILD_MOVE:
            pos_delta = mouse_pos - self._mouse_position_on_left_press
            pos_delta /= state.height_scale_factor()
            map_page_actions.moved_child_view_update(state, pos_delta)

        elif mode == MapPageMode.DRAG_NAVIGATION:
            map_page_actions.mouse_drag_navigation_move(state, delta)

        elif mode == MapPageMode.CREATE_ARROW:
            anchor_props = self.arrow_anchor_at(mouse_pos)
            note_id, anchor_type = anchor_props or (None, ArrowAnchorType.NONE)
            map_page_actions.arrow_creation_move(state,
                                                 fixed_pos=unproj_mouse_pos,
                                                 anchor_note_id=note_id,
                                                 anchor_type=anchor_type)

        elif mode == MapPageMode.ARROW_EDGE_DRAG:
            anchor_props = self.arrow_anchor_at(mouse_pos)
            note_id, anchor_type = anchor_props or (None, ArrowAnchorType.NONE)
            map_page_actions.arrow_edge_drag_update(state,
                                                    fixed_pos=unproj_mouse_pos,
                                                    anchor_note_id=note_id,
                                                    anchor_type=anchor_type)

    def left_mouse_double_click_event(self, mouse_pos: Point2D):
        if self.state().mode() == MapPageMode.CREATE_ARROW:
            map_page_actions.abort_special_mode(self.state())
            return

        note_view = self.get_note_view_at(mouse_pos)

        if note_view:
            # Map the mouse position on the note and call its virtual method
            note_rect: Rectangle = note_view.state().get_note().rect()
            mouse_pos_unproj = self.state().unproject_point(mouse_pos)
            relative_pos = note_rect.top_left() - mouse_pos_unproj
            note_view.left_mouse_double_click_event(relative_pos)
        else:
            note_pos = self.state().unproject_point(mouse_pos)
            note = TextNote().with_id(page_id=self.state().page_id)
            note.x = note_pos.x()
            note.y = note_pos.y()
            note_actions.create_new_note(self.parent_tab.state(), note)

    def middle_click_event(self, mouse_pos: Point2D):
        note_view = self.get_note_view_at(mouse_pos)

        if note_view:
            note_view.middle_click_event(mouse_pos)

    def handle_resize_event(self, new_size: Point2D):
        map_page_actions.resize_page(self.state(), new_size)

    def undo(self):
        pamet.undo_service().back_one_step(self.state().page_id)

    def redo(self):
        pamet.undo_service().forward_one_step(self.state().page_id)
