# 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


import re

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


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):

    stdoutDelimiter = "\r\n"
    stderrDelimiter = "\r\n"

    _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):
        super().__init__(**kwargs)
        self.logwarnings = logwarnings
        self.logerrors = logerrors

    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', 'mode']

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

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

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

    def setupEnvironment(self, cmd):
        super().setupEnvironment(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 = super().describe(done)
        if done:
            if not description:
                description = ['compile']
            description.append(
                '%d projects' % self.getStatistic('projects', 0))
            description.append('%d files' % self.getStatistic('files', 0))
            warnings = self.getStatistic('warnings', 0)
            if warnings > 0:
                description.append('%d warnings' % warnings)
            errors = self.getStatistic('errors', 0)
            if errors > 0:
                description.append('%d errors' % errors)
        return description

    def createSummary(self, log):
        self.setStatistic('projects', self.logobserver.nbProjects)
        self.setStatistic('files', self.logobserver.nbFiles)
        self.setStatistic('warnings', self.logobserver.nbWarnings)
        self.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
        return SUCCESS

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


class VC6(VisualStudio):

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

    def setupEnvironment(self, cmd):
        super().setupEnvironment(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",
            self.projectfile,
            "/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 super().start()


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

    def setupEnvironment(self, cmd):
        super().setupEnvironment(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",
            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 super().start()


# 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 !
        super().__init__(**kwargs)

    def setupEnvironment(self, cmd):
        # Do not use super() here. We want to override VC7.setupEnvironment().
        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",
            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)
        # Do not use super() here. We want to override VC7.start().
        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


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


VS2013 = VC12


# VC14 doesn't look like it needs extra stuff.
class VC14(VC12):
    default_installdir = 'C:\\Program Files (x86)\\Microsoft Visual Studio 14.0'


VS2015 = VC14


class VC141(VC14):
    default_installdir = r"C:\\Program Files (x86)\\Microsoft Visual Studio\\2017\\Community"


VS2017 = VC141


class MsBuild4(VisualStudio):
    platform = None
    vcenv_bat = r"${VS110COMNTOOLS}..\..\VC\vcvarsall.bat"
    renderables = ['platform']

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

    def setupEnvironment(self, cmd):
        super().setupEnvironment(cmd)
        cmd.args['env']['VCENV_BAT'] = self.vcenv_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 "%s" /p:Configuration="%s" /p:Platform="%s" /maxcpucount'
                   % (self.projectfile, self.config, self.platform))

        if self.project is not None:
            command += ' /t:"%s"' % (self.project)
        elif self.mode == "build":
            command += ' /t:Build'
        elif self.mode == "clean":
            command += ' /t:Clean'
        elif self.mode == "rebuild":
            command += ' /t:Rebuild'

        self.setCommand(command)

        return super().start()


MsBuild = MsBuild4


class MsBuild12(MsBuild4):
    vcenv_bat = r"${VS120COMNTOOLS}..\..\VC\vcvarsall.bat"


class MsBuild14(MsBuild4):
    vcenv_bat = r"${VS140COMNTOOLS}..\..\VC\vcvarsall.bat"


class MsBuild141(VisualStudio):
    platform = None
    vcenv_bat = r"\\VC\\Auxiliary\\Build\vcvarsall.bat"
    renderables = ['platform']

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

    def setupEnvironment(self, cmd):
        super().setupEnvironment(cmd)
        cmd.args['env']['VCENV_BAT'] = self.vcenv_bat
        addEnvPath(cmd.args['env'], "PATH", 'C:\\Program Files (x86)\\Microsoft Visual Studio\\Installer\\')
        addEnvPath(cmd.args['env'], "PATH", r'${PATH}')

    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 = ('FOR /F "tokens=*" %%%%I in (\'vswhere.exe -property  installationPath\') do "%%%%I\\%%VCENV_BAT%%" x86 '
                  '&& msbuild "%s" /p:Configuration="%s" /p:Platform="%s" /maxcpucount' % (self.projectfile, self.config, self.platform))

        if self.project is not None:
            command += ' /t:"%s"' % (self.project)
        elif self.mode == "build":
            command += ' /t:Build'
        elif self.mode == "clean":
            command += ' /t:Clean'
        elif self.mode == "rebuild":
            command += ' /t:Rebuild'

        self.setCommand(command)

        return super().start()
