# Copyright 2015 Canonical Ltd.
#
# This program is free software: you can redistribute it and/or modify it
# under the terms of the GNU General Public License version 3, as published
# by the Free Software Foundation.
#
# This program is distributed in the hope that it will be useful, but
# WITHOUT ANY WARRANTY; without even the implied warranties of
# MERCHANTABILITY, SATISFACTORY QUALITY, or FITNESS FOR A PARTICULAR
# PURPOSE.  See the GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License along
# with this program.  If not, see <http://www.gnu.org/licenses/>.

import email.utils
import errno
import logging
import os.path
import shutil
import subprocess

from debian import (
    changelog,
    deb822,
    )

from gitbuildrecipe.deb_version import substitute_changelog_vars
from gitbuildrecipe.recipe import SubstitutionUnavailable


class MissingDependency(Exception):
    pass


class NoSuchTag(Exception):

    def __init__(self, tag_name):
        super().__init__()
        self.tag_name = tag_name


def debian_source_package_name(control_path):
    """Open a debian control file and extract the package name.

    """
    with open(control_path, "r") as f:
        control = deb822.Deb822(f)
        return control["Source"]


def extract_upstream_tarball(path, package, version, dest_dir):
    """Extract the upstream tarball from a Git repository.

    :param path: Path to the Git repository
    :param package: Package name
    :param version: Package version
    :param dest_dir: Destination directory
    """
    prefix = "%s_%s.orig.tar." % (package, version)
    dest_filename = None
    pristine_tar_list = subprocess.Popen(
        ["pristine-tar", "list"], stdout=subprocess.PIPE, cwd=path)
    try:
        for line in pristine_tar_list.stdout:
            line = line.decode("UTF-8", errors="replace").rstrip("\n")
            if line.startswith(prefix):
                dest_filename = line
    finally:
        pristine_tar_list.wait()
    if dest_filename is not None:
        subprocess.check_call(
            ["pristine-tar", "checkout",
             os.path.abspath(os.path.join(dest_dir, dest_filename))],
            cwd=path)
    else:
        tag_names = ["upstream/%s" % version, "upstream-%s" % version]
        git_tag_list = subprocess.Popen(
            ["git", "tag"], stdout=subprocess.PIPE, cwd=path)
        try:
            for line in git_tag_list.stdout:
                line = line.decode("UTF-8", errors="replace").rstrip("\n")
                if line in tag_names:
                    tag = line
                    break
            else:
                raise NoSuchTag(tag_names[0])
        finally:
            git_tag_list.wait()
        # Default to .tar.gz
        dest_filename = prefix + "gz"
        with open(os.path.join(dest_dir, dest_filename), "wb") as dest:
            subprocess.check_call(
                ["git", "archive", "--format=tar.gz",
                 "--prefix=%s-%s/" % (package, version), tag],
                stdout=dest, cwd=path)


def add_autobuild_changelog_entry(base_branch, basedir, package,
                                  distribution=None, author_name=None,
                                  author_email=None, append_version=None):
    """Add a new changelog entry for an autobuild.

    :param base_branch: Recipe base branch
    :param basedir: Base working directory
    :param package: package name
    :param distribution: Optional distribution (defaults to last entry
        distribution)
    :param author_name: Name of the build requester
    :param author_email: Email of the build requester
    :param append_version: Optional version suffix to add
    """
    debian_dir = os.path.join(basedir, "debian")
    if not os.path.exists(debian_dir):
        os.makedirs(debian_dir)
    cl_path = os.path.join(debian_dir, "changelog")
    file_found = os.path.exists(cl_path)
    if file_found:
        with open(cl_path) as cl_f:
            contents = cl_f.read()
        cl = changelog.Changelog(file=contents)
    else:
        cl = changelog.Changelog()
    if len(cl._blocks) > 0:
        if distribution is None:
            distribution = cl._blocks[0].distributions.split()[0]
    else:
        if file_found:
            if len(contents.strip()) > 0:
                reason = (
                    "debian/changelog didn't contain any parseable stanzas")
            else:
                reason = "debian/changelog was empty"
        else:
            reason = "debian/changelog was not present"
        if distribution is None:
            distribution = "unstable"
    if base_branch.format_version in (0.1, 0.2, 0.3, 0.4):
        try:
            substitute_changelog_vars(base_branch, None, cl)
        except SubstitutionUnavailable as e:
            raise Exception(
                "No previous changelog to take the upstream version from as "
                "%s was used: %s: %s." % (e.name, e.reason, reason))
    # Use debian packaging environment variables
    # or default values if they don't exist
    if author_name is None or author_email is None:
        author_name, author_email = changelog.get_maintainer()
    author = "%s <%s>" % (author_name, author_email)

    date = email.utils.formatdate(localtime=True)
    version = base_branch.deb_version
    if append_version is not None:
        version += append_version
    try:
        changelog.Version(version)
    except (changelog.VersionError, ValueError) as e:
        raise Exception("Invalid deb-version: %s: %s" % (version, e))
    cl.new_block(package=package, version=version,
            distributions=distribution, urgency="low",
            changes=["", "  * Auto build.", ""],
            author=author, date=date)
    with open(cl_path, "w") as cl_f:
        cl.write_to_open_file(cl_f)


def build_source_package(basedir, tgz_check=True):
    command = ["/usr/bin/debuild"]
    if tgz_check:
        command.append("--tgz-check")
    else:
        command.append("--no-tgz-check")
    command.extend(["-i", "-I", "-S", "-uc", "-us"])
    logging.info("Building the source package")
    try:
        subprocess.check_call(command, cwd=basedir)
    except OSError as e:
        if e.errno == errno.ENOENT:
            raise MissingDependency(
                "debuild is not installed; please install the devscripts "
                "package.")
        raise


def get_source_format(path):
    """Retrieve the source format name from a package.

    :param path: Path to the package
    :return: String with package format
    """
    source_format_path = os.path.join(path, "debian", "source", "format")
    if not os.path.exists(source_format_path):
        return "1.0"
    with open(source_format_path) as f:
        return f.read().strip()


def convert_3_0_quilt_to_native(path):
    """Convert a package in 3.0 (quilt) format to 3.0 (native).

    This applies all patches in the package and updates the
    debian/source/format file.

    :param path: Path to the package on disk
    """
    path = os.path.abspath(path)
    patches_dir = os.path.join(path, "debian", "patches")
    series_file = os.path.join(patches_dir, "series")
    if os.path.exists(series_file):
        logging.info("Applying quilt patches")
        env = dict(os.environ)
        env["QUILT_SERIES"] = series_file
        env["QUILT_PATCHES"] = patches_dir
        try:
            subprocess.check_call(
                ["quilt", "push", "-a", "-v"], cwd=path, env=env)
        except OSError as e:
            if e.errno == errno.ENOENT:
                raise MissingDependency(
                    "quilt is not installed; please install it.")
            raise
        except subprocess.CalledProcessError as e:
            if e.returncode != 2:
                raise Exception("Failed to apply quilt patches")
    if os.path.exists(patches_dir):
        shutil.rmtree(patches_dir)
    with open(os.path.join(path, "debian", "source", "format"), "w") as f:
        f.write("3.0 (native)\n")


def force_native_format(working_tree_path, current_format):
    """Make sure a package is a format that supports native packages.

    :param working_tree_path: Path to the package
    """
    if current_format == "3.0 (quilt)":
        convert_3_0_quilt_to_native(working_tree_path)
    elif current_format not in ("1.0", "3.0 (native)"):
        raise Exception("Unknown source format %s" % current_format)
