
"""
    Copyright (C) 2020 SunSpec Alliance

    Permission is hereby granted, free of charge, to any person obtaining a
    copy of this software and associated documentation files (the "Software"),
    to deal in the Software without restriction, including without limitation
    the rights to use, copy, modify, merge, publish, distribute, sublicense,
    and/or sell copies of the Software, and to permit persons to whom the
    Software is furnished to do so, subject to the following conditions:

    The above copyright notice and this permission notice shall be included
    in all copies or substantial portions of the Software.

    THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
    IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
    FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
    THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
    LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
    FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
    IN THE SOFTWARE.
"""

import os
import xml.etree.ElementTree as ET

import sunspec2.mdef as mdef

SMDX_ROOT = 'sunSpecModels'
SMDX_MODEL = mdef.MODEL
SMDX_BLOCK = 'block'
SMDX_POINT = 'point'
SMDX_ATTR_VERS = 'v'
SMDX_ATTR_ID = 'id'
SMDX_ATTR_LEN = 'len'
SMDX_ATTR_NAME = mdef.NAME
SMDX_ATTR_TYPE = mdef.TYPE
SMDX_ATTR_COUNT = mdef.COUNT
SMDX_ATTR_VALUE = mdef.VALUE
SMDX_ATTR_TYPE_FIXED = 'fixed'
SMDX_ATTR_TYPE_REPEATING = 'repeating'
SMDX_ATTR_OFFSET = 'offset'
SMDX_ATTR_MANDATORY = mdef.MANDATORY
SMDX_ATTR_ACCESS = mdef.ACCESS
SMDX_ATTR_SF = mdef.SF
SMDX_ATTR_UNITS = mdef.UNITS

SMDX_SYMBOL = 'symbol'
SMDX_COMMENT = 'comment'

SMDX_STRINGS = 'strings'
SMDX_ATTR_LOCALE = 'locale'
SMDX_LABEL = mdef.LABEL
SMDX_DESCRIPTION = 'description'
SMDX_NOTES = 'notes'
SMDX_DETAIL = mdef.DETAIL

SMDX_TYPE_INT16 = mdef.TYPE_INT16
SMDX_TYPE_UINT16 = mdef.TYPE_UINT16
SMDX_TYPE_COUNT = mdef.TYPE_COUNT
SMDX_TYPE_ACC16 = mdef.TYPE_ACC16
SMDX_TYPE_ENUM16 = mdef.TYPE_ENUM16
SMDX_TYPE_BITFIELD16 = mdef.TYPE_BITFIELD16
SMDX_TYPE_PAD = mdef.TYPE_PAD
SMDX_TYPE_INT32 = mdef.TYPE_INT32
SMDX_TYPE_UINT32 = mdef.TYPE_UINT32
SMDX_TYPE_ACC32 = mdef.TYPE_ACC32
SMDX_TYPE_ENUM32 = mdef.TYPE_ENUM32
SMDX_TYPE_BITFIELD32 = mdef.TYPE_BITFIELD32
SMDX_TYPE_IPADDR = mdef.TYPE_IPADDR
SMDX_TYPE_INT64 = mdef.TYPE_INT64
SMDX_TYPE_UINT64 = mdef.TYPE_UINT64
SMDX_TYPE_ACC64 = mdef.TYPE_ACC64
SMDX_TYPE_IPV6ADDR = mdef.TYPE_IPV6ADDR
SMDX_TYPE_FLOAT32 = mdef.TYPE_FLOAT32
SMDX_TYPE_STRING = mdef.TYPE_STRING
SMDX_TYPE_SUNSSF = mdef.TYPE_SUNSSF
SMDX_TYPE_EUI48 = mdef.TYPE_EUI48

SMDX_ACCESS_R = 'r'
SMDX_ACCESS_RW = 'rw'

SMDX_MANDATORY_FALSE = 'false'
SMDX_MANDATORY_TRUE = 'true'

smdx_access_types = {SMDX_ACCESS_R: mdef.ACCESS_R, SMDX_ACCESS_RW: mdef.ACCESS_RW}

smdx_mandatory_types = {SMDX_MANDATORY_FALSE: mdef.MANDATORY_FALSE, SMDX_MANDATORY_TRUE: mdef.MANDATORY_TRUE}

smdx_type_types = [
    SMDX_TYPE_INT16,
    SMDX_TYPE_UINT16,
    SMDX_TYPE_COUNT,
    SMDX_TYPE_ACC16,
    SMDX_TYPE_ENUM16,
    SMDX_TYPE_BITFIELD16,
    SMDX_TYPE_PAD,
    SMDX_TYPE_INT32,
    SMDX_TYPE_UINT32,
    SMDX_TYPE_ACC32,
    SMDX_TYPE_ENUM32,
    SMDX_TYPE_BITFIELD32,
    SMDX_TYPE_IPADDR,
    SMDX_TYPE_INT64,
    SMDX_TYPE_UINT64,
    SMDX_TYPE_ACC64,
    SMDX_TYPE_IPV6ADDR,
    SMDX_TYPE_FLOAT32,
    SMDX_TYPE_STRING,
    SMDX_TYPE_SUNSSF,
    SMDX_TYPE_EUI48
]

SMDX_PREFIX = 'smdx_'
SMDX_EXT = '.xml'


def to_smdx_filename(model_id):
    return '%s%05d%s' % (SMDX_PREFIX, int(model_id), SMDX_EXT)


def model_filename_to_id(filename):
    f = filename
    if '.' in f:
        f = os.path.splitext(f)[0]
    try:
        mid = int(f.rsplit('_', 1)[1])
    except ValueError:
        raise mdef.ModelDefinitionError('Error extracting model id from filename')

    return mid

'''
    smdx to json mapping:
    
      fixed block -> top level group
        model 'name' attribute -> group 'name'
        ID point is created for model ID and 'value' is the model ID value as a number
        L point is created for model len - model len has no value specified in the model definition
        fixed block points are placed in top level group
      repeating block -> group with count = 0 (indicates model len shoud be used to determine number of groups)
        repeating block 'name' -> group 'name', if no 'name' is defined 'name' = 'repeating'
    
      points:
        all type, access, and mandatory attributes are preserved
        point symbol map to the symbol object and placed in the symbols list for the point
          symbol 'name' attribute -> symbol object 'name'
          symbol element content -> symbol object 'value'
        strings 'label', 'description', 'notes' elements map to point attributes 'label', 'desc', 'detail'
'''


def from_smdx_file(filename):
    tree = ET.parse(filename)
    root = tree.getroot()
    return(from_smdx(root))


def from_smdx(element):
    """ Sets the model type attributes based on an element tree model type
    element contained in an SMDX model definition.

    Parameters:

        element :
            Element Tree model type element.
    """

    model_def = {}

    m = element.find(SMDX_MODEL)
    if m is None:
        raise mdef.ModelDefinitionError('Model definition not found')
    try:
        mid = mdef.to_number_type(m.attrib.get(SMDX_ATTR_ID))
    except ValueError:
        raise mdef.ModelDefinitionError('Invalid model id: %s' % m.attrib.get(SMDX_ATTR_ID))

    name = m.attrib.get(SMDX_ATTR_NAME)
    if name is None:
        name = 'model_' + str(mid)
    model_def[mdef.NAME] = name

    strings = element.find(SMDX_STRINGS)

    # create top level group with ID and L points
    fixed_def = {mdef.NAME: name,
                 mdef.TYPE: mdef.TYPE_GROUP,
                 mdef.POINTS: [
                     {mdef.NAME: 'ID', mdef.VALUE: mid,
                      mdef.DESCRIPTION: 'Model identifier', mdef.LABEL: 'Model ID', mdef.SIZE: 1,
                      mdef.MANDATORY: mdef.MANDATORY_TRUE, mdef.STATIC: mdef.STATIC_TRUE, mdef.TYPE: mdef.TYPE_UINT16},
                     {mdef.NAME: 'L',
                      mdef.DESCRIPTION: 'Model length', mdef.LABEL: 'Model Length', mdef.SIZE: 1,
                      mdef.MANDATORY: mdef.MANDATORY_TRUE, mdef.STATIC: mdef.STATIC_TRUE, mdef.TYPE: mdef.TYPE_UINT16}
                 ]
                 }

    repeating_def = None

    fixed = None
    repeating = None
    for b in m.findall(SMDX_BLOCK):
        btype = b.attrib.get(SMDX_ATTR_TYPE, SMDX_ATTR_TYPE_FIXED)
        if btype == SMDX_ATTR_TYPE_FIXED:
            if fixed is not None:
                raise mdef.ModelDefinitionError('Duplicate fixed block type definition')
            fixed = b
        elif btype == SMDX_ATTR_TYPE_REPEATING:
            if repeating is not None:
                raise mdef.ModelDefinitionError('Duplicate repeating block type definition')
            repeating = b
        else:
            raise mdef.ModelDefinitionError('Invalid block type: %s' % btype)

    fixed_points_map = {}
    if fixed is not None:
        points = []
        for e in fixed.findall(SMDX_POINT):
            point_def = from_smdx_point(e)
            if point_def[mdef.NAME] not in fixed_points_map:
                fixed_points_map[point_def[mdef.NAME]] = point_def
                points.append(point_def)
            else:
                raise mdef.ModelDefinitionError('Duplicate point definition: %s' % point_def[mdef.NAME])
        if points:
            fixed_def[mdef.POINTS].extend(points)

    repeating_points_map = {}
    if repeating is not None:
        name = repeating.attrib.get(SMDX_ATTR_NAME)
        if name is None:
            name = 'repeating'
        repeating_def = {mdef.NAME: name, mdef.TYPE: mdef.TYPE_GROUP, mdef.COUNT: 0}
        points = []
        for e in repeating.findall(SMDX_POINT):
            point_def = from_smdx_point(e)
            if point_def[mdef.NAME] not in repeating_points_map:
                repeating_points_map[point_def[mdef.NAME]] = point_def
                points.append(point_def)
            else:
                raise mdef.ModelDefinitionError('Duplicate point definition: %s' % point_def[mdef.NAME])
        if points:
            repeating_def[mdef.POINTS] = points
        fixed_def[mdef.GROUPS] = [repeating_def]

    e = element.find(SMDX_STRINGS)
    if e.attrib.get(SMDX_ATTR_ID) == str(mid):
        m = e.find(SMDX_MODEL)
        if m is not None:
            for a in m.findall('*'):
                if a.tag == SMDX_LABEL and a.text:
                    fixed_def[mdef.LABEL] = a.text
                elif a.tag == SMDX_DESCRIPTION and a.text:
                    fixed_def[mdef.DESCRIPTION] = a.text
                elif a.tag == SMDX_NOTES and a.text:
                    fixed_def[mdef.DETAIL] = a.text

        # Assign point info to point definitions
        for p in e.findall(SMDX_POINT):
            pid = p.attrib.get(SMDX_ATTR_ID)
            label = desc = notes = None
            for a in p.findall('*'):
                if a.tag == SMDX_LABEL and a.text:
                    label = a.text
                elif a.tag == SMDX_DESCRIPTION and a.text:
                    desc = a.text
                elif a.tag == SMDX_NOTES and a.text:
                    notes = a.text

            for points_map in [fixed_points_map, repeating_points_map]:
                point_def = points_map.get(pid)
                if point_def is None:
                    continue

                if label:
                    point_def[mdef.LABEL] = label
                if desc:
                    point_def[mdef.DESCRIPTION] = desc
                if notes:
                    point_def[mdef.DETAIL] = notes

                # Assign symbol info to the point's symbol definitions
                for s in p.findall(SMDX_SYMBOL):
                    sid = s.attrib.get(SMDX_ATTR_ID)
                    s_label = s_desc = s_notes = None
                    for a in s.findall('*'):
                        if a.tag == SMDX_LABEL and a.text:
                            s_label = a.text
                        elif a.tag == SMDX_DESCRIPTION and a.text:
                            s_desc = a.text
                        elif a.tag == SMDX_NOTES and a.text:
                            s_notes = a.text

                    for s in point_def.get(mdef.SYMBOLS):
                        if s[mdef.NAME] != sid:
                            continue

                        if s_label:
                            s[mdef.LABEL] = s_label
                        if s_desc:
                            s[mdef.DESCRIPTION] = s_desc
                        if s_notes:
                            s[mdef.DETAIL] = s_notes
                        break

    model_def = {'id': mid, 'group': fixed_def}
    return model_def


def from_smdx_point(element):
    """ Sets the point attributes based on an element tree point element
    contained in an SMDX model definition.

    Parameters:

        element :
            Element Tree point type element.

        strings :
            Indicates if *element* is a subelement of the 'strings'
            definintion within the model definition.
    """
    point_def = {}
    pid = element.attrib.get(SMDX_ATTR_ID)
    if pid is None:
        raise mdef.ModelDefinitionError('Missing point id attribute')
    point_def[mdef.NAME] = pid
    ptype = element.attrib.get(SMDX_ATTR_TYPE)
    if ptype is None:
        raise mdef.ModelDefinitionError('Missing type attribute for point: %s' % pid)
    elif ptype not in smdx_type_types:
        raise mdef.ModelDefinitionError('Unknown point type %s for point %s' % (ptype, pid))
    point_def[mdef.TYPE] = ptype
    plen = mdef.to_number_type(element.attrib.get(SMDX_ATTR_LEN))
    if ptype == SMDX_TYPE_STRING:
        if plen is None:
            raise mdef.ModelDefinitionError('Missing len attribute for point: %s' % pid)
        point_def[mdef.SIZE] = plen
    else:
        point_def[mdef.SIZE] = mdef.point_type_info.get(ptype)['len']
    mandatory = element.attrib.get(SMDX_ATTR_MANDATORY, SMDX_MANDATORY_FALSE)
    if mandatory not in smdx_mandatory_types:
        raise mdef.ModelDefinitionError('Unknown mandatory type: %s' % mandatory)
    if mandatory == SMDX_MANDATORY_TRUE:
        point_def[mdef.MANDATORY] = smdx_mandatory_types.get(mandatory)
    access = element.attrib.get(SMDX_ATTR_ACCESS, SMDX_ACCESS_R)
    if access not in smdx_access_types:
        raise mdef.ModelDefinitionError('Unknown access type: %s' % access)
    if access == SMDX_ACCESS_RW:
        point_def[mdef.ACCESS] = smdx_access_types.get(access)
    units = element.attrib.get(SMDX_ATTR_UNITS)
    if units:
        point_def[mdef.UNITS] = units
    # if scale factor is an number, convert to correct type
    sf = mdef.to_number_type(element.attrib.get(SMDX_ATTR_SF))
    if sf is not None:
        point_def[mdef.SF] = sf
    # if scale factor is an number, convert to correct type
    value = mdef.to_number_type(element.attrib.get(SMDX_ATTR_VALUE))
    if value is not None:
        point_def[mdef.VALUE] = value

    symbols = []
    for e in element.findall('*'):
        if e.tag == SMDX_SYMBOL:
            sid = e.attrib.get(SMDX_ATTR_ID)
            value = e.text
            try:
                value = int(value)
            except ValueError:
                pass
            symbols.append({mdef.NAME: sid, mdef.VALUE: value})
    if symbols:
        point_def[mdef.SYMBOLS] = symbols

    return point_def


def indent(elem, level=0):
    i = os.linesep + level*"  "
    if len(elem):
        if not elem.text or not elem.text.strip():
            elem.text = i + "  "
        if not elem.tail or not elem.tail.strip():
            elem.tail = i
        for elem in elem:
            indent(elem, level+1)
        if not elem.tail or not elem.tail.strip():
            elem.tail = i
    else:
        if level and (not elem.tail or not elem.tail.strip()):
            elem.tail = i
