# file: generate_openmm_source.py
# Run this script from the above src directory

import os
import sys
import re
from pyplusplus import module_builder
from pyplusplus.module_builder.call_policies import *
from simtk_wrap_utils import *
from pyplusplus import function_transformers as FT
from doxygen_doc_extractor import doxygen_doc_extractor

def generate_openmm_source():
    """
    Use pyplusplus to generate boost.python source code for OpenMM.
    See pyplusplus documentation at 
        http://www.language-binding.net/pyplusplus/pyplusplus.html
    """
    # Find the path to the gccxml program on this computer
    gccxml_executable = find_program(['gccxml'], paths=['C:/Program Files/gccxml_sherm/bin'])
    if (not gccxml_executable) or (not os.path.exists(gccxml_executable)):
        raise IOError("gccxml executable not found. Is it on your PATH?")
    # Find the path to the OpenMM include files
    openmm_include_dir = find_path(['OpenMM.h'], 
            paths=['ENV OpenMM_SDK', 'C:/Program Files/OpenMM', '/usr/local/openmm'], 
            path_suffixes=['include'])
    if (not openmm_include_dir) or (not os.path.exists(openmm_include_dir)):
        raise IOError("OpenMM include directory not found.  Is OpenMM_SDK env var set?")
    mb = module_builder.module_builder_t(files = ["openmm/wrap_openmm.h",]
           , gccxml_path=gccxml_executable
           , include_paths=[openmm_include_dir,]
           , indexing_suite_version=2
           # "_HAS_TR1=0" prevents crazy errors with VC9 express
           , define_symbols=["_HAS_TR1=0", "BOOST_PYTHON_MAX_ARITY=22"]
         )
    mb.BOOST_PYTHON_MAX_ARITY = 22 # Prevents warnings on 10-22 argument methods    

    # Delegate wrapping details to other subroutines, to keep this subrouting from growing too large
    mb.namespace('OpenMM').include()
    perform_usual_wrapping_tasks(mb)
    exclude_impls(mb)
    transform_return_values(mb)
    exclude_nonpublic(mb)
    expose_exception(mb)
    expose_rbtorsion(mb)
    expose_platform(mb)
    expose_system(mb)
    expose_force(mb)
    expose_integrator(mb)
    expose_vec3(mb)
    
    # Don't rewrap anything already wrapped by std module
    # See http://www.language-binding.net/pyplusplus/documentation/multi_module_development.html
    mb.register_module_dependency('external/std/generated_code/')

    extractor = doxygen_doc_extractor()

    mb.build_code_creator(module_name='_openmm', doc_extractor=extractor)
    mb.split_module(os.path.join(os.path.abspath('.'), 'openmm', 'generated_code'))
    # If all succeeds, record this accomplishment by touching a particular file
    open(os.path.join(os.path.abspath('.'), 'openmm', 'generated_code', 'generate_openmm.stamp'), "w").close()

def expose_vec3(mb):
    vec3 = mb.class_('Vec3')
    # Use indexing_suite v2 to get python-style indexing
    vec3.include_files.append("indexing_suite/container_suite.hpp")
    vec3.include_files.append("indexing_suite/suite_utils.hpp")
    vec3.include_files.append("indexing_suite/list.hpp")
    vec3.add_declaration_code("""
        using namespace boost::python::indexing;
        struct openmm_vec3_container_traits {
            typedef OpenMM::Vec3                   container;
            typedef int                            size_type;
            typedef double                         value_type;
            typedef value_type*                    iterator;

            typedef value_type&                    reference;
            typedef value_type                     key_type;
            typedef int                            index_type; // signed!

            typedef value_type                     value_param;
            typedef key_type                       key_param;
            typedef index_type                     index_param;

            static bool const has_copyable_iter = false;
            static bool const has_mutable_ref   = true;
            static bool const has_find          = true;
            static bool const has_insert        = false;
            static bool const has_erase         = false;
            static bool const has_pop_back      = false;
            static bool const has_push_back     = false;
            static bool const is_reorderable    = false;
          
            BOOST_STATIC_CONSTANT(
                method_set_type,
                supported_methods = (
                      method_len
                      | method_getitem
                      | method_getitem_slice
                      | method_setitem
                      | method_setitem_slice
                ));

            static boost::python::indexing::index_style_t const index_style
                = boost::python::indexing::index_style_linear;

            struct value_traits_ {
                static bool const equality_comparable = true;
                static bool const lessthan_comparable = true;
            };

            template<typename PythonClass, typename Policy>
                static void visit_container_class (PythonClass &, Policy const &)
            {
                // Empty
            }
        };
        namespace boost { namespace python { namespace indexing {
            struct vec3_algorithms : public list_algorithms<openmm_vec3_container_traits, vec3_algorithms> {
                // Vec3 does not have a "size" method
                static size_t size(container& c) {return 3;}
            };
        }}}
    """)
    vec3.add_registration_code("""
        def(bp::indexing::container_suite<
                OpenMM::Vec3, 
                bp::indexing::all_methods, 
                vec3_algorithms >())
        """)

def expose_integrator(mb):
    # compile error, I need to somehow get bp::no_init into the class declaration
    integrator = mb.class_('Integrator')
    integrator.no_init = True

def expose_force(mb):
    # Force classes need auto_ptr held_type, to permit transfer of raw pointer to System
    force = mb.class_('Force')
    force.held_type = 'std::auto_ptr< %s >' % force.decl_string
    # Compile error
    force.constructors().exclude()
    # And all derived classes
    for cls in mb.classes(lambda c: c.bases and c.bases[0].related_class == force):
        cls.held_type = 'std::auto_ptr< %s >' % cls.decl_string
    
def expose_system(mb):
    system = mb.class_('System')
    # transfer ownership
    addForce = system.member_function('addForce')
    addForce.call_policies = with_custodian_and_ward(1, 2) # check this
    addForce.add_transformation( FT.transfer_ownership(0) )

def expose_rbtorsion(mb):
    #~ WARNING: OpenMM::RBTorsionForce::RBTorsionInfo [class]^M
    #~ > warning W1027: `Py++` will generate class wrapper - class contains "c"^M
    #~ > - array member variable
    mb.class_('RBTorsionForce').class_('RBTorsionInfo').exclude()

def expose_platform(mb):
    #~ WARNING: OpenMM::ContextImpl [class declaration]^M
    #~ > execution error W1040: The declaration is unexposed, but there are^M
    #~ > other declarations, which refer to it. This could cause "no to_python^M
    #~ > converter found" run time error. Declarations:    void^M
    #~ > OpenMM::Platform::contextCreated(OpenMM::ContextImpl & context,^M
    #~ > std::map<std::string, std::string, std::less<std::string>,^M
    #~ > std::allocator<std::pair<std::string const, std::string> > > const &^M
    #~ > properties) const [member function]   void^M
    #~ > OpenMM::Platform::contextDestroyed(OpenMM::ContextImpl & context)^M
    #~ > const [member function]   OpenMM::Kernel^M
    #~ > OpenMM::Platform::createKernel(std::string const & name,^M
    #~ > OpenMM::ContextImpl & context) const [member function]
    #~ WARNING: OpenMM::KernelFactory [class declaration]^M
    #~ > execution error W1040: The declaration is unexposed, but there are^M
    #~ > other declarations, which refer to it. This could cause "no to_python^M
    #~ > converter found" run time error. Declarations:    void^M
    #~ > OpenMM::Platform::registerKernelFactory(std::string const & name,^M
    #~ > OpenMM::KernelFactory * factory) [member function]
    platform = mb.class_('Platform')
    for fn_name in ['contextDestroyed', 'contextCreated', 'createKernel', 'registerKernelFactory']:
        platform.member_function(fn_name).exclude()
    #~ WARNING: std::string const & OpenMM::Platform::getName() const [member function]^M
    #~ > warning W1049: This method could not be overriden in Python - method^M
    #~ > returns reference to local variable!
    # Does not help...
    platform.member_function('getName').call_policies = return_value_policy(copy_const_reference)
    platform.member_function('getPropertyValue').call_policies = return_value_policy(copy_const_reference)
    # transfer ownership
    platform.held_type = 'std::auto_ptr< %s >' % platform.decl_string
    registerPlatform = platform.member_function('registerPlatform')
    registerPlatform.call_policies = with_custodian_and_ward(1, 2) # check this
    registerPlatform.add_transformation( FT.transfer_ownership(0) )
    # compile error, I need to somehow get bp::no_init into the class declaration
    # and this does not do it :(
    platform.constructors().exclude()
    platform.no_init = True
    platform.noncopyable = True # somehow this helps

def exclude_nonpublic(mb):
    #~ WARNING: OpenMM::LangevinIntegrator [class]^M
    #~ > warning W1031: `Py++` will generate class wrapper - user asked to^M
    #~ > expose non - public member function "initialize"
    mb.member_functions('initialize').exclude()
    mb.member_functions('getKernelNames').exclude()    
    mb.member_functions('getContextImpl').exclude()    

def expose_exception(mb):
    #~ WARNING: char const * OpenMM::OpenMMException::what() const [member function]^M
    #~ > warning W1046: The virtual function was declared with empty throw.^M
    #~ > Adding the ability to override the function from Python breaks the^M
    #~ > exception specification. The function wrapper can throw any exception.^M
    #~ > In case of exception in run-time, the behaviour of the program is^M
    #~ > undefined!
    mb.class_('OpenMMException').member_function('what').exclude()

def exclude_impls(mb):
    #~ WARNING: OpenMM::HarmonicBondForce [class]^M
    #~ > warning W1031: `Py++` will generate class wrapper - user asked to^M
    #~ > expose non - public member function "createImpl"
    mb.member_functions('createImpl').exclude()
    
def transform_return_values(mb):
    "Peter assures us that every non-const reference function argument is a return value"
    #~ WARNING: void OpenMM::HarmonicBondForce::getBondParameters(int index, int & particle1, int & particle2, double & length, double & k) const [member function]^M
    #~ > execution error W1009: The function takes as argument (name=particle1,^M
    #~ > pos=1) non-const reference to Python immutable type - function could^M
    #~ > not be called from Python. Take a look on "Function Transformation"^M
    #~ > functionality and define the transformation.
    ns = mb.namespace('OpenMM')
    for fn in ns.member_functions(lambda f: f.name.startswith('get')):
        return_args = []
        for arg in fn.arguments:
            if arg.type.decl_string.find('Context') >= 0:
                continue
            arg_type = arg.type
            if declarations.is_reference(arg_type):
                if not declarations.is_const(arg_type.base):
                    return_args.append(FT.output(arg.name))
        if return_args:
            # print fn
            func_wrapper(fn.add_transformation, return_args)

# http://stackoverflow.com/questions/817087/call-a-function-with-argument-list-in-python
def func_wrapper(func, args):
    func(*args)
    
if __name__ == "__main__":
    generate_openmm_source()
