# -*- coding: utf-8 -*-
# Elisa - Home multimedia server
# Copyright (C) 2006-2008 Fluendo Embedded S.L. (www.fluendo.com).
# All rights reserved.
#
# This file is available under one of two license agreements.
#
# This file is licensed under the GPL version 3.
# See "LICENSE.GPL" in the root of this distribution including a special
# exception to use Elisa with Fluendo's plugins.
#
# The GPL part of Elisa is also available under a commercial licensing
# agreement from Fluendo.
# See "LICENSE.Elisa" in the root directory of this distribution package
# for details on that license.

"""
Functions used to extend setuptools and add elisa specific metadata
when distributing plugins.
"""

__maintainer__ = 'Philippe Normand <philippe@fluendo.com>'

import os
import sys
import types
import shutil
import tempfile

from elisa.core import config
from elisa.core.utils import i18n
from elisa.extern import path

import setuptools
from setuptools import setup, find_packages
from setuptools.command.bdist_egg import bdist_egg
from setuptools.command import test
from setuptools.command import sdist
from setuptools.command import build_py

from distutils.errors import DistutilsSetupError, DistutilsOptionError
from distutils.core import Command
from distutils.command import clean

from twisted.scripts import trial

PACKAGE_DATA = {'': ['*.conf', '*.png', '*.mo', '*.po', '*.xml', '*.lirc']}

class SDist(sdist.sdist):

    def make_distribution(self):
        files = self.filelist.files[:]
        for filename in files:
            dummy, ext = os.path.splitext(filename)
            ext = ext[1:].lower()
            if ext == 'mo':
                self.filelist.files.remove(filename)
                self.announce("Removed mo file: %r" % filename)
        sdist.sdist.make_distribution(self)

    def make_release_tree(self, base_dir, files):
        if self.distribution.package_dir:
            dirs = self.distribution.package_dir.values()
            egg_name = self.distribution.get_name().replace('-','_')
            stripped_filelist = []

            for dirname in dirs:
                for filename in files:
                    to_include = os.path.abspath(filename).startswith(dirname)
                    is_egg_info = filename.find(egg_name) > -1
                    if to_include or is_egg_info:
                        if filename not in stripped_filelist:
                            stripped_filelist.append(filename)

            files = stripped_filelist
        
        sdist.sdist.make_release_tree(self, base_dir, files)

        script_name = os.path.basename(sys.argv[0])
        self.copy_file(sys.argv[0], os.path.join(base_dir, script_name))

        # if we're building a bundle we need to manually copy some files
        dist_name = self.distribution.get_name()
        if self.distribution.package_dir and 'elisa-plugins' in dist_name:
            
            elisa_init = os.path.join(self.distribution.package_dir['elisa'],
                                      '__init__.py')
            dest_dir = os.path.join(base_dir, 'elisa-plugins')
            self.copy_file(elisa_init, os.path.join(dest_dir, 'elisa',
                                                    '__init__.py'))
            self.copy_file(elisa_init, os.path.join(dest_dir, 'elisa',
                                                    'plugins',
                                                    '__init__.py'))
        if dist_name == 'elisa':
            src_svg = os.path.join('elisa', 'core', 'data', 'elisa.svg')
            dest_svg = os.path.join(base_dir, src_svg)
            self.copy_file(src_svg, dest_svg)
            


class LocaleBuildPy(build_py.build_py):
    """
    create .mo files from .po locales if needed
    """
 
    def run(self):
        pobuild = build_po(self.distribution)
        pobuild.finalize_options()
        pobuild.run()
        build_py.build_py.run(self)
        
class TrialTest(test.test):
    """
    Twisted Trial setuptools command
    """

    user_options = test.test.user_options + [
        ('coverage','c', "Report coverage data"),
    ]

    def initialize_options(self):
        test.test.initialize_options(self)
        self.coverage = None

    def finalize_options(self):
        if self.test_suite is None:
            if self.test_module is None:
                self.test_suite = self.distribution.test_suite
            else:
                self.test_suite = self.test_module
        elif self.test_module:
            raise DistutilsOptionError(
                "You may specify a module or a suite, but not both"
            )

        self.test_args = self.test_suite

    def run_tests(self):
        config = trial.Options()
        config.parseOptions()

        args = self.test_args
        if type(args) == str:
            args = [args,]

        config['tests'] = args

        if self.coverage:
            config.opt_coverage()

        trial._initialDebugSetup(config)
        trialRunner = trial._makeRunner(config)
        suite = trial._getSuite(config)

        # run the tests
        if config['until-failure']:
            test_result = trialRunner.runUntilFailure(suite)
        else:
            test_result = trialRunner.run(suite)

        # write coverage data
        if config.tracer:
            sys.settrace(None)
            results = config.tracer.results()
            results.write_results(show_missing=1, summary=False,
                                  coverdir=config.coverdir)

class Clean(clean.clean):
    def run(self):
        clean.clean.run(self)

        # Removing .pyc, .pyo and .cover files
        for root, dirs, files in os.walk('.', topdown=False):
            for name in files:
                if name.endswith('.pyc') or name.endswith('.pyo') or \
                       name.endswith('.cover'):
                    os.remove(os.path.join(root, name))


CMD_CLASS = {'test': TrialTest, #'build_py': LocaleBuildPy,
             'clean': Clean, 'sdist': SDist}

def override_check_test_suite(dist, attr, value):
    if not isinstance(value, list):
        raise DistutilsSetupError("test_suite must be a list")
    
setuptools.dist.check_test_suite = override_check_test_suite


def write_dict(cmd, basename, filename, force=False):
    """
    """
    argname = os.path.splitext(basename)[0]
    arg_value = getattr(cmd.distribution, argname, None)
    text_value = ""
    if arg_value is not None:
        for key, value in arg_value.iteritems():
            
            text_value += "%s = %s\n" % (key, value)
    cmd.write_or_delete_file(argname, filename, text_value, force)

def assert_dict_or_none(dist, attr, value):
    """Verify that value is a dictionary"""
    if type(value) not in (dict, types.NoneType):
        raise DistutilsSetupError("%r must be a dictionary or None value. "\
                                  "Got %r" % (attr,value))

def scan_plugin_conf(conf_path, name_prefix='elisa-plugin-'):
    """ Read the config located at given path and return data suitable
    to distutils' setup().

    @param conf_path:   absolute or relative path to a plugin.conf file
    @type conf_path:    string
    @param name_prefix: plugin's name prefix
    @type name_prefix:  string
    @rtype:             dict
    """
    cfg = config.Config(conf_path)
    section = cfg.get_section('general')

    # Basic distutils supported keywords
    name = section.get('name', default='unknown')
    version = section.get('version', default='0.1')
    description = section.get('description', default='no description')
    long_description = section.get('long_description', default=description)
    author = section.get('author', default='John Doe')
    author_email = section.get('author_email', default='john@doe.com')
    keywords = ','.join(section.get('keywords', default=[]))
    license = section.get('license', default='UNKNOWN')
    bundles = section.get('bundles', default=[])
    replaces = section.get('replaces', default=[])
    
    # Elisa specific metadata, to go in elisa_infos.txt 
    category_id = section.get('category_id', default='unknown')
    plugin_deps = [d.strip() for d in
                   section.get('plugin_dependencies', default=[])]
    py_deps = section.get('external_dependencies', default=[])
    ext_deps = []
    for component_path, component_config in cfg.as_dict().iteritems():
        if component_path == 'general':
            continue
        for dep in component_config.get('external_dependencies',[]):
            dep = dep.strip()
            if dep not in ext_deps:
                ext_deps.append(dep)

    py_deps = ','.join(py_deps)
    ext_deps = ','.join(ext_deps)
    bundles = ','.join(bundles)
    replaces = ','.join(replaces)
    
    # TODO: eventually move categories to distutils trove classifiers
    elisa_infos = {'category_id': category_id, 'py_deps': py_deps,
                   'ext_deps': ext_deps, 'plugin_name': name,
                   'replaces': replaces, 'bundles': bundles}
        
    conf_dir = os.path.dirname(conf_path)
    if not conf_dir:
        conf_dir = os.getcwd()
        
    package_name = os.path.basename(conf_dir)

    if conf_dir == os.getcwd():
        plugin_pkg_name = 'elisa.plugins.%s' % package_name
        packages = [plugin_pkg_name,]
        packages.extend(['%s.%s' % (plugin_pkg_name, p)
                         for p in find_packages()])

        tmp_dir = tempfile.mkdtemp()
        f = open(os.path.join(tmp_dir, '__init__.py'), 'w')
        f.write("""\
__import__('pkg_resources').declare_namespace(__name__)

from pkgutil import extend_path
__path__ = extend_path(__path__, __name__)
""")
        f.close()
        
        infos = {'package_dir': {'elisa.plugins.%s' % package_name: '.',
                                 'elisa':tmp_dir, 'elisa.plugins': tmp_dir,
                                 },
                 'py_modules': ['elisa.__init__', 'elisa.plugins.__init__']
                 }
    else:
        infos = {}
        packages = find_packages()
        tmp_dir = None
        
    package_path = 'elisa.plugins.%s' % package_name
    entry_points = {'elisa.plugins': ['%s = %s'  % (name, package_path),]}

    requires = ['%s%s' % (name_prefix, n) for n in plugin_deps]
    
    infos.update({'name': '%s%s' % (name_prefix, name),
                  'description': description, 'license': license,
                  'keywords': keywords,
                  'version': version, 'long_description': long_description,
                  'author': author, 'author_email': author_email,
                  'elisa_infos': elisa_infos, 'packages': packages,
                  'namespace_packages': ['elisa.plugins',],
                  'entry_points': entry_points,
                  'install_requires': requires, 
                  'package_data': PACKAGE_DATA,
                  'cmdclass': CMD_CLASS,
                  })
    return tmp_dir, infos


def find_plugins(bundle_name, plugins_dir='.', prefix='', excludes=()):
    entry_points = []
    excludes = [os.path.join(plugins_dir, e) for e in excludes]
    packages = []

    for config_file in path.path(plugins_dir).walkfiles('*.conf',
                                                        errors='ignore'):
        need_continue = False
        for e in excludes:
            if config_file.startswith(e):
                need_continue = True
                break
        if need_continue:
            continue
        
        dir_name = os.path.dirname(config_file)
        pkg_name = os.path.basename(dir_name)

        plugin_config = config.Config(config_file)
        general = plugin_config.get_section('general',
                                            default={})
        name = general.get('name')
        if name:
            if bundle_name not in general.get('bundles', []):
                continue
            
            dir_name = os.path.dirname(config_file)
            pkg_name = dir_name.replace(os.path.sep, '.')
            pkg_name = pkg_name[len(plugins_dir)+1:]
            entry_point = "%s = %s" % (name, pkg_name)
            entry_points.append(entry_point)
            
            pkgs = ['%s.%s' % (pkg_name, p) for p in find_packages(dir_name)]
            packages.append(pkg_name)
            packages.extend(pkgs)

    return entry_points, packages

def elisa_setup(name, description, long_description, version, license,
                author, author_email, namespace, plugins_dir):
    
    entry_points, packages = find_plugins(name, plugins_dir, namespace)
        
    infos = {'name': name,
             'description': description, 'license': license,
             'version': version, 'long_description': long_description,
             'author': author, 'author_email': author_email,
             'entry_points': {'elisa.plugins': entry_points},
             'namespace_packages': [namespace,],
             'packages': packages,
             'package_data': PACKAGE_DATA,
             'cmdclass': CMD_CLASS,
             }

    tmp_dir = tempfile.mkdtemp()
    f = open(os.path.join(tmp_dir, '__init__.py'), 'w')
    f.write("""\
__import__('pkg_resources').declare_namespace(__name__)

from pkgutil import extend_path
__path__ = extend_path(__path__, __name__)
""")
    f.close()

    package_dir = {'elisa':tmp_dir, 'elisa.plugins': tmp_dir,}
    for pkg in packages:
        dirname = pkg.replace('.', os.path.sep)
        package_dir[pkg] = os.path.join(plugins_dir, dirname)

    py_modules = ['elisa.__init__', 'elisa.plugins.__init__']
    
    infos['package_dir'] = package_dir
    infos['py_modules'] = py_modules

    # distutils doesn't like unicode strings...
    p = [i.encode() for i in infos['packages']]
    infos['packages'] = p

    #print infos
    
    setup(**infos)

    shutil.rmtree(tmp_dir, ignore_errors=True)

def run_plugin_setup(plugin_conf='plugin.conf'):
    tmpdir, infos = scan_plugin_conf(plugin_conf)
    
    setup(**infos)

    shutil.rmtree(tmpdir, ignore_errors=True)


class bdist_elisa(bdist_egg):
    """
    A customized bdist_egg command that creates .elisa.egg files. They
    are the same as bare eggs for now. Only filename differs.
    """
    
    def finalize_options(self):
        bdist_egg.finalize_options(self)
        
        egg_file = self.egg_output

        basename, ext = os.path.splitext(self.egg_output)
        self.egg_output = "%s.elisa.egg" % basename

class build_po(Command):
    """
    Read a set of Elisa translation files, compile po files existing
    in the directories listed in those files and ship mo files as
    package_data of the distribution.

    Translation files have the following format::
    
      #
      # domain path/to/input /path/to/output
      # other-domain path/to/input/only
      #
      #
      # Don't forget the empty line at the end!

    """

    description = "compile Elisa translation file(s)"

    user_options = [
        ('trans-files=', 't', 'Elisa translation files')
        ]

    
    def initialize_options(self):
        self.trans_files = []

    def finalize_options (self):
        self.ensure_filename_list('trans_files')
        if not self.trans_files:
            self.trans_files = ['data/translations.lst',]
            
    def run(self):
        mo_files = i18n.compile_translations_from_files(self.trans_files)
        self.distribution.package_data.update(mo_files)

    def ensure_filename_list(self, option):
        self.ensure_string_list(option)
        for fname in getattr(self, option):
            if not os.path.exists(fname):
                msg = "'%s' does not exist or is not a file" % fname
                raise DistutilsOptionError("error in '%s' option: %s" % (option,
                                                                         msg))
