# pylint: disable=invalid-name,missing-docstring
import re
import platform
from StringUtils import StringUtils
from SpecStructures import rpmMacro, dependentPackageData, Package
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 parseSpecFile(self, specfile):
        self._createDefaultPackage()
        currentPkg = "default"
        with open(specfile) as 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._updateSpecMacro(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

    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()) \
                or (macro in constants.getAdditionalMacros(self.packages["default"].name).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]
            elif macro in constants.getAdditionalMacros(self.packages["default"].name).keys():
                return constants.getAdditionalMacros(self.packages["default"].name)[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 parts
                retv = ""
                if _test_conditional(macro_name):  # ?
                    if _is_macro_defined(parts[0]):
                        if len(parts) == 2:
                            retv = parts[1]
                        else:
                            retv = _get_macro(parts[0])
                else:  # !
                    if not _is_macro_defined(parts[0]):
                        if len(parts) == 2:
                            retv = parts[1]
                return retv

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

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

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

    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 _updateSpecMacro(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:',
                           r'^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] in (">=", "<=", "=", "<", ">"):
                        compare = listContents[i+1]

                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 'source' in headerName:
            pkg.sources.append(headerContent)
            return True
        if 'patch' in headerName:
            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 not matchedSources:
            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"