##
## Copyright (c) 2006 Jason Dillon
##
## Licensed under the Apache License, Version 2.0 (the "License");
## you may not use this file except in compliance with the License.
## You may obtain a copy of the License at
##
##     http://www.apache.org/licenses/LICENSE-2.0
##
## Unless required by applicable law or agreed to in writing, software
## distributed under the License is distributed on an "AS IS" BASIS,
## WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
## See the License for the specific language governing permissions and
## limitations under the License.
##

##
## $Id: htmlmessage.py 82 2009-08-26 09:26:36Z mindwanderer $
##

from p4spam import config, jiralinker
from p4spam.message import MessageBuilder

class HtmlMessageBuilder(MessageBuilder):
    def __init__(this, info):
        MessageBuilder.__init__(this, info)
        this.contentType = 'html'
    
    def escapeLine(this, line):
        ##
        ## HACK: Need a library to handle all this mess
        ##
        line = line.replace('&', '&')
        line = line.replace('>', '>')
        line = line.replace('<', '&lt;')
        line = line.replace('\t', '&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;')
        
        ##
        ## HACK: Trim the line here too... :-(
        ##
        
        l = len(line)
        m = config.MAX_DIFF_LINE_LENGTH
        if l > m:
            this.log.warning("Diff line longer than %s; truncating %s remaining characters" % (m, l - m))
            line = line[:m]
            
        return line

    # parses the Description of the changelist. Separate from above, generic escapeLine because
    # perhaps we want to display the entire Description no matter the length and not invoke
    # MAX_DIFF_LINE_LENGTH to cut it off
    def escapeDescLine(this, line):
        ##
        ## HACK: Need a library to handle all this mess
        ##
        line = line.replace('&', '&amp;')
        line = line.replace('>', '&gt;')
        line = line.replace('<', '&lt;')
        line = line.replace('\t', '&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;')
        
        return line

    def generatePathAnchor(this, path):
        anchor = path
        
        # Condense the path...
        anchor = anchor.replace('/', '')
        anchor = anchor.replace('.', '')
        anchor = anchor.replace(' ', '')
        
        return anchor
    
    def writeDocument(this, buff, recipient):
        buff.writeln('<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.1//EN"')
        buff.writeln('"http://www.w3.org/TR/xhtml11/DTD/xhtml11.dtd">')
        buff.writeln('<html xmlns="http://www.w3.org/1999/xhtml">')
        
        this.writeHeader(buff)
        this.writeBody(buff, recipient)
        
        buff.writeln('</html>')
        
    def writeHeader(this, buff):
        buff.writeln('<head>')
        buff.writeln("<style type=\"text/css\"><!--\n%s\n--></style>" % config.HTML_STYLE)
        
        # Write user-header
        if config.USER_HEADER != None:
            buff.writeln(config.USER_HEADER)
        
        buff.writeln("<title>%s</title>" % this.getTitle())
        buff.writeln('</head>')
    
    def writeOverview(this, buff):
        # Change description header
        buff.writeln('<table border="0" cellspacing="0" cellpadding="3" width="100%">')
        buff.writeln("<tr><td width=\"70\"><b>Change</b></td><td>%s</td></tr>" % this.info.getChange())
        buff.writeln("<tr><td><b>Author</b></td><td>%s</td></tr>" % this.info.getAuthor())
        buff.writeln("<tr><td><b>Client</b></td><td>%s</td></tr>" % this.info.getClient())
        buff.writeln("<tr><td><b>Date</b></td><td>%s</td></tr>" % this.info.getDateTime())
        buff.writeln('</table>')
        buff.writeln()
        
        # Change description/comment
        buff.writeln('<h3>Description</h3>')
        buff.writeln('<pre>')
        
        lines = this.info.getComments()
        for line in lines:
            line = this.escapeDescLine(line)
            line = jiralinker.render(line)
            buff.writeln("%s<br/>" % line)
        
        buff.writeln('</pre>')
        buff.writeln()
        
        # Link to change description URL
        buff.writeln('<h3>URL</h3>')
        url = config.CHANGE_LIST_URL % this.info.getChange()
        buff.writeln("<a href=\"%s\">%s</a>" % (url, url))
        
        # If this change fixes a job, include the job detail
        fixes = this.info.getJobsFixed()
        if len(fixes) != 0:
            buff.writeln('<h3>Jobs Fixed</h3>')
            buff.writeln('<ul>')
            
            for fix in fixes:
                # Link to P4Web if configured
                if config.P4WEB_JOB_VIEW_URL != None:
                    fixurl = config.P4WEB_JOB_VIEW_URL % fix.name
                    fixlink = '<a href="%s">%s</a>' % (fixurl, fix.name)
                else:
                    fixlink = fix.name
                
                buff.write('<li>%s on %s by %s %s</li>' % (fixlink, fix.date, fix.author, fix.status))
                
                ##
                ## NOTE: Ignore the Job description for now.
                ##
            
            buff.writeln('</ul>')
    
    def writeFileActionTOC(this, buff):
        actions = ('edit', 'add', 'delete', 'branch', 'integrate', 'purge', 'move/delete', 'move/add', 'import')
        
        actionLables = {
            'edit':         'Edited Files',
            'add':          'Added Files',
            'delete':       'Deleted Files',
            'branch':       'Branched Files',
            'integrate':    'Integrated Files',
            'purge':        'Purged Files',
            'move/delete':  'Moved Files (delete)',
            'move/add':     'Moved Files (add)',
	    'import':       'Imported Files (add from remote depot)'
        }
        
        # Init the map
        actionToFilesMap = {}
        for a in actions:
            actionToFilesMap[a] = []
        
        # Fill the map
        for f in this.info.getAffectedFiles():
            actionToFilesMap[f.action].append(f)
        
        this.log.debug("Action to files map: %s" % (actionToFilesMap))
        
        for action in actions:
            files = actionToFilesMap[action]
            if len(files) == 0:
                # Skip section if there are no files
                this.log.debug("Skipping '%s' section; no files" % action)
                continue
            
            buff.writeln("<h3>%s</h3>" % actionLables[action])
            buff.writeln('<ul>')

            fileslimit = 0

            for f in files:
                buff.writeln('<li>')
                
                anchor = this.generatePathAnchor(f.path)
                pathspec = "%s#%s" % (f.path, f.rev)
                link = "<a href=\"%s\">[history]</a>" % (config.FILE_HISTORY_URL % f.path)
                buff.writeln("<a href=\"#%s\">%s</a>&nbsp;%s" % (anchor, pathspec, link))
                buff.writeln('</li>')

                fileslimit = fileslimit+1

		if fileslimit >= config.MAX_FILE_LIST:
			buff.write('<strong class="error">')
			buff.write('File list truncated at %s by configuration' % config.MAX_FILE_LIST)
			buff.write('</strong>') 
			break

            buff.writeln('</ul>')
            buff.writeln()
    
    def writeBody(this, buff, recipient):
        buff.writeln('<body>')
        
        # Write user-body-header
        if config.USER_BODY_HEADER != None:
            buff.writeln(config.USER_BODY_HEADER)
            
        buff.writeln('<div id="msg">')
        
        this.writeOverview(buff)
        buff.writeln()
        
        this.writeFileActionTOC(buff)
        buff.writeln('</div>')
        buff.writeln()
        
        this.writeDifferences(buff, recipient)
        buff.writeln()
        
        # Write user-body-footer
        if config.USER_BODY_FOOTER != None:
            buff.writeln(config.USER_BODY_FOOTER)
        
        buff.writeln('</body>')
    
    def writeDifferences(this, buff, recipient):
        buff.writeln('<div id="patch">')
        buff.writeln('<h3>Differences</h3>')
       
	fileslimit = 0

        # log the recipient into p4 as the admin so we can masquerade as him/her
	loginresult = this.p4.raw('login', recipient)
 
        for diff in this.info.getDifferences():
            # Write the anchor
            anchor = this.generatePathAnchor(diff.path)
            buff.writeln("<a id=\"%s\"></a>" % anchor)
            
            buff.writeln('<div class="modfile">')
            buff.writeln("<h4>%s#%s (%s)</h4>" % (diff.path, diff.rev, diff.filetype))
            
	    # this is where we'll take the recipient then decide whether or not they have
	    # permissions to view this diff
	    
	    # try to print the file as the recipient
            syncresult = this.p4.raw('-u', recipient, 'print', diff.path)
	    
	    # if the 'p4 print' results are empty, it means the output went to stderr, which
	    # is to say this user doesn't have access to that file
	    if len(syncresult) == 0:
                buff.write('<strong class="error">')
                buff.write("You do not have appropriate Perforce permissions to view this diff!")
                buff.write('</strong>')
                buff.writeln('</div>')

		# Allow some filetypes to not diff
    
		if diff.filetype in config.DISABLE_DIFF_FOR_FILETYPE:
		    buff.write('<strong class="error">')
		    buff.write('Diff disabled for type %s by configuration' % diff.filetype)
		    buff.write('</strong>')
		    
		else:
		    buff.writeln('<pre class="diff">')
		    
		    i = 0
		    maxlines = config.MAX_DIFF_LINES
		    style = None
		    lastStyle = None
		    
		    for line in diff.lines:
			line = this.escapeLine(line)
			
			if line.startswith('@@'):
			    style = 'lines'
			elif line.startswith('+'):
			    style = 'add'
			elif line.startswith('-'):
			    style = 'rem'
			else:
			    style = 'cx'
			
			# Only write style spans if the sytle was changed
			if style != lastStyle:
			    if lastStyle != None:
				buff.write('</span>')
			    buff.write('<span class="%s">' % style)
			
			buff.write(line)
			
			lastStyle = style
			
			i = i + 1
			if i >= maxlines:
			    break
    
		    # Need to close up the last span
		    buff.writeln('</span>')
		    
		    buff.writeln('</pre>')
		    
		    # If we truncated, then spit out a message
		    totallines = len(diff.lines)
		    if i >= maxlines and totallines != maxlines:
			buff.write('<strong class="error">')
			buff.write("Truncated at %s lines; %s more skipped" % (maxlines, totallines - maxlines))
			buff.writeln('</strong>')
	       
		buff.writeln('</div>')
    
            fileslimit = fileslimit + 1
    
            if fileslimit >= config.MAX_DIFF_FILES:
                buff.write('<strong class="error">')
                buff.write('Diff section truncated at %s files by configuration' % config.MAX_DIFF_FILES)
                buff.write('</strong>')
                break
    
        buff.writeln('</div>')