## @file
#
# Copyright (c) 2011 - 2018, Intel Corporation. All rights reserved.<BR>
#
# This program and the accompanying materials are licensed and made available
# under the terms and conditions of the BSD License which accompanies this
# distribution. The full text of the license may be found at
# http://opensource.org/licenses/bsd-license.php
#
# THE PROGRAM IS DISTRIBUTED UNDER THE BSD LICENSE ON AN "AS IS" BASIS,
# WITHOUT WARRANTIES OR REPRESENTATIONS OF ANY KIND, EITHER EXPRESS OR IMPLIED.
#

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, *args, **kwargs)
        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, *args, **kwargs)

        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 '<br>\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
