#!/usr/bin/env python
# -*- coding: utf-8 -*-

"""
This implementation of EventListener keeps track of which space on a Page is available
"""
import typing
from decimal import Decimal
from math import ceil
from pathlib import Path


from borb.pdf.canvas.event.begin_page_event import BeginPageEvent
from borb.pdf.canvas.event.chunk_of_text_render_event import ChunkOfTextRenderEvent
from borb.pdf.canvas.event.event_listener import Event, EventListener
from borb.pdf.canvas.event.image_render_event import ImageRenderEvent
from borb.pdf.canvas.geometry.rectangle import Rectangle
from borb.pdf.pdf import PDF


class FreeSpaceFinder(EventListener):
    """
    This implementation of EventListener keeps track of which space on a Page is available
    """

    class Grid:
        """
        This class represents a rasterized, low-res view of a Page.
        But rather than rendering instructions on this raster, each cell simply keeps track
        of whether or not the cell is available. This enables a quick lookup for availability of a given Rectangle.
        """

        def __init__(
            self, page_width: Decimal, page_height: Decimal, resolution: Decimal
        ):
            self._page_width = page_width
            self._page_height = page_height
            self._resolution = resolution
            self._availability: typing.List[typing.List[bool]] = [
                [True for _ in range(0, ceil(self._page_width / self._resolution))]
                for _ in range(0, ceil(self._page_height / self._resolution))
            ]

        def _mark_as_unavailable(self, rectangle: Rectangle) -> "FreeSpaceFinder.Grid":
            x_grid = int(int(rectangle.x) / self._resolution)
            y_grid = int(int(rectangle.y) / self._resolution)
            w = int(int(rectangle.width) / self._resolution)
            h = int(int(rectangle.height) / self._resolution)
            for i in range(x_grid - 1, x_grid + w + 1):
                for j in range(y_grid - 1, y_grid + h + 1):
                    if i < 0 or i >= len(self._availability):
                        continue
                    if j < 0 or j >= len(self._availability[i]):
                        continue
                    self._availability[i][j] = False
            return self

        def _get_free_space(
            self, desired_rectangle: Rectangle
        ) -> typing.Optional[Rectangle]:
            """
            This function returns a Rectangle (or None) of free space (no text rendering operations, no drawing operations) near the given Rectangle
            """
            w = int(int(desired_rectangle.width) / self._resolution)
            h = int(int(desired_rectangle.height) / self._resolution)
            possible_points: typing.List[typing.Tuple[Decimal, Decimal]] = []
            for i in range(0, len(self._availability) - w):
                for j in range(0, len(self._availability[i]) - h):
                    is_free = True
                    for k in range(0, w):
                        for l in range(0, h):
                            if not self._availability[i + k][j + l]:
                                is_free = False
                                break
                        if not is_free:
                            break
                    if is_free:
                        possible_points.append(
                            (
                                Decimal(i * self._resolution),
                                Decimal(j * self._resolution),
                            )
                        )

            # find point closest to desired location
            if len(possible_points) == 0:
                return None
            min_dist = (desired_rectangle.x - possible_points[0][0]) ** 2 + (
                desired_rectangle.y - possible_points[0][1]
            ) ** 2
            min_dist_point = possible_points[0]
            for p in possible_points:
                d = (desired_rectangle.x - p[0]) ** 2 + (
                    desired_rectangle.y - p[1]
                ) ** 2
                if d < min_dist:
                    min_dist = d
                    min_dist_point = p

            # return
            return Rectangle(
                min_dist_point[0],
                min_dist_point[1],
                desired_rectangle.width,
                desired_rectangle.height,
            )

    def __init__(self):
        self._page_number: int = -1
        self._grid_per_page: typing.Dict[int, FreeSpaceFinder.Grid] = {}

    @staticmethod
    def find_free_space_for_page(
        file: Path, page_number: int, desired_rectangle: Rectangle
    ) -> typing.Optional[Rectangle]:
        l: FreeSpaceFinder = FreeSpaceFinder()
        with open(file, "rb") as pdf_file_handle:
            PDF.loads(pdf_file_handle, [l])
        return l.get_free_space_for_page(page_number, desired_rectangle)

    def get_free_space_for_page(
        self, page_number: int, desired_rectangle: Rectangle
    ) -> typing.Optional[Rectangle]:
        if page_number in self._grid_per_page:
            return self._grid_per_page[page_number]._get_free_space(desired_rectangle)
        return None

    def _event_occurred(self, event: Event) -> None:

        # BeginPageEvent
        if isinstance(event, BeginPageEvent):
            self._page_number += 1
            self._grid_per_page[self._page_number] = FreeSpaceFinder.Grid(
                event.get_page().get_page_info().get_width() or Decimal(0),
                event.get_page().get_page_info().get_height() or Decimal(0),
                Decimal(10),
            )

        # ChunkOfTextRenderEvent
        if isinstance(event, ChunkOfTextRenderEvent):
            assert isinstance(event, ChunkOfTextRenderEvent)
            bounding_box_001: typing.Optional[Rectangle] = event.get_bounding_box()
            if bounding_box_001 is not None:
                self._grid_per_page[self._page_number]._mark_as_unavailable(
                    bounding_box_001
                )

        # ImageRenderEvent
        if isinstance(event, ImageRenderEvent):
            assert isinstance(event, ImageRenderEvent)
            bounding_box_002: typing.Optional[Rectangle] = Rectangle(
                event.get_x(), event.get_y(), event.get_width(), event.get_height()
            )
            if bounding_box_002 is not None:
                self._grid_per_page[self._page_number]._mark_as_unavailable(
                    bounding_box_002
                )
