# vim: ai ts=4 sts=4 et sw=4
#
# Copyright (C) 2010, 2011, 2012, 2013, 2014 Intel, Inc.
#
#    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; version 2 of the License.
#
#    This program is distributed in the hope that it will be useful,
#    but WITHOUT ANY WARRANTY; without even the implied warranty of
#    MERCHANTABILITY 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, write to the Free Software
#    Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA  02111-1307, USA.
#

"""
RepoMaker - creates download repos
"""

import os
import shutil
from hashlib import sha256 # pylint: disable-msg=E0611
import rpm

from common.builddata import BuildData, BuildDataError
from common.imagedata import ImageData
from common import manifest
import xml.etree.cElementTree as ElementTree

# arch map table:
# key->val: map the arch from 'key' to 'val' to make the url unify
ARCH_MAP = {
            'i686': 'ia32',
            'i586': 'ia32',
            }

class RepoMakerError(Exception):
    """Custom RepoMaker exception."""
    pass

# Helper functions
def find_files(topdir, prefix='', suffix='.rpm'):
    """Find files in the tree."""
    for root, _dirs, files in os.walk(topdir):
        for item in files:
            if item.startswith(prefix) and item.endswith(suffix):
                yield os.path.join(root, item)

def collect(in_dir, archs, repo_name=None):
    """Collect files, and destinations."""
    files = []
    find_dir = in_dir
    if repo_name:
        find_dir = os.path.join(in_dir, repo_name)
        if not os.path.exists(find_dir):
            #TODO: Do not handle non-existing repo.
            find_dir = in_dir
            print "WARNING! No matching repo(%s) exist." % repo_name
    for fname in find_files(find_dir):
        ftype = fname.split('.')[-2]

        # Unify arch name
        if ftype not in ("src", "noarch"):
            if ftype not in archs:
                # skip packages for unknown architectures
                continue

        basename = os.path.basename(fname)
        is_debug = "-debugsource-" in fname or "-debuginfo-" in fname
        is_group = basename.startswith("package-groups-") and not is_debug \
            and ftype in archs
        is_imageconf = not is_debug and \
                       basename.startswith("image-configurations-") and \
                       basename.endswith("noarch.rpm")
        files.append((fname, ftype, is_debug, is_group, is_imageconf))
    return files

def create_dirs(repo_dir, archs):
    """Create directory structure of the repos."""
    dirs = [("binary", arch, os.path.join(repo_dir, subdir, arch)) \
                 for arch in archs \
                     for subdir in ['packages']]
    dirs.append(("noarch", "noarch", os.path.join(repo_dir,
                                                  "packages",
                                                  "noarch")))
    dirs.append(("source", None, os.path.join(repo_dir, "source")))
    dirs.append(("debug", None, os.path.join(repo_dir, "debug")))

    repo_dirs = set([])
    # create and get repo_dirs
    for rtype, rarch, rdirname in dirs:
        if not os.path.exists(rdirname):
            os.makedirs(rdirname)

        if rarch:
            # binary repo
            repo_dirs.add((rtype, rarch, os.path.dirname(rdirname)))
        else:
            # source repo
            repo_dirs.add((rtype, rarch, rdirname))

    return repo_dirs

def gen_target_dirs(repo_dir, ftype, is_debug):
    """Prepare list of target dirs depending on type of package."""
    if ftype == "src":
        return [os.path.join(repo_dir, "source")]
    elif is_debug:
        return [os.path.join(repo_dir, "debug")]
    else:
        return [os.path.join(repo_dir, "packages", ftype)]

def move_or_hardlink(fpath, target_dirs, move=False):
    """Move or hardlink file to target directories."""
    fname = os.path.basename(fpath)
    for tdir in target_dirs:
        tpath = os.path.join(tdir, fname)
        if move:
            if os.path.exists(fpath):
                shutil.move(fpath, tpath)
            else:
                # already moved noarch package. symlink it.
                os.symlink(os.path.join(target_dirs[0], fname), tpath)
        else:
            if os.path.isfile(tpath):
                os.remove(tpath)
            os.link(fpath, tpath)

class RepoMaker(object):
    """Makes rpm repositories."""

    def __init__(self, build_id, outdir):
        """
        RepoMaker init

        Args:
            build_id (str): Build identifier
            out_dir (str): Top output directory.
        """

        self.build_id = build_id
        self.outdir = outdir
        self.repos = {}
        self.imagedata = ImageData()

    def add_repo(self, in_dir, name, archs=(), buildconf=None,
                 move=False, gpg_key=None, signer='/usr/bin/sign'):
        """
        Convert repository to download structure.
        Create or update repository using packages from repo_dir

        Args:
            in_dir (str):  path to repository to convert
            name (str):  name of the repository to create
            archs (tuple): list of architectures
            buildconf(str): content of build configuration
            move (bool): move files instead of hardlinking
            gpg_key (str): path to file with gpg key
            signer (str): command to sign the repo

        Raises: RepoMakerError

        """
        if not os.path.exists(in_dir):
            raise RepoMakerError("Directory %s doesn't exist" % in_dir)

        wrong_archs = set(archs).intersection(set(ARCH_MAP))
        if wrong_archs:
            raise RepoMakerError("Wrong output architecture(s) specified: %s" \
                                 % ", ".join(wrong_archs))

        repo_dir = os.path.join(self.outdir, "repos", name)
        if name not in self.repos:
            self.repos[name] = {'archs': list(set(archs))}

        # translate unified archs name 'ia23' to 'i586' 'i686' matched
        # with OBS archs
        if set(archs).intersection(set(ARCH_MAP.values())):
            new_archs = [arch for arch in ARCH_MAP
                            if ARCH_MAP.get(arch) in archs]
            archs = new_archs + \
                    list(set(archs).difference(set(ARCH_MAP.values())))

        files = sorted(collect(in_dir, archs, name))

        # Create directory structure
        repo_dirs = create_dirs(repo_dir, archs)

        for fpath, ftype, is_debug, is_group, is_imageconf in files:
            # Prepare list of target directories
            target_dirs = gen_target_dirs(repo_dir, ftype, is_debug)

            # Move or hardlink .rpm to target dirs
            move_or_hardlink(fpath, target_dirs, move)

            # For package-groups package update package groups and patterns
            if is_group:
                repodata_dir = os.path.join(repo_dir, "packages",
                                            "repodata")
                if not os.path.exists(repodata_dir):
                    os.makedirs(repodata_dir)
                for filename in ("group.xml", "patterns.xml"):
                    os.system("rpm2cpio %s | cpio -i --to-stdout "\
                              "./usr/share/package-groups/%s > %s" % \
                              (fpath, filename, os.path.join(repodata_dir,
                                                             filename)))
            # get names and content of .ks files from rpm
            if is_imageconf:
                self.load_imagedata(fpath)

        # Generate or update build.xml
        self.update_builddata(name, repo_dirs)

        # Run createrepo
        for _rtype, _rarch, rpath in repo_dirs:
            # run createrepo
            os.system('createrepo_c --quiet %s' % rpath)
            for filename in ("group.xml", "patterns.xml"):
                metafile = os.path.join(rpath, "repodata", filename)
                if os.path.exists(metafile):
                    os.system('modifyrepo_c %s %s' % (metafile,
                              os.path.join(rpath, "repodata")))
                    #os.unlink(metafile)

            # update build configuration file to repodata
            if buildconf:
                confpath = os.path.join(rpath, "repodata",
                                           'build.conf')
                with open(confpath, 'w') as conf:
                    conf.write(buildconf)
                os.system('modifyrepo_c %s %s' % (confpath,
                          os.path.join(rpath, "repodata")))
                os.unlink(confpath)

        # sign if gpg_key is provided
        if gpg_key and os.path.exists(signer) and os.access(signer, os.X_OK):
            for _rtype, _rarch, rpath in repo_dirs:
                repomd_path = os.path.join(rpath, "repodata",
                                           "repomd.xml")
                os.system('%s -d % s' % (signer, repomd_path))

    def has_images(self):
        """
        return True if there already has images infomation
        """
        return bool(self.imagedata.images)

    def load_imagedata(self, rpm):
        """

        Args:

        Raises: ImageDataError

        """
        self.imagedata.extract_image_conf(rpm)

    def gen_image_info(self, updated_ks=None):
        """
        Generate images.xml and save a copy of ks file under builddata dir
        """

        if updated_ks:
            self.imagedata.ks = updated_ks

        for repo in self.repos:
            self.imagedata.save(os.path.join(self.outdir,
                                             'builddata/images/%s' % repo))

    def update_builddata(self, name, dirs, buildconf=None):
        """
        Update or create build.xml

        Args:
            name (str):  name of the target repository
            buildconf(str): content of build configuration

        Raises: RepoMakerError

        """
        repos = self.repos[name]
        target = {"name":  name, "archs": list(repos['archs'])}
        if buildconf:
            target["buildconf"] = {
                "location": os.path.join("repos", name),
                "checksum": {
                   "type": "sh256",
                   "value": sha256(buildconf).hexdigest()
                }
            }
        target["repos"] = [(rtype, rarch,
                            os.path.join('repos', rpath.split('repos/')[1])) \
                                for rtype, rarch, rpath in dirs]

        try:
            bdata = BuildData(self.build_id)
            # Create or update build.xml
            outf = os.path.join(self.outdir, 'build.xml')
            if os.path.exists(outf):
                bdata.load(open(outf).read())

            bdata.add_target(target)
            bdata.save(outf)
        except BuildDataError, err:
            raise RepoMakerError("Unable to generate build.xml: %s" % err)

    def update_extend_builddata(self, base_id=None, ref_id=None):
        """
        Update or create build.xml
        Args:
            base id : id of Base snapshot
            ref id : id of Ref Project
        """
        try:
            bdata = BuildData(self.build_id)
            # Create or update build.xml
            outf = os.path.join(self.outdir, 'build.xml')
            if os.path.exists(outf):
                bdata.load(open(outf).read())
            if base_id:
                bdata.set_base_id(base_id)
            if ref_id:
                bdata.set_ref_id(ref_id)
            bdata.save(outf)
        except BuildDataError, err:
            raise RepoMakerError("Unable to generate build.xml: %s" % err)

    def gen_manifest_info(self, name, gerrit_fetch_url, gerrit_review_url):
        """
        Generate manifest for repo

        Args:
            name (str):  name of the target repository
            gerrit_fetch_url (str): url base to fetch gerrit project
            gerrit_review_url (str): url base for changes review
        Raises:
            RepoMakerError
        """

        manifest_dir = os.path.join(self.outdir, 'builddata', 'manifest')

        if not os.path.exists(manifest_dir):
            os.makedirs(manifest_dir)

        repo_primary = manifest.get_repo_primary_md(self.outdir, name)

        # Generate manifest for every repo arch
        # package and vcs tag dict
        package_vcs_tag = manifest.get_package_vcs_tag(repo_primary)
        # tuple set in (gitprj, git_path, revision)
        data = set([])
        manifestdata = set([])
        for pkg in package_vcs_tag:
            try:
                git_prj, commit_id = pkg[1].split('#')
                data.add((git_prj, git_prj, commit_id))
                manifestdata.add((pkg[0], git_prj, commit_id))
            except ValueError:
                # No vcs tag found
                data.add((pkg[0], '', ''))
                manifestdata.add((pkg[0], '', ''))

        manifest_string = manifest.gen_repo_manifest(sorted(data),
                                            gerrit_fetch_url,
                                            gerrit_review_url)
        with open(os.path.join(manifest_dir,
                               "%s_%s.xml"  %(self.build_id, name)),
                  'w') as manifest_fh:
            manifest_fh.write(manifest_string)

        manifest_items = []
        try:
            for pkg in manifestdata:
               manifest_items.append({pkg[1]: [pkg[2],pkg[0]]})
        except Exception as err:
            print repr(err)
        return manifest_items

    def get_rpm_vcs_from(self, rpm_file):
        """Returns rpm information by querying a rpm"""
        ts = rpm.ts()
        fdno = os.open(rpm_file, os.O_RDONLY)
        try:
            hdr = ts.hdrFromFdno(fdno)
        except rpm.error:
            fdno = os.open(rpm_file, os.O_RDONLY)
            ts.setVSFlags(rpm._RPMVSF_NOSIGNATURES)
            hdr = ts.hdrFromFdno(fdno)
        os.close(fdno)
        try:
            vcs_path = hdr['vcs'].split('#')
            vcs_from = hdr['description'].split('\n')[0].split('#')
            if vcs_path[0] == vcs_from[0] and len(vcs_from[1]) == 40:
                return vcs_from
            else:
                return None
        except:
            return None

    def gen_manifest_info_app_from_rpm(self, name, gerrit_fetch_url, gerrit_review_url, in_dir, archs=()):
        """
        Generate manifest for ABS outputs(tpk) using rpms
        """
        if not os.path.exists(in_dir):
            raise RepoMakerError("Directory %s doesn't exist" % in_dir)

        wrong_archs = set(archs).intersection(set(ARCH_MAP))
        if wrong_archs:
            raise RepoMakerError("Wrong output architecture(s) specified: %s" \
                                 % ", ".join(wrong_archs))

        repo_dir = os.path.join(self.outdir, "repos", name)
        if name not in self.repos:
            self.repos[name] = {'archs': list(set(archs))}

        # translate unified archs name 'ia23' to 'i586' 'i686' matched
        # with OBS archs
        if set(archs).intersection(set(ARCH_MAP.values())):
            new_archs = [arch for arch in ARCH_MAP
                            if ARCH_MAP.get(arch) in archs]
            archs = new_archs + \
                    list(set(archs).difference(set(ARCH_MAP.values())))

        # Create directory structure
        repo_dirs = create_dirs(repo_dir, archs)

        # tuple set in (gitprj, git_path, revision)
        data = set([])
        for _rtype, _rarch, rpath in repo_dirs:
            #Add list if _rtype == binary
            if _rtype == 'binary':
                for i in find_files(rpath):
                    vcs_from = self.get_rpm_vcs_from(i)
                    if vcs_from is not None:
                        data.add((vcs_from[0], vcs_from[0], vcs_from[1]))

        if len(data) == 0:
            return

        manifest_dir = os.path.join(self.outdir, 'builddata', 'manifest')

        if not os.path.exists(manifest_dir):
            os.makedirs(manifest_dir)

        manifest_string = manifest.gen_repo_manifest(sorted(data),
                                            gerrit_fetch_url,
                                            gerrit_review_url)
        with open(os.path.join(manifest_dir,
                               "%s_%s_preloadapp.xml"  %(self.build_id, name)),
                  'w') as manifest_fh:
            manifest_fh.write(manifest_string)

