#!/usr/bin/env python3

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

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*u[\'"]([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()
