#! /usr/bin/env python3
import logging
import sys
import os
import subprocess
from argparse import ArgumentParser, RawDescriptionHelpFormatter
from collections import OrderedDict
import json
import time
from urllib.request import urlopen
from urllib.error import HTTPError


descriptions = OrderedDict([
    ("firedrake", "an automated finite element system"),
    ("PyOP2", "Framework for performance-portable parallel computations on unstructured meshes"),
    ("SLOPE", "a library for fusing and tiling irregular loops."),
    ("tsfc", "The Two Stage Form Compiler"),
    ("COFFEE", "A Compiler for Fast Expression Evaluation"),
    ("ufl", "The Unified Form Language"),
    ("FInAT", "a smarter library of finite elements"),
    ("fiat", "The Finite Element Automated Tabulator"),
    ("petsc", "Portable, Extensible Toolkit for Scientific Computation"),
    ("petsc4py", "The Python interface to PETSc")])

projects = dict(
    [("firedrake", "firedrakeproject"),
     ("PyOP2", "OP2"),
     ("SLOPE", "coneoproject"),
     ("tsfc", "firedrakeproject"),
     ("COFFEE", "coneoproject"),
     ("ufl", "firedrakeproject"),
     ("FInAT", "FInAT"),
     ("fiat", "firedrakeproject"),
     ("petsc", "firedrakeproject"),
     ("petsc4py", "firedrakeproject")])

components = list(descriptions.keys())

optional_components = ("SLOPE",)

parser = ArgumentParser(description="""Create Zenodo DOIs for specific versions of Firedrake components.

If you are a Firedrake user, this script creates a JSON file encoding
the precise versions of all the Firedrake components you are using,
and a documentation string. You should create an issue on the
Firedrake github page and attach this file. The Firedrake core
developers will generate DOIs for your packages and report the
corresponding release tag.

If you have a release tag from a Firedrake Zenodo release, then

   firedrake-zenodo --bibtex TAG

will download the corresponding bibliography entries in BibTeX format.

If you are a Firedrake core developer, this script enables you to
create DOIs directly, or to create them from a user-supplied JSON file.""",
                        epilog="""""",
                        formatter_class=RawDescriptionHelpFormatter)
group = parser.add_mutually_exclusive_group()
group.add_argument("--output", "-o", action="store", nargs=1, default=["firedrake.json"],
                   help="Output to the named file instead of firedrake.json.", dest="output_file")
group.add_argument("--input", "-i", action="store", nargs=1,
                   help="Release based on the named input file", dest="input_file")
group.add_argument("--release", action="store_true",
                   help="Release based on the current checked out versions.")
group.add_argument("--bibtex", action="store", nargs=1,
                   help="Retrieve the BibTex entries corresponding to the release tag provided.", dest="release_tag")
group.add_argument("--bibtex-file", action="store", nargs=1, default=["firedrake-zenodo.bib"],
                   help="Output to the named bibtex file rather than firedrake-zenodo.bib")
parser.add_argument("--message", "-m", action="store", nargs=1,
                    help="Short description of the reason for this release. E.g. Version of Firedrake used in <paper name>.")
parser.add_argument("--honour-petsc-dir", action="store_true",
                    help="Does your firedrake use a self-built PETSc?")

for component in components:
    parser.add_argument("--%s" % component, action="store", nargs=1,
                        help="Use this git hash for %s instead of that in the file or the checked out version."
                        % component)

parser.add_argument("--log", action='store_true',
                    help="Produce a verbose log of the installation process in firedrake-zenodo.log. If you have problem running this script, please include this log in any bug report you file.")

args = parser.parse_args()

# Set up logging
if args.log:
    # Log to file at DEBUG level
    logging.basicConfig(level=logging.DEBUG,
                        format='%(asctime)s %(levelname)-6s %(message)s',
                        filename='firedrake-zenodo.log',
                        filemode='w')
    # Log to console at INFO level
    console = logging.StreamHandler()
    console.setLevel(logging.INFO)
    formatter = logging.Formatter('%(message)s')
    console.setFormatter(formatter)
    logging.getLogger().addHandler(console)
else:
    # Log to console at INFO level
    logging.basicConfig(level=logging.INFO,
                        format='%(message)s')
log = logging.getLogger()

cwd = os.getcwd()
try:
    src = os.environ["VIRTUAL_ENV"] + "/src"
except KeyError:
    log.error("VIRTUAL_ENV environment variable not set. Please activate virtualenv before running firedrake-zenodo.")
    sys.exit(1)


def check_call(arguments):
    if args.log:
        try:
            log.debug(subprocess.check_output(arguments, stderr=subprocess.STDOUT).decode())
        except subprocess.CalledProcessError as e:
            log.debug(e.output.decode())
            raise
    else:
        subprocess.check_call(arguments)


def check_output(args):
    try:
        return subprocess.check_output(args, stderr=subprocess.STDOUT).decode()
    except subprocess.CalledProcessError as e:
        log.debug(e.output.decode())
        raise


def collect_repo_shas():
    shas = {}

    for component in components:
        try:
            os.chdir(src + "/" + component)
            try:
                check_call(["git", "diff-index", "--quiet", "HEAD"])
            except subprocess.CalledProcessError:
                log.error("Component %s has uncommitted changes, cannot create release" % component)
                sys.exit(0)
            shas[component] = check_output(["git", "rev-parse", "HEAD"]).strip()
        except (subprocess.CalledProcessError, OSError):
            if component in optional_components:
                log.warning("Failed to retrieve git hash for optional "
                            "component '%s', continuing without it" % component)
            else:
                if component == "petsc" and args.honour_petsc_dir:
                    log.warning("Cannot retrieve git hash for PETSc, if you want a release "
                                "you will have to provide it manually")
                else:
                    log.error("Failed to retrieve git hash for %s" % component)
                    raise
    return shas


if args.release_tag:
    tag = args.release_tag[0]
    log.info("Retrieving BibTeX data for Firedrake release %s." % tag)
    log.info("This may take a few seconds.")
    try:
        request = urlopen("http://firedrakeproject.org/zenodo-bibtex.cgi?tag=%s" % tag)
    except HTTPError as e:
        if e.code == 400:
            log.error("%s is not a legal Firedrake Zenodo release tag" % tag)
            sys.exit(1)

    bibtex = request.read()
    if not bibtex.strip():
        log.error("""No data returned. Please check that the release tag is correct.

If the release has only just been created, then the information may not yet have propagated to Zenodo yet. Please try again later.""")
        sys.exit(1)
    open(args.bibtex_file[0], "wb").write(bibtex)
    log.info("Bibliography written to %s" % args.bibtex_file[0])
    sys.exit(0)

if args.release or not args.input_file:
    if not args.message:
        log.error("You must provide a message using the -m option")
        sys.exit(1)

    # Collect hashes from the current repo.
    shas = collect_repo_shas()
else:
    # Read hashes from file.
    infile = open(os.path.abspath(args.input_file[0]), "r")
    shas = json.loads(infile.read())

if args.message:
    shas["message"] = args.message[0]

# Override hashes with any read from the command line.
for component in components:
    new_sha = getattr(args, component)
    if new_sha:
        shas[component] = new_sha[0]

if not (args.release or args.input_file):
    # Dump json and exit.
    out = open(cwd+"/"+args.output_file[0], "w")
    out.write(json.dumps(shas) + "\n")

    log.info("Wrote release information to %s" % args.output_file[0])
    sys.exit(0)

try:
    import github3
except ImportError:
    log.error("Publishing releases requires the github3 module. Please pip install github3.py")
    sys.exit(1)

message = shas["message"] + """

This release is specifically created to document the version of
Firedrake used in a particular set of experiments. Please do not cite
this as a general source for Firedrake or any of its
dependencies. Instead, refer to
http://www.firedrakeproject.org/publications.html"""


# Github authentication.
if os.getenv("FIREDRAKE_OAUTH"):
    gh = github3.login(token=os.getenv("FIREDRAKE_OAUTH"))
else:
    log.error("""Actually releasing Firedrake and creating DOIs can only be done by
a Firedrake core developer.

If you are not a core developer, please run firedrake-zenodo without
the --input or --release options and upload the resulting json file to
a github issue. One of the core developers will then create a release
from that file.

If you are a Firedrake core developer, please set the environment
variable FIREDRAKE_OAUTH to a github OAUTH token with repository
access.""")
    sys.exit(1)

fd = gh.repository("firedrakeproject", "firedrake")

tag = time.strftime("Firedrake_%Y%m%d", time.localtime())
index = -1

for r in fd.iter_releases():
    if r.tag_name.startswith(tag):
        newindex = int(r.tag_name.split(".")[1])
        index = max(index, newindex)
tag += "." + str(index + 1)

# Verify commits. This ensures that an invalid sha will cause us to fail before we release any component.
# This step also replaces short shas with long ones. This seems to be necessary for release creation.
for component in components:
    repo = gh.repository(projects[component], component)

    try:
        commit = repo.commit(shas[component])
        if not commit:
            log.error("Failed to find specified commit for %s" % component)

        shas[component] = commit.sha

    except KeyError:
        log.warn("No commit specified for %s. No release will be created for this component." % component)

# Now create releases.
for component in (set(shas) & set(components)):
    log.info("Releasing %s" % component)
    repo = gh.repository(projects[component], component)

    repo.create_release(
        tag_name=tag,
        target_commitish=shas[component],
        name=descriptions[component],
        body=message,
        draft=False,
        prerelease=True)

log.info("Releases complete. The release tag is %s" % tag)
