#=============================================================================
#
#    Copyright (c) Kitware, Inc.
#    All rights reserved.
#    See LICENSE.txt for details.
#
#    This software is distributed WITHOUT ANY WARRANTY; without even
#    the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR
#    PURPOSE.    See the above copyright notice for more information.
#
#=============================================================================
import os
import re

RE_KEYWORD = r'^\s*(\w+)\s*(:)'  # left-hand side for either section or assignment

class TokenType:
    KEYWORD = 'keyword'
    END_SECTION = 'end-section'
    NULL = 'null'

class TokenInfo:
    def __init__(self, token_type, line_number, keyword=None, rest_of_line=None):
        self.type = token_type
        self.line_number = line_number
        self.keyword = keyword
        self.rest_of_line = rest_of_line

    def __str__(self):
        s = 'Token type {} line {}'.format(self.type, self.line_number)
        if self.type == TokenType.KEYWORD:
            s = ' keyword {} res_of_line \"{}\"'.format(s, self.keyword, self.rest_of_line)
        return s

class Section:
    """Stores contents of individual section"""
    def __init__(self, name, first_line):
        self.name = name
        self.first_line = first_line
        self.current_line = 0
        self.last_line = None
        self.children = list()    # children sections
        self.data = dict()    # <keyword, value>

    def __str__(self):
        return 'Section {} lines {}-{}'.format(
            self.name, self.first_line, self.last_line)


class ACE3PFileValidator:
    """"""
    def __init__(self, path):
        """"""
        self.current_pos = 0     # current line number
        self.exists = False      # file exists
        self.lines = list()      # file contents
        self.sections = dict()   # <keyword, set-of-sections>
        self.number_of_lines = 0

        self._parse_file(path)

    def file_exists(self):
        return self.exists

    def get_number_of_lines(self):
        return self.number_of_lines

    def get_sections(self, name):
        """Returns set of sections with given name, or None"""
        return self.sections.get(name)

    def has_section(self, name, min_count=1, max_count=1):
        """"""
        dict_item = self.sections.get(name)
        if dict_item is None:
            return False
        count = len(dict_item)
        if min_count is not None and count < min_count:
            return False
        if max_count is not None and count > max_count:
            return False
        # (else)
        return True

    def section_has_bool(self, section, keyword, bool_value):
        value = section.data.get(keyword)
        if value is None:
            print('{} section has no keyword {}'.format(section.name, keyword))
            return False
        match_value = '.true.' if bool_value else '.false'
        return value == match_value

    def section_has_double(self, section, keyword, double_value, tolerance=1e-6):
        value = section.data.get(keyword)
        if value is None:
            print('{} section has no keyword {}'.format(section.name, keyword))
            return False
        dvalue = float(value)  # raises ValueError if not valid
        return abs(dvalue - double_value) < tolerance

    def section_has_int(self, section, keyword, int_value):
        value = section.data.get(keyword)
        if value is None:
            print('{} section has no keyword {}'.format(section.name, keyword))
            return False
        ivalue = int(value)  # raises ValueError if not valid
        return ivalue == int_value

    def section_has_string(self, section, keyword, string_value):
        value = section.data.get(keyword)
        if value is None:
            print('{} section has no keyword {}'.format(section.name, keyword))
            return False
        return value == string_value


    def _get_next_line(self):
        """"""
        while self.lines:
            raw_line = self.lines.pop()
            self.current_pos += 1
            line = raw_line.strip()
            if len(line) == 0 or line.startswith('//'):
                continue
            # (else)
            # print(self.current_pos, line)
            return line

        # End of input
        return None

    def _get_next_token(self):
        """Parse input until next token

        Should be one of: keyword, end of section brace, end of input
        """
        line = self._get_next_line()
        while line is not None:
            match = re.match(RE_KEYWORD, line)
            if match:
                keyword = match.group(1)
                rest = line[match.end(2):].strip()
                return TokenInfo(TokenType.KEYWORD,  self.current_pos, keyword=keyword, rest_of_line=rest)

            match = re.match('\s*}', line)
            if match:
                return TokenInfo(TokenType.END_SECTION, self.current_pos)

            line = self._get_next_line()

        return TokenInfo(TokenType.NULL, self.current_pos)

    def _is_new_section(self, info):
        """"""
        rest = info.rest_of_line.strip()
        return rest == '' or rest.startswith('{')

    def _parse_file(self, path):
        if not os.path.exists(path):
            self.exists = False
            return;

        self.exists = True
        with open(path) as fp:
            self.lines = fp.readlines()
            self.number_of_lines = len(self.lines)

        # To traverse, reverse self.lines and pop one line at a time
        # until self.lines is empty
        self.lines.reverse()

        # current_section = None
        while self.lines:
            info = self._get_next_token()
            if info.type == TokenType.NULL:
                break

            if info.type == TokenType.KEYWORD:
                # print('* Keyword', info.keyword)
                if self._is_new_section(info):
                    self._parse_section(info)

    def _parse_section(self, token_info):
        """Parse contents of section, which can be sub-sections and/or assignments"""
        # print('** Start section', token_info.keyword)
        # Parse forward to the starting brace
        self._parse_section_start(token_info)

        # Initialize section
        name = token_info.keyword
        section = Section(name, token_info.line_number)
        dict_item = self.sections.get(name)
        if dict_item is None:
            dict_item = set()
        dict_item.add(section)
        self.sections[name] = dict_item

        # Continue parsing for assignments until ending brace
        next_info = self._get_next_token()
        while next_info:
            # print('next_info', next_info)

            if next_info.type == TokenType.END_SECTION:
                # print('**End section', name)
                return

            if next_info.type == TokenType.NULL:
                raise RuntimeError('Unexpected end of file parsing section')

            if not next_info.type == TokenType.KEYWORD:
                raise RuntimeError('Unexpected token type {}'.format(next_info.type))

            if self._is_new_section(next_info):
                # Parse subsection
                child_section = self._parse_section(next_info)
                section.children.append(child_section)
            else:
                # Add regular assignment to section data
                section.data[next_info.keyword] = next_info.rest_of_line

            next_info = self._get_next_token()

        return section

    def _parse_section_start(self, token_info):
        """Parses to the end of line with section start brace ('{')"""
        if token_info.rest_of_line.strip().startswith('{'):
            return

        line = self._get_next_line()
        while line:
            if line.strip().startswith('{'):
                return
            line = self._get_next_line()

        raise RuntimeError('expected left brace but reached end of file')
