## @file
#
# Copyright (c) 2011 - 2018, Intel Corporation. All rights reserved.
#
# SPDX-License-Identifier: BSD-2-Clause-Patent
#
from __future__ import absolute_import
from .message import *
import re
import os
section_re = re.compile(r'^\[([\w., "]+)\]')
class BaseINIFile(object):
    _objs = {}
    def __new__(cls, *args, **kwargs):
        """Maintain only a single instance of this object
        @return: instance of this class
        """
        if len(args) == 0: return object.__new__(cls)
        filename = args[0]
        parent   = None
        if len(args) > 1:
            parent = args[1]
        key = os.path.normpath(filename)
        if key not in cls._objs.keys():
            cls._objs[key] = object.__new__(cls)
        if parent is not None:
            cls._objs[key].AddParent(parent)
        return cls._objs[key]
    def __init__(self, filename=None, parent=None):
        self._lines    = []
        self._sections = {}
        self._filename = filename
        self._globals  = []
        self._isModify = True
    def AddParent(self, parent):
        if parent is None: return
        if not hasattr(self, "_parents"):
            self._parents = []
        if parent in self._parents:
            ErrorMsg("Duplicate parent is found for INI file %s" % self._filename)
            return
        self._parents.append(parent)
    def GetFilename(self):
        return os.path.normpath(self._filename)
    def IsModified(self):
        return self._isModify
    def Modify(self, modify=True, obj=None):
        if modify == self._isModify: return
        self._isModify = modify
        if modify:
            for parent in self._parents:
                parent.Modify(True, self)
    def _ReadLines(self, filename):
        #
        # try to open file
        #
        if not os.path.exists(filename):
            return False
        try:
            handle = open(filename, 'r')
            self._lines  = handle.readlines()
            handle.close()
        except:
            raise EdkException("Fail to open file %s" % filename)
        return True
    def GetSectionInstance(self, parent, name, isCombined=False):
        return BaseINISection(parent, name, isCombined)
    def GetSectionByName(self, name):
        arr = []
        for key in self._sections.keys():
            if '.private' in key:
                continue
            for item in self._sections[key]:
                if item.GetBaseName().lower().find(name.lower()) != -1:
                    arr.append(item)
        return arr
    def GetSectionObjectsByName(self, name):
        arr = []
        sects = self.GetSectionByName(name)
        for sect in sects:
            for obj in sect.GetObjects():
                arr.append(obj)
        return arr
    def Parse(self):
        if not self._isModify: return True
        if not self._ReadLines(self._filename): return False
        sObjs    = []
        inGlobal = True
        # process line
        for index in range(len(self._lines)):
            templine = self._lines[index].strip()
            # skip comments
            if len(templine) == 0: continue
            if re.match("^\[=*\]", templine) or re.match("^#", templine) or \
               re.match("\*+/", templine):
                continue
            m = section_re.match(templine)
            if m is not None: # found a section
                inGlobal = False
                # Finish the latest section first
                if len(sObjs) != 0:
                    for sObj in sObjs:
                        sObj._end = index - 1
                        if not sObj.Parse():
                            ErrorMsg("Fail to parse section %s" % sObj.GetBaseName(),
                                     self._filename,
                                     sObj._start)
                # start new section
                sname_arr = m.groups()[0].split(',')
                sObjs = []
                for name in sname_arr:
                    sObj = self.GetSectionInstance(self, name, (len(sname_arr) > 1))
                    sObj._start = index
                    sObjs.append(sObj)
                    if name.lower() not in self._sections:
                        self._sections[name.lower()] = [sObj]
                    else:
                        self._sections[name.lower()].append(sObj)
            elif inGlobal:  # not start any section and find global object
                gObj = BaseINIGlobalObject(self)
                gObj._start = index
                gObj.Parse()
                self._globals.append(gObj)
        # Finish the last section
        if len(sObjs) != 0:
            for sObj in sObjs:
                sObj._end = index
                if not sObj.Parse():
                    ErrorMsg("Fail to parse section %s" % sObj.GetBaseName(),
                             self._filename,
                             sObj._start)
        self._isModify = False
        return True
    def Destroy(self, parent):
        # check referenced parent
        if parent is not None:
            assert parent in self._parents, "when destory ini object, can not found parent reference!"
            self._parents.remove(parent)
        if len(self._parents) != 0: return
        for sects in self._sections.values():
            for sect in sects:
                sect.Destroy()
        # dereference from _objs array
        assert self.GetFilename() in self._objs.keys(), "When destroy ini object, can not find obj reference!"
        assert self in self._objs.values(), "When destroy ini object, can not find obj reference!"
        del self._objs[self.GetFilename()]
        # dereference self
        self.Clear()
    def GetDefine(self, name):
        sects = self.GetSectionByName('Defines')
        for sect in sects:
            for obj in sect.GetObjects():
                line = obj.GetLineByOffset(obj._start).split('#')[0].strip()
                arr = line.split('=')
                if arr[0].strip().lower() == name.strip().lower():
                    return arr[1].strip()
        return None
    def Clear(self):
        for sects in self._sections.values():
            for sect in sects:
                del sect
        self._sections.clear()
        for gObj in self._globals:
            del gObj
        del self._globals[:]
        del self._lines[:]
    def Reload(self):
        self.Clear()
        ret = self.Parse()
        if ret:
            self._isModify = False
        return ret
    def AddNewSection(self, sectName):
        if sectName.lower() in self._sections.keys():
            ErrorMsg('Section %s can not be created for conflict with existing section')
            return None
        sectionObj = self.GetSectionInstance(self, sectName)
        sectionObj._start = len(self._lines)
        sectionObj._end   = len(self._lines) + 1
        self._lines.append('[%s]\n' % sectName)
        self._lines.append('\n\n')
        self._sections[sectName.lower()] = sectionObj
        return sectionObj
    def CopySectionsByName(self, oldDscObj, nameStr):
        sects = oldDscObj.GetSectionByName(nameStr)
        for sect in sects:
            sectObj = self.AddNewSection(sect.GetName())
            sectObj.Copy(sect)
    def __str__(self):
        return ''.join(self._lines)
    ## Get file header's comment from basic INI file.
    #  The file comments has two style:
    #  1) #/** @file
    #  2) ## @file
    #
    def GetFileHeader(self):
        desc = []
        lineArr  = self._lines
        inHeader = False
        for num in range(len(self._lines)):
            line = lineArr[num].strip()
            if not inHeader and (line.startswith("#/**") or line.startswith("##")) and \
                line.find("@file") != -1:
                inHeader = True
                continue
            if inHeader and (line.startswith("#**/") or line.startswith('##')):
                inHeader = False
                break
            if inHeader:
                prefixIndex = line.find('#')
                if prefixIndex == -1:
                    desc.append(line)
                else:
                    desc.append(line[prefixIndex + 1:])
        return '
\n'.join(desc)
class BaseINISection(object):
    def __init__(self, parent, name, isCombined=False):
        self._parent     = parent
        self._name       = name
        self._isCombined = isCombined
        self._start      = 0
        self._end        = 0
        self._objs       = []
    def __del__(self):
        for obj in self._objs:
            del obj
        del self._objs[:]
    def GetName(self):
        return self._name
    def GetObjects(self):
        return self._objs
    def GetParent(self):
        return self._parent
    def GetStartLinenumber(self):
        return self._start
    def GetEndLinenumber(self):
        return self._end
    def GetLine(self, linenumber):
        return self._parent._lines[linenumber]
    def GetFilename(self):
        return self._parent.GetFilename()
    def GetSectionINIObject(self, parent):
        return BaseINISectionObject(parent)
    def Parse(self):
        # skip first line in section, it is used by section name
        visit = self._start + 1
        iniObj = None
        while (visit <= self._end):
            line = self.GetLine(visit).strip()
            if re.match("^\[=*\]", line) or re.match("^#", line) or len(line) == 0:
                visit += 1
                continue
            line = line.split('#')[0].strip()
            if iniObj is not None:
                if line.endswith('}'):
                    iniObj._end = visit - self._start
                    if not iniObj.Parse():
                        ErrorMsg("Fail to parse ini object",
                                 self.GetFilename(),
                                 iniObj.GetStartLinenumber())
                    else:
                        self._objs.append(iniObj)
                    iniObj = None
            else:
                iniObj = self.GetSectionINIObject(self)
                iniObj._start = visit - self._start
                if not line.endswith('{'):
                    iniObj._end = visit - self._start
                    if not iniObj.Parse():
                        ErrorMsg("Fail to parse ini object",
                                 self.GetFilename(),
                                 iniObj.GetStartLinenumber())
                    else:
                        self._objs.append(iniObj)
                    iniObj = None
            visit += 1
        return True
    def Destroy(self):
        for obj in self._objs:
            obj.Destroy()
    def GetBaseName(self):
        return self._name
    def AddLine(self, line):
        end = self.GetEndLinenumber()
        self._parent._lines.insert(end, line)
        self._end += 1
    def Copy(self, sectObj):
        index = sectObj.GetStartLinenumber() + 1
        while index < sectObj.GetEndLinenumber():
            line = sectObj.GetLine(index)
            if not line.strip().startswith('#'):
                self.AddLine(line)
            index += 1
    def AddObject(self, obj):
        lines = obj.GenerateLines()
        for line in lines:
            self.AddLine(line)
    def GetComment(self):
        comments = []
        start  = self._start - 1
        bFound = False
        while (start > 0):
            line = self.GetLine(start).strip()
            if len(line) == 0:
                start -= 1
                continue
            if line.startswith('##'):
                bFound = True
                index = line.rfind('#')
                if (index + 1) < len(line):
                    comments.append(line[index + 1:])
                break
            if line.startswith('#'):
                start -= 1
                continue
            break
        if bFound:
            end = start + 1
            while (end < self._start):
                line = self.GetLine(end).strip()
                if len(line) == 0: break
                if not line.startswith('#'): break
                index = line.rfind('#')
                if (index + 1) < len(line):
                    comments.append(line[index + 1:])
                end += 1
        return comments
class BaseINIGlobalObject(object):
    def __init__(self, parent):
        self._start = 0
        self._end   = 0
    def Parse(self):
        return True
    def __str__(self):
        return parent._lines[self._start]
    def __del__(self):
        pass
class BaseINISectionObject(object):
    def __init__(self, parent):
        self._start  = 0
        self._end    = 0
        self._parent = parent
    def __del__(self):
        self._parent = None
    def GetParent(self):
        return self._parent
    def GetFilename(self):
        return self.GetParent().GetFilename()
    def GetPackageName(self):
        return self.GetFilename()
    def GetFileObj(self):
        return self.GetParent().GetParent()
    def GetStartLinenumber(self):
        return self.GetParent()._start + self._start
    def GetLineByOffset(self, offset):
        sect_start = self._parent.GetStartLinenumber()
        linenumber = sect_start + offset
        return self._parent.GetLine(linenumber)
    def GetLinenumberByOffset(self, offset):
        return offset + self._parent.GetStartLinenumber()
    def Parse(self):
        return True
    def Destroy(self):
        pass
    def __str__(self):
        return self.GetLineByOffset(self._start).strip()
    def GenerateLines(self):
        return ['default setion object string\n']
    def GetComment(self):
        comments = []
        start  = self.GetStartLinenumber() - 1
        bFound = False
        while (start > 0):
            line = self.GetParent().GetLine(start).strip()
            if len(line) == 0:
                start -= 1
                continue
            if line.startswith('##'):
                bFound = True
                index = line.rfind('#')
                if (index + 1) < len(line):
                    comments.append(line[index + 1:])
                break
            if line.startswith('#'):
                start -= 1
                continue
            break
        if bFound:
            end = start + 1
            while (end <= self.GetStartLinenumber() - 1):
                line = self.GetParent().GetLine(end).strip()
                if len(line) == 0: break
                if not line.startswith('#'): break
                index = line.rfind('#')
                if (index + 1) < len(line):
                    comments.append(line[index + 1:])
                end += 1
        return comments