##
# @package xml
# @brief   XML parser focused on reading VDMS config files
# @author  Henry Daitx
# @date    Apr 2017
# @copyright Racelogic Ltd., 2017
#

"""
This module supplies an XML parser. It is focused on reading Racelogic VDMS
config files and does not try to be very general.
"""

class XMLSyntaxError(Exception):
    pass
class Parser:
    """
    XML parser. To parse a file, use xml.Parser().run('file.xml')
    """
    TEXT      = "TEXT"
    START_TAG = "START_TAG"
    END_TAG   = "END_TAG"
    PI        = "PI"
    ATTR      = "ATTR"

    class Tokenizer:
        def __init__(self, f):
            self.f = f
            self.nextch()

        def curch(self):
            return self.c

        def getch(self):
            c = self.c
            self.nextch()
            return c

        def eof(self):
            return self.c == ""

        def nextch(self):
            self.c = self.f.read(1)
            if not self.c:
                raise StopIteration
            return self.c

        def skip_ws(self):
            while self.curch().isspace():
                self.nextch()

        def isident(self):
            self.skip_ws()
            return self.curch().isalpha()

        def getident(self):
            self.skip_ws()
            ident = ""
            while True:
                c = self.curch()
                if not(c.isalpha() or c.isdigit() or c in "_-."):
                    break
                ident += self.getch()
            return ident

        def getnsident(self):
            ns = ""
            ident = self.getident()
            if self.curch() == ":":
                self.nextch()
                ns = ident
                ident = self.getident()
            return (ns, ident)

        def match(self, c):
            self.skip_ws()
            if self.curch() == c:
                self.nextch()
                return True
            return False

        def expect(self, c):
            if not self.match(c):
                raise XMLSyntaxError

        def lex_attrs_till(self):
            while self.isident():
                attr = self.getnsident()
                #yield (ATTR, attr)
                self.expect("=")
                self.expect('"')
                val = ""
                while self.curch() != '"':
                    val += self.getch()
                #yield (ATTR_VAL, val)
                self.expect('"')
                yield (Parser.ATTR, attr, val)

        def tokenize(self):
            while not self.eof():
                if self.match("<"):
                    if self.match("/"):
                        yield (Parser.END_TAG, self.getnsident())
                        self.expect(">")
                    elif self.match("?"):
                        yield (Parser.PI, self.getident())
                        yield from self.lex_attrs_till()
                        self.expect("?")
                        self.expect(">")
                    else:
                        tag = self.getnsident()
                        yield (Parser.START_TAG, tag)
                        yield from self.lex_attrs_till()
                        if self.match("/"):
                            yield (Parser.END_TAG, tag)
                        self.expect(">")
                else:
                    text = ""
                    while self.curch() != "<":
                        text += self.getch()
                    if text:
                        yield (Parser.TEXT, text)

    @staticmethod
    def decode_val(s):
        if s is None or s == '':
            return None
        try:
            return int(s)
        except ValueError:
            try:
                return float(s)
            except ValueError:
                if s == 'True':
                    return True
                elif s == 'False':
                    return False
                else:
                    # Plain string
                    return s

    def run(self, fn, verbose=False):
        """
        Run the parser.

            Parameters:
                fn          input file name (string)
            Optional parameters:
                verbose     print extra information (bool, default False)
            Output:
                list of tag_t objects
        """
        from ucollections import namedtuple
        # Make the type global so that it can be used outside the module
        global tag_t
        tag_t = namedtuple('tag', ('name', 'attrs', 'cont', 'tags'))
        v = []
        stack = []
        cur_tag = None
        with open(fn) as file:
            iter = 0
            try:
                for tok in Parser.Tokenizer(file).tokenize():
                    if tok[0] == Parser.START_TAG:
                        if cur_tag is not None:
                            stack.append(cur_tag)
                        cur_tag = [tok[1][1], {}, None, []]
                        if verbose:
                            iter += 1
                            print('iter:{}'.format(iter))
                    elif tok[0] == Parser.PI:
                        if verbose:
                            print('xml: ignoring PI')
                            iter += 1
                            print('iter:{}'.format(iter))
                        continue
                    elif tok[0] == Parser.ATTR:
                        if cur_tag is None:
                            if verbose:
                                print('xml: ignoring stray ATTR')
                                iter += 1
                                print('iter:{}'.format(iter))
                            continue
                        cur_tag[1][tok[1][1]] = Parser.decode_val(tok[2])
                        if verbose:
                            iter += 1
                            print('iter:{}'.format(iter))
                    elif tok[0] == Parser.TEXT and tok[1]:
                        if cur_tag is None:
                            if verbose:
                                print('xml: ignoring stray TEXT')
                                iter += 1
                                print('iter:{}'.format(iter))
                            continue
                        if cur_tag[2] is not None:
                            if verbose:
                                print('xml: ignoring more than one TEXT')
                                iter += 1
                                print('iter:{}'.format(iter))
                            continue
                        cur_tag[2] = tok[1]
                        if verbose:
                            iter += 1
                            print('iter:{}'.format(iter))
                    elif tok[0] == Parser.END_TAG:
                        if tok[1][1] != cur_tag[0]:
                            raise XMLSyntaxError('parsing error: mismatched nesting')
                        if len(stack):
                            parent = stack.pop()
                            parent[3].append(tag_t(*cur_tag))
                            cur_tag = parent
                        else:
                            v.append(tag_t(*cur_tag))
                            cur_tag = None
                        if verbose:
                            iter += 1
                            print('iter:{}, tag: {}'.format(iter, cur_tag))
                    else:
                        # Should never happen
                        raise XMLSyntaxError('parsing error: unknown token')
            except RuntimeError as error:
                pass # Not an error, raised Stop Iteration when file finished
        return v

if __name__ == "__main__":
    file_conf = Parser().run('/sd/Nissan_JP_Ex2.XML', True)[0] # test
