# Copyright Bruno da Silva de Oliveira 2003. Use, modification and 
# distribution is subject to 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)

"""
Pyste version %s

Usage:
    pyste [options] interface-files

where options are:
    --module=<name>         The name of the module that will be generated;
                            defaults to the first interface filename, without
                            the extension.
    -I <path>               Add an include path    
    -D <symbol>             Define symbol    
    --multiple              Create various cpps, instead of only one 
                            (useful during development)                        
    --out=<name>            Specify output filename (default: <module>.cpp)
                            in --multiple mode, this will be a directory
    --no-using              Do not declare "using namespace boost";
                            use explicit declarations instead
    --pyste-ns=<name>       Set the namespace where new types will be declared;
                            default is the empty namespace
    --debug                 Writes the xml for each file parsed in the current
                            directory
    --cache-dir=<dir>       Directory for cache files (speeds up future runs)
    --only-create-cache     Recreates all caches (doesn't generate code).
    --generate-main         Generates the _main.cpp file (in multiple mode)
    --file-list             A file with one pyste file per line. Use as a 
                            substitute for passing the files in the command
                            line.
    --gccxml-path=<path>    Path to gccxml executable (default: gccxml)
    --no-default-include    Do not use INCLUDE environment variable for include
                            files to pass along gccxml.
    -h, --help              Print this help and exit
    -v, --version           Print version information                         
"""

import sys
import os
import getopt
import exporters
import SingleCodeUnit
import MultipleCodeUnit
import infos
import exporterutils
import settings
import gc
import sys
from policies import *
from CppParser import CppParser, CppParserError
import time
import declarations

__version__ = '0.9.30'

def RecursiveIncludes(include):
    'Return a list containg the include dir and all its subdirectories'
    dirs = [include]
    def visit(arg, dir, names):
        # ignore CVS dirs
        if os.path.split(dir)[1] != 'CVS':
            dirs.append(dir)
    os.path.walk(include, visit, None)
    return dirs

    
def GetDefaultIncludes():
    if 'INCLUDE' in os.environ:
        include = os.environ['INCLUDE']
        return include.split(os.pathsep)
    else:
        return []


def ProcessIncludes(includes):
    if sys.platform == 'win32':
        index = 0
        for include in includes:
            includes[index] = include.replace('\\', '/')
            index += 1


def ReadFileList(filename):
    f = file(filename)
    files = []
    try:
        for line in f:
            line = line.strip()
            if line:
                files.append(line)
    finally:
        f.close()
    return files

        
def ParseArguments():

    def Usage():
        print __doc__ % __version__
        sys.exit(1)
        
    try:
        options, files = getopt.getopt(
            sys.argv[1:], 
            'R:I:D:vh', 
            ['module=', 'multiple', 'out=', 'no-using', 'pyste-ns=', 'debug', 'cache-dir=', 
             'only-create-cache', 'version', 'generate-main', 'file-list=',  'help',
             'gccxml-path=', 'no-default-include'])
    except getopt.GetoptError, e:
        print
        print 'ERROR:', e
        Usage()
    
    default_includes = GetDefaultIncludes()
    includes = []
    defines = []
    module = None
    out = None
    multiple = False
    cache_dir = None
    create_cache = False
    generate_main = False
    gccxml_path = 'gccxml'
    
    for opt, value in options:
        if opt == '-I':
            includes.append(value)
        elif opt == '-D':
            defines.append(value)
        elif opt == '-R':
            includes.extend(RecursiveIncludes(value))
        elif opt == '--module':
            module = value
        elif opt == '--out':
            out = value 
        elif opt == '--no-using':
            settings.namespaces.python = 'boost::python::'
            settings.USING_BOOST_NS = False
        elif opt == '--pyste-ns':
            settings.namespaces.pyste = value + '::'
        elif opt == '--debug':
            settings.DEBUG = True
        elif opt == '--multiple':
            multiple = True
        elif opt == '--cache-dir':
            cache_dir = value
        elif opt == '--only-create-cache':
            create_cache = True
        elif opt == '--file-list':
            files += ReadFileList(value)
        elif opt in ['-h', '--help']:
            Usage()
        elif opt in ['-v', '--version']:
            print 'Pyste version %s' % __version__
            sys.exit(2)
        elif opt == '--generate-main':
            generate_main = True
        elif opt == '--gccxml-path':
            gccxml_path = value
        elif opt == '--no-default-include':
            default_includes = [] 
        else:
            print 'Unknown option:', opt
            Usage()

    includes[0:0] = default_includes
    if not files:
        Usage() 
    if not module:
        module = os.path.splitext(os.path.basename(files[0]))[0]
    if not out:
        out = module
        if not multiple:
            out += '.cpp'
    for file in files:
        d = os.path.dirname(os.path.abspath(file))
        if d not in sys.path:
            sys.path.append(d) 

    if create_cache and not cache_dir:
        print 'Error: Use --cache-dir to indicate where to create the cache files!'
        Usage()
        sys.exit(3)

    if generate_main and not multiple:
        print 'Error: --generate-main only valid in multiple mode.'
        Usage()
        sys.exit(3)

    ProcessIncludes(includes)
    return includes, defines, module, out, files, multiple, cache_dir, create_cache, \
        generate_main, gccxml_path


def PCHInclude(*headers):
    code = '\n'.join(['#include <%s>' % x for x in headers])
    infos.CodeInfo(code, 'pchinclude')

    
def CreateContext():
    'create the context where a interface file will be executed'
    context = {}
    context['Import'] = Import
    # infos
    context['Function'] = infos.FunctionInfo
    context['Class'] = infos.ClassInfo
    context['Include'] = lambda header: infos.CodeInfo('#include <%s>\n' % header, 'include')
    context['PCHInclude'] = PCHInclude
    context['Template'] = infos.ClassTemplateInfo
    context['Enum'] = infos.EnumInfo
    context['AllFromHeader'] = infos.HeaderInfo
    context['Var'] = infos.VarInfo
    # functions
    context['rename'] = infos.rename
    context['set_policy'] = infos.set_policy
    context['exclude'] = infos.exclude
    context['set_wrapper'] = infos.set_wrapper
    context['use_shared_ptr'] = infos.use_shared_ptr
    context['use_auto_ptr'] = infos.use_auto_ptr
    context['holder'] = infos.holder
    context['add_method'] = infos.add_method
    context['final'] = infos.final
    context['export_values'] = infos.export_values
    # policies
    context['return_internal_reference'] = return_internal_reference
    context['with_custodian_and_ward'] = with_custodian_and_ward
    context['return_value_policy'] = return_value_policy
    context['reference_existing_object'] = reference_existing_object
    context['copy_const_reference'] = copy_const_reference
    context['copy_non_const_reference'] = copy_non_const_reference
    context['return_opaque_pointer'] = return_opaque_pointer
    context['manage_new_object'] = manage_new_object
    context['return_by_value'] = return_by_value
    context['return_self'] = return_self
    # utils
    context['Wrapper'] = exporterutils.FunctionWrapper
    context['declaration_code'] = lambda code: infos.CodeInfo(code, 'declaration-outside')
    context['module_code'] = lambda code: infos.CodeInfo(code, 'module')
    context['class_code'] = infos.class_code
    return context                                        

    
def Begin():
    # parse arguments
    includes, defines, module, out, interfaces, multiple, cache_dir, create_cache, generate_main, gccxml_path = ParseArguments()
    # run pyste scripts
    for interface in interfaces:
        ExecuteInterface(interface)
    # create the parser
    parser = CppParser(includes, defines, cache_dir, declarations.version, gccxml_path)
    try:
        if not create_cache:
            if not generate_main:
                return GenerateCode(parser, module, out, interfaces, multiple)
            else:
                return GenerateMain(module, out, OrderInterfaces(interfaces))
        else:
            return CreateCaches(parser)
    finally:
        parser.Close()


def CreateCaches(parser):
    # There is one cache file per interface so we organize the headers
    # by interfaces.  For each interface collect the tails from the
    # exporters sharing the same header.
    tails = JoinTails(exporters.exporters)

    # now for each interface file take each header, and using the tail
    # get the declarations and cache them.
    for interface, header in tails:        
        tail = tails[(interface, header)]
        declarations = parser.ParseWithGCCXML(header, tail)
        cachefile = parser.CreateCache(header, interface, tail, declarations)
        print 'Cached', cachefile
    
    return 0
        

_imported_count = {}  # interface => count

def ExecuteInterface(interface):
    old_interface = exporters.current_interface
    if not os.path.exists(interface):
        if old_interface and os.path.exists(old_interface):
            d = os.path.dirname(old_interface)
            interface = os.path.join(d, interface)
    if not os.path.exists(interface):
        raise IOError, "Cannot find interface file %s."%interface
    
    _imported_count[interface] = _imported_count.get(interface, 0) + 1
    exporters.current_interface = interface
    context = CreateContext()
    context['INTERFACE_FILE'] = os.path.abspath(interface)
    execfile(interface, context)
    exporters.current_interface = old_interface


def Import(interface):
    exporters.importing = True
    ExecuteInterface(interface)
    exporters.importing = False

    
def JoinTails(exports):
    '''Returns a dict of {(interface, header): tail}, where tail is the
    joining of all tails of all exports for the header.  
    '''
    tails = {}
    for export in exports:
        interface = export.interface_file
        header = export.Header()
        tail = export.Tail() or ''
        if (interface, header) in tails:
            all_tails = tails[(interface,header)]
            all_tails += '\n' + tail
            tails[(interface, header)] = all_tails
        else:
            tails[(interface, header)] = tail         

    return tails



def OrderInterfaces(interfaces):
    interfaces_order = [(_imported_count[x], x) for x in interfaces]
    interfaces_order.sort()
    interfaces_order.reverse()
    return [x for _, x in interfaces_order]



def GenerateMain(module, out, interfaces):
    codeunit = MultipleCodeUnit.MultipleCodeUnit(module, out)
    codeunit.GenerateMain(interfaces)
    return 0
    

def GenerateCode(parser, module, out, interfaces, multiple):    
    # prepare to generate the wrapper code
    if multiple:
        codeunit = MultipleCodeUnit.MultipleCodeUnit(module, out)
    else:
        codeunit = SingleCodeUnit.SingleCodeUnit(module, out) 
    # stop referencing the exporters here
    exports = exporters.exporters
    exporters.exporters = None 
    exported_names = dict([(x.Name(), None) for x in exports])

    # order the exports
    order = {}
    for export in exports:
        if export.interface_file in order:
            order[export.interface_file].append(export)
        else:
            order[export.interface_file] = [export]
    exports = []
    interfaces_order = OrderInterfaces(interfaces)
    for interface in interfaces_order:
        exports.extend(order[interface])
    del order
    del interfaces_order

    # now generate the code in the correct order 
    #print exported_names
    tails = JoinTails(exports)
    for i in xrange(len(exports)):
        export = exports[i]
        interface = export.interface_file
        header = export.Header()
        if header:
            tail = tails[(interface, header)]
            declarations, parsed_header = parser.Parse(header, interface, tail)
        else:
            declarations = []
            parsed_header = None
        ExpandTypedefs(declarations, exported_names)
        export.SetDeclarations(declarations)
        export.SetParsedHeader(parsed_header)
        if multiple:
            codeunit.SetCurrent(export.interface_file, export.Name())
        export.GenerateCode(codeunit, exported_names)
        # force collect of cyclic references
        exports[i] = None
        del declarations
        del export
        gc.collect()
    # finally save the code unit
    codeunit.Save()
    if not multiple:
        print 'Module %s generated' % module
    return 0


def ExpandTypedefs(decls, exported_names):
    '''Check if the names in exported_names are a typedef, and add the real class 
    name in the dict.
    '''
    for name in exported_names.keys():
        for decl in decls:
            if isinstance(decl, declarations.Typedef):
                exported_names[decl.type.FullName()] = None

def UsePsyco():
    'Tries to use psyco if possible'
    try:
        import psyco
        psyco.profile()
    except: pass         


def main():
    start = time.clock()
    UsePsyco()
    status = Begin()
    print '%0.2f seconds' % (time.clock()-start)
    sys.exit(status) 

    
if __name__ == '__main__':
    main()
