from datetime import datetime
from os import makedirs, path
from pickle import dumps, loads
from time import time

from fpdf import FPDF
from progressbar import ETA, Bar, Percentage, ProgressBar

from .constants import (
    GENERATING_PDF_INFO,
    INFO_TEXT_FONT,
    MARGIN_FONT,
    MARGIN_LR,
    MARGIN_TB,
    MEDIUM_TEXT_FONT,
    MEDIUM_TEXT_FONT_BOLD,
    RECURSION_ERROR,
    SMALL_TEXT_FONT,
    SUBTITLE_FONT,
    TITLE_FONT,
    TITLE_PAGE_INFO_FONT,
    WRITING_PDF_INFO,
)
from .logger import logger


class FPDF_PDF:
    """PDF generation class implementing 2 methods of generation (``gen2a`` and
    ``gen2b``.
    """

    def __init__(
        self,
        commits: object,
        output: str,
        filename: str,
        appearance: dict,
        mode: str,
        scaling: str,
    ) -> None:
        super().__init__()  # portrait; millimetres; a4
        FPDF_PDF._set_scaling(scaling)

        self.err_flag: bool = False
        self.timestamp = datetime.fromtimestamp(int(time())).strftime(
            "%Y-%m-%d %H:%M:%S"
        )
        self._commits, self._output, self._filename, self._ap, self._mode = (
            commits,
            output,
            filename,
            appearance,
            mode,
        )
        # Keep track of generation progress for the user
        self._progress = ProgressBar(
            maxval=len(self._commits.filtered_commits),
            widgets=[Percentage(), Bar(), ETA()],
        )

        self._p = FPDF()  # Contains the PDF data
        self._configure_fpdf()
        logger.info(GENERATING_PDF_INFO)
        self._prepare_and_draw()
        logger.info(
            WRITING_PDF_INFO.format(
                self._output + " ..."
                if self._output != "."
                else "your current directory..."
            )
        )
        self._write()

    @staticmethod
    def _set_scaling(scaling: float) -> None:
        """Scale the fonts based on the user-selected scaling. Set to 1.0 by
        default.
        """
        TITLE_FONT[2] *= scaling
        SUBTITLE_FONT[2] *= scaling
        SMALL_TEXT_FONT[2] *= scaling
        MEDIUM_TEXT_FONT[2] *= scaling
        MEDIUM_TEXT_FONT_BOLD[2] *= scaling
        INFO_TEXT_FONT[2] *= scaling

    def _configure_fpdf(self):
        self._p.set_fill_color(*self._ap["background"])
        self._p.set_creator("commits2pdf")
        self._p.set_title(f"Commits Report - {self._commits.rname}")
        self._p.set_author(f"{self._commits.owner}")
        self._p.set_subject("Git Commit Report")
        self._p.set_keywords(
            "git;repo;repository;report;documentation;cli;python;git-commit"
        )
        self._p.set_margins(MARGIN_LR, MARGIN_TB)

    def _prepare_and_draw(self):
        self._p.add_page()
        self._draw_page_bg()
        self._draw_title_page()
        self._p.set_auto_page_break(auto=True)
        if self._mode == "unstable":
            self.do_pre_vis: bool = True
            self._p.set_auto_page_break(auto=False)
        else:
            self.do_pre_vis: bool = False

        if len(self._commits.filtered_commits) > 0:
            self._draw_commits()

    def _draw_footer(self) -> None:
        self._p.set_y(-1 * (MARGIN_TB / 2))
        self._set_font(*MARGIN_FONT)
        self._p.set_text_color(*self._ap["text"])
        self._p.cell(1, 0, f"Page {self._p.page_no()}", 0, 0, "L")
        self._p.cell(
            0, 0, f"Generated by commits2pdf at {self.timestamp}", 0, 0, "C"
        )

    def _draw_page_bg(self) -> None:
        """Draw the page background as a rectangle."""
        self._p.rect(h=self._p.h, w=self._p.w, x=0, y=0, style="DF")

    def _write(self) -> None:
        """Save the file where the user has specified."""
        if not path.exists(self._output):
            makedirs(self._output)
        self._p.output(path.join(self._output, self._filename), "F")

    def _set_font(
        self, *args: list[float], obj: str | object = "main"
    ) -> None:
        """Set the current font to a RGB value."""
        p = self._p if obj == "main" else obj
        p.set_font(args[0], args[1], args[2])

    def _draw_commits(self) -> None:
        """Driver function to draw all the commits using either the
        pre-visualisation method or the commit height estimation method.
        """
        self._p.add_page()  # Draw separate to the title page
        self._draw_page_bg()

        self._progress.start()
        counter: int = 0
        # gen2b
        if self._mode == "unstable":
            for commit in self._commits.filtered_commits:
                result = self._draw_commit(commit, pre_vis=True)
                if result == "NEW_PAGE_OK":  # Break page
                    self._draw_footer()
                    self._p.add_page()
                    self._draw_page_bg()
                    self._draw_commit(commit)
                elif (
                    result == "NEW_PAGE_OK_BUT_NO_DIVIDER"
                ):  # The divider just
                    # barely doesn't fit
                    self._draw_commit(commit, no_divider=True)
                else:  # No need to break the page
                    self._draw_commit(commit)

                counter += 1
                self._progress.update(counter)

        # gen2a
        elif self._mode == "stable":
            for commit in self._commits.filtered_commits:
                if self._commit_exceeds_size(commit):  # Break page
                    self._draw_footer()
                    self._p.add_page()
                    self._draw_page_bg()
                self._draw_commit(commit)

                counter += 1
                self._progress.update(counter)

        self._progress.finish()

    def _draw_commit(
        self,
        commit: dict[str, str],
        pre_vis: bool = False,
        no_divider: bool = False,
    ) -> str | None:
        """Draw all the parts of a commit to the current ``FPDF`` instance
        or a copy of it as part of the ``gen2b`` generation implementation.
        """
        if not pre_vis or not self.do_pre_vis:
            p = self._p
        else:
            try:
                p = loads(dumps(self._p))
            except RecursionError:
                logger.error(RECURSION_ERROR)
                self.err_flag = True
                exit(1)
            except Exception as ex:
                logger.error(
                    f"An unrecognised error occured -- {type(ex).__name__}: {ex}"
                )
                self.err_flag = True
                exit(1)

        p.set_text_color(*self._ap["text"])
        self._set_font(*INFO_TEXT_FONT, obj=p)
        p.multi_cell(w=0, h=p.font_size * 1.25, align="C", txt=commit["info"])
        p.ln(p.font_size * 0.75)
        # Tells the driver code that it must add a page
        if self.do_pre_vis and pre_vis and p.get_y() > p.h * 0.95:
            return "NEW_PAGE_OK"

        self._set_font(*MEDIUM_TEXT_FONT_BOLD, obj=p)
        p.multi_cell(w=0, h=p.font_size * 1.25, align="L", txt=commit["title"])
        p.ln(-1 * p.font_size * 0.5)
        if self.do_pre_vis and pre_vis and p.get_y() > p.h * 0.95:
            return "NEW_PAGE_OK"

        self._set_font(*SMALL_TEXT_FONT, obj=p)
        p.multi_cell(
            w=0, h=p.font_size * 1.5, align="L", txt=commit["description"]
        )
        p.ln()
        if self.do_pre_vis and pre_vis and p.get_y() > p.h * 0.95:
            return "NEW_PAGE_OK"

        p.set_text_color(*self._ap["diff_url"])
        self._set_font(*SMALL_TEXT_FONT, obj=p)
        p.write(p.font_size * 1.5, "View diff on GitHub", commit["diff_url"])
        p.ln(p.font_size * 4.75)
        if self.do_pre_vis and pre_vis and p.get_y() > p.h * 0.95:
            return "NEW_PAGE_OK"

        if pre_vis or not no_divider or not self.do_pre_vis:
            div_y = p.get_y() - p.font_size * 3.25 / 2
            self._p.set_draw_color(*self._ap["text"])
            p.line(p.l_margin, div_y, p.w - p.r_margin, div_y)
            self._p.set_draw_color(*self._ap["background"])
            if self.do_pre_vis and pre_vis and p.get_y() > p.h * 0.97:
                return "NEW_PAGE_OK_BUT_NO_DIVIDER"

    def _commit_exceeds_size(self, commit: dict[str, str]) -> bool:
        """Estimate the height of the upcoming commit to see if a new page must
        be added. It has a poor consistency that worsens with the increase/
        decrease of scaling. It is used as part of the gen2a generation
        implementation.
        """
        info_lines = (
            len(commit["info"]) * (INFO_TEXT_FONT[2] // 2)
        ) // self._p.w
        title_lines = (
            len(commit["title"]) * (MEDIUM_TEXT_FONT_BOLD[2] // 2)
        ) // self._p.w
        desc_lines = (
            len(commit["description"]) * (SMALL_TEXT_FONT[2] // 2)
        ) // self._p.w
        diff_lines = (
            len(commit["diff_url"]) * (SMALL_TEXT_FONT[2] // 2)
        ) // self._p.w

        height = (
            (info_lines * INFO_TEXT_FONT[2])
            + (title_lines * MEDIUM_TEXT_FONT_BOLD[2])
            + (desc_lines * SMALL_TEXT_FONT[2])
            + (diff_lines * SMALL_TEXT_FONT[2])
        )

        return height * 1.2 > self._p.h - self._p.get_y()

    def _draw_title_page(self) -> None:
        """Draw the title, repository name, and filtering information on the
        first page of the PDF.
        """
        self._set_font(*TITLE_FONT)
        self._p.set_text_color(*self._ap["text"])
        self._p.cell(
            w=0, h=self._p.font_size * 1.5, txt="Commits Report", align="C"
        )
        self._p.ln()

        self._set_font(*SUBTITLE_FONT)
        self._p.multi_cell(
            w=0,
            h=self._p.font_size * 1.25,
            txt=f"Repository: {self._commits.rname}",
            align="C",
        )
        self._p.ln(self._p.font_size)

        self._set_font(*TITLE_PAGE_INFO_FONT)
        self._p.multi_cell(
            0,
            self._p.font_size * 1.5,
            txt=f"Owner: {self._commits.owner}",
            align="C",
        )
        self._p.ln()
        self._p.multi_cell(
            0,
            self._p.font_size * 1.5,
            align="C",
            txt=f"Selected authors: "
            f"{', '.join(self._commits.authors.split(',')) if self._commits.authors else 'All'}",
        )
        self._p.ln()
        self._p.multi_cell(
            0,
            self._p.font_size * 1.5,
            align="C",
            txt=f"Start date: "
            f"{self._commits.start_date.strftime('%Y-%m-%d') if self._commits.start_date else 'N/A'} "
            f"| End date: {self._commits.end_date.strftime('%Y-%m-%d') if self._commits.end_date else 'N/A'}",
        )
        self._p.ln()
        self._p.multi_cell(
            0,
            self._p.font_size * 1.5,
            txt=f"Newest n commits: "
            f"{self._commits.newest_n_commits} | Oldest n commits: "
            f"{self._commits.oldest_n_commits}",
            align="C",
        )
        self._p.ln()
        self._p.multi_cell(
            0,
            self._p.font_size * 1.5,
            align="C",
            txt=f"AND queries: "
            f"{', '.join(self._commits.queries_all) if self._commits.queries_all else 'None'}",
        )
        self._p.ln()
        self._p.multi_cell(
            0,
            self._p.font_size * 1.5,
            align="C",
            txt=f"OR queries: "
            f"{', '.join(self._commits.queries_any) if self._commits.queries_any else 'None'}",
        )
        self._p.ln()
        self._p.multi_cell(
            0,
            self._p.font_size * 1.5,
            align="C",
            txt=f"Sorting: "
            f"{'Oldest to newest' if not self._commits.reverse else 'Newest to oldest'}",
        )
        self._p.ln()
