#!/usr/bin/python3

# Script to generate RPM spec file based on template
# (c) 2014,2017 Andrey Cherepanov <cas@altlinux.org>
# (c) 2017-2018 Mikhail Gordeev <obirvalger@altlinux.org>

# This program is free software; you can redistribute it and/or modify it
# under the terms of GNU General Public License (GPL) version 3 or later.
from string import Template
from functools import partial
import os
import sys
import shutil
import datetime
import argparse
import subprocess
import re
import tempfile
import readline

templates_path = "/usr/share/spectemplates/"

# Get templates path and build list of available templates
t = os.getenv('GENSPEC_TEMPLATES')
if t != None:
        templates_path = t

m = os.getenv('GENSPEC_PACKAGER')
if m is not None:
    maintainer = m
else:
    # Get configured packager
    try:
        p = subprocess.Popen(['rpm', '--eval', '%{packager}'], stdout=subprocess.PIPE)
        maintainer = p.stdout.read().decode().rstrip()
    except FileNotFoundError:
        print('You do not have rpm, use GENSPEC_PACKAGER environment variable to specify packager.', file=sys.stderr)
        exit(1)

if maintainer == '%{packager}':
    print('No packager privided not by rpm nor by GENSPEC_PACKAGER')
    exit(1)

spec_prefixes = {  'python': 'python-module-',
                   'python2': 'python-module-',
                   'python3': 'python3-module-',
                   'ruby': 'ruby-',
                   'vim': 'vim-plugin-',
                }

class Spec:
    """RPM Spec class"""

    class _Decorators:
        '''Decorators for Spec class'''
        @classmethod
        def overwrite(decorated_class, decorated_function):
            def wrapper(spec, **kwargs):
                name = spec.package
                if kwargs['force'] and os.path.exists(name):
                    with tempfile.TemporaryDirectory() as tmpdir:
                        olddir = os.getcwd()
                        os.chdir(tmpdir)
                        decorated_function(spec, **kwargs)
                        os.chdir(olddir)
                        with tempfile.TemporaryDirectory() as deldir:
                            shutil.move(name, deldir)
                            shutil.rmtree(os.path.join(deldir, name))
                        shutil.move(os.path.join(tmpdir, name), name)
                else:
                    decorated_function(spec, **kwargs)
            return wrapper

    def __init__(
            self,
            keys,
            call,
            check_call,
            tag='',
            date=None,
            disable_name_translation=False,
        ):
        """Constructor"""

        def generate_package_name():
            name = self.keys['module']
            spec_type = self.keys['spec_type']
            if spec_type in spec_prefixes:
                if not self.disable_name_translation:
                    type_regexes = ['ruby', 'python3?']
                    sep_regex = '[-_]'
                    for type_regex in type_regexes:
                        if re.match(type_regex, spec_type):
                            del_regex = '^{type}{sep}|{sep}{type}$'.format(type=type_regex, sep=sep_regex)
                            name = re.sub(del_regex, '', name)
                            break
                package = spec_prefixes[spec_type] + name
                self.keys['module'] = name # fix original modulename
            else:
                package = name
            return package

        if date is None:
            self.date = datetime.datetime.now()
        else:
            self.date = date

        self.keys = keys
        self.disable_name_translation = disable_name_translation
        self.package = generate_package_name()

        self.tag = tag
        self.call = call
        self.check_call = check_call
        if not tag:
            self.build_place = '.'
        elif tag.startswith('v'):
            self.build_place = 'v@version@:.'
        else:
            self.build_place = '@version@:.'

        self._apply()

    def _apply(self):
        """Apply values to spec template"""
        self.keys['packager'] = maintainer
        spectype = self.keys['spec_type']

        file = open( templates_path + spectype + ".spec", "r" )

        # Generate changelog entry
        self.keys['stamp'] = '* ' \
          + self.date.strftime("%a %b %d %Y") \
          + ' ' + self.keys['packager'] \
          + ' ' + self.keys['version']

        # Fill template
        self.spec = file.read()
        file.close()
        self.spec = Template( self.spec ).safe_substitute( self.keys )

    @_Decorators.overwrite
    def deploy(self, *, use_git=False, here=False, force=False, gitignore=False):
        """Generate directory with spec and gear rules"""
        # Create directory
        if not here:
            if os.path.exists(self.package):
                sys.exit('Error: file or directory {} already exists'.format(self.package))

            if use_git:
                self.__gitClone()
            else:
                os.mkdir( self.package )
                os.chdir( self.package )

        # Create .gitignore
        if gitignore:
            if os.path.exists('.gitignore'):
                if force:
                    shutil.rm('.gitignore')
                else:
                    print('File .gitignore already exists, ignoring...', file=sys.stderr)

            if not os.path.exists('.gitignore'):
                gitignores = "*~\n*.sw*\n*.log"
                with open('.gitignore', 'w') as gitignore_file:
                    gitignore_file.write(gitignores)

        # Create gear rules
        if os.path.exists('.gear'):
            if force:
                shutil.rmtree('.gear')
            else:
                print('Directory .gear already exists', file=sys.stderr)
                exit(1)
        os.mkdir( '.gear' )

        if self.package != self.keys['module']:
                gear_rules = "tar: {0} name={1}-@version@ base={1}-@version@\n".format(self.build_place, self.keys['module'])
        else:
                gear_rules = "tar: {}\n".format(self.build_place)
        with open('.gear/rules', 'w') as gear_rules_file:
            gear_rules_file.write(gear_rules)

        # Save spec file
        name = self.package + '.spec'
        if os.path.exists(name):
            if force:
                os.remove(name)
            else:
                print('Spec file already exists', file=sys.stderr)
                exit(1)
        with open(name, 'w') as spec_file:
            spec_file.write(self.spec)

        if use_git:
            try:
                self.__gearPost()
            except FileNotFoundError:
                print('Gear operations skipped, because you do not have such tools', file=sys.stderr)

        if here:
            print('Created spec here')
        else:
            print('Created directory {}'.format(self.package))

    def test(self, original):
        with tempfile.TemporaryDirectory() as tmpdir:
            olddir = os.getcwd()
            os.chdir(tmpdir)
            self.deploy()
            os.chdir(olddir)
            exit_status = self.call(['diff', '-rq', '-x', '.git', os.path.join(tmpdir, self.package), original])
            if exit_status:
                subprocess.check_call(['diff', '-r', os.path.join(tmpdir, self.package), original])

    def __gitClone(self):
        self.check_call(["git", "clone", "-o", "upstream", self.keys['url'], self.package])
        os.chdir( self.package )
        if self.tag:
            self.check_call("git reset --hard " + self.tag, shell=True)
        else:
            self.check_call("git reset --hard `git describe --abbrev=0 --tags`", shell=True)
        self.check_call(["git", "checkout", "-b", "sisyphus"])
        self.check_call(["git", "rm", "-rfq", "*"])
        self.check_call(["git", "commit", "-m", "Create empty branch"])

    def __gearPost(self):
        self.check_call(["gear-update-tag", "-a"])
        self.check_call("gear-remotes-save")
        self.check_call(["git", "add", "."])

# Read command-line parameters
parser = argparse.ArgumentParser(
    description='Script to generate RPM spec file based on template',
    formatter_class=argparse.RawDescriptionHelpFormatter,
    epilog=
"""
Supported environment variables:
GENSPEC_TEMPLATES       Path to spec templates (default /usr/share/spectemplates/)
GENSPEC_PACKAGER        Packager name (default from `rpm --eval '%{packager}'`)
""")

parser.add_argument('--version', action='version', version="1.3.9")
if len(sys.argv) >= 2:
    parser.add_argument('-n', action='store', dest='module', help='Package or module name', required=True)
parser.add_argument('-t', action='store', dest='spec_type', help='Package type', default='common')
parser.add_argument('-v', action='store', dest='version', help='Package version', default='')
parser.add_argument('-s', action='store', dest='summary', help='Package summary', default='')
parser.add_argument('-l', action='store', dest='license', help='Package license', default='')
parser.add_argument('-u', action='store', dest='url', help='Package URL', default='')
parser.add_argument('-d', action='store', dest='description', help='Package description', default='')
parser.add_argument('-c', action='store', dest='lastchange', help='Package changelog', default='')
parser.add_argument('-o', action='store', dest='owner', help='Github repository owner', default='')
parser.add_argument('-f', '--force', action='store_true', help='Overwrite directory or spec and .gear if already exists')
parser.add_argument('--gitignore', action='store_true', dest='gitignore', help='Store default .gitignore to a new package')
parser.add_argument('--no-gitignore', action='store_false', dest='gitignore', help='Don\'t store default .gitignore to a new package')
parser.add_argument('--check', action='store_true', dest='check', help='Treat all failures when executing external programs as errors')
parser.add_argument('--no-check', action='store_false', dest='check', help='Skip all failures when executing external programs')
parser.add_argument('--disable-name-translation', action='store_true', help='Do not change given name')
parser.add_argument('--verbose', action='store_true', help='Show output of executed programs')
parser.add_argument('--here', action='store_true', help='Create spec in current directory')
parser.add_argument('--tag', action='store', help='Git tag (uses in .gear/rules)')
parser.add_argument('--git', action='store_true', help='Git clone from url, clear repo and configure remotes update')
parser.add_argument('--test', action='store', dest='original', help='Run in test mode to compare result with original dir')
parser.add_argument('--date', type=lambda d: datetime.datetime.strptime(d, '%Y-%m-%d'), help='Pass date to changelog in format YYYY-MM-DD')

# Fill values...
args = parser.parse_args()
if args.git and args.here:
    print('Only one of --git and --here options could be used', file=sys.stderr)
    exit(1)
# print(args.git)
# print(args.tag)
# print(args.original)
# exit()

# ======================================================================================================
def get_available_types():
    available_types = []
    try:
        file_names = os.listdir(templates_path)
        available_types = [x.replace('.spec','') for x in file_names if x.endswith('.spec')]
    except:
        print('Directory ' + templates_path + ' does not exist or does not contain any spec templates')

    return (' (' + ','.join(list(sorted(list(available_types)))) + ')')


def input_with_check(prompt):
    res = input(prompt)
    # skip empty string and string containing only white spaces
    while (re.match('^\s*$', res)):
        print("Parameter should not be empty")
        res = input(prompt)
    return res

read_dict = {}

if len(sys.argv) < 2:
    # Go to interactive mode
    read_dict['module']      = input_with_check('Package or module name: ')
    read_dict['spec_type']   = input_with_check('Package type' + get_available_types() + ': ')
    read_dict['version']     = input_with_check('Package version: ')
    read_dict['summary']     = input_with_check('Package summary: ')
    read_dict['license']     = input_with_check('Package license: ')
    read_dict['url']         = input_with_check('Package URL: ')
    read_dict['description'] = input_with_check('Package description: ')
    read_dict['lastchange']  = input_with_check('Package changelog: ')

    keys = read_dict
else:
    keys = args.__dict__

call = subprocess.call
check_call = subprocess.check_call
if not args.verbose:
    call = partial(subprocess.call, stderr=subprocess.DEVNULL, stdout=subprocess.DEVNULL)
    check_call = partial(subprocess.check_call, stderr=subprocess.DEVNULL, stdout=subprocess.DEVNULL)

if args.check:
    call = check_call
else:
    check_call = call

spec = Spec(keys, call, check_call, args.tag, args.date, args.disable_name_translation)
# Prepare place
if args.original is None:
    spec.deploy(
        use_git=args.git,
        here=args.here,
        force=args.force,
        gitignore=args.gitignore,
    )
else:
    spec.test(args.original)
