# This file is part of Buildbot.  Buildbot 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, version 2.
#
# 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., 51
# Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
#
# Copyright Buildbot Team Members

# Visual studio steps

from buildbot.steps.shell import ShellCommand
from buildbot.process.buildstep import LogLineObserver
from buildbot import config
from buildbot.status.results import SUCCESS, WARNINGS, FAILURE

import re


def addEnvPath(env, name, value):
    """ concat a path for this name """
    try:
        oldval = env[name]
        if not oldval.endswith(';'):
            oldval = oldval + ';'
    except KeyError:
        oldval = ""
    if not value.endswith(';'):
        value = value + ';'
    env[name] = oldval + value

class MSLogLineObserver(LogLineObserver):

    _re_delimiter = re.compile(r'^(\d+>)?-{5}.+-{5}$')
    _re_file = re.compile(r'^(\d+>)?[^ ]+\.(cpp|c)$')
    _re_warning = re.compile(r' ?: warning [A-Z]+[0-9]+:')
    _re_error = re.compile(r' ?error ([A-Z]+[0-9]+)?\s?: ')

    nbFiles = 0
    nbProjects = 0
    nbWarnings = 0
    nbErrors = 0

    logwarnings = None
    logerrors = None

    def __init__(self, logwarnings, logerrors, **kwargs):
        LogLineObserver.__init__(self, **kwargs)
        self.logwarnings = logwarnings
        self.logerrors = logerrors
        self.stdoutParser.delimiter = "\r\n"
        self.stderrParser.delimiter = "\r\n"

    def outLineReceived(self, line):
        if self._re_delimiter.search(line):
            self.nbProjects += 1
            self.logwarnings.addStdout("%s\n" % line)
            self.logerrors.addStdout("%s\n" % line)
            self.step.setProgress('projects', self.nbProjects)
        elif self._re_file.search(line):
            self.nbFiles += 1
            self.step.setProgress('files', self.nbFiles)
        elif self._re_warning.search(line):
            self.nbWarnings += 1
            self.logwarnings.addStdout("%s\n" % line)
            self.step.setProgress('warnings', self.nbWarnings)
        elif self._re_error.search("%s\n" % line):
            # error has no progress indication
            self.nbErrors += 1
            self.logerrors.addStderr("%s\n" % line)


class VisualStudio(ShellCommand):
    # an *abstract* base class, which will not itself work as a buildstep

    name = "compile"
    description = "compiling"
    descriptionDone = "compile"


    progressMetrics = ( ShellCommand.progressMetrics +
                            ('projects', 'files','warnings',))

    logobserver = None

    installdir = None
    default_installdir = None

    # One of build, or rebuild
    mode = "rebuild"

    projectfile = None
    config = None
    useenv = False
    project = None
    PATH = []
    INCLUDE = []
    LIB = []

    renderables = [ 'projectfile', 'config', 'project' ]

    def __init__(self,
                installdir = None,
                mode = "rebuild",
                projectfile = None,
                config = 'release',
                useenv = False,
                project = None,
                INCLUDE = [],
                LIB = [],
                PATH = [],
                **kwargs):
        self.installdir = installdir
        self.mode = mode
        self.projectfile = projectfile
        self.config = config
        self.useenv = useenv
        self.project = project
        if len(INCLUDE) > 0:
            self.INCLUDE = INCLUDE
            self.useenv = True
        if len(LIB) > 0:
            self.LIB = LIB
            self.useenv = True
        if len(PATH) > 0:
            self.PATH = PATH
        # always upcall !
        ShellCommand.__init__(self, **kwargs)

    def setupLogfiles(self, cmd, logfiles):
        logwarnings = self.addLog("warnings")
        logerrors = self.addLog("errors")
        self.logobserver = MSLogLineObserver(logwarnings, logerrors)
        self.addLogObserver('stdio', self.logobserver)
        ShellCommand.setupLogfiles(self, cmd, logfiles)

    def setupInstalldir(self):
        if not self.installdir:
            self.installdir = self.default_installdir

    def setupEnvironment(self, cmd):
        ShellCommand.setupEnvironment(self, cmd)
        if cmd.args['env'] is None:
            cmd.args['env'] = {}

        # setup the custom one, those one goes first
        for path in self.PATH:
            addEnvPath(cmd.args['env'], "PATH", path)
        for path in self.INCLUDE:
            addEnvPath(cmd.args['env'], "INCLUDE", path)
        for path in self.LIB:
            addEnvPath(cmd.args['env'], "LIB", path)

        self.setupInstalldir()

    def describe(self, done=False):
        description = ShellCommand.describe(self, done)
        if done:
            description.append('%d projects' % self.step_status.getStatistic('projects', 0))
            description.append('%d files' % self.step_status.getStatistic('files', 0))
            warnings = self.step_status.getStatistic('warnings', 0)
            if warnings > 0:
                description.append('%d warnings' % warnings)
            errors = self.step_status.getStatistic('errors', 0)
            if errors > 0:
                description.append('%d errors' % errors)
        return description

    def createSummary(self, log):
        self.step_status.setStatistic('projects', self.logobserver.nbProjects)
        self.step_status.setStatistic('files', self.logobserver.nbFiles)
        self.step_status.setStatistic('warnings', self.logobserver.nbWarnings)
        self.step_status.setStatistic('errors', self.logobserver.nbErrors)

    def evaluateCommand(self, cmd):
        if cmd.didFail():
            return FAILURE
        if self.logobserver.nbErrors > 0:
            return FAILURE
        if self.logobserver.nbWarnings > 0:
            return WARNINGS
        else:
            return SUCCESS

    def finished(self, result):
        self.getLog("warnings").finish()
        self.getLog("errors").finish()
        ShellCommand.finished(self, result)

class VC6(VisualStudio):

    default_installdir = 'C:\\Program Files\\Microsoft Visual Studio'

    def setupEnvironment(self, cmd):
        VisualStudio.setupEnvironment(self, cmd)

        # Root of Visual Developer Studio Common files.
        VSCommonDir = self.installdir + '\\Common'
        MSVCDir = self.installdir + '\\VC98'
        MSDevDir = VSCommonDir + '\\msdev98'

        addEnvPath(cmd.args['env'], "PATH", MSDevDir + '\\BIN')
        addEnvPath(cmd.args['env'], "PATH", MSVCDir + '\\BIN')
        addEnvPath(cmd.args['env'], "PATH", VSCommonDir + '\\TOOLS\\WINNT')
        addEnvPath(cmd.args['env'], "PATH", VSCommonDir + '\\TOOLS')

        addEnvPath(cmd.args['env'], "INCLUDE", MSVCDir + '\\INCLUDE')
        addEnvPath(cmd.args['env'], "INCLUDE", MSVCDir + '\\ATL\\INCLUDE')
        addEnvPath(cmd.args['env'], "INCLUDE", MSVCDir + '\\MFC\\INCLUDE')

        addEnvPath(cmd.args['env'], "LIB", MSVCDir + '\\LIB')
        addEnvPath(cmd.args['env'], "LIB", MSVCDir + '\\MFC\\LIB')

    def start(self):
        command = ["msdev"]
        command.append(self.projectfile)
        command.append("/MAKE")
        if self.project is not None:
            command.append(self.project + " - " + self.config)
        else:
            command.append("ALL - " + self.config)
        if self.mode == "rebuild":
            command.append("/REBUILD")
        elif self.mode == "clean":
            command.append("/CLEAN")
        else:
            command.append("/BUILD")
        if self.useenv:
            command.append("/USEENV")
        self.setCommand(command)
        return VisualStudio.start(self)

class VC7(VisualStudio):
    default_installdir = 'C:\\Program Files\\Microsoft Visual Studio .NET 2003'

    def setupEnvironment(self, cmd):
        VisualStudio.setupEnvironment(self, cmd)

        VSInstallDir = self.installdir + '\\Common7\\IDE'
        VCInstallDir = self.installdir
        MSVCDir = self.installdir + '\\VC7'

        addEnvPath(cmd.args['env'], "PATH", VSInstallDir)
        addEnvPath(cmd.args['env'], "PATH", MSVCDir + '\\BIN')
        addEnvPath(cmd.args['env'], "PATH", VCInstallDir + '\\Common7\\Tools')
        addEnvPath(cmd.args['env'], "PATH", VCInstallDir + '\\Common7\\Tools\\bin')

        addEnvPath(cmd.args['env'], "INCLUDE", MSVCDir + '\\INCLUDE')
        addEnvPath(cmd.args['env'], "INCLUDE", MSVCDir + '\\ATLMFC\\INCLUDE')
        addEnvPath(cmd.args['env'], "INCLUDE", MSVCDir + '\\PlatformSDK\\include')
        addEnvPath(cmd.args['env'], "INCLUDE", VCInstallDir + '\\SDK\\v1.1\\include')

        addEnvPath(cmd.args['env'], "LIB", MSVCDir + '\\LIB')
        addEnvPath(cmd.args['env'], "LIB", MSVCDir + '\\ATLMFC\\LIB')
        addEnvPath(cmd.args['env'], "LIB", MSVCDir + '\\PlatformSDK\\lib')
        addEnvPath(cmd.args['env'], "LIB", VCInstallDir + '\\SDK\\v1.1\\lib')

    def start(self):
        command = ["devenv.com"]
        command.append(self.projectfile)
        if self.mode == "rebuild":
            command.append("/Rebuild")
        elif self.mode == "clean":
            command.append("/Clean")
        else:
            command.append("/Build")
        command.append(self.config)
        if self.useenv:
            command.append("/UseEnv")
        if self.project is not None:
            command.append("/Project")
            command.append(self.project)
        self.setCommand(command)
        return VisualStudio.start(self)

#alias VC7 as VS2003
VS2003 = VC7

class VC8(VC7):

    # Our ones
    arch = None
    default_installdir = 'C:\\Program Files\\Microsoft Visual Studio 8'

    renderables = ['arch']

    def __init__(self, arch = "x86", **kwargs):
        self.arch = arch

        # always upcall !
        VisualStudio.__init__(self, **kwargs)

    def setupEnvironment(self, cmd):
        VisualStudio.setupEnvironment(self, cmd)

        VSInstallDir = self.installdir
        VCInstallDir = self.installdir + '\\VC'

        addEnvPath(cmd.args['env'], "PATH", VSInstallDir + '\\Common7\\IDE')
        if self.arch == "x64":
            addEnvPath(cmd.args['env'], "PATH", VCInstallDir + '\\BIN\\x86_amd64')
        addEnvPath(cmd.args['env'], "PATH", VCInstallDir + '\\BIN')
        addEnvPath(cmd.args['env'], "PATH", VSInstallDir + '\\Common7\\Tools')
        addEnvPath(cmd.args['env'], "PATH", VSInstallDir + '\\Common7\\Tools\\bin')
        addEnvPath(cmd.args['env'], "PATH", VCInstallDir + '\\PlatformSDK\\bin')
        addEnvPath(cmd.args['env'], "PATH", VSInstallDir + '\\SDK\\v2.0\\bin')
        addEnvPath(cmd.args['env'], "PATH", VCInstallDir + '\\VCPackages')
        addEnvPath(cmd.args['env'], "PATH", r'${PATH}')

        addEnvPath(cmd.args['env'], "INCLUDE", VCInstallDir + '\\INCLUDE')
        addEnvPath(cmd.args['env'], "INCLUDE", VCInstallDir + '\\ATLMFC\\include')
        addEnvPath(cmd.args['env'], "INCLUDE", VCInstallDir + '\\PlatformSDK\\include')

        archsuffix = ''
        if self.arch == "x64":
            archsuffix = '\\amd64'
        addEnvPath(cmd.args['env'], "LIB", VCInstallDir + '\\LIB' + archsuffix)
        addEnvPath(cmd.args['env'], "LIB", VCInstallDir + '\\ATLMFC\\LIB' + archsuffix)
        addEnvPath(cmd.args['env'], "LIB", VCInstallDir + '\\PlatformSDK\\lib' + archsuffix)
        addEnvPath(cmd.args['env'], "LIB", VSInstallDir + '\\SDK\\v2.0\\lib' + archsuffix)

#alias VC8 as VS2005
VS2005 = VC8

class VCExpress9(VC8):
    def start(self):
        command = ["vcexpress"]
        command.append(self.projectfile)
        if self.mode == "rebuild":
            command.append("/Rebuild")
        elif self.mode == "clean":
            command.append("/Clean")
        else:
            command.append("/Build")
        command.append(self.config)
        if self.useenv:
            command.append("/UseEnv")
        if self.project is not None:
            command.append("/Project")
            command.append(self.project)
        self.setCommand(command)
        return VisualStudio.start(self)

# Add first support for VC9 (Same as VC8, with a different installdir)
class VC9(VC8):
    default_installdir = 'C:\\Program Files\\Microsoft Visual Studio 9.0'

VS2008 = VC9

# VC10 doesn't look like it needs extra stuff.
class VC10(VC9):
    default_installdir = 'C:\\Program Files\\Microsoft Visual Studio 10.0'

VS2010 = VC10

# VC11 doesn't look like it needs extra stuff.
class VC11(VC10):
    default_installdir = 'C:\\Program Files\\Microsoft Visual Studio 11.0'

VS2012 = VC11

class MsBuild(VisualStudio):
    platform = None

    def __init__(self, platform, **kwargs):
        self.platform = platform
        VisualStudio.__init__(self, **kwargs)

    def setupEnvironment(self, cmd):
        VisualStudio.setupEnvironment(self, cmd)
        cmd.args['env']['VCENV_BAT'] = "\"${VS110COMNTOOLS}..\\..\\VC\\vcvarsall.bat\""

    def describe(self, done=False):
        rv = []
        if done:
            rv.append("built")
        else:
            rv.append("building")
        if self.project is not None:
            rv.append("%s for" % (self.project))
        else:
            rv.append("solution for")
        rv.append("%s|%s" % (self.config, self.platform))
        return rv

    def start(self):
        if self.platform is None:
            config.error('platform is mandatory. Please specify a string such as "Win32"')

        command = ["%VCENV_BAT%",
                   "x86",
                   "&&",
                   "msbuild",
                   self.projectfile,
                   "/p:Configuration=%s" % (self.config),
                   "/p:Platform=%s" % (self.platform)]
        if self.project is not None:
            command.append("/t:%s" % (self.project))

        self.setCommand(command)

        return VisualStudio.start(self)
