
import glob
import os
import re
import sys
import time
import types

from stat import ST_MODE

try:
    from distutils import log
    from distutils.core import setup
    from distutils.ccompiler import *
    from distutils.command.build import build
    from distutils.command.clean import clean
    from distutils.command.install import install
    from distutils.command.install_egg_info import install_egg_info
    from distutils.command.sdist import sdist
    from distutils.dep_util import newer
    from distutils.dir_util import copy_tree, remove_tree
    from distutils.errors import DistutilsFileError
    from distutils.versionpredicate import VersionPredicate
    import distutils.filelist
except:
  sys.exit("Your python distutils version is too old, please try a newer one")

## Basics ...
package_nam = 'Mobyle'
package_ver = '1.5.5'
package_url = 'https://projets.pasteur.fr/wiki/mobyle/'
package_aut = 'The Mobyle team'
package_mel = 'mobyle@pasteur.fr'
package_platforms = ['Unix']
package_license = 'GNU General Public License, version 2 (GPLv2)'

## Prerequisites ...
require_pyt = [ 'python (>=2.5, <3.0)' ]
require_mod = [ ('Image (>=1.1.5)', 'PIL (>=2.0.0)'), #Imaging or pillow
                'lxml (>=2.2.4)',
                'simpletal (>=4.1, <5.0)',
                'simplejson (>=1.7.1)' ]

## Installation ...
core_dat = [ 'Src/Mobyle/*.py', 'Src/Mobyle/Classes/*.py',
             'Src/Mobyle/Execution/*.py', 'Src/Mobyle/Converter/*.py', 'Src/Mobyle/Captcha/*',
             'Src/Mobyle/Captcha/data/fonts/vera/*', 'Src/Mobyle/Captcha/data/pictures/abstract/*',
             'Src/Mobyle/Captcha/data/pictures/nature/*', 'Src/Mobyle/Captcha/data/words/*',
             'Schema/*', 'Local/*.py', 'Local/Config/Execution/*.py',
             'Local/*/*.py', 'Tools/validation/*.xsl',
             'Tools/README', 'Example/Local/*.py', 'Example/Local/*/*.py',
             'Doc/Admin/*.pdf', 'Doc/*.pdf', 'Doc/*.html',
             'Doc/Admin/*.tex', 'Doc/Admin/*.png', 'Doc/*.tex', 'Doc/*.jpeg']
core_cfg = [ 'Local/Policy.py', 'Local/black_list.py', 'Local/mailTemplate.py', 'Local/CustomClasses/__init__.py' ]
core_scr = [ 'Src/Mobyle/RunnerChild.py',
             'Tools/mob*',
             'Tools/job_updater.py',
             'Tools/session_updater.py',
             'Local/Config/Execution/__init__.py' ]

core_tst = [ 'Src/Mobyle/Test/*.py', 'Src/Mobyle/Test/Converter/*.py',
             'Src/Mobyle/Test/Converter/DataSequences/*',
             'Src/Mobyle/Test/Converter/DataAlignments/*' ]
core_exe = [ 'Tools/setsid.c' ]
core_fix = core_scr + [ 'Src/Mobyle/ConfigManager.py',
                        'Src/Mobyle/Converter/__init__.py',
                        'Src/Mobyle/Execution/__init__.py' ]
core_tre = [ 'Services', 
             'Services/Programs', 
             'Services/Viewers', 
             'Services/Workflows', 
             'Services/Tutorials',
             'Local/Services', 
             'Local/Services/Programs', 
             'Local/Services/Viewers',
             'Local/Services/Workflows', 
             'Local/Services/Tutorials' ]

cgis_scr = [ 'Src/Portal/cgi-bin/MobylePortal/*' ]
cgis_fix = [ 'Src/Portal/cgi-bin/MobylePortal/mb_cgi.py' ]

html_dat = [ 'Src/Portal/htdocs/MobylePortal/*/*',
             'Src/Portal/htdocs/MobylePortal/*/openid/*' ]
html_cfg = [ 'Src/Portal/htdocs/MobylePortal/css/local.css',
             'Src/Portal/htdocs/MobylePortal/html/announcement.txt' ]
html_tre = [ 'sessions/anonymous', 'sessions/authentified',
             'jobs/ADMINDIR' ]

misc_dat = [ 'AUTHORS', 'COPYING', 'INSTALL', 'NEWS', 'UPDATE' ]

## Distribution ...
dist_lst  = core_dat + core_cfg + core_scr + core_exe + core_tst
dist_lst += cgis_scr + html_dat + html_cfg + misc_dat

excl_lst  = [ 'setup.cfg', 
              'Local/Policy.pasteur.py', 'Local/Config/Config.py' ]

## Let the hard code begins ...

core_dir = 'core'
cgis_dir = 'cgis'
html_dir = 'html'
niaid_dir = 'niaid'

#############
#           #
# Utilities #
#           #
#############

def copy_script(src, dst, exe):
    ## Mostly stolen from distutils.build_scripts.run() ...
    if not newer(src, dst): return
    dir = os.path.dirname(dst)
    log.info("copying and adjusting %s -> %s", src, dir)
    try:
        f = open(src, "r")
    except os.error, (errstr):
        raise DistutilsFileError, "could not open '%s': %s" % (src, errstr)
    try:
        o = open(dst, "w")
    except:
        raise DistutilsFileError, "could not create '%s': %s" % (dst, errstr)
    l = f.readline()
    l = re.sub("^#!.*python", "#!" + exe, l)
    o.write(l)
    o.writelines(f.readlines())
    o.close()
    f.close()
    omod = os.stat(dst)[ST_MODE] & 07777
    nmod = (omod | 0555) & 07777
    if nmod != omod:
        log.info("changing mode of %s from %o to %o", dst, omod, nmod)
        os.chmod(dst, nmod)

def fix_script(src, dst, mob, htm):
    log.info("adjusting %s -> %s", src, dst)
    try:
        f = open(src, "r")
    except os.error, (errstr):
        raise DistutilsFileError, "could not open '%s': %s" % (src, errstr)
    try:
        g = open(dst, "w")
    except os.error, (errstr):
        raise DistutilsFileError, "could not create '%s': %s" % (dst, errstr)
    while True:
        l = f.readline()
        if not l: 
            break
        if mob and l == 'MOBYLEHOME = None\n':
            l = l.replace('None', "'"+mob+"'", 1)
        if htm and l == 'MOBYLEHTDOCS = None\n':
            l = l.replace('None', "'"+htm+"'", 1)
        g.write(l)
    g.close()
    f.close()
    mod = os.stat(src)[ST_MODE] & 07777
    os.chmod(dst, mod)

def find_file(src, msk):
    lst = distutils.filelist.FileList()
    lst.findall(src)
    lst.include_pattern(msk, anchor=False)
    return lst.files

###################
#                 #
# building Mobyle # 
#                 #
###################

class build_mobyle(build):
    
    def run(self):
        chk = True
        for req in require_pyt:
            chk &= self.chkpython(req)
        for req in require_mod:
            if isinstance(req, types.StringTypes):
                req = [req]
            chk = False
            for alt in req:
                predicate = VersionPredicate(alt)
                if self.chkmodule(predicate):
                     print >> sys.stderr, "%s python module found" % predicate.name
                     chk = True
                     break 
            if not chk:   
                print >> sys.stderr, "Missing mandatory %s python module" % predicate.name
                sys.exit(1)
        build.run(self)
        
    def chkpython(self, req):
        chk = VersionPredicate(req)
        ver = '.'.join([str(v) for v in sys.version_info[:2]])
        if not chk.satisfied_by(ver):
            print >> sys.stderr, "Invalid python version, expected %s" % req
            return False
        return True

    def chkmodule(self, predicate):
        try:
            mod = __import__(predicate.name)
        except:
            return False
        for v in [ '__version__', 'version' ]:
            ver = getattr(mod, v, None)
            break
        try:
            if ver and not predicate.satisfied_by(ver):
                print >> sys.stderr, "Invalid module version, expected %s" % req
                return False
        except:
            pass
        return True


class build_core(build):
    
    build.sub_commands += [('build_core', lambda self:True)]
  
    def run(self):
        base = os.path.join(self.build_base, core_dir)
        self.mkpath(base)
        (dat, cfg, scr, exe) = ([], [], [], [])
        for m in core_dat: 
            dat += glob.glob(m)
        for m in core_cfg: 
            cfg += glob.glob(m)
        for m in core_scr: 
            scr += glob.glob(m)
        for m in core_exe: 
            exe += glob.glob(m)
        for f in dat:
          if f in scr + cfg + exe: 
              continue
          if not os.path.isfile(f): 
              continue
          dst = os.path.join(base, f)
          self.mkpath(os.path.dirname(dst))
          self.copy_file(f, dst, preserve_mode=0)
        for f in cfg:
          if not os.path.isfile(f): 
              continue
          dst = os.path.join(base, f + '.new')
          self.mkpath(os.path.dirname(dst))
          self.copy_file(f, dst, preserve_mode=0)
        for f in scr:
            if f in exe: 
                continue
            if not os.path.isfile(f): 
                continue
            dst = os.path.join(base, f)
            self.mkpath(os.path.dirname(dst))
            copy_script(f, dst, self.executable)
        cc = new_compiler()
        for f in core_exe:
            objs = cc.compile([f], self.build_temp)
            cc.link_executable(objs, f[0:-2], base)
        for f in core_tre:
            dir = os.path.join(base, f)
            self.mkpath(dir)


class build_html(build):
    
    build.sub_commands += [('build_html', lambda self:True)]
      
    def run(self):
        base = os.path.join(self.build_base, html_dir)
        self.mkpath(base)
        (dat, cfg) = ([], [])
        for m in html_dat: 
            dat += glob.glob(m)
        for m in html_cfg: 
            cfg += glob.glob(m)
        com = os.path.commonprefix(dat + cfg)
        for f in dat:
            if f in cfg: 
                continue
            if not os.path.isfile(f): 
                continue
            dst = os.path.join(base, 'portal', f)
            dst = dst.replace(com, '', 1)
            self.mkpath(os.path.dirname(dst))
            self.copy_file(f, dst, preserve_mode=0)
        for f in cfg:
            if not os.path.isfile(f): 
                continue
            dst = os.path.join(base, 'portal', f + '.new')
            dst = dst.replace(com, '', 1)
            self.mkpath(os.path.dirname(dst))
            self.copy_file(f, dst, preserve_mode=0)
        for f in html_tre:
            dir = os.path.join(base, 'data', f)
            self.mkpath(dir)


class build_cgis(build):
    
    build.sub_commands += [('build_cgis', lambda self:True)]
  
    def run(self):
        base = os.path.join(self.build_base, cgis_dir)
        self.mkpath(base)
        scr = []
        for m in cgis_scr: 
            scr += glob.glob(m)
        for s in scr:
            if not os.path.isfile(s): 
                continue
            dst = os.path.join(base, os.path.basename(s))
            copy_script(s, dst, self.executable)


class build_bmps(build):
        
    build.sub_commands.append( ('build_bmps', lambda self:True) )
    
    def run(self):
        if not os.path.exists( os.path.join( niaid_dir, 'Workflow' ) ):
            return
        workflow_src_dir = os.path.join( niaid_dir, 'Workflow', 'target', 'mobyleWorkflow' )
        workflow_dest_dir = os.path.join(self.build_base, html_dir , "workflow")
        #by default copy_tree preserve permissions
        copy_tree( workflow_src_dir, workflow_dest_dir )
        
    
class build_bmid(build):
    
    build.sub_commands.append( ('build_bmid', lambda self:True) )
    
    def run(self):
        if not os.path.exists( os.path.join( niaid_dir, 'InterfaceDesigner' ) ):
            return
        bmid_src_dir = os.path.join( niaid_dir, 'InterfaceDesigner', 'target', 'BMID' )
        bmid_dest_dir = os.path.join(self.build_base, html_dir , "BMID")
        #by default copy_tree preserve permissions
        copy_tree( bmid_src_dir, bmid_dest_dir )
    
    
#######################
#                     #
# Mobyle Installation #  
#                     # 
#######################
    
class install_core(install):
    
    install.sub_commands += [('install_core', lambda self:True)]
    setattr(install, 'install_core', None)
    install.user_options.append(
                                 ('install-core=', None, 'installation directory for Core files'))
  
    def run(self):
        base = os.path.join(self.build_base, core_dir)
        inst = self.distribution.command_options.get('install')
        if not inst.has_key('install_core'):
            sys.exit('Missing mandatory Core installation directory')
        if not inst.has_key('install_htdocs'):
            sys.exit('Missing mandatory htdocs installation directory')
        self.install_dir = inst['install_core'][1]
        copy_tree(base, self.install_dir)
        for f in find_file(base, '*.new'):
            dst = f.replace(base, self.install_dir, 1)[:-4]
            if os.path.lexists(dst): 
                continue
            move_file(dst + '.new', dst)
        fix = []
        for m in core_fix: 
            fix += glob.glob(m)
        for f in fix:
          f = os.path.join(self.install_dir, f)
          n = f + '.tmp'
          fix_script(f, n, self.install_dir, inst['install_htdocs'][1])
          os.unlink(f)
          move_file(n, f)


class install_html(install):
    
    install.sub_commands += [('install_html', lambda self:True)]
    setattr(install, 'install_htdocs', None)
    install.user_options.append(
    ('install-htdocs=', None, 'installation directory for HTML files'))
  
    def run(self):
        inst = self.distribution.command_options.get('install')
        if not inst.has_key('install_htdocs'):
            sys.exit('Missing mandatory htdocs installation directory')
        for target_dir in ('portal' , 'data'):
            build_dir = os.path.join(self.build_base, html_dir, target_dir )
            inst_dir = os.path.join(inst['install_htdocs'][1], target_dir)
            copy_tree(build_dir, inst_dir)
            for f in find_file(build_dir, '*.new'):
                dst = f.replace(build_dir, inst_dir, 1)[:-4]
                if os.path.lexists(dst): 
                    continue
                move_file(dst + '.new', dst)


class install_cgis(install):
    
    install.sub_commands += [('install_cgis', lambda self:True)]
    setattr(install, 'install_cgis', None)
    install.user_options.append(('install-cgis=', None, 'installation directory for CGIs files')) 
          
    def run(self):
        cgi_build_dir = os.path.join(self.build_base, cgis_dir)
        inst = self.distribution.command_options.get('install')
        if not inst.has_key('install_cgis'):
            sys.exit('Missing mandatory CGIs installation directory')
        if not inst.has_key('install_core'):
            sys.exit('Missing mandatory Core installation directory')
        cgi_inst_dir = inst['install_cgis'][1]
        copy_tree(cgi_build_dir, cgi_inst_dir)
        fix = []
        for m in cgis_fix: 
            fix += glob.glob(m)
        for f in fix:
          f = os.path.join(cgi_inst_dir, os.path.basename(f))
          n = f + '.tmp'
          fix_script(f, n, inst['install_core'][1], None)
          os.unlink(f)
          move_file(n, f)


class install_bmps(install):
        
    install.sub_commands.append( ('install_bmps', lambda self: self.install_bmps ) )
    setattr(install, 'install_bmps', False)
    install.user_options.append(('install-bmps', None, 'installation of BCBB Mobyle Pipeline System'))
    
    def initialize_options(self):
        install.initialize_options(self)
        self.install_bmps = False
        
    def run(self):
        inst = self.distribution.command_options.get('install')
        if not inst.has_key('install_htdocs'):
            sys.exit('Missing mandatory htdocs installation directory')
        print "[DEBUG] ",os.path.join( self.build_base, html_dir, 'workflow' )
        if not os.path.exists( os.path.join( self.build_base, html_dir, 'workflow' ) ):
            sys.exit('BMPS is not part of this tarball. Get a tarball with BMPS from Mobyle website (https://projets.pasteur.fr/projects/mobyle/wiki/download)')
        bmps_build_dir = os.path.join(self.build_base, html_dir , 'workflow')
        bmps_inst_dir = os.path.join( inst['install_htdocs'][1] , 'workflow')
        copy_tree(bmps_build_dir, bmps_inst_dir)
            
    
class install_bmid(install):
    
    install.sub_commands.append( ('install_bmid', lambda self: self.install_bmid ) )
    setattr(install, 'install_bmid', False)
    install.user_options.append(('install-bmid', None, 'installation of BCBB Mobyle Interface Designer'))
    
    def initialize_options(self):
        install.initialize_options(self)
        self.install_bmid = False
        
    def run(self):
        inst = self.distribution.command_options.get('install')
        if not inst.has_key('install_htdocs'):
            sys.exit('Missing mandatory htdocs installation directory')
        if not os.path.exists( os.path.join( self.build_base, html_dir, 'BMID' ) ):
            sys.exit('BMID is not part of this tarball. Get a tarball with BMID from Mobyle website (https://projets.pasteur.fr/projects/mobyle/wiki/download)')
        bmid_build_dir = os.path.join(self.build_base, html_dir , 'BMID')
        bmid_inst_dir = os.path.join( inst['install_htdocs'][1] , 'BMID')
        copy_tree(bmid_build_dir, bmid_inst_dir)

###################################
#                                 #
# creation of source distribution #
#                                 #
###################################

class sdist_mobyle(sdist):
    
    user_options = [('with-bmps', None, 'build the gwt libraries for BMPS, as part of mobyle distribution [DEFAULT=False]'),
                    ('with-bmid', None, 'build the gwt libraries for BMID, as part of mobyle distribution [DEFAULT=False]'),
                    ('niaid-home-dir=', None, 'the home directory of niaid BMID and BMPS src project [DEFAULT ./niaid]' ),
                    ('maven-bin-name=', None, 'the maven binary name to use [DEFAULT mvn]')] + sdist.user_options
   
    def initialize_options(self):
        sdist.initialize_options(self)
        self.with_bmps = False
        self.with_bmid = False
        self.niaid_home_dir = os.path.realpath('niaid')
        self.maven_bin_name = 'mvn'
        
           
    def run(self):
        self.niaid_link_name = 'niaid'
        self.must_clean = False
        if self.with_bmid or self.with_bmps:
            self.set_up()
        if self.with_bmid:
            self.build_bmid()   
            self.distribution.metadata.name = self.distribution.metadata.name + "+BMID" 
        if self.with_bmps:
            self.build_bmps()
            self.distribution.metadata.name = self.distribution.metadata.name + "+BMPS"
        self.mktemplate(dist_lst, excl_lst)
        sdist.run(self)
        if self.with_bmid or self.with_bmps:
            self.clean_up()
        
    def mktemplate(self, incl, excl):
        fd = open('MANIFEST.in', 'w')
        print >> fd, '# WARNING: This is a generated file, *DO NOT* edit.'
        for nam in incl:
            print >> fd, 'include %s' % nam
        for nam in excl:
            print >> fd, 'exclude %s' % nam
        if self.with_bmid:
            print >> fd, 'recursive-include %s *' % os.path.join( self.niaid_link_name, 'InterfaceDesigner', 'target', 'BMID')
        if self.with_bmps:
            print >> fd, 'recursive-include %s *' % os.path.join( self.niaid_link_name, 'Workflow', 'target', 'mobyleWorkflow')
        fd.close()

    def set_up(self):
        if os.path.exists( self.niaid_link_name ):
            if os.path.realpath(self.niaid_link_name) == os.path.realpath(self.niaid_home_dir):
                self.must_clean = False
                return
            else:
                raise RuntimeError( "there already exist a 'niaid' path in %s and is different than %s"%( os.getcwd(), self.niaid_home_dir) )
        os.symlink( self.niaid_home_dir , self.niaid_link_name )
        self.must_clean = True
        
    def clean_up(self):
        if self.must_clean:  
            os.unlink( self.niaid_link_name )
    
    def build_niaid(self, command):
        from subprocess import Popen, PIPE
        import select 
        process_ = Popen( 
                      command,
                      shell = True ,
                      stdout= sys.stdout,
                      stdin = None ,
                      stderr = sys.stderr,
                      cwd = self.niaid_home_dir
                      )
        process_.wait()
        if process_.returncode != 0:
            raise RuntimeError
    
    def build_bmps(self):
        print "\n[INFO] Building BMPS project"
        command = '%s -pl CommonLib,Workflow package' %self.maven_bin_name 
        try:
            self.build_niaid( command )
        except RuntimeError,err:
            print >> sys.stderr , "[ERROR] BMPS project BUILD FAILURE" 
            sys.exit(0)
            
    def build_bmid(self):
        print "\n[INFO] Building BMID project"
        command = '%s -pl CommonLib,InterfaceDesigner package' %self.maven_bin_name 
        try:
            self.build_niaid( command )
        except Exception, err:
            print >> sys.stderr , "[ERROR] BMID project BUILD FAILURE"
        


    
###################
#                 #
# clean all stuff #
#                 #
###################

class clean_mobyle(clean):
    
    def run(self):
        for d in [ core_dir, cgis_dir, html_dir ]:
            base = os.path.join(self.build_base, d)
            if not os.path.exists(base): 
                continue
            remove_tree(base)
        clean.run(self)


class install_mobyle_egg_info(install_egg_info):
    
    def run(self):
        pass

#############################
# Overwrite default methods #
#############################

cmdclass = {'build': build_mobyle, 
            'build_core': build_core, 
            'build_html': build_html,
            'build_cgis': build_cgis,
            'build_bmid': build_bmid,
            'build_bmps': build_bmps,
            'install_egg_info': install_mobyle_egg_info,
            'install_core': install_core,
            'install_cgis': install_cgis,
            'install_html': install_html, 
            'install_bmid': install_bmid,
            'install_bmps': install_bmps,
            'sdist': sdist_mobyle,
            'clean': clean_mobyle, 
            }

setup( name = package_nam, 
       version = package_ver, 
       url = package_url,
       author = package_aut, 
       author_email = package_mel,
       platforms = package_platforms ,
       license = package_license,
       cmdclass = cmdclass 
       )
