#!/usr/bin/env python3

"""A utility script for automating the beets release process.
"""
import datetime
import os
import re
import subprocess
from contextlib import contextmanager

import click

BASE = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
CHANGELOG = os.path.join(BASE, "docs", "changelog.rst")


@contextmanager
def chdir(d):
    """A context manager that temporary changes the working directory."""
    olddir = os.getcwd()
    os.chdir(d)
    yield
    os.chdir(olddir)


@click.group()
def release():
    pass


# Locations (filenames and patterns) of the version number.
VERSION_LOCS = [
    (
        os.path.join(BASE, "beets", "__init__.py"),
        [
            (
                r'__version__\s*=\s*[\'"]([0-9\.]+)[\'"]',
                "__version__ = '{version}'",
            )
        ],
    ),
    (
        os.path.join(BASE, "docs", "conf.py"),
        [
            (
                r'version\s*=\s*[\'"]([0-9\.]+)[\'"]',
                "version = '{minor}'",
            ),
            (
                r'release\s*=\s*[\'"]([0-9\.]+)[\'"]',
                "release = '{version}'",
            ),
        ],
    ),
    (
        os.path.join(BASE, "setup.py"),
        [
            (
                r'\s*version\s*=\s*[\'"]([0-9\.]+)[\'"]',
                "    version='{version}',",
            )
        ],
    ),
]

GITHUB_USER = "beetbox"
GITHUB_REPO = "beets"


def bump_version(version):
    """Update the version number in setup.py, docs config, changelog,
    and root module.
    """
    version_parts = [int(p) for p in version.split(".")]
    assert len(version_parts) == 3, "invalid version number"
    minor = "{}.{}".format(*version_parts)
    major = "{}".format(*version_parts)

    # Replace the version each place where it lives.
    for filename, locations in VERSION_LOCS:
        # Read and transform the file.
        out_lines = []
        with open(filename) as f:
            found = False
            for line in f:
                for pattern, template in locations:
                    match = re.match(pattern, line)
                    if match:
                        # Check that this version is actually newer.
                        old_version = match.group(1)
                        old_parts = [int(p) for p in old_version.split(".")]
                        assert (
                            version_parts > old_parts
                        ), "version must be newer than {}".format(old_version)

                        # Insert the new version.
                        out_lines.append(
                            template.format(
                                version=version,
                                major=major,
                                minor=minor,
                            )
                            + "\n"
                        )

                        found = True
                        break

                else:
                    # Normal line.
                    out_lines.append(line)

            if not found:
                print(f"No pattern found in {filename}")

        # Write the file back.
        with open(filename, "w") as f:
            f.write("".join(out_lines))

    # Generate bits to insert into changelog.
    header_line = f"{version} (in development)"
    header = "\n\n" + header_line + "\n" + "-" * len(header_line) + "\n\n"
    header += "Changelog goes here!\n"

    # Insert into the right place.
    with open(CHANGELOG) as f:
        contents = f.read()
    location = contents.find("\n\n")  # First blank line.
    contents = contents[:location] + header + contents[location:]

    # Write back.
    with open(CHANGELOG, "w") as f:
        f.write(contents)


@release.command()
@click.argument("version")
def bump(version):
    """Bump the version number."""
    bump_version(version)


def get_latest_changelog():
    """Extract the first section of the changelog."""
    started = False
    lines = []
    with open(CHANGELOG) as f:
        for line in f:
            if re.match(r"^--+$", line.strip()):
                # Section boundary. Start or end.
                if started:
                    # Remove last line, which is the header of the next
                    # section.
                    del lines[-1]
                    break
                else:
                    started = True

            elif started:
                lines.append(line)
    return "".join(lines).strip()


def rst2md(text):
    """Use Pandoc to convert text from ReST to Markdown."""
    pandoc = subprocess.Popen(
        ["pandoc", "--from=rst", "--to=markdown", "--wrap=none"],
        stdin=subprocess.PIPE,
        stdout=subprocess.PIPE,
        stderr=subprocess.PIPE,
    )
    stdout, _ = pandoc.communicate(text.encode("utf-8"))
    md = stdout.decode("utf-8").strip()

    # Fix up odd spacing in lists.
    return re.sub(r"^-   ", "- ", md, flags=re.M)


def changelog_as_markdown():
    """Get the latest changelog entry as hacked up Markdown."""
    rst = get_latest_changelog()

    # Replace plugin links with plugin names.
    rst = re.sub(r":doc:`/plugins/(\w+)`", r"``\1``", rst)

    # References with text.
    rst = re.sub(r":ref:`([^<]+)(<[^>]+>)`", r"\1", rst)

    # Other backslashes with verbatim ranges.
    rst = re.sub(r"(\s)`([^`]+)`([^_])", r"\1``\2``\3", rst)

    # Command links with command names.
    rst = re.sub(r":ref:`(\w+)-cmd`", r"``\1``", rst)

    # Bug numbers.
    rst = re.sub(r":bug:`(\d+)`", r"#\1", rst)

    # Users.
    rst = re.sub(r":user:`(\w+)`", r"@\1", rst)

    # Convert with Pandoc.
    md = rst2md(rst)

    # Restore escaped issue numbers.
    md = re.sub(r"\\#(\d+)\b", r"#\1", md)

    return md


@release.command()
def changelog():
    """Get the most recent version's changelog as Markdown."""
    print(changelog_as_markdown())


def get_version(index=0):
    """Read the current version from the changelog."""
    with open(CHANGELOG) as f:
        cur_index = 0
        for line in f:
            match = re.search(r"^\d+\.\d+\.\d+", line)
            if match:
                if cur_index == index:
                    return match.group(0)
                else:
                    cur_index += 1


@release.command()
def version():
    """Display the current version."""
    print(get_version())


@release.command()
def datestamp():
    """Enter today's date as the release date in the changelog."""
    dt = datetime.datetime.now()
    stamp = "({} {}, {})".format(dt.strftime("%B"), dt.day, dt.year)
    marker = "(in development)"

    lines = []
    underline_length = None
    with open(CHANGELOG) as f:
        for line in f:
            if marker in line:
                # The header line.
                line = line.replace(marker, stamp)
                lines.append(line)
                underline_length = len(line.strip())
            elif underline_length:
                # This is the line after the header. Rewrite the dashes.
                lines.append("-" * underline_length + "\n")
                underline_length = None
            else:
                lines.append(line)

    with open(CHANGELOG, "w") as f:
        for line in lines:
            f.write(line)


@release.command()
def prep():
    """Run all steps to prepare a release.

    - Tag the commit.
    - Build the sdist package.
    - Generate the Markdown changelog to ``changelog.md``.
    - Bump the version number to the next version.
    """
    cur_version = get_version()

    # Tag.
    subprocess.check_call(["git", "tag", f"v{cur_version}"])

    # Build.
    with chdir(BASE):
        subprocess.check_call(["python", "setup.py", "sdist"])

    # Generate Markdown changelog.
    cl = changelog_as_markdown()
    with open(os.path.join(BASE, "changelog.md"), "w") as f:
        f.write(cl)

    # Version number bump.
    # FIXME It should be possible to specify this as an argument.
    version_parts = [int(n) for n in cur_version.split(".")]
    version_parts[-1] += 1
    next_version = ".".join(map(str, version_parts))
    bump_version(next_version)


@release.command()
def publish():
    """Unleash a release unto the world.

    - Push the tag to GitHub.
    - Upload to PyPI.
    """
    version = get_version(1)

    # Push to GitHub.
    with chdir(BASE):
        subprocess.check_call(["git", "push"])
        subprocess.check_call(["git", "push", "--tags"])

    # Upload to PyPI.
    path = os.path.join(BASE, "dist", f"beets-{version}.tar.gz")
    subprocess.check_call(["twine", "upload", path])


@release.command()
def ghrelease():
    """Create a GitHub release using the `github-release` command-line
    tool.

    Reads the changelog to upload from `changelog.md`. Uploads the
    tarball from the `dist` directory.
    """
    version = get_version(1)
    tag = "v" + version

    # Load the changelog.
    with open(os.path.join(BASE, "changelog.md")) as f:
        cl_md = f.read()

    # Create the release.
    subprocess.check_call(
        [
            "github-release",
            "release",
            "-u",
            GITHUB_USER,
            "-r",
            GITHUB_REPO,
            "--tag",
            tag,
            "--name",
            f"{GITHUB_REPO} {version}",
            "--description",
            cl_md,
        ]
    )

    # Attach the release tarball.
    tarball = os.path.join(BASE, "dist", f"beets-{version}.tar.gz")
    subprocess.check_call(
        [
            "github-release",
            "upload",
            "-u",
            GITHUB_USER,
            "-r",
            GITHUB_REPO,
            "--tag",
            tag,
            "--name",
            os.path.basename(tarball),
            "--file",
            tarball,
        ]
    )


if __name__ == "__main__":
    release()
