package com.urbancode.air.plugin.scm

import com.urbancode.air.*
import com.urbancode.air.plugin.scm.changelog.*
import java.util.regex.Matcher

public class SCMQuietPeriod extends SCMChangelog {
    
    //**************************************************************************
    // CLASS
    //**************************************************************************

    //**************************************************************************
    // INSTANCE
    //**************************************************************************
    
    // Clientspec Creation Variables
    boolean createNewClientSpec
    boolean isUsingTemplate
    String templateName
    String clientspec
    
    // Client Info Variables
    final def ROOT_TOKEN        = 'Root:'
    final def VIEW_TOKEN        = 'View:'
    final def SERVER_DATE_TOKEN = 'Server date:'
    final def PERFORCE_DATE     = new java.text.SimpleDateFormat("yyyy/MM/dd HH:mm:ss")
    Double timezoneDifference
    def clientInfo
    
    // Changeset Variables
    final def PERFORCE_CHANGELOG_OUT_DATE = new java.text.SimpleDateFormat("yyyy/MM/dd HH:mm:ss")
    final def CHANGE_SUMMARY_PATTERN = ~'(?m)^Change \\d+ by (\\S+)@\\S+ on (\\d+/\\d+/\\d+ \\d+:\\d+:\\d+)'
    
    public def execute() {
        this.login()
        
        if (createNewClientSpec && isUsingTemplate) {
            createClientSpec()
        }
        
        getClientspecInfo()
        
        def logOutput = super.runLogCommand()
        Date latestDate = parseLogIntoChangesets(logOutput)
        
        if (createNewClientSpec && usUsingTemplate) {
            deleteClientSpec()
        }
        
        return latestDate
    }
    
    public void createClientSpec() {
        if (isUsingTemplate) {
            def command = this.createP4BaseCommand()
            command << 'client' << '-o' << '-t' << templateName << client
            ch.runCommand("Getting Clientspec Template", command) { proc ->
                proc.out.close() // close stdin
                proc.consumeProcessErrorStream(out)
                clientspec = proc.text
            }
        }
        
        // modify the root in the clientspec config to reflect the directoryOffset if
        // one was specified in the command
        clientspec = modifyClientSpecConfigRoot(clientspec, dirOffset)
        
        // run the process
        def createClientCommand = createP4BaseCommand()
        createClientCommand << 'client' << '-i'
        runCommand('Create Clientspec', createClientCommand) { proc ->
            proc.consumeProcessOutput(out, out)
            proc.withWriter{ it << clientspec} // pipe clientspec to child process stdIn and close the stream afterwards
            proc.out.close()
        }
    }
    
    public void getClientspecInfo() {
        def clientInfoCmd = createP4BaseCommand()
        clientInfoCmd << 'client' << '-o'
      
        ch.runCommand('Geting Clientspec Info', clientInfoCmd) { proc ->
           proc.consumeProcessErrorStream(out)
           proc.in.withReader{ clientInfo = readClientInfo(it) } // read the client info from stdout
        }
      
        println("Found client root: " + clientInfo.rootPath)
        println("Found client depot paths: ")
        for (depoPath in clientInfo.depoPaths) {
            println("\t${depoPath}")
        }
      
        //
        // timeDiff
        //
        def serverInfoCmd = createP4BaseCommand()
        serverInfoCmd << 'info'
        ch.runCommand('Getting P4 Server Info', serverInfoCmd) { proc ->
            proc.consumeProcessErrorStream(System.out)
            proc.in.withReader{ timezoneDifference = readServerInfo(it) } // read the p4server info from stdout
        }
    }
    
    public def parseLogIntoChangesets(String logOutput) {
        Date latestChangeDate
        
        logOutput.eachLine{ line ->
            def matcher = ( line =~ /(?m)^Change (\d+)/ ) // Change X on yyyy/MM/dd by user@client 'comment'
            if (matcher) { // find pattern
                int changeId = matcher.group(1).toInteger()
                def summary = ''
        
                def descCommand = createP4BaseCommand()
                descCommand << 'describe' << '-s' << changeId
                ch.runCommand("Describe Change " + changeId, descCommand) { proc ->
                    proc.consumeProcessErrorStream(System.out)
                    summary = proc.text // get the stdout of the process
                    proc.in.close() // close the stdin of the process
                }
        
                // first line is "Change X by user@client on yyyy/MM/dd HH:mm:ss"
                def summaryMatcher = CHANGE_SUMMARY_PATTERN.matcher(summary)
                if (summaryMatcher) {
                    
                    def user  = summaryMatcher.group(1)
                    def date = PERFORCE_CHANGELOG_OUT_DATE.parse(summaryMatcher.group(2))
                    
                    ChangeSet changeSet = new ChangeSet()
                    
                    changeSet.id = changeId
                    changeSet.user = user
                    changeSet.date = date
                    // Eventually there is a line "Affected files ..."
                    // followed by lines of "... //depot/file#3 edit"
                    summary.eachMatch(/(?m)^\.\.\. (.+)#\d+ (\S+)/){entry, file, operation ->
                        ChangeSetFileEntry csEntry = new ChangeSetFileEntry()
                        csEntry.path = file
                        changeSet.fileSet << csEntry
                    }
                    
                    boolean hasAllowedFile = changeSet.hasAllowedPath(changeSetFilter);
                    boolean hasAllowedAuthor = changeSet.hasAllowedAuthor(changeSetFilter)
                    if (hasAllowedAuthor && hasAllowedFile) {
                        latestChangeDate = changeSet.date
                    }
                    else {
                        def message = new StringBuilder("Changeset ${changeSet.id} skipped because ")
                        if (!hasAllowedAuthor) {
                            message << "it has excluded author ${changeSet.user}"
                        }
                        if (!hasAllowedAuthor && !hasAllowedFile) {
                            message << " and "
                        }
                        if (!hasAllowedFile) {
                            message << "it contains only excluded file paths (${changeSet.fileSet.collect{it.path}})"
                        }
                        println message
                    }
                }
            }
        }
        
        return latestChangeDate
    }
    
    public void deleteClientSpec() {
        def deleteClientCommand = createP4BaseCommand()
        
        deleteClientCommand << 'client' << '-d'
        if (isForceDelete) {
            deleteClientCommand << '-f'
        }
        deleteClientCommand << client
        ch.runCommand('P4 Delete ClientSpec', deleteClientCommand)
    }
    
    /**
    * replace path in a configspec with given path
    */
    def modifyClientSpecConfigRoot = {def config, def newRoot ->
        if (!config) {
            return config
        }
        else if (!newRoot) {
           return config.replaceAll(/(?mi)Root:.*$/, Matcher.quoteReplacement("Root: ${new File('.').canonicalPath}"))
        }
        return config.replaceAll(/(?mi)Root:.*$/, Matcher.quoteReplacement("Root: ${new File('.', newRoot).canonicalPath}"))
    }
   
    /**
    * @param base the base time in milliseconds since Epoc
    * @param target the target time in milliseconds since Epoc
    * @return the number of hours between the two times (rounded to nearest half-hour)
    */
    public double getHourDifference(long base, long target) {
        double difference = (double)(base - target) / (1000*60*60) // difference in hours
        return Math.round(difference / 0.5) * 0.5                  // round to nearest 1/2 hour
    }
  
    /**
    *  read stdout of get-client-info afor the workspacepath and depoPathArray
    *  @param br the buffered reader of the p4 command output
    *  @return a map containing 'rootPath':String and 'depoPaths':String[]
    */
    def readClientInfo = {def br ->
        def result = [rootPath: null, depoPaths: []]
  
        String line = null
        while( (line = br.readLine()) != null )  {
            if (line.startsWith(ROOT_TOKEN)) {
                result.rootPath = line.substring(ROOT_TOKEN.length()).trim()
            }
            else if (line.startsWith(VIEW_TOKEN)) {
                while (line = br.readLine()?.trim()) { // read until we hit a blank line (or null)
                    // match first token '//....' where ... has no white-space OR '"//...."' where ... has no quote
                    def m = ( line =~ '((?:"//[^"]+)|(?://\\S+))' )
                    m.find()
                    result.depoPaths << m.group(1)
                }
            }
        }
  
        return result
    }

    /**
    * read the timezone offset between client and server
    * @param the server info output
    * @return the number of hours difference between local time and the server time-stamp (as a double)
    */
    def readServerInfo = { br ->
       double timezoneDifference = 0
       long currentTime = System.currentTimeMillis()

       def serverDateLine = br.readLines().find{it.startsWith(SERVER_DATE_TOKEN) && it.length() > 20}
       if (serverDateLine) {
           // TODO the displayed date-format for perforce appears to be
           //   'yyyy/MM/dd HH:mm:ss Z z' i.e. '2008/08/18 16:02:19 -0400 EDT'
           //   which we could use more directly to get the hr offset and/or the timezone?
           def serverDate = PERFORCE_DATE.parse(serverDateLine.substring(13,32))
           timezoneDifference = getHourDifference(serverDate.getTime(), currentTime)
       }
  
       // print the results
       println("Time difference between the server and the client is ${timezoneDifference} hours!");
       return timezoneDifference
    }
}