from __future__ import annotations

import contextlib

import cairocffi
import xcffib.xproto

from libqtile import utils
from libqtile.backend.base import drawer
from libqtile.backend.x11 import window, xcbq


class Drawer(drawer.Drawer):
    """A helper class for drawing to Internal windows.

    The underlying surface here is an XCBSurface backed by a pixmap. We draw to the
    pixmap starting at offset 0, 0, and when the time comes to display to the window (on
    draw()), we copy the appropriate portion of the pixmap onto the window. In the event
    that our drawing area is resized, we invalidate the underlying surface and pixmap
    and recreate them when we need them again with the new geometry.
    """

    def __init__(self, conn: xcbq.Connection, win: window.Internal, width: int, height: int):
        drawer.Drawer.__init__(self, win, width, height)
        self.conn = conn
        self._xcb_surface = None
        self._gc = None
        self._depth, self._visual = conn.default_screen._get_depth_and_visual(win._depth)
        # Create an XCBSurface and pixmap
        self._check_xcb()

    def finalize(self):
        self._free_xcb_surface()
        self._free_pixmap()
        self._free_gc()
        drawer.Drawer.finalize(self)

    @property
    def width(self):
        return self._width

    @width.setter
    def width(self, width):
        if width > self._width:
            self._free_xcb_surface()
            self._free_pixmap()
        self._width = width

    @property
    def height(self):
        return self._height

    @height.setter
    def height(self, height):
        if height > self._height:
            self._free_xcb_surface()
            self._free_pixmap()
        self._height = height

    @property
    def pixmap(self):
        if self._pixmap is None:
            # draw here since the only use case of this function is in the
            # systray widget which expects a filled pixmap.
            self.draw()
        return self._pixmap

    def _create_gc(self):
        gc = self.conn.conn.generate_id()
        self.conn.conn.core.CreateGC(
            gc,
            self._win.wid,
            xcffib.xproto.GC.Foreground | xcffib.xproto.GC.Background,
            [
                self.conn.default_screen.black_pixel,
                self.conn.default_screen.white_pixel,
            ],
        )
        return gc

    def _free_gc(self):
        if self._gc is not None:
            with contextlib.suppress(xcffib.ConnectionException):
                self.conn.conn.core.FreeGC(self._gc)
            self._gc = None

    def _create_xcb_surface(self):
        surface = cairocffi.XCBSurface(
            self.conn.conn,
            self._pixmap,
            self._visual,
            self.width,
            self.height,
        )
        return surface

    def _free_xcb_surface(self):
        if self._xcb_surface is not None:
            self._xcb_surface.finish()
            self._xcb_surface = None

    def _create_pixmap(self):
        pixmap = self.conn.conn.generate_id()
        self.conn.conn.core.CreatePixmap(
            self._depth,
            pixmap,
            self._win.wid,
            self.width,
            self.height,
        )
        return pixmap

    def _free_pixmap(self):
        if self._pixmap is not None:
            with contextlib.suppress(xcffib.ConnectionException):
                self.conn.conn.core.FreePixmap(self._pixmap)
            self._pixmap = None

    def _check_xcb(self):
        # If the Drawer has been resized/invalidated we need to recreate these
        if self._xcb_surface is None:
            self._pixmap = self._create_pixmap()
            self._xcb_surface = self._create_xcb_surface()

    def _paint(self):
        # Paint RecordingSurface operations to the XCBSurface
        ctx = cairocffi.Context(self._xcb_surface)
        ctx.set_source_surface(self.surface, 0, 0)
        ctx.paint()

    def _draw(
        self,
        offsetx: int = 0,
        offsety: int = 0,
        width: int | None = None,
        height: int | None = None,
        src_x: int = 0,
        src_y: int = 0,
    ):
        # If this is our first draw, create the gc
        if self._gc is None:
            self._gc = self._create_gc()

        # Recreate an XCBSurface
        self._check_xcb()

        # paint stored operations(if any) to XCBSurface
        self._paint()

        # Finally, copy XCBSurface's underlying pixmap to the window.
        self.conn.conn.core.CopyArea(
            self._pixmap,
            self._win.wid,
            self._gc,
            src_x,
            src_y,  # srcx, srcy
            offsetx,
            offsety,  # dstx, dsty
            self.width if width is None else width,
            self.height if height is None else height,
        )

    def _find_root_visual(self):
        for i in self.conn.default_screen.allowed_depths:
            for v in i.visuals:
                if v.visual_id == self.conn.default_screen.root_visual:
                    return v

    def set_source_rgb(self, colour, ctx=None):
        # Remove transparency from non-32 bit windows
        if utils.has_transparency(colour) and self._depth != 32:
            colour = utils.remove_transparency(colour)

        drawer.Drawer.set_source_rgb(self, colour, ctx)

    def clear_rect(self, x=0, y=0, width=0, height=0):
        """
        Erases the background area specified by parameters. By default,
        the whole Drawer is cleared.

        The ability to clear a smaller area may be useful when you want to
        erase a smaller area of the drawer (e.g. drawing widget decorations).
        """
        if width <= 0:
            width = self.width
        if height <= 0:
            height = self.height

        self._check_xcb()

        # Using OPERATOR_CLEAR in a RecordingSurface does not clear the
        # XCBSurface so we clear the XCBSurface directly.
        with cairocffi.Context(self._xcb_surface) as ctx:
            ctx.set_operator(cairocffi.OPERATOR_CLEAR)
            ctx.rectangle(x, y, width, height)
            ctx.fill()
