"""
Converts a folder of images to EPUB/PDF/CBZ/MOBI
"""
import datetime
import os
import re
import shutil
import tempfile
import textwrap
import unicodedata
import zipfile
from dataclasses import dataclass
from enum import Enum
from pathlib import Path
from typing import Iterator

from natsort import natsorted

try:
    from PIL import Image

    HAS_PILLOW = True
except ImportError:
    HAS_PILLOW = False

from mandown.sources.base_source import MangaMetadata

PDF_IMAGE_MAX_INTERVAL = 200  # adjust for memory as necessary
PDF_IMAGE_MIN_INTERVAL_FACTOR = 0.12

ACCEPTED_IMAGE_EXTENSIONS = {
    ".gif": "gif",
    ".png": "png",
    ".jpg": "jpeg",
    ".jpeg": "jpeg",
    ".jpe": "jpeg",
}

# temporary measure
KOBO_WIDTH, KOBO_HEIGHT = 1005, 1430


class PageProgression(str, Enum):
    LEFT_TO_RIGHT = "ltr"
    RIGHT_TO_LEFT = "rtl"


@dataclass
class Options:
    page_progression: PageProgression = PageProgression.LEFT_TO_RIGHT


class Converter:
    def __init__(
        self,
        folder_path: str,
        metadata: MangaMetadata | None = None,
        chapter_list: list[tuple[str, str]] | None = None,
        options: Options | None = None,
    ) -> None:
        """
        Get the file path of a comic and identify its metadata.
        The chapter list is a list of tuples in the format
        (`chapter_path`, `chapter_title`), where `chapter_path`
        is the path of the chapter relative to `folder_path`.

        If the chapter list or metadata are not provided, they will
        be guessed from the directory structure.
        """
        if not Path(folder_path).is_dir():
            raise ValueError(f"{folder_path} is not a directory.")

        self.folder_path = Path(os.path.realpath(folder_path))
        self.metadata = metadata or MangaMetadata(
            title=os.path.basename(self.folder_path),
            authors=[],
            url="",
            genres=[],
            description="Generated by mandown",
            cover_art="",
        )

        # self.chapters is found either by traversing the path (natsort) or by being fed
        working_chapters = chapter_list or []
        if not working_chapters:
            for item in natsorted(os.listdir(self.folder_path)):
                item = str(item)  # get rid of type complaints
                if (Path(self.folder_path) / item).is_dir():
                    working_chapters.append((item, item))

        padding = f"0{len(str(len(working_chapters)))}"

        self.chapters: list[tuple[str, str, str, list[str]]] = [
            (
                title,
                sanitised,
                f"{i+1:{padding}}-{self.slugify(sanitised)}",
                list(
                    filter(
                        lambda s: any(
                            str(s).lower().endswith(i)
                            for i in ACCEPTED_IMAGE_EXTENSIONS
                        ),
                        natsorted(os.listdir(self.folder_path / sanitised)),  # type: ignore
                    )
                ),
            )
            for i, (title, sanitised) in enumerate(working_chapters)
        ]

        for file in Path(folder_path).iterdir():
            if file.stem == "cover" and file.suffix in ACCEPTED_IMAGE_EXTENSIONS:
                self.cover = file
                break
        else:
            self.cover = None

        num_files = len(list(Path.rglob(self.folder_path, "*")))
        # useful for progress bars
        self.max_operations: dict[str, int] = {
            "epub": len(self.chapters) * 3,
            "cbz": len(self.chapters),
            "pdf": round(
                num_files
                / min(
                    PDF_IMAGE_MAX_INTERVAL,
                    num_files * PDF_IMAGE_MIN_INTERVAL_FACTOR,
                )
            ),
        }
        self.options = options or Options()

    def to_cbz_progress(self, dest_folder: str) -> Iterator:
        dest_file = Path(dest_folder) / f"{self.metadata.title}.cbz"
        with zipfile.ZipFile(dest_file, "w", zipfile.ZIP_DEFLATED) as file:  # type: ignore
            for dirpath, _, filenames in os.walk(self.folder_path):  # from kcc
                yield "Compressing"
                for name in filenames:
                    if (Path(dirpath) / name).is_file():
                        file.write(
                            Path(dirpath) / name,  # type: ignore
                            Path(dirpath.lstrip(str(self.folder_path))) / name,
                            zipfile.ZIP_DEFLATED,
                        )

    def to_cbz(self, dest_folder: str) -> None:
        for _ in self.to_cbz_progress(dest_folder):
            pass

    def to_epub_progress(self, dest_folder: str) -> Iterator:
        if len(self.chapters) == 0:
            raise IOError(
                f"{self.folder_path} must contain at least one folder for EPUB conversion"
                " for chapter recognition. If there is only one chapter, move your"
                f" images to a new folder in {self.folder_path} and rerun the command."
            )
        # taken from KCC generated epub file structure
        with tempfile.TemporaryDirectory() as temp:
            root = Path(temp)
            EpubGenerator.create_skeleton(root)

            oebps = root / "OEBPS"
            with open(oebps / "Text" / "style.css", "w", encoding="utf-8") as file:
                file.write(EpubGenerator.style_css)

            with open(oebps / "content.opf", "w", encoding="utf-8") as file:
                file.write(
                    EpubGenerator.generate_content_opf(
                        self.metadata,
                        self.chapters,
                        PageProgression.LEFT_TO_RIGHT,
                        self.cover,
                    )
                )

            with open(oebps / "toc.ncx", "w", encoding="utf-8") as file:
                file.write(EpubGenerator.generate_toc_ncx(self.metadata, self.chapters))

            with open(oebps / "nav.xhtml", "w", encoding="utf-8") as file:
                file.write(
                    EpubGenerator.generate_nav_xhtml(self.metadata, self.chapters)
                )

            for _, path, slug, images in self.chapters:
                yield "Building"
                os.mkdir(oebps / "Text" / slug)
                os.mkdir(oebps / "Images" / slug)
                padding = f"0{len(str(len(images)))}"

                for index, i in enumerate(images, start=1):
                    with open(
                        oebps / "Text" / slug / f"{index:{padding}}.xhtml",
                        "w",
                        encoding="utf-8",
                    ) as file:
                        file.write(
                            EpubGenerator.generate_image_html(
                                slug, f"{index:{padding}}{Path(i).suffix}"
                            )
                        )
                    shutil.copyfile(
                        self.folder_path / path / i,
                        oebps / "Images" / slug / f"{index:{padding}}{Path(i).suffix}",
                    )

            if self.cover is not None:
                shutil.copyfile(self.cover, oebps / "Images" / self.cover.name)

            # compress and move epub
            dest_file = Path(dest_folder) / f"{self.metadata.title}.epub"

            with zipfile.ZipFile(dest_file, "w", zipfile.ZIP_DEFLATED) as zipout:
                zipout.writestr("mimetype", "application/epub+zip", zipfile.ZIP_STORED)
                for dirpath, _, filenames in os.walk(root):  # from kcc
                    yield "Compressing"
                    for name in filenames:
                        if (Path(dirpath) / name).is_file():
                            zipout.write(
                                Path(dirpath) / name,
                                Path(dirpath.lstrip(str(root))) / name,
                                zipfile.ZIP_DEFLATED,
                            )

    def to_epub(self, dest_folder: str) -> None:
        for _ in self.to_epub_progress(dest_folder):
            pass

    def to_pdf_progress(self, dest_folder: str) -> Iterator:
        if not HAS_PILLOW:
            raise ImportError(
                "Pillow could not be found and is required for PDF conversion."
            )

        images: list[Image.Image] = []
        for f in Path.rglob(self.folder_path, "*"):
            if f.suffix in ACCEPTED_IMAGE_EXTENSIONS:
                images.append(Image.open(f))

        if len(images) > 0:
            interval = round(
                min(
                    PDF_IMAGE_MAX_INTERVAL,
                    len(images) * PDF_IMAGE_MIN_INTERVAL_FACTOR,
                )
            )

            for i in range(0, len(images), interval):
                images[i].save(
                    Path(dest_folder) / (self.metadata.title + ".pdf"),
                    "PDF",
                    resolution=100.0,
                    save_all=True,
                    append_images=images[i + 1 : i + interval]
                    if len(images) > i + 1
                    else None,
                    title=self.metadata.title,
                    author=""
                    if len(self.metadata.authors) == 0
                    else self.metadata.authors[0],
                    append=i != 0,
                    creator="mandown",
                    producer="mandown",
                )

                for j in images[i : i + interval]:
                    j.close()
                yield

    def to_pdf(self, dest_folder: str) -> None:
        for _ in self.to_pdf_progress(dest_folder):
            pass

    def to_mobi(self) -> None:
        pass

    @staticmethod
    def slugify(string: str) -> str:
        """
        Remove all potentially invalid characters for XML
        """
        slug = unicodedata.normalize("NFKD", string)
        slug = slug.lower()
        slug = re.sub(r"[^a-z0-9]+", "-", slug).strip("-")
        slug = re.sub(r"[-]+", "-", slug)
        return slug


# pylint: disable=line-too-long
class EpubGenerator:
    style_css = textwrap.dedent(
        """\
        @page {
        margin: 0;
        }
        body {
        display: block;
        margin: 0;
        padding: 0;
        }\
        """
    )

    @staticmethod
    def create_skeleton(root: Path) -> None:
        os.mkdir(root / "META-INF")
        with open(root / "META-INF" / "container.xml", "w", encoding="utf-8") as file:
            file.write(
                textwrap.dedent(
                    """\
                    <?xml version="1.0"?>
                    <container version="1.0" xmlns="urn:oasis:names:tc:opendocument:xmlns:container">
                    <rootfiles>
                    <rootfile full-path="OEBPS/content.opf" media-type="application/oebps-package+xml"/>
                    </rootfiles>
                    </container>\
                    """
                )
            )

        os.mkdir(root / "OEBPS")
        os.mkdir(root / "OEBPS" / "Images")
        os.mkdir(root / "OEBPS" / "Text")

    @staticmethod
    def generate_image_html(
        chapter_slug: str,
        image_path: str,
        width: int = KOBO_WIDTH,
        height: int = KOBO_HEIGHT,
    ) -> str:
        image_base = Path(image_path).with_suffix("")
        return textwrap.dedent(
            f"""\
            <?xml version="1.0" encoding="UTF-8"?>
            <!DOCTYPE html>
            <html xmlns="http://www.w3.org/1999/xhtml" xmlns:epub="http://www.idpf.org/2007/ops">
            <head>
            <title>{image_base}</title>
            <link href="../style.css" type="text/css" rel="stylesheet"/>
            </head>
            <body style="">
            <div style="text-align:center;top:0.0%;">
            <img width="{width}" height="{height}" src="../../Images/{chapter_slug}/{image_path}"/>
            </div>
            </body>
            </html>\
            """
        )

    @staticmethod
    def generate_toc_ncx(
        metadata: MangaMetadata, chapters: list[tuple[str, str, str, list[str]]]
    ) -> str:
        nav_map: list[str] = []
        for title, _, slug, images in chapters:
            padding = f"0{(len(str(len(images))))}"
            nav_map.append(
                f'<navPoint id="Text_{slug}"><navLabel><text>{title}</text></navLabel><content src="Text/{slug}/{1:{padding}}.xhtml"/></navPoint>'
            )

        return textwrap.dedent(
            f"""\
            <?xml version="1.0" encoding="UTF-8"?>
            <ncx version="2005-1" xml:lang="en-US" xmlns="http://www.daisy.org/z3986/2005/ncx/">
            <head>
            <meta name="dtb:uid" content="urn:uuid:bafec70e-0286-49d3-b1e2-9e9f297c8cfe"/>
            <meta name="dtb:depth" content="1"/>
            <meta name="dtb:totalPageCount" content="0"/>
            <meta name="dtb:maxPageNumber" content="0"/>
            <meta name="generated" content="true"/>
            </head>
            <docTitle><text>{metadata.title}</text></docTitle>
            <navMap>
            {(chr(10) + " "*12).join(nav_map)}
            </navMap>
            </ncx>\
            """
        )

    @staticmethod
    def generate_content_opf(
        metadata: MangaMetadata,
        chapters: list[tuple[str, str, str, list[str]]],
        progression: PageProgression,
        cover: Path | None = None,
    ) -> str:
        """
        `chapters` must be a list containing a tuple of elements in this order:
          - `title`, the title of the chapter
          - `sanitised_title`, the relative path to the chapter folder
          - `slug`, a safe-for-html version of the title
          - `images`, a list of paths to each image
        """
        # fmt: off
        cover_html = (
            f'<item id="cover" href="Images/{cover.name}" media-type="image/{ACCEPTED_IMAGE_EXTENSIONS[cover.suffix]}" properties="cover-image"/>'
            if cover else ""
        )
        # fmt: on

        time_now = datetime.datetime.utcnow().replace(microsecond=0).isoformat()

        item_ids: list[str] = []
        item_refs: list[str] = []
        for *_, slug, images in chapters:
            padding = f"0{len(str(len(images)))}"
            for index, i in enumerate(images, start=1):
                marker = f"{index:{padding}}"
                item_ids.append(
                    f'<item id="page_Images_{slug}_{marker}" href="Text/{slug}/{marker}.xhtml" media-type="application/xhtml+xml" />'
                )
                item_ids.append(
                    f'<item id="img_Images_{slug}_{marker}" href="Images/{slug}/{marker}{Path(i).suffix}" media-type="image/{ACCEPTED_IMAGE_EXTENSIONS[Path(i).suffix]}" />'
                )

                item_refs.append(f'<itemref idref="page_Images_{slug}_{marker}" />')

        return textwrap.dedent(
            f"""\
            <?xml version="1.0" encoding="UTF-8"?>
            <package version="3.0" unique-identifier="BookID" xmlns="http://www.idpf.org/2007/opf">
            <metadata xmlns:opf="http://www.idpf.org/2007/opf" xmlns:dc="http://purl.org/dc/elements/1.1/">
            <dc:title>{metadata.title}</dc:title>
            <dc:language>en-US</dc:language>
            <dc:identifier id="BookID">urn:uuid:bafec70e-0286-49d3-b1e2-9e9f297c8cfe</dc:identifier>
            <dc:contributor id="contributor">mandown</dc:contributor>
            {(chr(10) + " "*12).join(
                f'<dc:creator id="id-{i}">{name}</dc:creator>'
                for i, name in enumerate(metadata.authors)
            )}
            <dc:description>{
                (chr(10) + " "*12).join(
                    metadata.description.split(chr(10))
                )
            }</dc:description>
            {
                (chr(10) + " "*12).join(
                    f"<dc:subject>{genre}</dc:subject>"
                    for genre in metadata.genres
                )
            }
            <meta property="dcterms:modified">{time_now}Z</meta>
            <meta name="cover" content="cover"/>
            <meta property="rendition:orientation">portrait</meta>
            <meta property="rendition:spread">both</meta>
            <meta property="rendition:layout">pre-paginated</meta>
            </metadata>
            <manifest>
            <item id="ncx" href="toc.ncx" media-type="application/x-dtbncx+xml"/>
            <item id="nav" href="nav.xhtml" properties="nav" media-type="application/xhtml+xml"/>
            <item id="css" href="Text/style.css" media-type="text/css"/>
            {cover_html}
            {(chr(10) + " "*12).join(item_ids)}
            </manifest>
            <spine page-progression-direction="{progression.value}" toc="ncx">
            {(chr(10) + " "*12).join(item_refs)}
            </spine>
            </package>\
            """
        )

    @staticmethod
    def generate_nav_xhtml(
        metadata: MangaMetadata, chapters: list[tuple[str, str, str, list[str]]]
    ) -> str:
        lis: list[str] = []
        for title, _, slug, images in chapters:
            padding = f"0{len(str(len(images)))}"
            lis.append(
                f'<li><a href="Text/{slug}/{1:{padding}}.xhtml">{title}</a></li>'
            )

        return textwrap.dedent(
            f"""\
            <?xml version="1.0" encoding="utf-8"?>
            <!DOCTYPE html>
            <html xmlns="http://www.w3.org/1999/xhtml" xmlns:epub="http://www.idpf.org/2007/ops">
            <head>
            <title>{metadata.title}</title>
            <meta charset="utf-8"/>
            </head>
            <body>
            <nav xmlns:epub="http://www.idpf.org/2007/ops" epub:type="toc" id="toc">
            <ol>
            {(chr(10) + " "*12).join(lis)}
            </ol>
            </nav>
            <nav epub:type="page-list">
            <ol>
            {(chr(10) + " "*12).join(lis)}
            </ol>
            </nav>
            </body>
            </html>\
            """
        )
