#!/usr/bin/python
# -*- coding: utf-8 -*-

# A simple script to update version number in spec, dsc or arch linux files
#
# (C) 2010 by Adrian Schröter <adrian@suse.de>
# (C) 2015 by Thomas Bechtold <tbechtold@suse.com>
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
# as published by the Free Software Foundation; either version 2
# of the License, or (at your option) any later version.
# See http://www.gnu.org/licenses/gpl-2.0.html for full license text.

from __future__ import print_function

import argparse
import glob
import os
import re
import shutil
import sys
import tarfile
import zipfile

try:
    from packaging.version import LegacyVersion, Version, parse
except:
    HAS_PACKAGING = False
    import warnings
    warnings.warn("install 'packaging' to improve python package versions",
                  RuntimeWarning)
else:
    HAS_PACKAGING = True


outdir = None
suffixes = ('obscpio', 'tar', 'tar.gz', 'tgz', 'tar.bz2', 'tbz2', 'tar.xz',
            'zip')
suffixes_re = "|".join(map(lambda x: re.escape(x), suffixes))


def _get_local_files():
    """ sorted local file list by modification time (newest first)"""
    files = glob.glob('*')
    files.sort(key=lambda x: os.stat(os.path.join(os.getcwd(), x)).st_mtime,
               reverse=True)
    return files


class VersionDetector(object):
    @staticmethod
    def _autodetect(files, basename):
        version = VersionDetector._get_version_via_obsinfo(
            files, basename)
        if not version:
            version = VersionDetector._get_version_via_archive_dirname(
                files, basename)
        if not version:
            version = VersionDetector._get_version_via_filename(
                files, basename)
        if not version:
            version = VersionDetector._get_version_via_debian_changelog(
                "debian.changelog")
        return version

    @staticmethod
    def _get_version_via_filename(files, basename):
        """ detect version based on file names"""
        for f in files:
            regex = r"^%s.*[-_]([\d].*)\.(?:%s)$" % (re.escape(basename),
                                                     suffixes_re)
            m = re.match(regex, f)
            if m:
                return m.group(1)
        # Nothing found
        return None

    @staticmethod
    def __get_version(str_list, basename):
        regex = "%s.*[-_]([\d][^\/]*).*" % basename
        for s in str_list:
            m = re.match(regex, s)
            if m:
                return m.group(1)
        # Nothing found
        return None

    @staticmethod
    def _get_version_via_archive_dirname(files, basename):
        """ detect version based tar'd directory name"""
        for f in filter(lambda x: x.endswith(suffixes), files):
            # handle tarfiles
            if tarfile.is_tarfile(f):
                with tarfile.open(f) as tf:
                    v = VersionDetector.__get_version(tf.getnames(), basename)
                    if v:
                        return v
            # handle zipfiles
            if zipfile.is_zipfile(f):
                with zipfile.ZipFile(f, 'r') as zf:
                    v = VersionDetector.__get_version(zf.namelist(), basename)
                    if v:
                        return v
        # Nothing found
        return None

    @staticmethod
    def _get_version_via_obsinfo(files, basename):
        join_suffix = basename + ".obsinfo"
        for filename in filter(lambda x: x.endswith(join_suffix), files):
            if os.path.exists(filename):
                with open(filename, "r") as fp:
                    for line in fp:
                        if line.startswith("version: "):
                            string = line[9:]
                            string = string.rstrip()
                            return string
        # Nothing found
        return None

    @staticmethod
    def _get_version_via_debian_changelog(filename):
        # from http://anonscm.debian.org/cgit/pkg-python-debian/\
            # python-debian.git/tree/lib/debian/changelog.py
        topline = re.compile(r'^(\w%(name_chars)s*) \(([^\(\) \t]+)\)'
                             '((\s+%(name_chars)s+)+)\;'
                             % {'name_chars': '[-+0-9a-z.]'},
                             re.IGNORECASE)
        if os.path.exists(filename):
            with open(filename, "r") as f:
                firstline = f.readline()
                topmatch = topline.match(firstline)
                if topmatch:
                    return topmatch.group(2)
        # Nothing found
        return None


class PackageTypeDetector(object):
    @staticmethod
    def _get_package_type(files):
        pt_found = False
        for f in filter(lambda x: x.endswith(suffixes), files):
            pt_found = PackageTypeDetector._is_python(f)
            if pt_found:
                return "python"
        # no package type found
        return None

    @staticmethod
    def _is_python(f):
        names = []
        if tarfile.is_tarfile(f):
            with tarfile.open(f) as tf:
                names = tf.getnames()
        if zipfile.is_zipfile(f):
            with zipfile.ZipFile(f, 'r') as zf:
                names = zf.namelist()
        for n in map(lambda x: os.path.normpath(x), names):
            if n.endswith("egg-info/PKG-INFO"):
                return True
        return False


def _replace_define(filename, def_name, def_value, add_if_missing=True):
    # first, modify a copy of filename and then move it
    with open(filename, 'r+') as f:
        contents = f.read()
        f.seek(0)
        contents_new, subs = re.subn(
            r'^%define {def_name}(\s*)[^%].*'.format(
                def_name=def_name),
            r'%define {def_name}\g<1>{def_value}'.format(
                def_name=def_name, def_value=def_value),
            contents, flags=re.MULTILINE)
        if subs == 0 and add_if_missing:
            # seems there was no define. add new one before 'Name:'
            contents_new, subs = re.subn(
                r'^(Name:.*)$',
                r'%define {def_name} {def_value}\n\n\g<1>'.format(
                    def_name=def_name, def_value=def_value),
                contents, flags=re.MULTILINE)

        f.truncate()
        f.write(contents_new)


def _replace_spec_setup(filename, version_define):
    # first, modify a copy of filename and then move it
    with open(filename, 'r+') as f:
        contents = f.read()
        f.seek(0)
        # %setup without "-n" uses implicit "-n" as "%{name}-%{version}"
        contents_new, subs = re.subn(
            r'^%setup\s*((?:-q)?)?\s*$',
            r'%setup \1 -n %{{name}}-%{{{version_define}}}'.format(
                version_define=version_define),
            contents, flags=re.MULTILINE)
        if subs == 0:
            # keep inline macros for rpm
            contents_new, subs = re.subn(
                r'^%setup(.*)%{version}(.*)$',
                r'%setup\g<1>%{{{version_define}}}\g<2>'.format(
                    version_define=version_define),
                contents, flags=re.MULTILINE)
        if subs > 0:
            f.truncate()
            f.write(contents_new)


def _replace_tag(filename, tag, string):
    # first, modify a copy of filename and then move it
    with open(filename, 'r+') as f:
        contents = f.read()
        f.seek(0)
        if filename.endswith("PKGBUILD") or filename.endswith("build.collax"):
            contents_new, subs = re.subn(
                r"^{tag}=.*".format(tag=tag),
                r"{tag}={string}".format(tag=tag, string=string), contents,
                flags=re.MULTILINE)
        else:
            # keep inline macros for rpm
            contents_new, subs = re.subn(
                r'^{tag}:([ \t\f\v]*)[^%\n\r]*'.format(tag=tag),
                r'{tag}:\g<1>{string}'.format(
                    tag=tag, string=string),
                contents, flags=re.MULTILINE)
        if subs > 0:
            f.truncate()
            f.write(contents_new)


def _replace_debian_changelog_version(filename, version_new):
    # first, modify a copy of filename and then move it
    # get current version
    version_current = VersionDetector._get_version_via_debian_changelog(
        filename)
    with open(filename, 'r+') as f:
        content_lines = f.readlines()
        f.seek(0)
        content_lines[0] = content_lines[0].replace(
            version_current, version_new, 1)
        f.truncate()
        f.writelines(content_lines)


def _version_python_pip2rpm(version_pip):
    """generate a rpm compatible version from a python pip version"""
    version_rpm = version_pip
    if not HAS_PACKAGING:
        return version_rpm

    v = parse(version_pip)
    if isinstance(v, Version):
        # this is a PEP440 conform version
        # TODO(toabctl): there's a epoch in PEP440. Howto handle these without
        # using rpm's epoch feature?
        parts = []
        # Release segment
        parts.append(".".join(str(x) for x in v._version.release))

        # Pre-release
        if v._version.pre is not None:
            parts.append("~")
            parts.append("".join(str(x) for x in v._version.pre))

        # Post-release
        if v._version.post is not None:
            parts.append(".post{0}".format(v._version.post[1]))

        # Development release
        if v._version.dev is not None:
            if v._version.pre is None:
                parts.append("~a0")
            parts.append("~dev{0}".format(v._version.dev[1]))

        # Local version segment
        if v._version.local is not None:
            parts.append(
                "+{0}".format(".".join(str(x) for x in v._version.local))
            )
        version_rpm = "".join(parts)
    elif isinstance(v, LegacyVersion):
        # TODO(toabctl): handle setuptools style legacy version
        pass

    return version_rpm


if __name__ == '__main__':
    parser = argparse.ArgumentParser(
        description='Open Build Service source service "set_version".'
        'Used to update build description files with a '
        'detected or given version number.')
    parser.add_argument('--outdir', required=True,
                        help='output directory for modified sources')
    parser.add_argument('--version',
                        help='use given version string, do not detect it '
                        'from source files')
    parser.add_argument('--basename', default="",
                        help='detect version based on the file name with '
                        'a given prefix')
    parser.add_argument('--file', action='append',
                        help='modify only this build description. '
                        'maybe used multiple times.')
    args = vars(parser.parse_args())

    version = args['version']

    files_local = _get_local_files()

    outdir = args['outdir']

    if not outdir:
        print("no outdir specified")
        sys.exit(-1)

    if not version:
        version = VersionDetector._autodetect(files_local, args["basename"])

    if not version:
        print("unable to detect the version")
        sys.exit(-1)

    # if no files explicitly specified process whole directory
    files = args['file'] or files_local

    # do version convertion if needed
    pack_type = PackageTypeDetector._get_package_type(files)
    version_converted = None
    if pack_type == "python":
        version_converted = _version_python_pip2rpm(version)

    # handle rpm specs
    for f in filter(lambda x: x.endswith(".spec"), files):
        filename = outdir + "/" + f
        shutil.copyfile(f, filename)
        _replace_define(filename, "version_unconverted", version,
                        add_if_missing=False)
        if version_converted and version_converted != version:
            _replace_define(filename, "version_unconverted", version)
            _replace_tag(filename, 'Version', version_converted)
            _replace_spec_setup(filename, "version_unconverted")
        else:
            _replace_tag(filename, 'Version', version)
        _replace_tag(filename, 'Release', "0")

    # handle debian packages
    for f in filter(lambda x: x.endswith(".dsc"), files):
        filename = outdir + "/" + f
        shutil.copyfile(f, filename)
        _replace_tag(filename, 'Version', version + "-0")

    for f in filter(lambda x: x.endswith(("debian.changelog")), files):
        filename = outdir + "/" + f
        shutil.copyfile(f, filename)
        _replace_debian_changelog_version(filename, version + "-0")

    # handle build.collax recipes
    for f in filter(lambda x: x.endswith(("build.collax")), files):
        filename = outdir + "/" + f
        shutil.copyfile(f, filename)
        _replace_tag(filename, "version", version)
        _replace_tag(filename, "build", "0")

    # handle arch linux PKGBUILD files
    # TODO: Handle the md5sums generation!
    for f in filter(lambda x: x.endswith(("PKGBUILD")), files):
        filename = outdir + "/" + f
        shutil.copyfile(f, filename)
        _replace_tag(filename, "md5sums", "('SKIP')")
        _replace_tag(filename, "pkgver", version)
        _replace_tag(filename, "pkgrel", "0")
