#!/usr/bin/env python

from   __future__ import print_function
import re
import os
import os.path
import sys
import time
import shutil
import socket
import subprocess
import logging
import optparse


def log ( message ):
    print( message )
    logging.info( message )
    return


class ErrorMessage ( Exception ):

    def __init__ ( self, code, *arguments ):
        self._code   = code
        self._errors = [ 'Malformed call to ErrorMessage()'
                       , '{}'.format(arguments) ]
        text = None
        if len(arguments) == 1:
            if isinstance(arguments[0],Exception): text = str(arguments[0]).split('\n')
            else:
                self._errors = arguments[0]
        elif len(arguments) > 1:
            text = list(arguments)
        if text:
            self._errors = []
            while len(text[0]) == 0: del text[0]
            lstrip = 0
            if text[0].startswith('[ERROR]'): lstrip = 8 
            for line in text:
                if line[0:lstrip  ] == ' '*lstrip or \
                   line[0:lstrip-1] == '[ERROR]':
                    self._errors += [ line[lstrip:] ]
                else:
                    self._errors += [ line.lstrip() ]

    def __str__ ( self ):
        if not isinstance(self._errors,list):
            return "[ERROR] %s" % self._errors
        formatted = "\n"
        for i in range(len(self._errors)):
            if i == 0: formatted += "[ERROR] {}".format( self._errors[i] )
            else:      formatted += "        {}".format( self._errors[i] )
            if i+1 < len(self._errors): formatted += "\n"
        return formatted

    def addMessage ( self, message ):
        if not isinstance(self._errors,list):
            self._errors = [ self._errors ]
        if isinstance(message,list):
            for line in message:
                self._errors += [ line ]
        else:
            self._errors += [ message ]

    def terminate ( self ):
        print( self )
        sys.exit(self._code)

    def _getCode ( self ): return self._code

    code = property(_getCode)


class Configuration ( object ):

    def __init__ ( self ):
        self.onSource = False
        self.onLepka  = False
        self.onDocker = False
        hostname = socket.gethostname()
        if hostname.startswith('lepka'): self.onLepka  = True
        else:                            self.onDocker = True
        scriptDir = os.path.abspath(os.getcwd())
        if scriptDir.endswith( 'coriolis/documentation' ) and not self.onLepka:
            self.onLepka  = False
            self.onSource = True
        if self.onDocker:
            log( 'Using *Docker* configuration.' )
            self.pelicanDir = '/data/git/coriolis.lip6.fr/pelican'
            self.apacheDir  = '/var/www/html'
        if self.onLepka:
            log( 'Using *Lepka* configuration.' )
           #self.pelicanDir = os.path.join( os.environ['HOME'], 'cms/coriolis.lip6.fr/pelican' )
            self.pelicanDir = scriptDir
            self.apacheDir  = '/dsk/l1/httpd/coriolis'
        if self.onSource:
            log( 'Using *Source* configuration.' )
            self.pelicanDir = scriptDir
            self.apacheDir  = None
        self.localDocDir      = '/dsk/l1/jpc/coriolis-2.x/Linux.el7_64/Release.Shared/install/share/doc/coriolis2/en/html/doc'
        self.remoteDocDir     = '/data'
        self.remoteGitDir     = '/data/git'
        self.remotePelicanDir = os.path.join( self.remoteGitDir, 'coriolis.lip6.fr/pelican' )
        self.pluginsDir       = os.path.join( self.remoteGitDir, 'pelican-plugins' )
        self.logDir = os.path.join( self.pelicanDir, 'logs' )
        self.target = 'coriolis-d'


class Command ( object ):

    def __init__ ( self, command ):
        self.command = command
        self.outlog  = None
        self.errlog  = None

    def asString ( self ):
        s = ''
        for i in range(len(self.command)):
            if i: s += ' '
            if ' ' in self.command[i]: s += '"'+self.command[i]+'"'
            else:                      s +=     self.command[i]
        return s

    def execute ( self ):
        sys.stdout.flush()
        sys.stderr.flush()
        log( self.asString() )
        child = subprocess.Popen( self.command, stdin =subprocess.PIPE
                                              , stdout=subprocess.PIPE
                                              , stderr=subprocess.PIPE )
        self.outlog, self.errlog = child.communicate()
        if len(self.outlog): logging.info ( self.outlog )
        if len(self.errlog): logging.error( self.errlog )
        status   = child.returncode
        status >>= 8
        if status != 0:
            ErrorMessage( status, "{} (status:{}).".format(error,status) ).terminate()
        return status

    @staticmethod
    def rmtree ( path ):
        command = 'rm -rf {}'.format( path )
        try:
            log( command )
            shutil.rmtree( path )
        except shutil.Error, e:
            logging.error( str(e) )
            return 1
        return 0

    @staticmethod
    def copytree ( src, dst ):
        command = 'cp -r {} {}'.format( src, dst )
        try:
            log( command )
            shutil.copytree( src, dst )
        except OSError, e:
            logging.error( e )
            return 1
        except shutil.Error, errors:
            for error in errors:
                logging.error( 'cp {} {}: {}'.format(src,dst,error) )
            return 1
        return 0


class SshCommand ( object ):

    def __init__ ( self, scriptlet ):
        self.scriptlet = scriptlet

    def execute ( self, target ):
        command = [ 'ssh', '-x', target, self.scriptlet ]
        Command( command ).execute()


class Document ( object ):

    def __init__ ( self, conf, document ):
        self.conf     = conf
        self.document = os.path.basename( document )
        self.rdir     = os.path.dirname ( document )

    def toPdf ( self ):
        pdfDir         = '{}/content/pdfs'    .format( self.conf.pelicanDir )
        stylesheet     = '{}/etc/SoC-ReST.tex'.format( self.conf.pelicanDir )
        documentPdf    = '{}.pdf'.format( self.document )
        documentRst    = '{}.rst'.format( self.document )
        documentTex    = '{}.tex'.format( self.document )
        documentRawTex = '{}-raw.tex'.format( self.document )
        documentTmps   = [ documentTex
                         , documentRawTex
                         , '{}.log'.format( self.document )
                         , '{}.out'.format( self.document )
                         , '{}.aux'.format( self.document )
                         , '{}.toc'.format( self.document )
                         ]
        targetPdf      = os.path.join( pdfDir, documentPdf )
       
        cwd = os.getcwd()
        os.chdir( os.path.join(self.conf.pelicanDir,self.rdir) )
        os.environ[ 'TEXINPUTS' ] = '{}/etc/images//:./images//:'.format( self.conf.pelicanDir )
        Command( [ 'rst2latex', '--use-latex-toc'
                              , '--stylesheet={}'.format( stylesheet )
                              , documentRst
                              , documentRawTex
                 ] ).execute()
        pMulticol = re.compile( r' \\& \\\\multicolumn\{2\}\{l\|\}\{' )
        fdi = open( documentRawTex, 'r' )
        fdo = open( documentTex   , 'w' )
        for line in fdi.readlines():
          fdo.write( pMulticol.sub('  \\& \\\\multicolumn{2}{p{0.6\\\\DUtablewidth}|}{',line) )
        fdi.close()
        fdo.close()
        Command( [ 'pdflatex', '-halt-on-error', documentTex ] ).execute()
        Command( [ 'pdflatex', '-halt-on-error', documentTex ] ).execute()
        for file in documentTmps: os.unlink( file )
        if not os.path.isdir( pdfDir ):
            log( 'mkdir {}'.format( pdfDir ))
            os.mkdir( pdfDir )
        if os.path.exists( targetPdf ): os.unlink( targetPdf )
        log( 'mv {} {}'.format( documentPdf, pdfDir ))
        shutil.move( documentPdf, pdfDir )
        os.chdir( cwd )


class Site ( object ):

    def __init__ ( self, conf ):
        self.conf = conf

    def build ( self ):
        print( 'cd {}'.format( self.conf.pelicanDir ))
        os.chdir( self.conf.pelicanDir )
        status = Command( [ 'pelican', '-vD', '--ignore-cache', 'content' ] ).execute()
        if status: return status
        if self.conf.onLepka:
            Command.rmtree  ( self.conf.apacheDir )
            Command.copytree( './output', self.conf.apacheDir )
            Command.copytree( self.conf.localDocDir, os.path.join(self.conf.apacheDir,'doc') )

    def gitPush ( self ):
        print( 'cd {}'.format( self.conf.pelicanDir ))
        os.chdir( self.conf.pelicanDir )
        lines = ''
        cmd   = Command( ['git', 'status', 'content', 'common', 'theme'] )
        cmd.execute()
        if cmd.outlog: lines = cmd.outlog.split('\n')
        if lines[-2] != 'nothing to commit, working directory clean':
            message = [ 'There are some uncommited changes in the site contents.' ] + lines
            print( ErrorMessage( 1, message ))
        Command( ['git', 'push'] ).execute()
        return True

    def gitCommitPdfs ( self ):
        print( 'cd {}'.format( self.conf.pelicanDir ))
        os.chdir( self.conf.pelicanDir )
        lines = ''
        cmd   = Command( ['git', 'status', 'content/pdfs'] )
        cmd.execute()
        if cmd.outlog: lines = cmd.outlog.split('\n')
        if lines[-2] != 'nothing to commit, working directory clean':
            message = 'Updated PDFs, %s.' % time.strftime( '%B %d, %Y (%H:%M)' )
            Command( ['git', 'add'   , 'content/pdfs'] ).execute()
            Command( ['git', 'commit', '-m', message ] ).execute()
        return True

    def remoteBuild ( self ):
        Command( [ 'ssh', '-x', '-o', 'StrictHostKeyChecking no'
                 , self.conf.target, "echo 'Force SSH key loading.'" ] ).execute()
        Command( [ 'rsync', '--rsh=/usr/bin/ssh', '-avH'
                 , self.conf.localDocDir
                 , '{}:{}'.format(self.conf.target,self.conf.remoteDocDir) ] ).execute()
        remoteScript = \
          ' if [ ! -d {remotePelicanDir} ]; then'                                       \
          '   cd {remoteGitDir};'                                                       \
          '   git clone gitsoc@bop-t:coriolis.lip6.fr;'                                 \
          '   sudo pelican-themes -s {remotePelicanDir}/themes/nest-coriolis;'          \
          '   sudo chown -R pelican:pelican /var/www/html;'                             \
          ' fi;'                                                                        \
          ' cd {remotePelicanDir};'                                                     \
          ' git pull;'                                                                  \
          ' if [ ! -d {pluginsDir} ]; then'                                             \
          '   cd {remoteGitDir};'                                                       \
          '   git clone https://github.com/getpelican/pelican-plugins;'                 \
          '   cd pelican-plugins;'                                                      \
          '   patch -p1 -i {remotePelicanDir}/patchs/0001-bootstrap-rst.no-math.patch;' \
          ' fi;'                                                                        \
          ' cd {remotePelicanDir};'                                                     \
          ' ./build.py -p;'                                                             \
          .format( pluginsDir       = self.conf.pluginsDir
                 , remoteGitDir     = self.conf.remoteGitDir
                 , remotePelicanDir = self.conf.remotePelicanDir
                 )
        SshCommand( remoteScript ).execute( self.conf.target )


if __name__ == '__main__':
    usage = \
      '\n'  \
      '\nThe "build.py" script rebuild the "coriolis.lip6.fr" website hosted'   \
      '\nat the Sorbonne Universite/LIP6. It can work in the following modes:'  \
      '\n'                                                                      \
      '\n1. Fully install the website on a freshly generated docker image.'     \
      '\n2. Update the "coriolis.lip6.fr" website.'                             \
      '\n3. Generate a static website on my laptop ("lepka").'                  \
      '\n4. Generate the static documentation shipped with coriolis (in git).'  \
      '\n'                                                                      \
      '\nNOTE: About the "<-C|--coriolis> remote rebuild option."'              \
      '\n      Your ${HOME}/.ssh/config file must contain an alias for the'     \
      '\n      "coriolis-d" host, as shown below:'                              \
      '\n'                                                                      \
      '\n          Host coriolis-d'                                             \
      '\n              User          pelican'                                   \
      '\n              HostName      localhost'                                 \
      '\n              HostKeyAlias  coriolis-d'                                \
      '\n              Port          2250'                                      \
      '\n              CheckHostIP   no'                                        \
      '\n              ForwardAgent  yes'                                       \
      '\n              ForwardX11    no'                                        \
      '\n              RemoteForward 2227 localhost:2227'                       \
      '\n'                                                                      \
      '\n      It logs as user "pelican" using the ssh port 2250 and creates'   \
      '\n      a tunnel between port 2227 on the remote host (the web server)'  \
      '\n      and port 2227 on the local host (your machine). You *must* have' \
      '\n      an ssh tunnel between your machine and "bop" on the local port'  \
      '\n      2227 for this to work. The "2227" tunnel between the web server' \
      '\n      and "bop" is used to perform "git pull".'                        \
      '\n'                                                                      \
      '\n./build.py <options>'
    parser = optparse.OptionParser( usage )
    parser.add_option( '-p', '--pelican' , action='store_true', dest='doPelican' , help='Run pelican.' )
    parser.add_option( '-P', '--pdfs'    , action='store_true', dest='doPdfs'    , help='Generate the PDFs.' )
    parser.add_option( '-C', '--coriolis', action='store_true', dest='doCoriolis', help='Build/update the web site on the server (docker).' )
    (options, args) = parser.parse_args()
    conf = Configuration()
    if not os.path.isdir( conf.logDir ):
        os.mkdir( conf.logDir )
    logging.basicConfig( filename='logs/build-{}.log'.format(time.strftime( '%Y%m%d-%H%M' ))
                       ,   format='%(asctime)s:%(levelname)s| %(message)s'
                       ,    level=logging.INFO
                       )
    documents = [ Document( conf, 'content/pages/users-guide/UsersGuide' )
                , Document( conf, 'content/pages/python-tutorial/PythonTutorial' )
                , Document( conf, 'content/pages/python-cpp/PythonCpp' )
                , Document( conf, 'content/pages/stratus/Stratus' )
                , Document( conf, 'content/pages/check-toolkit/CheckToolkit' )
                , Document( conf, 'content/pages/rds/RDS' )
                ]
    coriolis = Site( conf )
    if options.doPdfs:
        for document in documents: document.toPdf()
        if not conf.onSource or not conf.onLepka:
            coriolis.gitCommitPdfs()
    if options.doPelican:
        coriolis.build()
    if options.doCoriolis:
        coriolis.gitPush()
        coriolis.remoteBuild()
    sys.exit( 0 )
