# Copyright (c) 2009-2026 - Simon Conseil

# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to
# deal in the Software without restriction, including without limitation the
# rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
# sell copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:

# The above copyright notice and this permission notice shall be included in
# all copies or substantial portions of the Software.

# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
# FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
# IN THE SOFTWARE.

import locale
import logging
import os
import pathlib
import socketserver
import sys
import time
import webbrowser
from http import server

import click
from click import argument, option

from .gallery import Gallery
from .log import init_logging
from .settings import read_settings
from .utils import copy, init_plugins

try:
    from .version import __version__
except ImportError:
    # package is not installed
    __version__ = None

_DEFAULT_CONFIG_FILE = "sigal.conf.py"


@click.group()
@click.version_option(version=__version__)
def main():
    """Sigal - Simple Static Gallery Generator.

    Sigal is yet another python script to prepare a static gallery of images:
    resize images, create thumbnails with some options, generate html pages.

    """


@main.command()
@argument("path", default=_DEFAULT_CONFIG_FILE)
def init(path):
    """Copy a sample config file in the current directory (default to
    'sigal.conf.py'), or use the provided 'path'."""

    path = pathlib.Path(path)
    if path.exists():
        print("Found an existing config file, will abort to keep it safe.")
        sys.exit(1)

    conf = pathlib.Path(__file__).parent / "templates" / "sigal.conf.py"
    path.write_text(conf.read_text())
    print(f"Sample config file created: {path}")


@main.command()
@argument("source", required=False)
@argument("destination", required=False)
@option("-f", "--force", is_flag=True, help="Force the reprocessing of existing images")
@option(
    "-a",
    "--force-album",
    multiple=True,
    help=(
        "Force reprocessing of any album that matches the given pattern. "
        "Patterns containing no wildcards will be matched against only "
        "the album name. (-a 'My Pictures/* Pics' -a 'Festival')"
    ),
)
@option("-v", "--verbose", is_flag=True, help="Show all messages")
@option(
    "-d",
    "--debug",
    is_flag=True,
    help=(
        "Show all messages, including debug messages. Also raise "
        "exception if an error happen when processing files."
    ),
)
@option("-q", "--quiet", is_flag=True, help="Show only error messages")
@option(
    "-c",
    "--config",
    default=_DEFAULT_CONFIG_FILE,
    show_default=True,
    help="Configuration file",
)
@option(
    "-t",
    "--theme",
    help=(
        "Specify a theme directory, or a theme name for the themes included with Sigal"
    ),
)
@option("--title", help="Title of the gallery (overrides the title setting.")
@option("-n", "--ncpu", help="Number of cpu to use (default: all)")
def build(
    source,
    destination,
    debug,
    verbose,
    quiet,
    force,
    force_album,
    config,
    theme,
    title,
    ncpu,
):
    """Run sigal to process a directory.

    If provided, 'source', 'destination' and 'theme' will override the
    corresponding values from the settings file.

    """
    if sum([debug, verbose, quiet]) > 1:
        sys.exit("Only one option of debug, verbose and quiet should be used")

    show_progress = False
    if debug:
        level = logging.DEBUG
    elif verbose:
        level = logging.INFO
    elif quiet:
        level = logging.ERROR
    else:
        level = logging.WARNING
        show_progress = True

    init_logging("sigal", level=level)
    logger = logging.getLogger(__name__)

    if not os.path.isfile(config):
        logger.error("Settings file not found: %s", config)
        sys.exit(1)

    start_time = time.time()
    settings = read_settings(config)

    for key in ("source", "destination", "theme"):
        arg = locals()[key]
        if arg is not None:
            settings[key] = os.path.abspath(arg)
        logger.info("%12s : %s", key.capitalize(), settings[key])

    if not settings["source"] or not os.path.isdir(settings["source"]):
        logger.error("Input directory not found: %s", settings["source"])
        sys.exit(1)

    # on windows os.path.relpath raises a ValueError if the two paths are on
    # different drives, in that case we just ignore the exception as the two
    # paths are anyway not relative
    relative_check = True
    try:
        relative_check = os.path.relpath(
            settings["destination"], settings["source"]
        ).startswith("..")
    except ValueError:
        pass

    if not relative_check:
        logger.error("Output directory should be outside of the input directory.")
        sys.exit(1)

    if title:
        settings["title"] = title

    locale.setlocale(locale.LC_ALL, settings["locale"])
    init_plugins(settings)

    gal = Gallery(settings, ncpu=ncpu, show_progress=show_progress)
    gal.build(force=force_album if len(force_album) else force)

    # copy extra files
    for src, dst in settings["files_to_copy"]:
        src = os.path.join(settings["source"], src)
        dst = os.path.join(settings["destination"], dst)
        logger.debug("Copy %s to %s", src, dst)
        copy(src, dst, symlink=settings["orig_link"], rellink=settings["rel_link"])

    stats = gal.stats

    def format_stats(_type):
        opt = [
            "{} {}".format(stats[_type + "_" + subtype], subtype)
            for subtype in ("skipped", "failed")
            if stats[_type + "_" + subtype] > 0
        ]
        opt = " ({})".format(", ".join(opt)) if opt else ""
        return f"{stats[_type]} {_type}s{opt}"

    if not quiet and len(stats) > 0:
        stats_str = ""
        types = sorted({t.rsplit("_", 1)[0] for t in stats})
        for t in types[:-1]:
            stats_str += f"{format_stats(t)} and "
        stats_str += f"{format_stats(types[-1])}"
        end_time = time.time() - start_time
        print(f"Done, processed {stats_str} in {end_time:.2f} seconds.")


@main.command()
@argument("destination", default="_build")
@option("-p", "--port", help="Port to use", default=8000)
@option(
    "-c",
    "--config",
    default=_DEFAULT_CONFIG_FILE,
    show_default=True,
    help="Configuration file",
)
@option("-b", "--browser", is_flag=True, help="Open in your default browser")
def serve(destination, port, config, browser):
    """Run a simple web server."""
    if os.path.exists(destination):
        pass
    elif os.path.exists(config):
        settings = read_settings(config)
        destination = settings.get("destination")
        if not os.path.exists(destination):
            click.echo(
                f"The '{destination}' directory doesn't exist, maybe try building"
                " first?",
                err=True,
            )
            sys.exit(1)
    else:
        click.echo(
            f"The {destination} directory doesn't exist "
            f"and the config file ({config}) could not be read.",
            err=True,
        )
        sys.exit(2)

    print(f"DESTINATION : {destination}")
    os.chdir(destination)
    Handler = server.SimpleHTTPRequestHandler
    httpd = socketserver.TCPServer(("", port), Handler, False)
    print(f" * Running on http://127.0.0.1:{port}/")

    if browser:
        webbrowser.open(f"http://127.0.0.1:{port}/")

    try:
        httpd.allow_reuse_address = True
        httpd.server_bind()
        httpd.server_activate()
        httpd.serve_forever()
    except KeyboardInterrupt:
        print("\nAll done!")


@main.command()
@argument("target")
@argument("keys", nargs=-1)
@option(
    "-o", "--overwrite", default=False, is_flag=True, help="Overwrite existing .md file"
)
def set_meta(target, keys, overwrite=False):
    """Write metadata keys to .md file.

    TARGET can be a media file or an album directory. KEYS are key/value pairs.

    Ex, to set the title of test.jpg to "My test image":

    sigal set_meta test.jpg title "My test image"
    """

    if not os.path.exists(target):
        click.echo(f"The target {target} does not exist.", err=True)
        sys.exit(1)
    if len(keys) < 2 or len(keys) % 2 > 0:
        click.echo("Need an even number of arguments.", err=True)
        sys.exit(1)

    if os.path.isdir(target):
        descfile = os.path.join(target, "index.md")
    else:
        descfile = os.path.splitext(target)[0] + ".md"
    if os.path.exists(descfile) and not overwrite:
        click.echo(
            f"Description file '{descfile}' already exists. "
            "Use --overwrite to overwrite it.",
            err=True,
        )
        sys.exit(2)

    with open(descfile, "w") as fp:
        for i in range(len(keys) // 2):
            k, v = keys[i * 2 : (i + 1) * 2]
            fp.write(f"{k.capitalize()}: {v}\n")
    print(f"{len(keys) // 2} metadata key(s) written to {descfile}")


if __name__ == "__main__":
    main()
