# Copyright 2010 Dirk Holtwick, holtwick.it
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
#     http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

# ruff: noqa: N802, N803
from __future__ import annotations

import contextlib
import copy
import logging
import sys
from hashlib import md5
from html import escape as html_escape
from io import BytesIO, StringIO
from typing import TYPE_CHECKING, ClassVar, Iterator
from uuid import uuid4

from PIL import Image as PILImage
from PIL import UnidentifiedImageError
from PIL.Image import Image
from reportlab.lib.enums import TA_RIGHT
from reportlab.lib.styles import ParagraphStyle
from reportlab.lib.utils import LazyImageReader, flatten, haveImages, open_for_read
from reportlab.pdfbase import pdfform
from reportlab.platypus.doctemplate import (
    BaseDocTemplate,
    IndexingFlowable,
    PageTemplate,
)
from reportlab.platypus.flowables import (
    CondPageBreak,
    Flowable,
    KeepInFrame,
    ParagraphAndImage,
)
from reportlab.platypus.tableofcontents import TableOfContents
from reportlab.platypus.tables import Table, TableStyle
from reportlab.rl_config import register_reset

from xhtml2pdf.builders.watermarks import WaterMarks
from xhtml2pdf.files import pisaFileObject, pisaTempFile
from xhtml2pdf.reportlab_paragraph import Paragraph
from xhtml2pdf.util import ImageWarning, getBorderStyle

if TYPE_CHECKING:
    from reportlab.graphics.shapes import Drawing
    from reportlab.pdfgen.canvas import Canvas


try:
    from reportlab.graphics import renderPM
    from svglib.svglib import svg2rlg
except ImportError:
    svg2rlg = None
    renderPM = None

log = logging.getLogger(__name__)

MAX_IMAGE_RATIO: float = 0.95
PRODUCER: str = "xhtml2pdf <https://github.com/xhtml2pdf/xhtml2pdf/>"


class PTCycle(list):
    def __init__(self) -> None:
        self._restart: int = 0
        self._idx: int = 0
        super().__init__()

    def cyclicIterator(self) -> Iterator:
        while 1:
            yield self[self._idx]
            self._idx += 1
            if self._idx >= len(self):
                self._idx = self._restart


class PmlMaxHeightMixIn:
    def setMaxHeight(self, availHeight: int) -> int:
        self.availHeightValue: int = availHeight
        if availHeight < 70000 and hasattr(self, "canv"):
            if not hasattr(self.canv, "maxAvailHeightValue"):
                self.canv.maxAvailHeightValue = 0
            self.availHeightValue = self.canv.maxAvailHeightValue = max(
                availHeight, self.canv.maxAvailHeightValue
            )
        return self.availHeightValue

    def getMaxHeight(self) -> int:
        return self.availHeightValue if hasattr(self, "availHeightValue") else 0


class PmlBaseDoc(BaseDocTemplate):
    """We use our own document template to get access to the canvas and set some information once."""

    def beforePage(self) -> None:
        self.canv._doc.info.producer = PRODUCER

        """
        # Convert to ASCII because there is a Bug in Reportlab not
        # supporting other than ASCII. Send to list on 23.1.2007
        author = toString(self.pml_data.get("author", "")).encode("ascii","ignore")
        subject = toString(self.pml_data.get("subject", "")).encode("ascii","ignore")
        title = toString(self.pml_data.get("title", "")).encode("ascii","ignore")
        # print repr((author,title,subject))
        self.canv.setAuthor(author)
        self.canv.setSubject(subject)
        self.canv.setTitle(title)
        if self.pml_data.get("fullscreen", 0):
            self.canv.showFullScreen0()
        if self.pml_data.get("showoutline", 0):
            self.canv.showOutline()
        if self.pml_data.get("duration", None) is not None:
            self.canv.setPageDuration(self.pml_data["duration"])
        """

    def afterFlowable(self, flowable: Flowable) -> None:
        # Does the flowable contain fragments?
        if getattr(flowable, "outline", False):
            self.notify(
                "TOCEntry",
                (
                    flowable.outlineLevel,
                    html_escape(copy.deepcopy(flowable.text), quote=True),
                    self.page,
                ),
            )

    def handle_nextPageTemplate(self, pt: str | int | list | tuple) -> None:
        """If pt has also templates for even and odd page convert it to list."""
        has_left_template: bool = self._has_template_for_name(f"{pt}_left")
        has_right_template: bool = self._has_template_for_name(f"{pt}_right")

        if has_left_template and has_right_template:
            pt = [f"{pt}_left", f"{pt}_right"]

        """On endPage change to the page template with name or index pt"""
        if isinstance(pt, str):
            if hasattr(self, "_nextPageTemplateCycle"):
                del self._nextPageTemplateCycle
            for t in self.pageTemplates:
                if t.id == pt:
                    self._nextPageTemplateIndex: int = self.pageTemplates.index(t)
                    return
            msg = f"can't find template('{pt}')"
            raise ValueError(msg)
        if isinstance(pt, int):
            if hasattr(self, "_nextPageTemplateCycle"):
                del self._nextPageTemplateCycle
            self._nextPageTemplateIndex = pt
        elif isinstance(pt, (list, tuple)):
            # used for alternating left/right pages
            # collect the refs to the template objects, complain if any are bad
            c: PTCycle = PTCycle()
            for ptn in pt:
                # special case name used to short circuit the iteration
                if ptn == "*":
                    c._restart = len(c)
                    continue
                for t in self.pageTemplates:
                    sys.exit()
                    if t.id == ptn.strip():
                        c.append(t)
                        break
            if not c:
                msg = "No valid page templates in cycle"
                raise ValueError(msg)
            if c._restart > len(c):
                msg = "Invalid cycle restart position"
                raise ValueError(msg)

            # ensure we start on the first one$
            self._nextPageTemplateCycle: PageTemplate = c.cyclicIterator()
        else:
            msg = "Argument pt should be string or integer or list"
            raise TypeError(msg)

    def _has_template_for_name(self, name: str) -> bool:
        return any(template.id == name.strip() for template in self.pageTemplates)


class PmlPageTemplate(PageTemplate):
    PORTRAIT: str = "portrait"
    LANDSCAPE: str = "landscape"
    # by default portrait
    pageorientation: str = PORTRAIT

    def __init__(self, **kw) -> None:
        self.pisaStaticList: list = []
        self.pisaBackgroundList: list[tuple] = []
        self.pisaBackground = None
        super().__init__(**kw)
        self._page_count: int = 0
        self._first_flow: bool = True

        # Background Image
        self.img = None
        self.ph: int = 0
        self.h: int = 0
        self.w: int = 0

        self.backgroundids: list[int] = []

    def isFirstFlow(self, canvas: Canvas) -> bool:
        if self._first_flow:
            if canvas.getPageNumber() <= self._page_count:
                self._first_flow = False
            else:
                self._page_count = canvas.getPageNumber()
                canvas._doctemplate._page_count = canvas.getPageNumber()
        return self._first_flow

    def isPortrait(self) -> bool:
        return self.pageorientation == self.PORTRAIT

    def isLandscape(self) -> bool:
        return self.pageorientation == self.LANDSCAPE

    def beforeDrawPage(self, canvas: Canvas, doc):
        canvas.saveState()
        try:
            if doc.pageTemplate.id not in self.backgroundids:
                pisaBackground = None
                if (
                    hasattr(self, "pisaBackground")
                    and self.pisaBackground
                    and (not self.pisaBackground.notFound())
                ):
                    if self.pisaBackground.getMimeType().startswith("image/"):
                        pisaBackground = WaterMarks.generate_pdf_background(
                            self.pisaBackground,
                            self.pagesize,
                            is_portrait=self.isPortrait(),
                            context=self.backgroundContext,
                        )
                    else:
                        pisaBackground = self.pisaBackground
                    self.backgroundids.append(doc.pageTemplate.id)
                if pisaBackground:
                    self.pisaBackgroundList.append(
                        (canvas.getPageNumber(), pisaBackground, self.backgroundContext)
                    )

            def pageNumbering(objList):
                for obj in flatten(objList):
                    if isinstance(obj, PmlParagraph):
                        for frag in obj.frags:
                            if frag.pageNumber:
                                frag.text = str(pagenumber)
                            elif frag.pageCount:
                                frag.text = str(canvas._doctemplate._page_count)

                    elif isinstance(obj, PmlTable):
                        # Flatten the cells ([[1,2], [3,4]] becomes [1,2,3,4])
                        flat_cells = [
                            item for sublist in obj._cellvalues for item in sublist
                        ]
                        pageNumbering(flat_cells)

            try:
                # Paint static frames
                pagenumber = canvas.getPageNumber()
                if pagenumber > self._page_count:
                    self._page_count = canvas.getPageNumber()
                    canvas._doctemplate._page_count = canvas.getPageNumber()

                for frame in self.pisaStaticList:
                    frame_copy = copy.deepcopy(frame)
                    story = frame_copy.pisaStaticStory
                    pageNumbering(story)

                    frame_copy.addFromList(story, canvas)

            except Exception:  # TODO: Kill this!
                log.debug("PmlPageTemplate", exc_info=True)
        finally:
            canvas.restoreState()


_ctr: int = 1


class PmlImageReader:  # TODO We need a factory here, returning either a class for java or a class for PIL
    """Wraps up either PIL or Java to get data from bitmaps."""

    _cache: ClassVar[dict] = {}
    # Experimental features, disabled by default
    use_cache: bool = False
    use_lazy_loader: bool = False
    process_internal_files: bool = False

    def __init__(self, fileName: PmlImage | Image | str) -> None:
        if isinstance(fileName, PmlImage):
            self.__dict__ = fileName.__dict__  # borgize
            return
            # start wih lots of null private fields, to be populated by
        # the relevant engine.
        self.fileName: PmlImage | Image | str = fileName or f"PILIMAGE_{id(self)}"
        self._image: Image = None
        self._width: int | None = None
        self._height: int | None = None
        self._transparent = None
        self._data: bytes | str | None = None
        self._dataA: PmlImageReader | None = None
        self.fp: BytesIO | StringIO | None = None
        if Image and isinstance(fileName, Image):
            self._image = fileName
            self.fp = getattr(fileName, "fp", None)
        else:
            try:
                self.fp = open_for_read(fileName, "b")
                if self.process_internal_files and isinstance(self.fp, StringIO):
                    data: str = self.fp.read()
                    with contextlib.suppress(Exception):
                        self.fp.close()
                    if self.use_cache:
                        if not self._cache:
                            register_reset(self._cache.clear)
                        cache_key = md5(data.encode("utf8")).digest()
                        data = self._cache.setdefault(cache_key, data)
                    self.fp = StringIO(data)
                elif self.use_lazy_loader and isinstance(fileName, str):
                    # try Ralf Schmitt's re-opening technique of avoiding too many open files
                    self.fp.close()
                    del self.fp  # will become a property in the next statement
                    self.__class__ = LazyImageReader
                if haveImages:
                    # detect which library we are using and open the image
                    if not self._image:
                        self._image = self._read_image(self.fp)
                    if getattr(self._image, "format", None) == "JPEG":
                        self.jpeg_fh = self._jpeg_fh
                else:
                    from reportlab.pdfbase.pdfutils import readJPEGInfo

                    try:
                        self._width, self._height, c = readJPEGInfo(self.fp)
                    except Exception as e:
                        msg = (
                            "Imaging Library not available, unable to import bitmaps"
                            " only jpegs"
                        )
                        raise ImageWarning(msg) from e
                    self.jpeg_fh = self._jpeg_fh
                    self._data = self.fp.read()
                    self.fp.seek(0)
            # Catch all errors that are known and don't need the stack trace
            except UnidentifiedImageError as e:
                msg = "Cannot identify image file"
                raise ImageWarning(msg) from e

    @staticmethod
    def _read_image(fp) -> Image:
        if sys.platform[:4] == "java":
            from java.io import ByteArrayInputStream
            from javax.imageio import ImageIO

            input_stream = ByteArrayInputStream(fp.read())
            return ImageIO.read(input_stream)
        return PILImage.open(fp)

    def _jpeg_fh(self) -> BytesIO | StringIO | None:
        fp = self.fp
        if isinstance(fp, (BytesIO, StringIO)):
            fp.seek(0)
        return fp

    def jpeg_fh(self) -> BytesIO | StringIO | None:  # noqa: PLR6301
        """Might be replaced with _jpeg_fh in some cases"""
        return None

    def getSize(self) -> tuple[int, int]:
        if self._width is None or self._height is None:
            if sys.platform[:4] == "java":
                self._width = self._image.getWidth()
                self._height = self._image.getHeight()
            else:
                self._width, self._height = self._image.size
            if TYPE_CHECKING:
                assert self._width is not None and self._height is not None
        return self._width, self._height

    def getRGBData(self) -> bytes | str:
        """Return byte array of RGB data as string."""
        if self._data is None:
            self._dataA = None
            if sys.platform[:4] == "java":
                import jarray  # TODO: Move to top.
                from java.awt.image import PixelGrabber

                width, height = self.getSize()
                buffer = jarray.zeros(width * height, "i")
                pg: PixelGrabber = PixelGrabber(
                    self._image, 0, 0, width, height, buffer, 0, width
                )
                pg.grabPixels()
                # there must be a way to do this with a cast not a byte-level loop,
                # I just haven't found it yet...
                pixels: list[str] = []
                a = pixels.append
                for rgb in buffer:
                    a(chr((rgb >> 16) & 0xFF))
                    a(chr((rgb >> 8) & 0xFF))
                    a(chr(rgb & 0xFF))
                self._data = "".join(pixels)
                self.mode = "RGB"
            else:
                im = self._image
                mode = self.mode = im.mode
                if mode == "RGBA":
                    im.load()
                    self._dataA = PmlImageReader(im.split()[3])
                    im = im.convert("RGB")
                    self.mode = "RGB"
                elif mode not in ("L", "RGB", "CMYK"):
                    im = im.convert("RGB")
                    self.mode = "RGB"
                self._data = im.tobytes() if hasattr(im, "tobytes") else im.tostring()
        return self._data

    def getImageData(self):
        width, height = self.getSize()
        return width, height, self.getRGBData()

    def getTransparent(self):
        if sys.platform[:4] == "java":
            return None
        if "transparency" in self._image.info:
            transparency = self._image.info["transparency"] * 3
            palette = self._image.palette
            if hasattr(palette, "palette"):
                palette = palette.palette
            elif hasattr(palette, "data"):
                palette = palette.data
            else:
                return None

            # 8-bit PNGs could give an empty string as transparency value, so
            # we have to be careful here.
            try:
                return list(palette[transparency : transparency + 3])
            except Exception as e:
                log.debug(str(e), exc_info=e)
                return None
        else:
            return None

    def __str__(self) -> str:
        if isinstance(self.fileName, (PmlImage, Image, BytesIO)):
            fn = self.fileName.read() or id(self)
            return f"PmlImageObject_{hash(fn)}"
        return str(self.fileName or id(self))


class PmlImage(Flowable, PmlMaxHeightMixIn):
    def __init__(
        self,
        data: pisaFileObject | pisaTempFile | bytes,
        src: str | None = None,
        width: int | None = None,
        height: int | None = None,
        mask: str = "auto",
        mimetype: str | None = None,
        **kw: dict,
    ) -> None:
        self.kw: dict = kw
        self.hAlign: str = "CENTER"
        self._mask: str = mask
        self._imgdata: bytes = b""
        if isinstance(data, bytes):
            self._imgdata = data
        elif isinstance(data, pisaTempFile):
            self._imgdata = data.getvalue()
        elif isinstance(data, pisaFileObject):
            self._imgdata = data.getData() or b""
        self.src: str | None = src
        # print "###", repr(data)
        self.mimetype: str | None = mimetype

        # Resolve size
        drawing = self.getDrawing()
        self.imageWidth: float = 0.0
        self.imageHeight: float = 0.0
        if drawing:
            _, _, self.imageWidth, self.imageHeight = drawing.getBounds() or (
                0,
                0,
                0,
                0,
            )
        else:
            img = self.getImage()
            if img:
                self.imageWidth, self.imageHeight = img.getSize()

        self.drawWidth: float = width or self.imageWidth
        self.drawHeight: float = height or self.imageHeight

    def wrap(self, availWidth, availHeight):
        """
        Resize the image if necessary.

        This can be called more than once! Do not overwrite important data like drawWidth.
        """
        availHeight = self.setMaxHeight(availHeight)
        # print "image wrap", id(self), availWidth, availHeight, self.drawWidth, self.drawHeight
        width = min(self.drawWidth, availWidth)
        wfactor = float(width) / self.drawWidth
        height = min(self.drawHeight, availHeight * MAX_IMAGE_RATIO)
        hfactor = float(height) / self.drawHeight
        factor = min(wfactor, hfactor)
        self.dWidth = self.drawWidth * factor
        self.dHeight = self.drawHeight * factor
        # print "imgage result", factor, self.dWidth, self.dHeight
        return self.dWidth, self.dHeight

    def getDrawing(
        self, width: float | None = None, height: float | None = None
    ) -> Drawing | None:
        """If this image is a vector image and the library is available, returns a ReportLab Drawing."""
        if svg2rlg:
            try:
                drawing = svg2rlg(BytesIO(self._imgdata))
            except Exception:
                return None
            if drawing:
                # Apply size
                scale_x = 1
                scale_y = 1
                try:
                    if getattr(self, "drawWidth", None) is not None:
                        if width is None:
                            width = self.drawWidth
                        scale_x = width / drawing.width
                    if getattr(self, "drawHeight", None) is not None:
                        if height is None:
                            height = self.drawHeight
                        scale_y = height / drawing.height
                    if scale_x != 1 or scale_y != 1:
                        drawing.scale(scale_x, scale_y)
                except ZeroDivisionError:
                    log.warning(
                        "SVG drawing could not be resized: %r",
                        self.src or self._imgdata[:50],
                    )
                return drawing
        return None

    def getDrawingRaster(self) -> BytesIO | None:
        """If this image is a vector image and the libraries are available, returns a PNG raster."""
        if svg2rlg and renderPM:
            svg: Drawing = self.getDrawing()
            if svg:
                imgdata = BytesIO()
                renderPM.drawToFile(svg, imgdata, fmt="PNG")
                return imgdata
        return None

    def getImage(self) -> PmlImageReader:
        """Return a raster image."""
        vectorRaster = self.getDrawingRaster()
        imgdata = vectorRaster or BytesIO(self._imgdata)
        return PmlImageReader(imgdata)

    def draw(self) -> None:
        # TODO this code should work, but untested
        # drawing = self.getDrawing(self.dWidth, self.dHeight)
        # if drawing and renderPDF:
        #     renderPDF.draw(drawing, self.canv, 0, 0)
        # else:
        img = self.getImage()
        self.canv.drawImage(img, 0, 0, self.dWidth, self.dHeight, mask=self._mask)

    def identity(self, maxLen=None):
        return Flowable.identity(self, maxLen)


class PmlParagraphAndImage(ParagraphAndImage, PmlMaxHeightMixIn):
    def wrap(self, availWidth, availHeight):
        self.I.canv = self.canv
        result = ParagraphAndImage.wrap(self, availWidth, availHeight)
        del self.I.canv
        return result

    def split(self, availWidth, availHeight):
        # print "# split", id(self)
        if not hasattr(self, "wI"):
            self.wI, self.hI = self.I.wrap(
                availWidth, availHeight
            )  # drawWidth, self.I.drawHeight
        return ParagraphAndImage.split(self, availWidth, availHeight)


class PmlParagraph(Paragraph, PmlMaxHeightMixIn):
    def _calcImageMaxSizes(self, availWidth, availHeight):
        self.hasImages = False
        for frag in self.frags:
            if hasattr(frag, "cbDefn") and frag.cbDefn.kind == "img":
                img = frag.cbDefn
                if img.width > 0 and img.height > 0:
                    self.hasImages = True
                    width = min(img.width, availWidth)
                    wfactor = float(width) / img.width
                    height = min(
                        img.height, availHeight * MAX_IMAGE_RATIO
                    )  # XXX 99% because 100% do not work...
                    hfactor = float(height) / img.height
                    factor = min(wfactor, hfactor)
                    img.height *= factor
                    img.width *= factor

    def wrap(self, availWidth, availHeight):
        availHeight = self.setMaxHeight(availHeight)

        style = self.style

        self.deltaWidth = (
            style.paddingLeft
            + style.paddingRight
            + style.borderLeftWidth
            + style.borderRightWidth
        )
        self.deltaHeight = (
            style.paddingTop
            + style.paddingBottom
            + style.borderTopWidth
            + style.borderBottomWidth
        )

        # reduce the available width & height by the padding so the wrapping
        # will use the correct size
        availWidth -= self.deltaWidth
        availHeight -= self.deltaHeight

        # Modify maxium image sizes
        self._calcImageMaxSizes(availWidth, availHeight)

        # call the base class to do wrapping and calculate the size
        Paragraph.wrap(self, availWidth, availHeight)

        # self.height = max(1, self.height)
        # self.width = max(1, self.width)

        # increase the calculated size by the padding
        self.width = self.width + self.deltaWidth
        self.height = self.height + self.deltaHeight

        return self.width, self.height

    def split(self, availWidth, availHeight):
        if len(self.frags) <= 0:
            return []

        # the split information is all inside self.blPara
        if not hasattr(self, "deltaWidth"):
            self.wrap(availWidth, availHeight)

        availWidth -= self.deltaWidth
        availHeight -= self.deltaHeight

        return Paragraph.split(self, availWidth, availHeight)

    def draw(self):
        # Create outline
        if getattr(self, "outline", False):
            # Check level and add all levels
            last = getattr(self.canv, "outlineLast", -1) + 1
            while last < self.outlineLevel:
                # print "(OUTLINE",  last, self.text
                key = uuid4().hex
                self.canv.bookmarkPage(key)
                self.canv.addOutlineEntry(self.text, key, last, not self.outlineOpen)
                last += 1
            self.canv.outlineLast = self.outlineLevel

            key = uuid4().hex

            self.canv.bookmarkPage(key)
            self.canv.addOutlineEntry(
                self.text, key, self.outlineLevel, not self.outlineOpen
            )
            last += 1

        # Draw the background and borders here before passing control on to
        # ReportLab. This is because ReportLab can't handle the individual
        # components of the border independently. This will also let us
        # support more border styles eventually.
        canvas = self.canv
        style = self.style
        bg = style.backColor
        leftIndent = style.leftIndent
        bp = 0  # style.borderPadding

        x = leftIndent - bp
        y = -bp
        w = self.width - (leftIndent + style.rightIndent) + 2 * bp
        h = self.height + 2 * bp

        if bg:
            # draw a filled rectangle (with no stroke) using bg color
            canvas.saveState()
            canvas.setFillColor(bg)
            canvas.rect(x, y, w, h, fill=1, stroke=0)
            canvas.restoreState()

        # we need to hide the bg color (if any) so Paragraph won't try to draw it again
        style.backColor = None

        # offset the origin to compensate for the padding
        canvas.saveState()
        canvas.translate(
            (style.paddingLeft + style.borderLeftWidth),
            -1 * (style.paddingTop + style.borderTopWidth),
        )  # + (style.leading / 4)))

        # Call the base class draw method to finish up
        Paragraph.draw(self)
        canvas.restoreState()

        # Reset color because we need it again if we run 2-PASS like we
        # do when using TOC
        style.backColor = bg

        canvas.saveState()

        def _drawBorderLine(bstyle, width, color, x1, y1, x2, y2):
            # We need width and border style to be able to draw a border
            if width and getBorderStyle(bstyle):
                # If no color for border is given, the text color is used (like defined by W3C)
                if color is None:
                    color = style.textColor
                    # print "Border", bstyle, width, color
                if color is not None:
                    canvas.setStrokeColor(color)
                    canvas.setLineWidth(width)
                    canvas.line(x1, y1, x2, y2)

        _drawBorderLine(
            style.borderLeftStyle,
            style.borderLeftWidth,
            style.borderLeftColor,
            x,
            y,
            x,
            y + h,
        )
        _drawBorderLine(
            style.borderRightStyle,
            style.borderRightWidth,
            style.borderRightColor,
            x + w,
            y,
            x + w,
            y + h,
        )
        _drawBorderLine(
            style.borderTopStyle,
            style.borderTopWidth,
            style.borderTopColor,
            x,
            y + h,
            x + w,
            y + h,
        )
        _drawBorderLine(
            style.borderBottomStyle,
            style.borderBottomWidth,
            style.borderBottomColor,
            x,
            y,
            x + w,
            y,
        )

        canvas.restoreState()


class PmlKeepInFrame(KeepInFrame, PmlMaxHeightMixIn):
    def wrap(self, availWidth, availHeight):
        availWidth = max(availWidth, 1.0)
        availHeight = max(availHeight, 1.0)
        self.maxWidth = availWidth
        self.maxHeight = self.setMaxHeight(availHeight)
        return KeepInFrame.wrap(self, availWidth, availHeight)


class PmlTable(Table, PmlMaxHeightMixIn):
    @staticmethod
    def _normWidth(w, maxw):
        """Normalize width when using percentages."""
        if isinstance(w, str):
            w = (maxw / 100.0) * float(w[:-1])
        elif (w is None) or (w == "*"):
            w = maxw
        return min(w, maxw)

    def _listCellGeom(self, V, w, s, W=None, H=None, aH=72000):
        # print "#", self.availHeightValue
        if aH == 72000:
            aH = self.getMaxHeight() or aH
        return Table._listCellGeom(self, V, w, s, W=W, H=H, aH=aH)

    def wrap(self, availWidth, availHeight):
        self.setMaxHeight(availHeight)

        # Strange bug, sometime the totalWidth is not set !?
        if not hasattr(self, "totalWidth"):
            self.totalWidth = availWidth

        # Prepare values
        totalWidth = self._normWidth(self.totalWidth, availWidth)
        remainingWidth = totalWidth
        remainingCols = 0
        newColWidths = self._colWidths

        # Calculate widths that are fix
        # IMPORTANT!!! We can not substitute the private value
        # self._colWidths therefore we have to modify list in place
        for i, colWidth in enumerate(newColWidths):
            if colWidth is not None:
                newColWidth = self._normWidth(colWidth, totalWidth)
                remainingWidth -= newColWidth
            else:
                remainingCols += 1
                newColWidth = None
            newColWidths[i] = newColWidth

        # Distribute remaining space
        minCellWidth = totalWidth * 0.01
        if remainingCols > 0:
            for i, colWidth in enumerate(newColWidths):
                if colWidth is None:
                    newColWidths[i] = max(
                        minCellWidth, remainingWidth / remainingCols
                    )  # - 0.1

        # Bigger than totalWidth? Lets reduce the fix entries propotionally

        if sum(newColWidths) > totalWidth:
            quotient = totalWidth / sum(newColWidths)
            for i in range(len(newColWidths)):
                newColWidths[i] = newColWidths[i] * quotient

        # To avoid rounding errors adjust one col with the difference
        diff = sum(newColWidths) - totalWidth
        if diff > 0:
            newColWidths[0] -= diff

        return Table.wrap(self, availWidth, availHeight)


class PmlPageCount(IndexingFlowable):
    def __init__(self) -> None:
        super().__init__()
        self.second_round = False

    def isSatisfied(self):
        s = self.second_round
        self.second_round = True
        return s

    def drawOn(self, canvas, x, y, _sW=0):
        pass


class PmlTableOfContents(TableOfContents):
    def wrap(self, availWidth, availHeight):
        """All table properties should be known by now."""
        widths = (availWidth - self.rightColumnWidth, self.rightColumnWidth)

        # makes an internal table which does all the work.
        # we draw the LAST RUN's entries!  If there are
        # none, we make some dummy data to keep the table
        # from complaining
        if len(self._lastEntries) == 0:
            _tempEntries = [(0, "Placeholder for table of contents", 0)]
        else:
            _tempEntries = self._lastEntries

        lastMargin = 0
        tableData = []
        tableStyle = [
            ("VALIGN", (0, 0), (-1, -1), "TOP"),
            ("LEFTPADDING", (0, 0), (-1, -1), 0),
            ("RIGHTPADDING", (0, 0), (-1, -1), 0),
            ("TOPPADDING", (0, 0), (-1, -1), 0),
            ("BOTTOMPADDING", (0, 0), (-1, -1), 0),
        ]
        for i, entry in enumerate(_tempEntries):
            level, text, pageNum = entry[:3]
            leftColStyle = self.levelStyles[level]
            if i:  # Not for first element
                tableStyle.append(
                    (
                        "TOPPADDING",
                        (0, i),
                        (-1, i),
                        max(lastMargin, leftColStyle.spaceBefore),
                    )
                )
                # print leftColStyle.leftIndent
            lastMargin = leftColStyle.spaceAfter
            # right col style is right aligned
            rightColStyle = ParagraphStyle(
                name="leftColLevel%d" % level,
                parent=leftColStyle,
                leftIndent=0,
                alignment=TA_RIGHT,
            )
            leftPara = Paragraph(text, leftColStyle)
            rightPara = Paragraph(str(pageNum), rightColStyle)
            tableData.append([leftPara, rightPara])

        self._table = Table(tableData, colWidths=widths, style=TableStyle(tableStyle))

        self.width, self.height = self._table.wrapOn(self.canv, availWidth, availHeight)
        return self.width, self.height


class PmlRightPageBreak(CondPageBreak):
    def __init__(self) -> None:
        pass

    def wrap(self, availWidth, availHeight):
        if not self.canv.getPageNumber() % 2:
            self.width = availWidth
            self.height = availHeight
            return availWidth, availHeight
        self.width = self.height = 0
        return 0, 0


class PmlLeftPageBreak(CondPageBreak):
    def __init__(self) -> None:
        pass

    def wrap(self, availWidth, availHeight):
        if self.canv.getPageNumber() % 2:
            self.width = availWidth
            self.height = availHeight
            return availWidth, availHeight
        self.width = self.height = 0
        return 0, 0


# --- Pdf Form


class PmlInput(Flowable):
    def __init__(
        self,
        name,
        input_type="text",
        width=10,
        height=10,
        default="",
        options=None,
        multiline=0,
    ) -> None:
        self.width = width
        self.height = height
        self.type = input_type
        self.name = name
        self.default = default
        self.options = options if options is not None else []
        self.multiline = multiline

    def wrap(self, *args):
        return self.width, self.height

    def draw(self):
        c = self.canv

        c.saveState()
        c.setFont("Helvetica", 10)
        if self.type == "text":
            pdfform.textFieldRelative(
                c, self.name, 0, 0, self.width, self.height, multiline=self.multiline
            )
            c.rect(0, 0, self.width, self.height)
        elif self.type == "radio":
            c.rect(0, 0, self.width, self.height)
        elif self.type == "checkbox":
            if self.default:
                pdfform.buttonFieldRelative(c, self.name, "Yes", 0, 0)
            else:
                pdfform.buttonFieldRelative(c, self.name, "Off", 0, 0)
            c.rect(0, 0, self.width, self.height)
        elif self.type == "select":
            pdfform.selectFieldRelative(
                c, self.name, self.default, self.options, 0, 0, self.width, self.height
            )
            c.rect(0, 0, self.width, self.height)

        c.restoreState()
