#=============================================================================
#
#    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.
#
#=============================================================================

"""Python script to generate attribute definitions from ACDTool sample.rfpost file."""

import os
import re

import smtk
import smtk.attribute
import smtk.io

RE_SECTION = r'^(\w+)$'  # first line for section

class TokenType:
    START_SECTION = 'start-section'
    DATA = 'data'
    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.START_SECTION:
            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 Converter:
    """Class to convert sample.rfpost to .sbt file."""
    def __init__(self):
        """"""
        self.att_resource = None
        self.att_defn = None
        self.current_pos = 0  # current line number
        self.lines = []
        self.number_of_lines = 0
        self.path = None  # path to rfpost file

    def convert_rfpost(self, path, defn_type, categories=None):
        """Parse rfpost file and generate attribute resource.

        Arguments:
            path: filesystem path to rfpost file
            defn_type: type to assign new attribute definition

        Return:
            smtk::attribute::Resource

        Raises Exception on error
        """
        assert os.path.exists(path), 'ERROR Input file not found at {}'.format(path)

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

        # Reverse self.lines so that we can pop one line at a time until self.lines is empty
        self.lines.reverse()

        # Initialize attribute resource
        self.att_resource = smtk.attribute.Resource.create()
        self.att_defn = self.att_resource.createDefinition(defn_type)
        if categories is not None:
            if not isinstance(categories, set):
                categories = set(categories)
            cat_set = smtk.attribute.Categories.Set()
            cat_set.setInclusions(categories, smtk.attribute.Categories.CombinationMode.Any)
            self.att_defn.setLocalCategories(cat_set)

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

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

        # Call finalizeDefitions to set resource categories
        self.att_resource.finalizeDefinitions()
        return self.att_resource

    def _add_itemdef(self, group_defn, item_defn, comment=None):
        """Finishes code to append item definition to parent group definition."""
        if comment is not None:
            item_defn.setBriefDescription(comment)
        group_defn.addItemDefinition(item_defn)


    def _check_discrete(self, group_defn, name, string_value, comment):
        """Check if item has discrete info in the comment."""
        if name == 'rot180':
            # Special case
            item_defn = smtk.attribute.IntItemDefinition.New(name)
            item_defn.addDiscreteValue(0, 'no rotation')
            item_defn.addDiscreteValue(1, 'model rotated 180deg about z')
            item_defn.setDefaultDiscreteIndex(0)
            self._add_itemdef(group_defn, item_defn)
            return True

        elif name == 'reversePowerFlow':
            # Special case
            item_defn = smtk.attribute.IntItemDefinition.New(name)
            item_defn.addDiscreteValue(0, 'decaying')
            item_defn.addDiscreteValue(1, 'charging')
            item_defn.setDefaultDiscreteIndex(0)
            self._add_itemdef(group_defn, item_defn, comment)
            return True

        if comment is None:
            return False

        left_pos = comment.rfind('[')
        right_pos = comment.rfind(']')
        if left_pos < 0 or right_pos < 0:
            return False

        range_string = comment[left_pos+1:right_pos]
        range_list = range_string.split(',')
        if not range_list:
            return False

        item_defn = smtk.attribute.StringItemDefinition.New(name)
        for s in range_list:
            item_defn.addDiscreteValue(s.strip())
        default_index = item_defn.findDiscreteIndex(string_value.strip())
        if default_index >= 0:
            item_defn.setDefaultDiscreteIndex(default_index)
        self._add_itemdef(group_defn, item_defn, comment)
        return True

    def _create_defn(self, group_defn, name, string_value, comment):
        """Creates item definition from parsed line elements."""
        if self._check_discrete(group_defn, name, string_value, comment):
            return

        # Special case for optional items
        is_optional = False
        if name[0] == '[' and string_value.endswith(']'):
            name = name[1:]
            string_value = string_value[:-1]
            is_optional = True

        # Can value be converted to float?
        try:
            float_value = float(string_value)
        except ValueError:
            float_value = None

        if float_value is None:
            # Create string item
            item_def = smtk.attribute.StringItemDefinition.New(name)
            item_def.setDefaultValue(string_value)

        # Does it include a decimal point?
        elif string_value.find('.') >= 0:
            item_def = smtk.attribute.DoubleItemDefinition.New(name)
            item_def.setDefaultValue(float_value)

        # (else)
        else:
            item_def = smtk.attribute.IntItemDefinition.New(name)
            item_def.setDefaultValue(int(string_value))

        # print(self.current_pos, line, ':', name, eq_pos, predicate, string_value)
        if comment is not None:
            item_def.setBriefDescription(comment)
        if is_optional:
            item_def.setIsOptional(True)
            item_def.setIsEnabledByDefault(False)
        group_defn.addItemDefinition(item_def)


    def _finish_coaxport(self, group_defn):
        """coaxPort input is special case with 3 related items."""
        port_defn = smtk.attribute.GroupItemDefinition.New('port-info')
        port_defn.setIsExtensible(True)
        port_defn.setNumberOfRequiredGroups(1)
        group_defn.addItemDefinition(port_defn)

        id_defn = smtk.attribute.IntItemDefinition.New('portID')
        port_defn.addItemDefinition(id_defn)
        for name in ['porta', 'portb']:
            double_defn = smtk.attribute.DoubleItemDefinition.New(name)
            port_defn.addItemDefinition(double_defn)


    def _get_next_line(self):
        """"""
        while self.lines:
            raw_line = self.lines.pop()
            self.current_pos += 1
            line = raw_line.rstrip()
            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: start of section, end of section, end of input
        """
        line = self._get_next_line()
        while line is not None:
            match = re.match(RE_SECTION, line)
            if match:
                keyword = match.group(1)
                return TokenInfo(TokenType.START_SECTION,  self.current_pos, keyword=keyword)

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

            # line = self._get_next_line()
            return TokenInfo(TokenType.DATA, self.current_pos, rest_of_line=line)

        return TokenInfo(TokenType.NULL, self.current_pos)


    def _parse_item(self, line, group_defn):
        """"""
        if line.startswith('ionoff'):
            group_defn.setIsOptional(True)
            group_defn.setIsEnabledByDefault(False)
            return

        # Split contents at = sign
        eq_pos = line.find('=')
        if eq_pos < 1:
            raise RuntimeError('Cannot parse line {}: {}'.format(self.current_pos, line))
        name = line[:eq_pos].strip()

        # Special logic for coax port
        if name.startswith('port') and group_defn.name() == 'coaxPort':
            return

        predicate = line[eq_pos+1:].strip()
        sp_pos = predicate.find(' ')
        if sp_pos < 0:
            string_value = predicate
            comment = None
        else:
            string_value = predicate[:sp_pos]
            text = predicate[sp_pos:].strip()
            comment = text.lstrip('//')
        self._create_defn(group_defn, name, string_value, comment)


    def _parse_section(self, token_info):
        """Parse contents of section."""
        self._parse_section_start(token_info)
        # print('Todo parse section', token_info.keyword)

        group_defn = smtk.attribute.GroupItemDefinition.New(token_info.keyword)
        self.att_defn.addItemDefinition(group_defn)

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

            if next_info.type == TokenType.END_SECTION:
                # Special case
                if token_info.keyword == 'coaxPort':
                    self._finish_coaxport(group_defn)
                return

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

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

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

            line = next_info.rest_of_line.lstrip()
            self._parse_item(line, group_defn)


    def _parse_section_start(self, token_info):
        """Parses to the end of line with section start brace ('{')"""
        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')


if __name__ == '__main__':
    import argparse
    desc = 'Script for generating CMB template file (.sbt) from sample.rfpost input.'
    parser = argparse.ArgumentParser(description=desc)
    parser.add_argument('rfpost_file', help='path to sample.rfpost file')
    parser.add_argument('-c', '--categories', nargs='*', default=['Rf-Postprocess'], help='Categories to assign.')
    parser.add_argument('-n', '--name', default='RfPostprocess', help='Name of attribute definition')
    parser.add_argument('-o', '--output_file', default='rfpost.sbt', help='path to output sbt file')
    args = parser.parse_args()

    converter = Converter()
    try:
        att_resource = converter.convert_rfpost(args.rfpost_file, args.name, args.categories)
    except Exception:
        raise

    if att_resource is not None:
        writer = smtk.io.AttributeWriter()
        writer.includeResourceID(False)
        logger = smtk.io.Logger()
        writer.write(att_resource, args.output_file, logger)
        print('Wrote', args.output_file)
