#
# Gramps - a GTK+ based genealogy program
#
# Copyright (C) 2006       Brian Matherly
# Copyright (C) 2008       Stephen George
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 2 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
#
# $Id: $

import os, glob, sys, shutil
import string
import os.path as path
import subprocess

CONFIGURE_IN = 'configure.in'
CONST_PY_IN = 'src/const.py.in'
TRANSLATE_FOLDER = 'po'

EXTRA_FILES = [ 'COPYING', 'NEWS', 'FAQ', 'AUTHORS']

FULL_COLON_SUBST = "~"

#min required version of NSIS
MIN_NSIS_VERSION = (2,42)

#tools used during build
MAKENSIS_exe = None
SVN_exe = None
po_errs = []
po_oks = []

import gobject


#==== Set up logging system
# need to also set up a logger for when run as a module.

# change to set up a console logger in module global space.
# then add the file logger later once I know the path
import logging
logging.basicConfig(level=logging.DEBUG,
                    format='%(asctime)s %(name)-10s %(levelname)-8s %(message)s',
                    datefmt='%H:%M',
                    filename= 'build.log', #path.join(out_dir,'build.log'),
                    filemode='w')
#create a Handler for the console
console = logging.StreamHandler()
console.setLevel(logging.INFO)
#Set a simle format for console
formatter = logging.Formatter('%(levelname)-8s %(message)s')
console.setFormatter(formatter)
#add the console handler to the root handler
log = logging.getLogger('BuildApp')
log.addHandler(console)


class buildbase(gobject.GObject):
    __gsignals__={
        "build_progresstext"        :  (gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE, [gobject.TYPE_STRING]),
        "build_progressfraction"    :  (gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE, [gobject.TYPE_FLOAT]),
    }

    def __init(self):
        gobject.GObject.__init__(self)
        self.gramps_version = 'VERSION-UNKNOWN'
        self.bTarball = bTarball
        self.build_root = '.'  # the directory were the build source is located
        self.out_dir = '.'     # the directory to output final installer to, and the expand source to
        self.repository_path = '.' #where the source comes from, either SVN root or a tarball
        self.bBuildInstaller = True
        
    def getbuild_src(self):
        return os.path.join(self.build_root, 'src')

    build_src = property(getbuild_src)

    def isGrampsRoot(self, root ):
        log.debug( 'isGrampsRoot: %s' % root )
        if path.isfile(path.join(root, CONFIGURE_IN)):
            if path.isfile(path.join(root, CONST_PY_IN)):
                if path.isdir(path.join(root, TRANSLATE_FOLDER)):
                    return True
        return False

    def getSVNRevision(self, dir ):
        log.debug('========== getSVNRevision(%s)' % dir)
        cmd = 'svnversion -n %s' % dir
        log.debug( "Running: %s" % cmd )
        
        proc = subprocess.Popen( cmd, shell = True, stdout=subprocess.PIPE, stderr=subprocess.PIPE )
        status = proc.wait()
        output = string.strip(proc.stdout.read())
        err = proc.stderr.read()
        proc.stderr.close()
        proc.stdout.close()
        del proc
        
        log.debug( output )
        if err:
            for line in err.split('\n'):
                log.error(line)
            if not stdout:
                output = '-UNKNOWN'
        return 'SVN' + output

    def exportSVN(self, svn_dir, destdir):
        '''
        svn export PATH1 PATH2
        exports a clean directory tree from the working copy specified by PATH1 into PATH2.
        All local changes will be preserved, but files not under version control will not be copied.

        destdir cannot exist, script will clean up dir first
        '''
        log.debug('========== exportSVN(%s, %s)' % (svn_dir, destdir) )
        cmd = '"%s" export %s %s' % (SVN_exe ,svn_dir, destdir)
        log.info( "Running: %s" % cmd)
        
        proc = subprocess.Popen( cmd, shell = True, stdout=subprocess.PIPE, stderr=subprocess.PIPE )
        status = proc.wait()
        output = string.strip(proc.stdout.read())
        err = proc.stderr.read()
        proc.stderr.close()
        proc.stdout.close()
        del proc
        
        log.info( output )
        if err:
            log.error(err)

    def copyExtraFilesToBuildDir(self, source_path):
        '''
        A few extra files not in src directory needs to be copied to the build dir
        '''
        log.debug('========== copyExtraFilesToBuildDir(%s)' % (source_path))
        for file in EXTRA_FILES:
            outfile = file
            if file == 'NEWS':
                #Jump through hoops tomake sure the end of line charactors are windows format (wont work on linux!!)
                outfile = 'NEWS.TXT' #Lets add .TXT suffix to filename so installer knows to call notepad 
                fnews = open(os.path.join(source_path,file), 'r')
                newslines = fnews.readlines()
                newsout = open(os.path.join(self.build_src,outfile), 'w')
                newsout.writelines(newslines)
                newsout.close()
                fnews.close()
            else:
                shutil.copy(os.path.join(source_path,file), os.path.join(self.build_src,outfile) )

    def compileInstallScript(self):
        '''
        Now we got a build directory, lets create the installation program
        '''
        log.debug('========== compileInstallScript()')
        log.info('Compiling NullSoft install script .... be patient')
        # calc path to gramps2.nsi
        # need to ensure __file__ has full path, under linux it does not.
        thisfilepath = os.path.abspath(__file__)
        pth = os.path.relpath(os.path.dirname( thisfilepath ), os.getcwd())
        pth2nsis_script = os.path.join(pth, 'gramps2.nsi') 

        #should tests be more along lines of os.name which returns 'posix', 'nt', 'mac', 'os2', 'ce', 'java', 'riscos'
        if sys.platform == 'win32':
            cmd = '"%s" /V3 %s' % (MAKENSIS_exe, pth2nsis_script)
        elif sys.platform == 'linux2':
            #assumption makensis is installed and on the path
            cmd = '%s -V3 %s' % (MAKENSIS_exe, pth2nsis_script)
        
        log.info( "Running: %s" % cmd)
        # Need to define the following enviroment variables for NSIS script
        os.environ['GRAMPS_VER'] = self.gramps_version
        os.environ['GRAMPS_BUILD_DIR'] = os.path.abspath(self.build_src)
        os.environ['GRAMPS_OUT_DIR'] = os.path.abspath(self.out_dir)
        
        proc = subprocess.Popen( cmd, shell = True, stdout=subprocess.PIPE, stderr=subprocess.PIPE )
        status = proc.wait()
        output = string.strip(proc.stdout.read())
        err = proc.stderr.read()
        proc.stderr.close()
        proc.stdout.close()
        del proc
        
        log.info( output )
        if err:
            log.error(err)

    def getVersionFromConfigureIn(self, repository_path):
        log.debug('========== read_config_in(%s)' % repository_path)
        fin = open('%s/configure.in' % repository_path, 'r')
        conf_lines = fin.readlines()
        fin.close()
        return self.getVersionFromLines(conf_lines)
        
    def getVersionFromLines(self, conf_lines):
        log.debug('========== getVersionFromLines()')
        for line in conf_lines:
            if 'AC_INIT(gramps' in line:
                junk, ver, junk2 = line.split(',')
            elif line[:7] == 'RELEASE':
                junk,release = line.split('=')
                if 'SVN$' in release:#not a release version
                    release = self.getSVNRevision( repository_path )
                elif not self.bTarball:  # This is aRelease, lets make sure svn working copy is prestine
#                elif not bTarball:  # This is aRelease, lets make sure svn working copy is prestine
                    test_num = getSVNRevision( repository_path )
                    if test_num.endswith('M'): # in test_num:   #endsWith
                        log.warning('*==========================================================')
                        log.warning('*     Building a Release from modified SVN Working Copy    ')
                        log.warning('*  ===>  Creating %s-%s from %s-%s  <==' % (ver.strip(), release.strip(),ver.strip(), test_num.strip()) )
                        log.warning('*==========================================================')
        gversion = '%s-%s' % (ver.strip(), release.strip())
        gversion = gversion.replace(":", FULL_COLON_SUBST) # if it's a mixed version, then need to replace the : with something else
        log.info( 'GrampsVersion: %s' % gversion )
        return gversion

    def processPO( self ):
        log.debug('========== processPO( )')
        po_dir = os.path.join(self.build_root, "po")
        mo_dir = os.path.join(self.build_src, "lang")
        if not os.path.exists(mo_dir):
            os.makedirs(mo_dir)
        #TODO: find a better way to handle  different platforms
        if sys.platform == 'win32':
            po_files = glob.glob(po_dir + "\*.po")
            # no longer using python msgfmt as it doesn't handle plurals (april 2010)
            # msgfmtCmd = path.normpath(path.join(sys.prefix, "Tools/i18n/msgfmt.py") )
            
            # GetText Win 32 obtained from http://gnuwin32.sourceforge.net/packages/gettext.htm
            # ....\gettext\bin\msgfmt.exe needs to be on the path
            msgfmtCmd = 'msgfmt.exe'
            #print 'msgfmtCmd = %s' % msgfmtCmd
        elif sys.platform == 'linux2':
            po_files = glob.glob(po_dir + "/*.po")
            msgfmtCmd = "%s/bin/msgfmt" % sys.prefix
        else:
            po_files = [] #empty list
            msgfmtCmd = "UNKNOWN_PLATFORM"
        log.debug( msgfmtCmd )
        #if not os.path.exists(msgfmtCmd):
        #    log.error( "msgfmt not found - unable to generate mo files")
        #    return
        log.info( "Generating mo files" )
        global po_errs, po_oks 
        po_total = len(po_files)
        po_count = 0 
        for po_file in po_files:
            po_count = po_count + 1
            #This will be interesting
            self.emit("build_progresstext", 'compiling %s' % po_file)
            self.emit("build_progressfraction", po_count/po_total)

            lan = os.path.basename(po_file).replace( ".po", "" )
            lan_path = os.path.join(mo_dir,lan,"LC_MESSAGES")
            if not os.path.exists(lan_path):
                os.makedirs(lan_path)
            mo_file = os.path.join(lan_path,"gramps.mo")
            log.info( mo_file )

            cmd = "%s --statistics -o %s %s" % (msgfmtCmd, mo_file, po_file)
            #log.debug( cmd )
            
            proc = subprocess.Popen( cmd, shell = True, stdout=subprocess.PIPE, stderr=subprocess.PIPE )
            status = proc.wait()
            output = string.strip(proc.stdout.read())
            err = proc.stderr.read()
            proc.stderr.close()
            proc.stdout.close()
            del proc
            
            # log.debug( output ) Nothing coming out here, statistics come out stderr ??
            if err:
                log.info(err) # statistics comming out stderr
                po_errs.append(lan)
            else:
                po_oks.append(lan)

    def generateConstPy(self ):
        log.debug('========== generate_const.py()')
        fin = open(os.path.join(self.build_src,'const.py.in'), 'r')
        in_lines = fin.readlines()
        fin.close()
        fout = open(os.path.join(self.build_src,'const.py'), 'w')
        for line in in_lines:
            if '@VERSIONSTRING@' in line: #VERSION        = "@VERSIONSTRING@"
                corrline = line.replace('@VERSIONSTRING@', self.gramps_version.replace(FULL_COLON_SUBST,":") )
                fout.write(corrline)
                #fout.write('VERSION        = "%s"\n'% self.gramps_version.replace(FULL_COLON_SUBST,":"))

            #elif '@prefix@' in line: #PREFIXDIR = "@prefix@"
            #       what to do? , doesnt seem to matter on windows

            #elif '@sysconfdir@' in line: #SYSCONFDIR = "@sysconfdir@"
            #       what to do? , doesnt seem to matter on windows

            else:
                fout.write(line)
        fout.close()

    def cleanBuildDir(self):
        log.debug( '========== cleanBuildDir()' )
        log.info( 'Cleaning build and output directories' )
        if sys.platform == 'win32': #both platforms emit different exceptions for the same operation, map the exception here
            MY_EXCEPTION = WindowsError
        elif sys.platform == 'linux2':
            MY_EXCEPTION = OSError
        if os.path.exists(self.build_root):
            try:
                log.info('removing directory: %s' % self.build_root )
                shutil.rmtree(self.build_root)
            except MY_EXCEPTION, e:
                log.error( e )

        for file in ['gramps-%s.exe'%self.gramps_version ]: #, 'build.log']:
            fname = os.path.join(self.out_dir, file)
            if os.path.isfile(fname):
                try:
                    log.info('removing file:      %s' % fname )
                    os.remove(fname)
                except MY_EXCEPTION, e:
                    log.error( e )

    def getNSISVersionNumber(self):
        #Check version of NSIS, to ensure NSIS is compatible with script features 
          # >"c:\Program Files\NSIS\makensis.exe" /version
          # v2.42
        cmd = '"%s" -VERSION' % (MAKENSIS_exe)
        log.debug(cmd)
        
        proc = subprocess.Popen( cmd, shell = True, stdout=subprocess.PIPE, stderr=subprocess.PIPE )
        status = proc.wait()
        output = string.strip(proc.stdout.read())
        err = proc.stderr.read()
        proc.stderr.close()
        proc.stdout.close()
        del proc
        
        if err:
            log.error(err)
            if sys.platform == 'win32': #'not recognized' in err:
                minor =0
                major =0
                return (major, minor)
            
        #parse the output to get version number into tuple
        ver =  output[1:].split('.') 
        major = int(ver[0])
        try:
            minor = int(ver[1]) 
        except ValueError, e:
            m = ver[1]
            minor = int(m[:2])        
        return (major, minor)

    def checkForBuildTools(self):
        global MAKENSIS_exe, SVN_exe
        log.debug( '========== checkForBuildTools()' )
        if sys.platform == 'win32':
            import _winreg as winreg
            # Find NSIS on system
            try:
                with winreg.OpenKey(winreg.HKEY_LOCAL_MACHINE, 'SOFTWARE\\NSIS') as key:
                    nsispath = winreg.QueryValue(key, '')
                    makensisexe = path.join( nsispath, 'makensis.exe')
                    if path.isfile( makensisexe ):
                        MAKENSIS_exe = makensisexe
            except WindowsError, e:
                log.warning('NSIS not found, in registory')
                log.warning('..Testing if makensis is on the path')
                MAKENSIS_exe = 'makensis'
                #cmd = os.path.join(nsis_dir, MAKENSIS_exe)
                
                cmd = '%s /VERSION' % MAKENSIS_exe
                proc = subprocess.Popen( cmd, shell = True, stdout=subprocess.PIPE, stderr=subprocess.PIPE )
                status = proc.wait()
                output = string.strip(proc.stdout.read())
                err = proc.stderr.read()
                proc.stderr.close()
                proc.stdout.close()
                del proc
                
                if err:
                    log.error(err)
                    log.error('....makensis.exe not found on path')
                    sys.exit(0)
                #else:
                #    log.info("makensis version %s" % output)

            # Find msgfmt on system
            cmd = os.path.join(msg_dir, 'msgfmt.exe')
            
            proc = subprocess.Popen( cmd, shell = True, stdout=subprocess.PIPE, stderr=subprocess.PIPE )
            status = proc.wait()
            output = string.strip(proc.stdout.read())
            err = proc.stderr.read()
            proc.stderr.close()
            proc.stdout.close()
            del proc
            
            if not err.startswith(cmd):
                #log.error(err)
                log.error('msgfmt.exe not found on path')
                log.error('    try the -m DIR , --msgdir=DIR option to specify the directory or put it on the path')
                sys.exit(0)
            
            # Find SVN on system  -  optional,  if building from tarball
            if not bTarball:
                try:
                    with winreg.OpenKey(winreg.HKEY_LOCAL_MACHINE, 'SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\App Paths\\svn.exe') as key:
                        svnpath = winreg.QueryValue(key, '')
                    if path.isfile(svnpath):
                        SVN_exe = svnpath
                except WindowsError, e:
                    log.warning('SVN not found, in registory')
                    log.warning('... Hoping svn is on the path')
                    SVN_exe = 'svn'
            
        elif sys.platform == 'linux2':
            #ASSUMPTION: these tools are on the path
            #TODO: check for svn on Linux
            log.info( 'TODO: Check for svn' )
            SVN_exe = 'svn'
            #TODO: check for nsis on Linux
            log.info( 'TODO: Check for nsis' )
            MAKENSIS_exe = 'makensis'
            
        # Check if we are running a compatible vesion of NSIS
        vers = self.getNSISVersionNumber()
        if vers < MIN_NSIS_VERSION:
            log.error( "Require NSIS version %d.%d or later  ..... found NSIS version %d.%d" % (MIN_NSIS_VERSION[0],MIN_NSIS_VERSION[1], vers[0], vers[1]) )
            log.info("Disabling NSIS compilation ... Please upgrade your NSIS version")
            self.bBuildInstaller = False
        else:
            self.bBuildInstaller = True
            log.info( "NSIS version %d.%d" % vers )
            
    def expandTarBall(self, tarball, expand_dir):
        # gramps-3.1.0.tar.gz
        log.info( 'expandTarBall(%s, %s)' % (tarball, expand_dir) )
        if tarfile.is_tarfile(self.repository_path):
            tar = tarfile.open(self.repository_path)
            tar.extractall(self.out_dir)
            tar.close()
            base = os.path.basename(self.repository_path)
            extractDir =  os.path.join(self.out_dir, base.replace('.tar.gz', '')  )
            try:
                os.rename( extractDir, self.build_root)
            except WindowsError, e:
                log.error("FAILED: extractDir=%s, build_root=%s" % (extractDir, self.build_root))
                raise WindowsError, e
        else:
            log.error( "Sorry %s is not a tar file" % self.repository_path )

    def getVersionFromTarBall(self, tarball):
        log.debug( 'getVersionFromTarBall(%s)' % (tarball))
        
        if tarfile.is_tarfile(self.repository_path):
            tar = tarfile.open(self.repository_path)
            members = tar.getnames()
            for member in members:
                if 'configure.in' in member:
                    log.debug('Reading version from: %s' % member)
                    file = tar.extractfile(member)
                    lines = file.readlines()
                    vers = self.getVersionFromLines(lines)
            tar.close()
        log.debug( 'Version (%s)' % (vers) )
        return vers
        
def buildGRAMPS( base, out_dir, bTarball):
    bo = buildbase()

    bo.repository_path = base
    bo.out_dir = out_dir
    bo.bTarball = bTarball
    bo.bBuildInstaller = bBuildInstaller
    
    if not bo.bTarball and not bo.isGrampsRoot(bo.repository_path):
        log.error( '$$$$ BAD Gramps Root specified $$$$')
    else:
        bo.checkForBuildTools()
        if bo.bTarball:
            bo.gramps_version = bo.getVersionFromTarBall( bo.repository_path )
            bo.build_root = path.normpath(os.path.join(bo.out_dir, 'gramps-%s' % bo.gramps_version))
            if bBuildAll:
                bo.cleanBuildDir()
                bo.expandTarBall(base, bo.out_dir)
            bo.copyExtraFilesToBuildDir(bo.build_root )
        else: #SVN Build
            bo.gramps_version = bo.getVersionFromConfigureIn( base )
            bo.build_root = path.normpath(os.path.join(bo.out_dir, 'gramps-%s' % bo.gramps_version))
            if bBuildAll:
                bo.cleanBuildDir()
                os.mkdir(bo.build_root)
                bo.exportSVN(os.path.join(base, 'src'), os.path.join(bo.build_root, 'src') )
                bo.exportSVN(os.path.join(base, 'po'), os.path.join(bo.build_root, 'po') )
                bo.exportSVN(os.path.join(base, 'example'), os.path.join(bo.build_root, 'examples') )
            bo.generateConstPy( ) 
            bo.copyExtraFilesToBuildDir(base)
        
        if bBuildAll:
            bo.processPO( )
        if bo.bBuildInstaller:
            bo.compileInstallScript()


if __name__ == '__main__':
    import getopt
    import os
    import sys
    import tarfile

    usage = '''Create Gramps Windows Installer.
Usage:
    python build_GrampsWin32.py [options] [repository_path]

Arguments:
    repository_path                   Path to the repository to build GRAMPS from, this can be either
                                        - The root path of a SVN working copy
                                        - A tarball that has been saved on local disk
                                        - Left blank to build the SVN working copy this file is part of
Options:
    -h,     --help                This help message.
    -oDIR,  --out=DIR             Directory to build files (optional)
            --nsis_only           Build NSIS only (does not Clean & Build All)
    -t      --tarball             Build release version from Tarball.
    -mDIR, --msgdir=DIR           Directory to msgfmt.exe
    '''
# TODO: nsis_dir option - a path to nsismake (for occasions script cannot work it out)
# TODO: svn_dir  option - a path to svn (for occasions script cannot work it out)
# TODO: tortoise_dir Option - accommodate windows user who dont have svn but use tortoiseSVN

    repository_path = '.' # Repository - either SVN working copy dir or Tarball file
    out_dir = None
    bBuildAll = True
    bBuildInstaller = True
    bTarball = False
    msg_dir = ""
    
    try:
        opts, args = getopt.getopt(sys.argv[1:], "ho:tm:",
                                  ["help", "out=", "nsis_only", "tarball", "msgdir="])

        for o, a in opts:
            if o in ("-h", "--help"):
                print usage
                sys.exit(0)

            if o in ("-o", "--out"):
                out_dir =  a
            if o in ("--nsis_only"):
                bBuildAll = False
            if o in ('-t', "--tarball"):
                print 'This is a tarball build'
                bTarball = True
            if o in ("-m", "--msgdir"):
                if os.path.isdir( a ):
                    msg_dir =  a
                else:
                    raise getopt.GetoptError, '\nERROR: msgfmt dir does not exist'
                
        if args: #got args use first one as base dir
            repository_path = path.normpath(args[0])
        else:   # no base dir passed in, work out one from current working dir
            repository_path = path.normpath("%s/../.." % os.getcwd() )

    #        raise getopt.GetoptError, '\nERROR: No base directory specified'

        if len(args) > 1:
            raise getopt.GetoptError, '\nERROR: Too many arguments'

    except getopt.GetoptError, msg:
        print msg
        print '\n %s' % usage
        sys.exit(2)

    if bTarball:
        if not tarfile.is_tarfile(repository_path):
            print "Tarball %s not a valid Tarball" % repository_path
            sys.exit(1)
            
    else:    
        if not os.path.isdir(repository_path):
            print "WC root directory not found; %s " % repository_path
            sys.exit(1)

    if out_dir == None: 
        if bTarball:
            out_dir = path.normpath(os.getcwd())
        else:
            out_dir = path.normpath(os.path.join(repository_path, 'windows'))
        log.info("Setting outdir to %s", out_dir)
        

    s_args = ''
    for value in sys.argv[1:]:
        s_args = s_args + ' %s'%value

    print "======= build_GrampsWin32.py %s ========"  % s_args
    log.debug('Using %s to find python tools' % sys.prefix)
    log.info('Platform: %s' % sys.platform)
    #==========================
    sys.exit(buildGRAMPS(repository_path,out_dir, bTarball))

gobject.type_register(buildbase)