import sys, os, os.path
from distutils.core import Extension
from distutils.errors import DistutilsOptionError
from distutils import log
from versioninfo import get_base_dir, split_version

try:
    from Cython.Distutils import build_ext as build_pyx
    import Cython.Compiler.Version
    CYTHON_INSTALLED = True
except ImportError:
    CYTHON_INSTALLED = False

EXT_MODULES = ["lxml.etree", "lxml.objectify"]

PACKAGE_PATH = "src/lxml/"

if sys.version_info[0] >= 3:
    _system_encoding = sys.getdefaultencoding()
    if _system_encoding is None:
        _system_encoding = "iso-8859-1" # :-)
    def decode_input(data):
        if isinstance(data, str):
            return data
        return data.decode(_system_encoding)
else:
    def decode_input(data):
        return data

def env_var(name):
    value = os.getenv(name)
    if value:
        return decode_input(value).split(os.pathsep)
    else:
        return []

def ext_modules(static_include_dirs, static_library_dirs, static_cflags): 
    if OPTION_BUILD_LIBXML2XSLT:
        build_libxml2xslt('build/tmp', 
                          static_include_dirs, static_library_dirs, static_cflags)
    if CYTHON_INSTALLED:
        source_extension = ".pyx"
        print("Building with Cython %s." % Cython.Compiler.Version.version)
    else:
        print ("NOTE: Trying to build without Cython, pre-generated "
               "'%slxml.etree.c' needs to be available." % PACKAGE_PATH)
        source_extension = ".c"

    if OPTION_WITHOUT_OBJECTIFY:
        modules = [ entry for entry in EXT_MODULES
                    if 'objectify' not in entry ]
    else:
        modules = EXT_MODULES

    lib_versions = get_library_versions()
    if lib_versions[0]:
        print("Using build configuration of libxml2 %s and libxslt %s" % 
              lib_versions)
    else:
        print("Using build configuration of libxslt %s" % 
              lib_versions[1])

    _include_dirs = include_dirs(static_include_dirs)
    _library_dirs = library_dirs(static_library_dirs)
    _cflags = cflags(static_cflags)
    _define_macros = define_macros()
    _libraries = libraries()

    if _library_dirs:
        message = "Building against libxml2/libxslt in "
        if len(_library_dirs) > 1:
            print(message + "one of the following directories:")
            for dir in _library_dirs:
                print("  " + dir)
        else:
            print(message + "the following directory: " +
                  _library_dirs[0])

    if OPTION_AUTO_RPATH:
        runtime_library_dirs = _library_dirs
    else:
        runtime_library_dirs = []
    
    result = []
    for module in modules:
        main_module_source = PACKAGE_PATH + module + source_extension
        dependencies = find_dependencies(module)
        result.append(
            Extension(
            module,
            sources = [main_module_source] + dependencies,
            extra_compile_args = ['-w'] + _cflags,
            define_macros = _define_macros,
            include_dirs = _include_dirs,
            library_dirs = _library_dirs,
            runtime_library_dirs = runtime_library_dirs,
            libraries = _libraries,
            ))
    return result

def find_dependencies(module):
    if not CYTHON_INSTALLED:
        return []
    from Cython.Compiler.Version import version
    if split_version(version) < (0,9,6,13):
        return []

    package_dir = os.path.join(get_base_dir(), PACKAGE_PATH)
    files = os.listdir(package_dir)
    pxd_files = [ os.path.join(PACKAGE_PATH, filename) for filename in files
                  if filename.endswith('.pxd') ]

    if 'etree' in module:
        pxi_files = [ os.path.join(PACKAGE_PATH, filename)
                      for filename in files
                      if filename.endswith('.pxi')
                      and 'objectpath' not in filename ]
        pxd_files = [ filename for filename in pxd_files
                      if 'etreepublic' not in filename ]
    elif 'objectify' in module:
        pxi_files = [ os.path.join(PACKAGE_PATH, 'objectpath.pxi') ]
    else:
        pxi_files = []

    return pxd_files + pxi_files

def extra_setup_args():
    result = {}
    if CYTHON_INSTALLED:
        result['cmdclass'] = {'build_ext': build_pyx}
    return result

def libraries():
    if sys.platform in ('win32',):
        libs = ['libxslt', 'libexslt', 'libxml2', 'iconv']
    else:
        libs = ['xslt', 'exslt', 'xml2', 'z', 'm']
    if OPTION_STATIC:
        if sys.platform in ('win32',):
            libs = ['%s_a' % lib for lib in libs]
    if sys.platform in ('win32',):
        libs.extend(['zlib', 'WS2_32'])
    return libs

def library_dirs(static_library_dirs):
    if OPTION_STATIC:
        if not static_library_dirs:
            static_library_dirs = env_var('LIBRARY')
        assert static_library_dirs, "Static build not configured, see doc/build.txt"
        return static_library_dirs
    # filter them from xslt-config --libs
    result = []
    possible_library_dirs = flags('libs')
    for possible_library_dir in possible_library_dirs:
        if possible_library_dir.startswith('-L'):
            result.append(possible_library_dir[2:])
    return result

def include_dirs(static_include_dirs):
    if OPTION_STATIC:
        if not static_include_dirs:
            static_include_dirs = env_var('INCLUDE')
        return static_include_dirs
    # filter them from xslt-config --cflags
    result = []
    possible_include_dirs = flags('cflags')
    for possible_include_dir in possible_include_dirs:
        if possible_include_dir.startswith('-I'):
            result.append(possible_include_dir[2:])
    return result

def cflags(static_cflags):
    result = []
    if OPTION_DEBUG_GCC:
        result.append('-g2')

    if OPTION_STATIC:
        if not static_cflags:
            static_cflags = env_var('CFLAGS')
        result.extend(static_cflags)
    else:
        # anything from xslt-config --cflags that doesn't start with -I
        possible_cflags = flags('cflags')
        for possible_cflag in possible_cflags:
            if not possible_cflag.startswith('-I'):
                result.append(possible_cflag)

    if sys.platform in ('darwin',):
        for opt in result:
            if 'flat_namespace' in opt:
                break
        else:
            result.append('-flat_namespace')

    return result

def define_macros():
    macros = []
    if OPTION_WITHOUT_ASSERT:
        macros.append(('PYREX_WITHOUT_ASSERTIONS', None))
    if OPTION_WITHOUT_THREADING:
        macros.append(('WITHOUT_THREADING', None))
    return macros

_ERROR_PRINTED = False

def run_command(cmd, *args):
    if not cmd:
        return ''
    if args:
        cmd = ' '.join((cmd,) + args)
    try:
        import subprocess
    except ImportError:
        # Python 2.3
        _, rf, ef = os.popen3(cmd)
    else:
        # Python 2.4+
        p = subprocess.Popen(cmd, shell=True,
                             stdout=subprocess.PIPE, stderr=subprocess.PIPE)
        rf, ef = p.stdout, p.stderr
    errors = ef.read()
    global _ERROR_PRINTED
    if errors and not _ERROR_PRINTED:
        _ERROR_PRINTED = True
        print("ERROR: %s" % errors)
        print("** make sure the development packages of libxml2 and libxslt are installed **\n")
    return decode_input(rf.read()).strip()

def get_library_versions():
    xml2_version = run_command(find_xml2_config(), "--version")
    xslt_version = run_command(find_xslt_config(), "--version")
    return xml2_version, xslt_version

def flags(option):
    xml2_flags = run_command(find_xml2_config(), "--%s" % option)
    xslt_flags = run_command(find_xslt_config(), "--%s" % option)

    flag_list = xml2_flags.split()
    for flag in xslt_flags.split():
        if flag not in flag_list:
            flag_list.append(flag)
    return flag_list

XSLT_CONFIG = None
XML2_CONFIG = None

def find_xml2_config():
    global XML2_CONFIG
    if XML2_CONFIG:
        return XML2_CONFIG
    option = '--with-xml2-config='
    for arg in sys.argv:
        if arg.startswith(option):
            sys.argv.remove(arg)
            XML2_CONFIG = arg[len(option):]
            return XML2_CONFIG
    else:
        # default: do nothing, rely only on xslt-config
        XML2_CONFIG = os.getenv('XML2_CONFIG', '')
    return XML2_CONFIG

def find_xslt_config():
    global XSLT_CONFIG
    if XSLT_CONFIG:
        return XSLT_CONFIG
    option = '--with-xslt-config='
    for arg in sys.argv:
        if arg.startswith(option):
            sys.argv.remove(arg)
            XSLT_CONFIG = arg[len(option):]
            return XSLT_CONFIG
    else:
        XSLT_CONFIG = os.getenv('XSLT_CONFIG', 'xslt-config')
    return XSLT_CONFIG

## Routines to download libxml2/xslt:

LIBXML2_LOCATION = 'ftp://xmlsoft.org/libxml2/'

def ftp_listdir(url):
    import ftplib, urlparse, posixpath
    scheme, netloc, path, qs, fragment = urlparse.urlsplit(url)
    assert scheme.lower() == 'ftp'
    server = ftplib.FTP(netloc)
    server.login()
    files = [posixpath.basename(fn) for fn in server.nlst(path)]
    return files

def download_libxml2(dest_dir, version=None):
    """Downloads libxml2, returning the filename where the library was downloaded"""
    import re
    version_re = re.compile(r'^LATEST_LIBXML2_IS_(.*)$')
    filename = 'libxml2-%s.tar.gz'
    return download_library(dest_dir, LIBXML2_LOCATION, 'libxml2',
                            version_re, filename, version=version)

def download_libxslt(dest_dir, version=None):
    """Downloads libxslt, returning the filename where the library was downloaded"""
    import re
    version_re = re.compile(r'^LATEST_LIBXSLT_IS_(.*)$')
    filename = 'libxslt-%s.tar.gz'
    return download_library(dest_dir, LIBXML2_LOCATION, 'libxslt',
                            version_re, filename, version=version)

def download_library(dest_dir, location, name, version_re, filename, 
                     version=None):
    import urllib, urlparse
    if version is None:
        fns = ftp_listdir(location)
        for fn in fns:
            match = version_re.search(fn)
            if match:
                version = match.group(1)
                print 'Latest version of %s is %s' % (name, version)
                break
        else:
            raise Exception(
                "Could not find the most current version of the %s from the files: %s"
                % (name, fns))
    filename = filename % version
    full_url = urlparse.urljoin(location, filename)
    dest_filename = os.path.join(dest_dir, filename)
    if os.path.exists(dest_filename):
        print ('Using existing %s downloaded into %s (delete this file if you want to re-download the package)'
               % (name, dest_filename))
    else:
        print 'Downloading %s into %s' % (name, dest_filename)
        urllib.urlretrieve(full_url, dest_filename)
    return dest_filename

## Backported method of tarfile.TarFile.extractall (doesn't exist in 2.4):
def _extractall(self, path=".", members=None):
    """Extract all members from the archive to the current working
       directory and set owner, modification time and permissions on
       directories afterwards. `path' specifies a different directory
       to extract to. `members' is optional and must be a subset of the
       list returned by getmembers().
    """
    import copy

    directories = []

    if members is None:
        members = self

    for tarinfo in members:
        if tarinfo.isdir():
            # Extract directories with a safe mode.
            directories.append(tarinfo)
            tarinfo = copy.copy(tarinfo)
            tarinfo.mode = 0700
        self.extract(tarinfo, path)

    # Reverse sort directories.
    directories.sort(lambda a, b: cmp(a.name, b.name))
    directories.reverse()

    # Set correct owner, mtime and filemode on directories.
    for tarinfo in directories:
        dirpath = os.path.join(path, tarinfo.name)
        try:
            self.chown(tarinfo, dirpath)
            self.utime(tarinfo, dirpath)
            self.chmod(tarinfo, dirpath)
        except ExtractError, e:
            if self.errorlevel > 1:
                raise
            else:
                self._dbg(1, "tarfile: %s" % e)

def unpack_tarball(tar_filename, dest):
    import tarfile
    print 'Unpacking %s into %s' % (os.path.basename(tar_filename), dest)
    tar = tarfile.open(tar_filename)
    base_dir = None
    for member in tar:
        base_name = member.name.split('/')[0]
        if base_dir is None:
            base_dir = base_name
        else:
            if base_dir != base_name:
                print 'Unexpected path in %s: %s' % (tar_filename, base_name)
    _extractall(tar, dest)
    tar.close()
    return os.path.join(dest, base_dir)

def call_subprocess(cmd, **kw):
    ## FIXME: should this be replaced with run_command?  That doesn't
    ## take cwd.
    import subprocess
    cwd = kw.get('cwd', '.')
    cmd_desc = ' '.join(cmd)
    log.info('Running "%s" in %s' % (cmd_desc, cwd))
    returncode = subprocess.call(cmd, **kw)
    if returncode:
        raise Exception('Command "%s" returned code %s' % (cmd_desc, returncode))

def safe_mkdir(dir):
    if not os.path.exists(dir):
        os.makedirs(dir)

def build_libxml2xslt(build_dir, 
                      static_include_dirs, static_library_dirs, static_cflags,
                      libxml2_version=None, libxslt_version=None):
    global XSLT_CONFIG, XML2_CONFIG
    if libxml2_version is None:
        libxml2_version = OPTION_LIBXML2_VERSION
    if libxslt_version is None:
        libxslt_version = OPTION_LIBXSLT_VERSION
    safe_mkdir(build_dir)
    libxml2_dir = unpack_tarball(download_libxml2(build_dir, libxml2_version), build_dir)
    libxslt_dir = unpack_tarball(download_libxslt(build_dir, libxslt_version), build_dir)
    prefix = os.path.join(os.path.abspath(build_dir), 'libxml2')
    safe_mkdir(prefix)
    call_subprocess(
        ['./configure', '--without-python', '--prefix=%s' % prefix], cwd=libxml2_dir)
    call_subprocess(
        ['make'], cwd=libxml2_dir)
    call_subprocess(
        ['make', 'install'], cwd=libxml2_dir)
    call_subprocess(
        ['./configure', '--without-python', '--prefix=%s' % prefix],
        cwd=libxslt_dir)
    call_subprocess(
        ['make'], cwd=libxslt_dir)
    call_subprocess(
        ['make', 'install'], cwd=libxslt_dir)
    XSLT_CONFIG = os.path.join(prefix, 'bin', 'xslt-config')
    XML2_CONFIG = os.path.join(prefix, 'bin', 'xml2-config')
    static_include_dirs.extend([
            os.path.join(prefix, 'include', 'libxml2'),
            os.path.join(prefix, 'include', 'libxslt'),
            os.path.join(prefix, 'include', 'libexslt')])
    static_library_dirs.append(os.path.join(prefix, 'lib'))

## Option handling:

def has_option(name):
    try:
        sys.argv.remove('--%s' % name)
        return True
    except ValueError:
        pass
    # allow passing all cmd line options also as environment variables
    env_val = os.getenv(name.upper().replace('-', '_'), 'false').lower()
    if env_val == "true":
        return True
    return False

def option_value(name):
    for index, option in enumerate(sys.argv):
        if option == '--' + name:
            if index+1 >= len(sys.argv):
                raise DistutilsOptionError(
                    'The option %s requires a value' % option)
            value = sys.argv[index+1]
            sys.argv[index:index+2] = []
            return value
        if option.startswith('--' + name + '='):
            value = option[len(name)+3:]
            sys.argv[index:index+1] = []
            return value
    env_val = os.getenv(name.upper().replace('-', '_'))
    return env_val

# pick up any commandline options
OPTION_WITHOUT_OBJECTIFY = has_option('without-objectify')
OPTION_WITHOUT_ASSERT = has_option('without-assert')
OPTION_WITHOUT_THREADING = has_option('without-threading')
OPTION_STATIC = has_option('static')
OPTION_DEBUG_GCC = has_option('debug-gcc')
OPTION_AUTO_RPATH = has_option('auto-rpath')
OPTION_BUILD_LIBXML2XSLT = has_option('build-libxml2xslt')
if OPTION_BUILD_LIBXML2XSLT:
    ## FIXME: is this necessary?  Seems kind of like it is:
    OPTION_STATIC = True
OPTION_LIBXML2_VERSION = option_value('libxml2-version')
OPTION_LIBXSLT_VERSION = option_value('libxslt-version')

