#!/usr/bin/env python

# depinst.py - installs the dependencies needed to test
#              a Boost library
#
# Copyright 2016-2020 Peter Dimov
#
# Distributed under the Boost Software License, Version 1.0.
# See accompanying file LICENSE_1_0.txt or copy at
# http://www.boost.org/LICENSE_1_0.txt

from __future__ import print_function

import re
import sys
import os
import argparse

verbose = 0

def vprint( level, *args ):

    if verbose >= level:
        print( *args, file=sys.stderr )


class CommandExecutionError(Exception):
    pass


class DependencyCycleError(Exception):
    pass


def is_module( m, gm ):

    return ( 'libs/' + m ) in gm


def module_for_header( h, x, gm ):

    if h in x:

        return x[ h ]

    else:

        # boost/function.hpp
        m = re.match( 'boost/([^\\./]*)\\.h[a-z]*$', h )

        if m and is_module( m.group( 1 ), gm ):

            return m.group( 1 )

        # boost/numeric/conversion.hpp
        m = re.match( 'boost/([^/]*/[^\\./]*)\\.h[a-z]*$', h )

        if m and is_module( m.group( 1 ), gm ):

            return m.group( 1 )

        # boost/numeric/conversion/header.hpp
        m = re.match( 'boost/([^/]*/[^/]*)/', h )

        if m and is_module( m.group( 1 ), gm ):

            return m.group( 1 )

        # boost/function/header.hpp
        m = re.match( 'boost/([^/]*)/', h )

        if m and is_module( m.group( 1 ), gm ):

            return m.group( 1 )

        vprint( 0, 'Cannot determine module for header', h )

        return None


def scan_header_dependencies( f, x, gm, deps, dep_path ):

    for line in f:

        m = re.match( '[ \t]*#[ \t]*include[ \t]*["<](boost/[^">]*)[">]', line )

        if m:

            h = m.group( 1 )

            mod = module_for_header( h, x, gm )

            if mod:

                if not mod in deps:

                    vprint( 1, 'Adding dependency', mod )
                    deps[ mod ] = 0

                elif len(dep_path) > 1 and mod == dep_path[0]:

                    raise DependencyCycleError( 'Dependency cycle detected: ' + ' -> '.join( dep_path + [mod] ) )


def scan_directory( d, x, gm, deps, dep_path ):

    vprint( 1, 'Scanning directory', d )

    if os.name == 'nt' and sys.version_info[0] < 3:
        d = unicode( d )

    for root, dirs, files in os.walk( d ):

        for file in files:

            fn = os.path.join( root, file )

            vprint( 2, 'Scanning file', fn )

            if sys.version_info[0] < 3:

                with open( fn, 'r' ) as f:
                    scan_header_dependencies( f, x, gm, deps, dep_path )

            else:

                with open( fn, 'r', encoding='latin-1' ) as f:
                    scan_header_dependencies( f, x, gm, deps, dep_path )


def scan_module_dependencies( m, x, gm, deps, dirs, dep_path ):

    vprint( 1, 'Scanning module', m )

    for dir in dirs:
        scan_directory( os.path.join( 'libs', m, dir ), x, gm, deps, dep_path )


def read_exceptions():

    # exceptions.txt is the output of "boostdep --list-exceptions"

    vprint( 1, 'Reading exceptions.txt' )

    x = {}

    module = None

    with open( os.path.join( os.path.dirname( sys.argv[0] ), 'exceptions.txt' ), 'r' ) as f:

        for line in f:

            line = line.rstrip()

            m = re.match( '(.*):$', line )

            if m:

                module = m.group( 1 ).replace( '~', '/' )

            else:

                header = line.lstrip()
                x[ header ] = module

    return x


def read_gitmodules():

    vprint( 1, 'Reading .gitmodules' )

    gm = []

    with open( '.gitmodules', 'r' ) as f:

        for line in f:

            line = line.strip()

            m = re.match( 'path[ \t]*=[ \t]*(.*)$', line )

            if m:

                gm.append( m.group( 1 ) )

    return gm

def install_modules( modules, git_args ):

    if len( modules ) == 0:
        return

    vprint( 0, 'Installing:', ', '.join(modules) )

    modules = [ 'libs/' + m for m in modules ]

    command = 'git submodule'

    if verbose <= 0:
        command += ' -q'

    command += ' update --init ' + git_args + ' ' + ' '.join( modules )

    vprint( 1, 'Executing:', command )
    r = os.system( command );

    if r != 0:
        raise CommandExecutionError( "The command '%s' failed with exit code %d" % (command, r) )


def install_module_dependencies( deps, x, gm, git_args, dep_path ):

    modules = []

    for m, i in deps.items():

        if not i:

            modules += [ m ]
            deps[ m ] = 1 # mark as installed

    if len( modules ) == 0:
        return 0

    install_modules( modules, git_args )

    for m in modules:
        next_dep_path = dep_path

        if dep_path:
            next_dep_path = dep_path + [m]

        scan_module_dependencies( m, x, gm, deps, [ 'include', 'src' ], next_dep_path )

    return len( modules )


if( __name__ == "__main__" ):

    parser = argparse.ArgumentParser( description='Installs the dependencies needed to test a Boost library.' )

    parser.add_argument( '-v', '--verbose', help='enable verbose output', action='count', default=0 )
    parser.add_argument( '-q', '--quiet', help='quiet output (opposite of -v)', action='count', default=0 )
    parser.add_argument( '-X', '--exclude', help="exclude a default subdirectory ('include', 'src', or 'test') from scan; can be repeated", metavar='DIR', action='append', default=[] )
    parser.add_argument( '-N', '--ignore', help="exclude top-level dependency even when found in scan; can be repeated", metavar='LIB', action='append', default=[] )
    parser.add_argument( '-I', '--include', help="additional subdirectory to scan; can be repeated", metavar='DIR', action='append', default=[] )
    parser.add_argument( '-g', '--git_args', help="additional arguments to `git submodule update`", default='', action='store' )
    parser.add_argument( '-u', '--update', help='update <library> before scanning', action='store_true' )
    parser.add_argument( '-C', '--reject-cycles', help='abort if <library> has cyclical dependencies', action='store_true' )
    parser.add_argument( 'library', help="name of library to scan ('libs/' will be prepended)" )

    args = parser.parse_args()

    verbose = args.verbose - args.quiet

    vprint( 2, '-X:', args.exclude )
    vprint( 2, '-I:', args.include )
    vprint( 2, '-N:', args.ignore )
    vprint( 2, '-C:', args.reject_cycles )

    x = read_exceptions()
    vprint( 2, 'Exceptions:', x )

    gm = read_gitmodules()
    vprint( 2, '.gitmodules:', gm )

    essentials = [ 'config', 'headers', '../tools/boost_install', '../tools/build', '../tools/cmake' ]

    essentials = [ e for e in essentials if os.path.exists( 'libs/' + e ) ]

    if args.update:
        essentials.append( args.library )

    install_modules( essentials, args.git_args )

    m = args.library

    main_dirs = [ 'include', 'src' ]
    extra_dirs = [ 'test' ]

    for dir in args.exclude:
        if dir in main_dirs:
            main_dirs.remove( dir )

        if dir in extra_dirs:
            extra_dirs.remove( dir )

    for dir in args.include:
      extra_dirs.append( dir )

    vprint( 1, 'Directories to scan:', *(main_dirs + extra_dirs) )

    dep_path = []
    if args.reject_cycles:
        dep_path.append( m )

    main_deps = { m : 1 }
    scan_module_dependencies( m, x, gm, main_deps, main_dirs, dep_path )

    extra_deps = { m : 1 }
    scan_module_dependencies( m, x, gm, extra_deps, extra_dirs, [] )

    for dep in args.ignore:
        reported = False

        if dep in main_deps:
            vprint( 1, 'Ignoring dependency', dep )
            reported = True
            del main_deps[dep]

        if dep in extra_deps:
            if not reported:
                vprint( 1, 'Ignoring dependency', dep )
                reported = True
            del extra_deps[dep]

    all_deps = dict( **main_deps )
    all_deps.update( extra_deps )
    vprint( 2, 'Dependencies:', all_deps )

    try:

        while install_module_dependencies( main_deps, x, gm, args.git_args, dep_path ):
            pass

        while install_module_dependencies( extra_deps, x, gm, args.git_args, [] ):
            pass

    except Exception as e:

        vprint( -1, 'Error:', e )
        sys.exit( 1 )
