import re
import platform
from StringUtils import StringUtils
from SpecStructures import *
from constants import constants

class SpecParser(object):

    def __init__(self):
        self.cleanMacro = rpmMacro().setName("clean")
        self.prepMacro = rpmMacro().setName("prep")
        self.buildMacro = rpmMacro().setName("build")
        self.installMacro = rpmMacro().setName("install")
        self.changelogMacro = rpmMacro().setName("changelog")
        self.checkMacro = rpmMacro().setName("check")
        self.packages = {}
        self.specAdditionalContent = ""
        self.globalSecurityHardening = ""
        self.defs = {}
        self.conditionalCheckMacroEnabled = False
        self.macro_pattern = re.compile(r'%{(\S+?)\}')


    def readPkgNameFromPackageMacro(self, data, basePkgName=None):
        data = " ".join(data.split())
        pkgHeaderName = data.split(" ")
        lenpkgHeaderName = len(pkgHeaderName)
        i = 1
        pkgName = None
        while i < lenpkgHeaderName:
            if pkgHeaderName[i] == "-n" and i+1 < lenpkgHeaderName:
                pkgName = pkgHeaderName[i+1]
                break
            if pkgHeaderName[i].startswith('-'):
                i = i + 2
            else:
                pkgName = basePkgName + "-" + pkgHeaderName[i]
                break
        if pkgName is None:
            return True, basePkgName
        return True, pkgName

    def replaceMacros(self, string):
        """Replace all macros in given string with corresponding values.

        For example: a string '%{name}-%{version}.tar.gz' will be transformed to 'foo-2.0.tar.gz'.

        :return A string where all macros in given input are substituted as good as possible.

        """
        def _is_conditional(macro):
            return macro.startswith(("?", "!"))

        def _test_conditional(macro):
            if macro[0] == "?":
                return True
            if macro[0] == "!":
                return False
            raise Exception("Given string is not a conditional macro")

        def _is_macro_defined(macro):
            return (macro in self.defs.keys()) or (macro in constants.userDefinedMacros.keys())

        def _get_macro(macro):
            if macro in self.defs.keys():
                return self.defs[macro]
            elif macro in constants.userDefinedMacros.keys():
                return constants.userDefinedMacros[macro]
            raise Exception("Unknown macro: " + macro)

        def _macro_repl(match):
            macro_name = match.group(1)
            if _is_conditional(macro_name):
                parts = macro_name[1:].split(":")
                assert len(parts) > 0
                if _test_conditional(macro_name):  # ?
                    if _is_macro_defined(parts[0]):
                        if len(parts) == 2:
                            return parts[1]
                        else:
                            return _get_macro(parts[0])
                    else:
                        return ""
                else:  # !
                    if not _is_macro_defined(parts[0]):
                        if len(parts) == 2:
                            return parts[1]
                    return ""

            if _is_macro_defined(macro_name):
                return _get_macro(macro_name)
            return match.string[match.start():match.end()]

        #User macros
        for macroName in constants.userDefinedMacros.keys():
            value = constants.userDefinedMacros[macroName]
            macro = "%" + macroName
            if string.find(macro) != -1:
                string = string.replace(macro, value)
        #Spec definitions
        for macroName in self.defs.keys():
            value = self.defs[macroName]
            macro = "%" + macroName
            if string.find(macro) != -1:
                string = string.replace(macro, value)
        return re.sub(self.macro_pattern, _macro_repl, string)

    def parseSpecFile(self, specfile):
        self.createDefaultPackage()
        currentPkg = "default"
        specFile = open(specfile)
        lines = specFile.readlines()
        totalLines = len(lines)
        i = 0
        while i < totalLines:
            line = lines[i].strip()
            if self.isConditionalArch(line):
                if platform.machine() != self.readConditionalArch(line):
                    # skip conditional body
                    deep = 1
                    while i < totalLines and deep != 0:
                        i = i + 1
                        line = lines[i].strip()
                        if self.isConditionalMacroStart(line):
                            deep = deep + 1
                        elif self.isConditionalMacroEnd(line):
                            deep = deep - 1
            elif self.isIfCondition(line):
                if not self.isConditionTrue(line):
                    # skip conditional body
                    deep = 1
                    while i < totalLines and deep != 0:
                        i = i + 1
                        line = lines[i].strip()
                        if self.isConditionalMacroStart(line):
                            deep = deep + 1
                        elif self.isConditionalMacroEnd(line):
                            deep = deep - 1
            elif self.isSpecMacro(line):
                macro, i = self.readMacroFromFile(i, lines)
                self.updateMacro(macro)
            elif self.isPackageMacro(line):
                defaultpkg = self.packages.get('default')
                returnVal, packageName = self.readPkgNameFromPackageMacro(line, defaultpkg.name)
                packageName = self.replaceMacros(packageName)
                if not returnVal:
                    return False
                if line.startswith('%package'):
                    pkg = Package(defaultpkg)
                    pkg.name = packageName
                    currentPkg = packageName
                    self.packages[pkg.name] = pkg
                else:
                    if defaultpkg.name == packageName:
                        packageName = 'default'
                    macro, i = self.readMacroFromFile(i, lines)
                    if packageName not in self.packages:
                        i = i + 1
                        continue
                    self.packages[packageName].updatePackageMacro(macro)
            elif self.isPackageHeaders(line):
                self.readPackageHeaders(line, self.packages[currentPkg])
            elif self.isGlobalSecurityHardening(line):
                self.readSecurityHardening(line)
            elif self.isChecksum(line):
                self.readChecksum(line, self.packages[currentPkg])
            elif self.isDefinition(line):
                self.readDefinition(line)
            elif self.isConditionalCheckMacro(line):
                self.conditionalCheckMacroEnabled = True
            elif self.conditionalCheckMacroEnabled and self.isConditionalMacroEnd(line):
                self.conditionalCheckMacroEnabled = False
            else:
                self.specAdditionalContent += line + "\n"
            i = i + 1
        specFile.close()

    def createDefaultPackage(self):
        pkg = Package()
        self.packages["default"] = pkg

    def readMacroFromFile(self, currentPos, lines):
        macro = rpmMacro()
        line = lines[currentPos]
        macro.position = currentPos
        macro.endposition = currentPos
        endPos = len(lines)
        line = " ".join(line.split())
        flagindex = line.find(" ")
        if flagindex != -1:
            macro.macroFlag = line[flagindex+1:]
            macro.macroName = line[:flagindex]
        else:
            macro.macroName = line

        if currentPos + 1 < len(lines) and self.isMacro(lines[currentPos+1]):
            return macro, currentPos

        for j in range(currentPos + 1, endPos):
            content = lines[j]
            if j+1 < endPos and self.isMacro(lines[j+1]):
                return macro, j
            macro.content += content +'\n'
            macro.endposition = j
        return macro, endPos

    def updateMacro(self, macro):
        if macro.macroName == "%clean":
            self.cleanMacro = macro
            return True
        if macro.macroName == "%prep":
            self.prepMacro = macro
            return True
        if macro.macroName == "%build":
            self.buildMacro = macro
            return True
        if macro.macroName == "%install":
            self.installMacro = macro
            return True
        if macro.macroName == "%changelog":
            self.changelogMacro = macro
            return True
        if macro.macroName == "%check":
            self.checkMacro = macro
            return True
        return False

    def isMacro(self, line):
        return (self.isPackageMacro(line) or
                self.isSpecMacro(line) or
                self.isConditionalMacroStart(line) or
                self.isConditionalMacroEnd(line))

    def isConditionalArch(self, line):
        if re.search('^'+'%ifarch', line):
            return True
        return False

    def isSpecMacro(self, line):
        if line.startswith(('%clean', '%prep', '%build', '%install', '%changelog', '%check')):
            return True
        return False

    def isPackageMacro(self, line):
        line = line.strip()
        if line.startswith(('%post', '%postun', '%files', '%description', '%package')):
            return True
        return False

    def isPackageHeaders(self, line):
        headersPatterns = ['^summary:', '^name:', '^group:',
                           '^license:', '^version:', '^release:',
                           '^distribution:', '^requires:',
                           '^requires\((pre|post|preun|postun)\):',
                           '^provides:', '^obsoletes:', '^conflicts:',
                           '^url:', '^source[0-9]*:', '^patch[0-9]*:',
                           '^buildrequires:', '^buildprovides:',
                           '^buildarch:']
        if any([re.search(r, line, flags=re.IGNORECASE) for r in headersPatterns]):
            return True
        return False

    def isGlobalSecurityHardening(self, line):
        if re.search('^%global *security_hardening', line, flags=re.IGNORECASE):
            return True
        return False

    def isChecksum(self, line):
        if re.search('^%define *sha1', line, flags=re.IGNORECASE):
            return True
        return False

    def isDefinition(self, line):
        if line.startswith(('%define', '%global')):
            return True
        return False

    def readConditionalArch(self, line):
        w = line.split()
        if len(w) == 2:
            return w[1]
        return None

    def readDefinition(self, line):
        listDefines = line.split()
        if len(listDefines) == 3:
            self.defs[listDefines[1]] = self.replaceMacros(listDefines[2])
            return True
        return False

    def readHeader(self, line):
        headerSplitIndex = line.find(":")
        if headerSplitIndex + 1 == len(line):
            print(line)
            print("Error:Invalid header")
            return False, None, None
        headerName = line[0:headerSplitIndex].lower()
        headerContent = line[headerSplitIndex + 1:].strip()
        return True, headerName, headerContent

    def readDependentPackageData(self, line):
        strUtils = StringUtils()
        listPackages = line.split(",")
        listdependentpkgs = []
        for line in listPackages:
            line = strUtils.getStringInConditionalBrackets(line)
            listContents = line.split()
            totalContents = len(listContents)
            i = 0
            while i < totalContents:
                dpkg = dependentPackageData()
                compare = None
                packageName = listContents[i]
                if listContents[i].startswith("/"):
                    provider = constants.providedBy.get(listContents[i], None)
                    i += 1
                    if provider is not None:
                        packageName = provider
                    else:
                        continue
                if i+2 < len(listContents):
                    if listContents[i+1] == ">=":
                        compare = "gte"
                    elif listContents[i+1] == "<=":
                        compare = "lte"
                    elif listContents[i+1] == "==":
                        compare = "eq"
                    elif listContents[i+1] == "<":
                        compare = "lt"
                    elif listContents[i+1] == ">":
                        compare = "gt"
                    elif listContents[i+1] == "=":
                        compare = "eq"

                if compare is not None:
                    dpkg.package = packageName
                    dpkg.compare = compare
                    dpkg.version = listContents[i+2]
                    i = i + 3
                else:
                    dpkg.package = packageName
                    i = i + 1
                listdependentpkgs.append(dpkg)
        return listdependentpkgs

    def readPackageHeaders(self, line, pkg):
        returnVal, headerName, headerContent = self.readHeader(line)
        if not returnVal:
            return False

        headerContent = self.replaceMacros(headerContent)
        if headerName == 'summary':
            pkg.summary = headerContent
            return True
        if headerName == 'name':
            pkg.name = headerContent
            if pkg == self.packages["default"]:
                self.defs["name"] = pkg.name
            return True
        if headerName == 'group':
            pkg.group = headerContent
            return True
        if headerName == 'license':
            pkg.license = headerContent
            return True
        if headerName == 'version':
            pkg.version = headerContent
            if pkg == self.packages["default"]:
                self.defs["version"] = pkg.version
            return True
        if headerName == 'buildarch':
            pkg.buildarch = headerContent
            return True
        if headerName == 'release':
            pkg.release = headerContent
            if pkg == self.packages["default"]:
                self.defs["release"] = pkg.release
            return True
        if headerName == 'distribution':
            pkg.distribution = headerContent
            return True
        if headerName == 'url':
            pkg.URL = headerContent
            return True
        if headerName.find('source') != -1:
            pkg.sources.append(headerContent)
            return True
        if headerName.find('patch') != -1:
            pkg.patches.append(headerContent)
            return True
        if (headerName.startswith('requires') or
                headerName == 'provides' or
                headerName == 'obsoletes' or
                headerName == 'conflicts' or
                headerName == 'buildrequires' or
                headerName == 'buildprovides'):
            dpkg = self.readDependentPackageData(headerContent)
            if dpkg is None:
                return False
            if headerName.startswith('requires'):
                pkg.requires.extend(dpkg)
            if headerName == 'provides':
                pkg.provides.extend(dpkg)
            if headerName == 'obsoletes':
                pkg.obsoletes.extend(dpkg)
            if headerName == 'conflicts':
                pkg.conflicts.extend(dpkg)
            if headerName == 'buildrequires':
                if self.conditionalCheckMacroEnabled:
                    pkg.checkbuildrequires.extend(dpkg)
                else:
                    pkg.buildrequires.extend(dpkg)
            if headerName == 'buildprovides':
                pkg.buildprovides.extend(dpkg)

            return True
        return False

    def readSecurityHardening(self, line):
        data = line.lower().strip()
        words = data.split(" ")
        nrWords = len(words)
        if nrWords != 3:
            print("Error: Unable to parse line: " + line)
            return False
        if words[2] != "none" and words[2] != "nonow" and words[2] != "nopie":
            print("Error: Invalid security_hardening value: " + words[2])
            return False
        self.globalSecurityHardening = words[2]
        return True

    def readChecksum(self, line, pkg):
        strUtils = StringUtils()
        line = self.replaceMacros(line)
        data = line.strip()
        words = data.split()
        nrWords = len(words)
        if nrWords != 3:
            print("Error: Unable to parse line: " + line)
            return False
        value = words[2].split("=")
        if len(value) != 2:
            print("Error: Unable to parse line: "+line)
            return False
        matchedSources = []
        for source in pkg.sources:
            sourceName = strUtils.getFileNameFromURL(source)
            if (sourceName.startswith(value[0])):
                matchedSources.append(sourceName)
        if (len(matchedSources) == 0):
            print("Error: Can not find match for sha1 " + value[0])
            return False
        if (len(matchedSources) > 1):
            print("Error: Too many matched Sources:" +
                  ' '.join(matchedSources) + " for sha1 " + value[0])
            return False
        pkg.checksums[sourceName] = value[1]
        return True;

    def isConditionalCheckMacro(self, line):
        data = line.strip()
        words = data.split()
        nrWords = len(words)
        if(nrWords != 2):
            return False
        if(words[0] != "%if" or words[1] != "%{with_check}"):
            return False
        return True

    def isIfCondition(self,line):
        return line.startswith("%if ")

    # Supports only %if %{}
    def isConditionTrue(self,line):
        data = line.strip()
        words = data.split()
        nrWords = len(words)
        # condition like %if a > b is not supported
        if(nrWords != 2):
            return True
        if (self.replaceMacros(words[1]) == "0"):
            return False
        return True

    def isConditionalMacroStart(self, line):
        return line.startswith("%if")

    def isConditionalMacroEnd(self, line):
        return (line.strip() == "%endif")