import datetime
import itertools
import json
import logging
import multiprocessing
import os
import pprint
import re

from pelican import signals
from pelican.generators import ArticlesGenerator, PagesGenerator
from pelican.settings import DEFAULT_CONFIG
from pelican.utils import pelican_open

logger = logging.getLogger(__name__)

try:
    from PIL import Image, ImageDraw, ImageEnhance, ImageFont, ImageOps
except ImportError:
    logger.error("PIL/Pillow not found")

try:
    import piexif
except ImportError:
    ispiexif = False
    logger.warning("piexif not found! Cannot use exif manipulation features")
else:
    ispiexif = True
    logger.debug("piexif found.")


def initialized(pelican):
    p = os.path.expanduser("~/Pictures")

    DEFAULT_CONFIG.setdefault("PHOTO_LIBRARY", p)
    DEFAULT_CONFIG.setdefault("PHOTO_GALLERY", (1024, 768, 80))
    DEFAULT_CONFIG.setdefault("PHOTO_ARTICLE", (760, 506, 80))
    DEFAULT_CONFIG.setdefault("PHOTO_THUMB", (192, 144, 60))
    DEFAULT_CONFIG.setdefault("PHOTO_SQUARE_THUMB", False)
    DEFAULT_CONFIG.setdefault("PHOTO_GALLERY_TITLE", "")
    DEFAULT_CONFIG.setdefault("PHOTO_ALPHA_BACKGROUND_COLOR", (255, 255, 255))
    DEFAULT_CONFIG.setdefault("PHOTO_WATERMARK", False)
    DEFAULT_CONFIG.setdefault("PHOTO_WATERMARK_THUMB", False)
    DEFAULT_CONFIG.setdefault("PHOTO_WATERMARK_TEXT", DEFAULT_CONFIG["SITENAME"])
    DEFAULT_CONFIG.setdefault("PHOTO_WATERMARK_TEXT_COLOR", (255, 255, 255))
    DEFAULT_CONFIG.setdefault("PHOTO_WATERMARK_IMG", "")
    DEFAULT_CONFIG.setdefault("PHOTO_WATERMARK_IMG_SIZE", False)
    DEFAULT_CONFIG.setdefault("PHOTO_RESIZE_JOBS", 1)
    DEFAULT_CONFIG.setdefault("PHOTO_EXIF_KEEP", False)
    DEFAULT_CONFIG.setdefault("PHOTO_EXIF_REMOVE_GPS", False)
    DEFAULT_CONFIG.setdefault("PHOTO_EXIF_AUTOROTATE", True)
    DEFAULT_CONFIG.setdefault("PHOTO_EXIF_COPYRIGHT", False)
    DEFAULT_CONFIG.setdefault("PHOTO_EXIF_COPYRIGHT_AUTHOR", DEFAULT_CONFIG["SITENAME"])
    DEFAULT_CONFIG.setdefault("PHOTO_LIGHTBOX_GALLERY_ATTR", "data-lightbox")
    DEFAULT_CONFIG.setdefault("PHOTO_LIGHTBOX_CAPTION_ATTR", "data-title")

    DEFAULT_CONFIG["queue_resize"] = {}
    DEFAULT_CONFIG["created_galleries"] = {}
    DEFAULT_CONFIG["plugin_dir"] = os.path.dirname(os.path.realpath(__file__))

    if pelican:
        pelican.settings.setdefault("PHOTO_LIBRARY", p)
        pelican.settings.setdefault("PHOTO_GALLERY", (1024, 768, 80))
        pelican.settings.setdefault("PHOTO_ARTICLE", (760, 506, 80))
        pelican.settings.setdefault("PHOTO_THUMB", (192, 144, 60))
        pelican.settings.setdefault("PHOTO_SQUARE_THUMB", False)
        pelican.settings.setdefault("PHOTO_GALLERY_TITLE", "")
        pelican.settings.setdefault("PHOTO_ALPHA_BACKGROUND_COLOR", (255, 255, 255))
        pelican.settings.setdefault("PHOTO_WATERMARK", False)
        pelican.settings.setdefault("PHOTO_WATERMARK_THUMB", False)
        pelican.settings.setdefault(
            "PHOTO_WATERMARK_TEXT", pelican.settings["SITENAME"]
        )
        pelican.settings.setdefault("PHOTO_WATERMARK_TEXT_COLOR", (255, 255, 255))
        pelican.settings.setdefault("PHOTO_WATERMARK_IMG", "")
        pelican.settings.setdefault("PHOTO_WATERMARK_IMG_SIZE", False)
        pelican.settings.setdefault("PHOTO_RESIZE_JOBS", 1)
        pelican.settings.setdefault("PHOTO_EXIF_KEEP", False)
        pelican.settings.setdefault("PHOTO_EXIF_REMOVE_GPS", False)
        pelican.settings.setdefault("PHOTO_EXIF_AUTOROTATE", True)
        pelican.settings.setdefault("PHOTO_EXIF_COPYRIGHT", False)
        pelican.settings.setdefault(
            "PHOTO_EXIF_COPYRIGHT_AUTHOR", pelican.settings["AUTHOR"]
        )
        pelican.settings.setdefault("PHOTO_LIGHTBOX_GALLERY_ATTR", "data-lightbox")
        pelican.settings.setdefault("PHOTO_LIGHTBOX_CAPTION_ATTR", "data-title")


def read_notes(filename, msg=None):
    notes = {}
    try:
        with pelican_open(filename) as text:
            for line in text.splitlines():
                if line.startswith("#"):
                    continue
                m = line.split(":", 1)
                if len(m) > 1:
                    pic = m[0].strip()
                    note = m[1].strip()
                    if pic and note:
                        notes[pic] = note
                else:
                    notes[line] = ""
    except Exception as e:
        if msg:
            logger.info(f"{msg} at file {filename}")
        logger.debug(f"read_notes issue: {msg} at file {filename}. Debug message:{e}")
    return notes


def enqueue_resize(orig, resized, spec=(640, 480, 80)):
    if resized not in DEFAULT_CONFIG["queue_resize"]:
        DEFAULT_CONFIG["queue_resize"][resized] = (orig, spec)
    elif DEFAULT_CONFIG["queue_resize"][resized] != (orig, spec):
        logger.error(
            "photos: resize conflict for {}, {}-{} is not {}-{}".format(
                resized,
                DEFAULT_CONFIG["queue_resize"][resized][0],
                DEFAULT_CONFIG["queue_resize"][resized][1],
                orig,
                spec,
            )
        )


def isalpha(img):
    return (
        True
        if img.mode in ("RGBA", "LA")
        or (img.mode == "P" and "transparency" in img.info)
        else False
    )


def remove_alpha(img, bg_color):
    background = Image.new("RGB", img.size, bg_color)
    background.paste(img, mask=img.split()[3])  # 3 is the alpha channel
    return background


def ReduceOpacity(im, opacity):
    """Reduces Opacity.

    Returns an image with reduced opacity.
    Taken from http://aspn.activestate.com/ASPN/Cookbook/Python/Recipe/362879
    """
    assert opacity >= 0 and opacity <= 1
    if isalpha(im):
        im = im.copy()
    else:
        im = im.convert("RGBA")

    alpha = im.split()[3]
    alpha = ImageEnhance.Brightness(alpha).enhance(opacity)
    im.putalpha(alpha)
    return im


def watermark_photo(image, settings):
    margin = [10, 10]
    opacity = 0.6

    watermark_layer = Image.new("RGBA", image.size, (0, 0, 0, 0))
    draw_watermark = ImageDraw.Draw(watermark_layer)
    text_reducer = 32
    image_reducer = 8
    text_size = [0, 0]
    mark_size = [0, 0]
    text_position = [0, 0]

    if settings["PHOTO_WATERMARK_TEXT"]:
        font_name = "SourceCodePro-Bold.otf"
        default_font = os.path.join(DEFAULT_CONFIG["plugin_dir"], font_name)
        font = ImageFont.FreeTypeFont(
            default_font, watermark_layer.size[0] // text_reducer
        )
        text_size = draw_watermark.textsize(settings["PHOTO_WATERMARK_TEXT"], font)
        text_position = [image.size[i] - text_size[i] - margin[i] for i in [0, 1]]
        draw_watermark.text(
            text_position,
            settings["PHOTO_WATERMARK_TEXT"],
            settings["PHOTO_WATERMARK_TEXT_COLOR"],
            font=font,
        )

    if settings["PHOTO_WATERMARK_IMG"]:
        mark_image = Image.open(settings["PHOTO_WATERMARK_IMG"])
        mark_image_size = [
            watermark_layer.size[0] // image_reducer for size in mark_size
        ]
        mark_image_size = (
            settings["PHOTO_WATERMARK_IMG_SIZE"]
            if settings["PHOTO_WATERMARK_IMG_SIZE"]
            else mark_image_size
        )
        mark_image.thumbnail(mark_image_size, Image.ANTIALIAS)
        mark_position = [
            watermark_layer.size[i] - mark_image.size[i] - margin[i] for i in [0, 1]
        ]
        mark_position = tuple(
            [
                mark_position[0] - (text_size[0] // 2) + (mark_image_size[0] // 2),
                mark_position[1] - text_size[1],
            ]
        )

        if not isalpha(mark_image):
            mark_image = mark_image.convert("RGBA")

        watermark_layer.paste(mark_image, mark_position, mark_image)

    watermark_layer = ReduceOpacity(watermark_layer, opacity)
    image.paste(watermark_layer, (0, 0), watermark_layer)

    return image


def rotate_image(img, exif_dict):
    if "exif" in img.info and piexif.ImageIFD.Orientation in exif_dict["0th"]:
        orientation = exif_dict["0th"].pop(piexif.ImageIFD.Orientation)
        if orientation == 2:
            img = img.transpose(Image.FLIP_LEFT_RIGHT)
        elif orientation == 3:
            img = img.rotate(180)
        elif orientation == 4:
            img = img.rotate(180).transpose(Image.FLIP_LEFT_RIGHT)
        elif orientation == 5:
            img = img.rotate(-90).transpose(Image.FLIP_LEFT_RIGHT)
        elif orientation == 6:
            img = img.rotate(-90, expand=True)
        elif orientation == 7:
            img = img.rotate(90).transpose(Image.FLIP_LEFT_RIGHT)
        elif orientation == 8:
            img = img.rotate(90)

    return (img, exif_dict)


def build_license(license, author):
    year = datetime.datetime.now().year
    license_file = os.path.join(DEFAULT_CONFIG["plugin_dir"], "licenses.json")

    with open(license_file) as data_file:
        licenses = json.load(data_file)

    if any(license in k for k in licenses):
        return licenses[license]["Text"].format(
            Author=author, Year=year, URL=licenses[license]["URL"]
        )
    else:
        return "Copyright {Year} {Author}, All Rights Reserved".format(
            Author=author, Year=year
        )


def manipulate_exif(img, settings):
    try:
        exif = piexif.load(img.info["exif"])
    except Exception:
        logger.debug("EXIF information not found")
        exif = {}

    if settings["PHOTO_EXIF_AUTOROTATE"]:
        img, exif = rotate_image(img, exif)

    if settings["PHOTO_EXIF_REMOVE_GPS"]:
        exif.pop("GPS")

    if settings["PHOTO_EXIF_COPYRIGHT"]:

        # Be minimally destructive to any preset EXIF author or copyright information.
        # If there is copyright or author information, prefer that over everything else.
        if not exif["0th"].get(piexif.ImageIFD.Artist):
            exif["0th"][piexif.ImageIFD.Artist] = settings[
                "PHOTO_EXIF_COPYRIGHT_AUTHOR"
            ]
            author = settings["PHOTO_EXIF_COPYRIGHT_AUTHOR"]

        if not exif["0th"].get(piexif.ImageIFD.Copyright):
            license = build_license(settings["PHOTO_EXIF_COPYRIGHT"], author)
            exif["0th"][piexif.ImageIFD.Copyright] = license

    return (img, piexif.dump(exif))


def resize_worker(orig, resized, spec, settings):
    logger.info(f"photos: make photo {orig} -> {resized}")
    im = Image.open(orig)

    if (
        ispiexif and settings["PHOTO_EXIF_KEEP"] and im.format == "JPEG"
    ):  # Only works with JPEG exif for sure.
        try:
            im, exif_copy = manipulate_exif(im, settings)
        except Exception:
            logger.info(f"photos: no EXIF or EXIF error in {orig}")
            exif_copy = b""
    else:
        exif_copy = b""

    icc_profile = im.info.get("icc_profile", None)

    if settings["PHOTO_SQUARE_THUMB"] and spec == settings["PHOTO_THUMB"]:
        im = ImageOps.fit(im, (spec[0], spec[1]), Image.ANTIALIAS)

    im.thumbnail((spec[0], spec[1]), Image.ANTIALIAS)
    directory = os.path.split(resized)[0]

    if isalpha(im):
        im = remove_alpha(im, settings["PHOTO_ALPHA_BACKGROUND_COLOR"])

    if not os.path.exists(directory):
        try:
            os.makedirs(directory)
        except Exception:
            logger.exception(f"Could not create {directory}")
    else:
        logger.debug(f"Directory already exists at {os.path.split(resized)[0]}")

    if settings["PHOTO_WATERMARK"]:
        isthumb = True if spec == settings["PHOTO_THUMB"] else False
        if not isthumb or (isthumb and settings["PHOTO_WATERMARK_THUMB"]):
            im = watermark_photo(im, settings)

    im.save(resized, "JPEG", quality=spec[2], icc_profile=icc_profile, exif=exif_copy)


def resize_photos(generator, writer):
    if generator.settings["PHOTO_RESIZE_JOBS"] == -1:
        debug = True
        generator.settings["PHOTO_RESIZE_JOBS"] = 1
    else:
        debug = False

    pool = multiprocessing.Pool(generator.settings["PHOTO_RESIZE_JOBS"])
    logger.debug(f"Debug Status: {debug}")
    for resized, what in DEFAULT_CONFIG["queue_resize"].items():
        resized = os.path.join(generator.output_path, resized)
        orig, spec = what
        if not os.path.isfile(resized) or os.path.getmtime(orig) > os.path.getmtime(
            resized
        ):
            if debug:
                resize_worker(orig, resized, spec, generator.settings)
            else:
                pool.apply_async(
                    resize_worker, (orig, resized, spec, generator.settings)
                )

    pool.close()
    pool.join()


def detect_content(content):
    hrefs = None

    def replacer(m):
        what = m.group("what")
        value = m.group("value")
        tag = m.group("tag")
        output = m.group(0)

        if what in ("photo", "lightbox"):
            if value.startswith("/"):
                value = value[1:]

            path = os.path.join(os.path.expanduser(settings["PHOTO_LIBRARY"]), value)

            if os.path.isfile(path):
                photo_prefix = os.path.splitext(value)[0].lower()

                if what == "photo":
                    photo_article = photo_prefix + "a.jpg"
                    enqueue_resize(
                        path,
                        os.path.join("photos", photo_article),
                        settings["PHOTO_ARTICLE"],
                    )

                    output = "".join(
                        (
                            "<",
                            m.group("tag"),
                            m.group("attrs_before"),
                            m.group("src"),
                            "=",
                            m.group("quote"),
                            os.path.join(settings["SITEURL"], "photos", photo_article),
                            m.group("quote"),
                            m.group("attrs_after"),
                        )
                    )

                elif what == "lightbox" and tag == "img":
                    photo_gallery = photo_prefix + ".jpg"
                    enqueue_resize(
                        path,
                        os.path.join("photos", photo_gallery),
                        settings["PHOTO_GALLERY"],
                    )

                    photo_thumb = photo_prefix + "t.jpg"
                    enqueue_resize(
                        path,
                        os.path.join("photos", photo_thumb),
                        settings["PHOTO_THUMB"],
                    )

                    lightbox_attr_list = [""]

                    gallery_name = value.split("/")[0]
                    lightbox_attr_list.append(
                        '{}="{}"'.format(
                            settings["PHOTO_LIGHTBOX_GALLERY_ATTR"], gallery_name
                        )
                    )

                    captions = read_notes(
                        os.path.join(os.path.dirname(path), "captions.txt"),
                        msg="photos: No captions for gallery",
                    )
                    caption = captions.get(os.path.basename(path)) if captions else None
                    if caption:
                        lightbox_attr_list.append(
                            '{}="{}"'.format(
                                settings["PHOTO_LIGHTBOX_CAPTION_ATTR"], caption
                            )
                        )

                    lightbox_attrs = " ".join(lightbox_attr_list)

                    output = "".join(
                        (
                            "<a href=",
                            m.group("quote"),
                            os.path.join(settings["SITEURL"], "photos", photo_gallery),
                            m.group("quote"),
                            lightbox_attrs,
                            "><img",
                            m.group("attrs_before"),
                            "src=",
                            m.group("quote"),
                            os.path.join(settings["SITEURL"], "photos", photo_thumb),
                            m.group("quote"),
                            m.group("attrs_after"),
                            "</a>",
                        )
                    )

            else:
                logger.error("photos: No photo %s", path)

        return output

    if hrefs is None:
        regex = r"""
            <\s*
            (?P<tag>[^\s\>]+)  # detect the tag
            (?P<attrs_before>[^\>]*)
            (?P<src>href|src)  # match tag with src and href attr
            \s*=
            (?P<quote>["\'])  # require value to be quoted
            (?P<path>{}(?P<value>.*?))  # the url value
            (?P=quote)
            (?P<attrs_after>[^\>]*>)
        """.format(
            content.settings["INTRASITE_LINK_REGEX"]
        )
        hrefs = re.compile(regex, re.X)

    if content._content and (
        "{photo}" in content._content or "{lightbox}" in content._content
    ):
        settings = content.settings
        content._content = hrefs.sub(replacer, content._content)


def galleries_string_decompose(gallery_string):
    splitter_regex = re.compile(r"[\s,]*?({photo}|{filename})")
    title_regex = re.compile(r"{(.+)}")
    galleries = map(
        str.strip,
        filter(None, splitter_regex.split(gallery_string)),
    )
    galleries = [
        gallery[1:] if gallery.startswith("/") else gallery for gallery in galleries
    ]
    if len(galleries) % 2 == 0 and " " not in galleries:
        galleries = zip(
            zip(["type"] * len(galleries[0::2]), galleries[0::2]),
            zip(["location"] * len(galleries[0::2]), galleries[1::2]),
        )
        galleries = [dict(gallery) for gallery in galleries]
        for gallery in galleries:
            title = re.search(title_regex, gallery["location"])
            if title:
                gallery["title"] = title.group(1)
                gallery["location"] = re.sub(
                    title_regex, "", gallery["location"]
                ).strip()
            else:
                gallery["title"] = DEFAULT_CONFIG["PHOTO_GALLERY_TITLE"]
        return galleries
    else:
        logger.error(
            f"Unexpected gallery location format! \n{pprint.pformat(galleries)}"
        )


def process_gallery(generator, content, location):
    content.photo_gallery = []

    galleries = galleries_string_decompose(location)

    for gallery in galleries:

        if gallery["location"] in DEFAULT_CONFIG["created_galleries"]:
            content.photo_gallery.append(
                (gallery["location"], DEFAULT_CONFIG["created_galleries"][gallery])
            )
            continue

        if gallery["type"] == "{photo}":
            dir_gallery = os.path.join(
                os.path.expanduser(generator.settings["PHOTO_LIBRARY"]),
                gallery["location"],
            )
            rel_gallery = gallery["location"]
        elif gallery["type"] == "{filename}":
            base_path = os.path.join(generator.path, content.relative_dir)
            dir_gallery = os.path.join(base_path, gallery["location"])
            rel_gallery = os.path.join(content.relative_dir, gallery["location"])

        if os.path.isdir(dir_gallery):
            logger.info(f"photos: Gallery detected: {rel_gallery}")
            dir_photo = os.path.join("photos", rel_gallery.lower())
            dir_thumb = os.path.join("photos", rel_gallery.lower())
            exifs = read_notes(
                os.path.join(dir_gallery, "exif.txt"), msg="photos: No EXIF for gallery"
            )
            captions = read_notes(
                os.path.join(dir_gallery, "captions.txt"),
                msg="photos: No captions for gallery",
            )
            blacklist = read_notes(
                os.path.join(dir_gallery, "blacklist.txt"),
                msg="photos: No blacklist for gallery",
            )
            content_gallery = []

            title = gallery["title"]
            for pic in sorted(os.listdir(dir_gallery)):
                if pic.startswith("."):
                    continue
                if pic.endswith(".txt"):
                    continue
                if pic in blacklist:
                    continue
                photo = os.path.splitext(pic)[0].lower() + ".jpg"
                thumb = os.path.splitext(pic)[0].lower() + "t.jpg"
                content_gallery.append(
                    (
                        pic,
                        os.path.join(dir_photo, photo),
                        os.path.join(dir_thumb, thumb),
                        exifs.get(pic, ""),
                        captions.get(pic, ""),
                    )
                )

                enqueue_resize(
                    os.path.join(dir_gallery, pic),
                    os.path.join(dir_photo, photo),
                    generator.settings["PHOTO_GALLERY"],
                )
                enqueue_resize(
                    os.path.join(dir_gallery, pic),
                    os.path.join(dir_thumb, thumb),
                    generator.settings["PHOTO_THUMB"],
                )

            content.photo_gallery.append((title, content_gallery))
            logger.debug(f"Gallery Data: {pprint.pformat(content.photo_gallery)}")
            DEFAULT_CONFIG["created_galleries"]["gallery"] = content_gallery
        else:
            logger.error(
                "photos: Gallery does not exist: {} at {}".format(
                    gallery["location"], dir_gallery
                )
            )


def detect_gallery(generator, content):
    if "gallery" in content.metadata:
        gallery = content.metadata.get("gallery")
        if gallery.startswith("{photo}") or gallery.startswith("{filename}"):
            process_gallery(generator, content, gallery)
        elif gallery:
            logger.error(f"photos: Gallery tag not recognized: {gallery}")


def image_clipper(x):
    return x[8:] if x[8] == "/" else x[7:]


def file_clipper(x):
    return x[11:] if x[10] == "/" else x[10:]


def process_image(generator, content, image):
    if image.startswith("{photo}"):
        path = os.path.join(
            os.path.expanduser(generator.settings["PHOTO_LIBRARY"]),
            image_clipper(image),
        )
        image = image_clipper(image)
    elif image.startswith("{filename}"):
        path = os.path.join(generator.path, content.relative_dir, file_clipper(image))
        image = file_clipper(image)

    if os.path.isfile(path):
        photo = os.path.splitext(image)[0].lower() + "a.jpg"
        thumb = os.path.splitext(image)[0].lower() + "t.jpg"
        content.photo_image = (
            os.path.basename(image).lower(),
            os.path.join("photos", photo),
            os.path.join("photos", thumb),
        )
        enqueue_resize(
            path, os.path.join("photos", photo), generator.settings["PHOTO_ARTICLE"]
        )
        enqueue_resize(
            path, os.path.join("photos", thumb), generator.settings["PHOTO_THUMB"]
        )
    else:
        logger.error(f"photo: No photo for {content.source_path} at {path}")


def detect_image(generator, content):
    image = content.metadata.get("image", None)
    if image:
        if image.startswith("{photo}") or image.startswith("{filename}"):
            process_image(generator, content, image)
        else:
            logger.error(f"photos: Image tag not recognized: {image}")


def detect_images_and_galleries(generators):
    """Runs generator on both pages and articles."""
    for generator in generators:
        if isinstance(generator, ArticlesGenerator):
            for article in itertools.chain(
                generator.articles, generator.translations, generator.drafts
            ):
                detect_image(generator, article)
                detect_gallery(generator, article)
        elif isinstance(generator, PagesGenerator):
            for page in itertools.chain(
                generator.pages, generator.translations, generator.hidden_pages
            ):
                detect_image(generator, page)
                detect_gallery(generator, page)


def register():
    """Uses the new style of registration based on GitHub Pelican issue #314."""
    signals.initialized.connect(initialized)
    try:
        signals.content_object_init.connect(detect_content)
        signals.all_generators_finalized.connect(detect_images_and_galleries)
        signals.article_writer_finalized.connect(resize_photos)
    except Exception as e:
        logger.exception(f"Plugin failed to execute: {pprint.pformat(e)}")
