
"""
    Copyright (C) 2017 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 math

try:
    import xml.etree.ElementTree as ET
except:
    import elementtree.ElementTree as ET

import sunspec.core.pics as pics
import sunspec.core.smdx as smdx
import sunspec.core.suns as suns
from sunspec.core.util import SunSpecError

# file path list
file_pathlist = None

MAX_READ_COUNT = 125

class Device(object):

    def __init__(self, addr=suns.SUNS_BASE_ADDR_DEFAULT):

        self.base_addr = addr
        self.models_list = []
        self.models = {}               # dict of model arrays to support more than one instance of a model

    def add_model(self, model):

        models = self.models.get(model.id)
        if models is None:
            self.models[model.id] = []
            models = self.models.get(model.id)
        models.append(model)
        self.models_list.append(model)

    def from_pics(self, element=None, filename=None, pathlist=None):

        global file_pathlist

        pics_data = ''

        try:
            if element is None:
                # try supplied path list
                if pathlist is not None:
                    try:
                        pics_data = pathlist.read(filename)
                    except NameError:
                        pass

                # try device file path list
                if not pics_data and file_pathlist is not None:
                    try:
                        pics_data = file_pathlist.read(filename)
                    except NameError:
                        pass

                # try local directory
                if not pics_data:
                    f = open(filename, 'r')
                    pics_data = f.read()
                    f.close()

                root = ET.fromstring(pics_data)
                if root.tag != pics.PICS_ROOT:
                    raise SunSpecError("Unexpected root element: %s" % (root.tag))

                d = root.find(pics.PICS_DEVICE)
                if d is None:
                    raise SunSpecError("No '%s' elements found in '%s' element" % (pics.PICS_DEVICE, root.tag))
            else:
                d = element
            if d.tag != pics.PICS_DEVICE:
                raise SunSpecError("Unexpected device tag: '%s'" % (d.tag))
            self.base_addr = d.attrib.get(pics.PICS_ATTR_BASE_ADDR, pics.PICS_BASE_ADDR_DEFAULT)
            addr = self.base_addr + 2

            for m in d.findall('*'):
                if m is None:
                    raise SunSpecError("No '%s' elements found in '%s' element" % (pics.PICS_MODEL, d.tag))
                if m.tag != pics.PICS_MODEL:
                    raise SunSpecError("Unexpected '%s' element in '%s' element" % (m.tag, d.tag))

                model_id = m.attrib.get(pics.PICS_ATTR_ID)
                if model_id is None:
                    raise SunSpecError('Module id error')
                model_len = m.attrib.get(pics.PICS_ATTR_LEN)
                if model_len is not None:
                    # raise SunSpecError('Module len error in model %d' % (model_id))
                    model_len = int(model_len)

                # move address past model id and length (even though address is not real in the case)
                model = Model(self, model_id, addr + 2, model_len)
                try:
                    model.load()
                except Exception, e:
                    model.load_error = str(e)
                model.from_pics(m)
                self.add_model(model)

                addr += model.len + 2
        
        except Exception, e:
            raise SunSpecError('Error loading PICS: %s' % str(e))

    """
    def to_pics(self, pretty_print=False, single_repeating=True):

        attr = {pics.PICS_ATTR_VERSION: str(pics.PICS_VERSION)}

        root = ET.Element(pics.PICS_ROOT)
        e = ET.SubElement(root, pics.PICS_DEVICE, attrib=attr)

        for model in self.models_list:
            model.to_pics(e, single_repeating=single_repeating)

        if pretty_print:
            util.indent(root)

        return ET.tostring(root)
    """

    def to_pics(self, parent, single_repeating=True):

        attr = {pics.PICS_ATTR_VERSION: str(pics.PICS_VERSION)}

        e = ET.SubElement(parent, pics.PICS_DEVICE, attrib=attr)

        for model in self.models_list:
            model.to_pics(e, single_repeating=single_repeating)

    def not_equal(self, device):

        if len(self.models_list) != len(device.models_list):
            return 'Devices not equal - model counts: %d  %d' % (len(self.models_list), len(device.models_list))
        for i in range(len(self.models_list)):
            s = self.models_list[i].not_equal(device.models_list[i])
            if s:
                return 'Devices not equal - %s' % (s)
        return False

    def __str__(self):

        device_str = ''
        for model in self.models_list:
            device_str += str(model)
        return device_str

class Block(object):

    def __init__(self, model, addr, blen, block_type, index=1):

        self.model = model
        self.block_type = block_type
        self.addr = addr
        self.len = blen
        self.type = block_type.type
        self.index = index
        self.points_list = []
        self.points = {}
        self.points_sf = {}

    def from_pics(self, element):

        for p in element.findall('*'):
            if p.tag != pics.PICS_POINT:
                raise SunSpecError("Unexpected '%s' element in '%s' element" % (p.tag, element.tag))
            pid = p.attrib.get(pics.PICS_ATTR_ID)
            point = self.points.get(pid)
            if point is None:
                point = self.points_sf.get(pid)
            if point is not None:
                point.from_pics(p)

        # resolve scale factor values in points, must be done after all points in block are read
        for point in self.points_list:
            if point.sf_point is not None:
                point.value_sf = point.sf_point.value_base

    def to_pics(self, parent):

        attr = {}

        if self.index > 1:
            attr[pics.PICS_ATTR_INDEX] = str(self.index)

        if self.block_type.type == suns.SUNS_BLOCK_REPEATING:
            attr[pics.PICS_ATTR_TYPE] = pics.PICS_TYPE_REPEATING

        e = ET.SubElement(parent, pics.PICS_BLOCK, attrib=attr)

        # use block type points list to preserve original order of points in the block
        for pt in self.block_type.points_list:
            point = self.points.get(pt.id)
            if point is None:
                point = self.points_sf.get(pt.id)
            if point is not None:
                point.to_pics(e)

    def not_equal(self, block):

        s = self.block_type.not_equal(block.block_type)
        if s:
            return 'block %s not equal - block type not equal: %s' % (self.block_type.type, s)

        for point in self.points_list:
            s = point.not_equal(block.points.get(point.point_type.id))
            if s:
                return 'block %d not equal - %s' % (self.index, s)
        return False

    def __str__(self):

        block_str = 'Block: type: %s index: %d:\n' % (self.type, self.index)
        for point in self.points_list:
            block_str += '  ' + str(point) + '\n'
        return block_str

class Point(object):

    def __init__(self, block=None, point_type=None, addr=None, sf_point=None, value=None):

        self.block = block
        self.point_type = point_type
        self.addr = addr
        self.sf_point = sf_point
        self.impl = True
        self.value_base = value
        self.value_sf = None
        self.dirty = False

    """ 
    @property
    def value(self):
        if self.value_sf:
            return self.value_base * math.pow(10, self.value_sf)
        else:
            return self.value_base

    @value.setter
    def value(self, v):
        if self.value_sf:
            self.value_base = int(round(float(v), abs(self.value_sf)) / math.pow(10, self.value_sf))
        else:
            self.value_base = v
        self.dirty = True

    @property
    def value_str(self):
        if self.value_sf:
            pass
        else:
            return str(self.value_base)

    @value.setter
    def value_str(self, v):
        if self.value_sf:
            pass
        else:
            self.value_base = int(v)
    """

    # use older property format to support earlier python 2.x versions
    def value_getter(self):

        if self.value_sf:
            return self.value_base * math.pow(10, self.value_sf)
        else:
            return self.value_base

    def value_setter(self, v):

        if self.value_sf:
            self.value_base = int(round(float(v), abs(self.value_sf)) / math.pow(10, self.value_sf))
        else:
            self.value_base = self.point_type.to_value(v)

        self.dirty = True
    
    value = property(value_getter, value_setter, None)

    def from_pics(self, element):

        impl = True
        impl_attr = element.attrib.get(pics.PICS_ATTR_IMPLEMENTED)
        if impl_attr:
            if impl_attr == pics.PICS_IMPLEMENTED_FALSE:
                impl = False

        value = None
        if impl:
            if element.text:
                value = self.point_type.to_value(element.text)
                self.impl = self.point_type.is_impl(value)
        else:
            self.impl = False

        if self.impl and value is not None:
            self.value_base = value

    def to_pics(self, parent):

        attr = {pics.PICS_ATTR_ID: str(self.point_type.id)}

        if self.value is None:
            attr[pics.PICS_ATTR_IMPLEMENTED] = str(pics.PICS_IMPLEMENTED_FALSE)
        else:
            if self.point_type.access != suns.SUNS_ACCESS_R:
                access =  [key for key, value in pics.pics_access_types.iteritems() if value == suns.SUNS_ACCESS_RW][0]
                attr[pics.PICS_ATTR_ACCESS] = str(access)

        e = ET.SubElement(parent, pics.PICS_POINT, attrib=attr)
        if self.value_base is not None:
            e.text = str(self.value_base).rstrip('\0')

    def not_equal(self, point):

        s = self.point_type.not_equal(point.point_type)
        if s:
            return 'point %s not equal - point type not equal: %s' % (self.point_type.id, s)

        if (((self.value_base is not None or point.value_base is not None) and (self.value_base != point.value_base)) or
            ((self.value_sf is not None or point.value_sf is not None) and (self.value_sf != point.value_sf))):
            if self.value_base is not None:
                print 'self.value_base'
            if point.value_base is not None:
                print 'point.value_base', type(point.value_base), point.value_base
            return 'point %s not equal: %s %s - %s %s' % (self.point_type.id, self.value_base, self.value_sf, point.value_base, point.value_sf)
        return False
        
    def __str__(self):
        point_str = 'Point: id = %s impl= %s addr = %s value_base = %s' % (self.point_type.id, str(self.impl), self.addr, str(self.value_base))
        if self.sf_point is not None:
            point_str += ' sf_value = %s' % (str(self.sf_point.value_base))
        return point_str

class ScaleFactor(object):

    def __init__(self, value=None):

        self.value_base = value

class Model(object):

    def __init__(self, device=None, mid=None, addr=0, mlen=0, index=1):

        self.device = device
        self.id = int(mid)
        self.index = index
        self.model_type = None
        self.addr = addr               # modbus address of first point in the model
        self.len = int(mlen)           # register count of the point elements in the model
        self.points_list = []          # fixed block non-scale factor points list ordered by offset
        self.points = {}               # fixed block non-scale factor points
        self.points_sf = {}            # fixed block scale factor points
        self.blocks = []
        self.load_error = None
        self.read_blocks = []

    def load(self, block_class=Block, point_class=Point):

        last_read_addr = self.addr
        self.read_blocks.append(last_read_addr)

        self.model_type = model_type_get(self.id)

        if self.model_type is not None:
            if self.len == 0:
                self.len = self.model_type.len
            end_addr = self.addr + self.len

            index = 0
            # model type always has a fixed block defined
            block_type = self.model_type.fixed_block
            block_addr = self.addr
            block_len = int(block_type.len)
            # adjustment for legacy common model len = 65
            if self.id == 1 and self.len == 65:
                block_len = self.len

            # while another block
            while end_addr >= block_addr + block_len:
                block = block_class(self, block_addr, block_len, block_type, index)
                self.blocks.append(block)

                for point_type in block_type.points_list:
                    if point_type.type != suns.SUNS_TYPE_PAD:
                        point_addr = int(block_addr) + int(point_type.offset)
                        point = point_class(block, point_type, str(point_addr))
                        if point_addr + point.point_type.len - last_read_addr > MAX_READ_COUNT:
                            last_read_addr = point_addr
                            self.read_blocks.append(last_read_addr)
                        if point_type.type == suns.SUNS_TYPE_SUNSSF:
                            block.points_sf[point_type.id] = point
                        else:
                            block.points_list.append(point)
                            block.points[point_type.id] = point

                # resolve scale factor addresses for repeating block
                for point in block.points_list:
                    if point.point_type.sf is not None and point.sf_point is None:
                        # check for constant scale factor
                        try:
                            sf_value = int(point.point_type.sf)
                            point.sf_point = ScaleFactor(sf_value)
                        except Exception:
                            pass
                        # try local repeating block first
                        if point.sf_point is None:
                            point.sf_point = block.points_sf.get(point.point_type.sf)
                        if point.sf_point is None:
                            # if repeating block, try fixed block
                            if index > 0:
                                point.sf_point = self.blocks[0].points_sf.get(point.point_type.sf)
                        if point.sf_point is None:
                            # ### what state should model be left in on exception
                            raise SunSpecError('Unable to resolve scale factor point %s for point %s in model %s' %
                                point.point_type.sf, point.point_type.pid, self.id)

                block_addr += block_len
                block_type = self.model_type.repeating_block
                if block_type is None:
                    ### check for extra registers?
                    break
                index += 1
                block_len = int(block_type.len)
        else:
            raise SunSpecError('Unknown model type - id: %s' % str(self.id))

        # expose fixed block points at model level if present
        try:
            self.points_list = self.blocks[0].points_list
            self.points = self.blocks[0].points
            self.points_sf = self.blocks[0].points_sf
        except IndexError:
            pass

    def from_pics(self, element):

        # update index if present
        self.index = element.attrib.get(pics.PICS_ATTR_INDEX, self.index)

        for b in element.findall('*'):
            if b.tag != pics.PICS_BLOCK:
                raise SunSpecError("Unexpected '%s' element in '%s' element" % (b.tag, element.tag))
            block_type = pics.pics_block_types.get(b.attrib.get(pics.PICS_ATTR_TYPE, pics.PICS_TYPE_FIXED))
            if block_type is None:
                raise SunSpecError('Unknown block type')
            if block_type == suns.SUNS_BLOCK_FIXED:
                if len(self.blocks) > 0:
                    self.blocks[0].from_pics(b)
            elif block_type == suns.SUNS_BLOCK_REPEATING:
                block_index = b.attrib.get(pics.PICS_ATTR_INDEX)
                # if no index specified, apply to all repeating blocks
                if block_index is None:
                    if len(self.blocks) > 1:
                        for block in self.blocks[1:]:
                            block.from_pics(b)
                else:
                    block_index = int(block_index)
                    if len(self.blocks) < block_index:
                        raise SunSpecError('Block index out of range: %s' % (str(block_index)))
                    self.blocks[block_index].from_pics(b)
            else:
                raise SunSpecError('Internal block type error')

    def to_pics(self, parent, single_repeating=True):

        attr = {pics.PICS_ATTR_ID: str(self.id), pics.PICS_ATTR_LEN: str(self.len)}

        if self.index != 1:
            attr[pics.PICS_ATTR_INDEX] = str(self.index)

        e = ET.SubElement(parent, pics.PICS_MODEL, attrib=attr)

        for block in self.blocks:
            if single_repeating == False or block.index <= 1:
                block.to_pics(e)

    def not_equal(self, model):

        if len(self.blocks) != len(model.blocks):
            return 'model %s not equal - block counts: %d  %d' % (self.model_type.id, len(self.blocks), len(model.blocks))
        s = self.model_type.not_equal(model.model_type)
        if s:
            return 'model %s not equal - model id not equal: %s' % (self.model_type.id, s)

        for i in range(len(self.blocks)):
            s = self.blocks[i].not_equal(model.blocks[i])
            if s:
                return 'model %s not equal - %s' % (self.model_type.id, s)
        return False

    def __str__(self):

        model_str = 'Model %s:\n' % self.id
        for point in self.points_list:
            model_str += '  ' + str(point) + '\n'
        for block in self.blocks[1:]:
            model_str += str(block)
        return model_str


model_type_path_default = os.path.join(os.path.dirname(os.path.abspath(__file__)), '..', 'models', 'smdx')
model_types = {}

def model_type_get(model_id):

    global file_pathlist
    global model_types

    model_type = model_types.get(str(model_id))
    if model_type is None:
        smdx_data = ''
        # create model file name
        filename = smdx.model_id_to_filename(model_id)
 
        # check in file path list if set
        if file_pathlist is not None:
            try:
                smdx_data = file_pathlist.read(filename)
            except NameError:
                pass

        if not smdx_data:
            if not os.path.exists(filename):
                filename = os.path.join(model_type_path_default, filename)

            if os.path.exists(filename):
                try:
                    f = open(filename, 'r')
                    smdx_data = f.read()
                    f.close()
                except Exception, e:
                    raise SunSpecError('Error loading model %s at %s: %s' % (model_id, filename, str(e)))
      
        if smdx_data:
            root = ET.fromstring(smdx_data)

            # load model type
            try:
                model_type = ModelType()
                model_type.from_smdx(root)
                model_types[model_type.id] = model_type
            except Exception, e:
                raise SunSpecError('Error loading model %s at %s: %s' % (model_id, filename, str(e)))
        else:
            raise SunSpecError('Model file for model %s not found' % (str(model_id)))

    return model_type

class ModelType(object):

    def __init__(self, mid=None):

        self.id = mid
        self.len = None
        self.name = None
        self.label = None
        self.description = None
        self.notes = None
        self.fixed_block = None
        self.repeating_block = None
        self.symbols = {}

    def from_smdx(self, element):

        smdx_data = ''

        for m in element.findall(smdx.SMDX_MODEL):
            if True:
                self.id = int(m.attrib.get(smdx.SMDX_ATTR_ID))
                self.len = m.attrib.get(smdx.SMDX_ATTR_LEN)
                self.name = m.attrib.get(smdx.SMDX_ATTR_NAME)
                if self.name is None:
                    self.name = 'model_' + str(self.id)

                if self.len is None:
                    raise SunSpecError('Module len error')
                self.len = int(self.len)

                for b in m.findall(smdx.SMDX_BLOCK):
                    block = BlockType(model_type=self)
                    block.from_smdx(b)

                    if block.type == suns.SUNS_BLOCK_FIXED:
                        if self.fixed_block is None:
                            self.fixed_block = block
                        else:
                            raise SunSpecError('Duplicate fixed block type definition')
                    elif block.type == suns.SUNS_BLOCK_REPEATING:
                        if self.repeating_block is None:
                            self.repeating_block = block
                        else:
                            raise SunSpecError('Duplicate repeating block type definition')
                break
            else:
                raise SunSpecError('Unexpected smdx element: %s' % m.tag)

        for s in element.findall(smdx.SMDX_STRINGS):
            if s.attrib.get(smdx.SMDX_ATTR_ID) == str(self.id):
                m = s.find(smdx.SMDX_MODEL)
                if m is not None:
                    for e in m.findall('*'):
                        if e.tag == smdx.SMDX_LABEL:
                            self.label = e.text
                        elif e.tag == smdx.SMDX_DESCRIPTION:
                            self.description = e.text
                        elif e.tag == smdx.SMDX_NOTES:
                            self.notes = e.text
                for e in s.findall(smdx.SMDX_POINT):
                    pid = e.attrib.get(smdx.SMDX_ATTR_ID)
                    point_type = None
                    if self.fixed_block is not None:
                        point_type = self.fixed_block.points.get(pid)
                    if point_type is None and self.repeating_block is not None:
                        point_type = self.repeating_block.points.get(pid)
                    if point_type:
                        point_type.from_smdx(e, strings=True)

        if self.fixed_block is None:
            self.fixed_block = BlockType(suns.SUNS_BLOCK_FIXED, model_type=self)

    def symbol_add(self, symbol):
        self.symbols[symbol.id] = symbol

    def symbol_get(self, sid):
        return self.symbols.get(sid)

    def not_equal(self, model_type):

        if self == model_type:
            return False

        if model_type is None:
            return "ModelType is None"
        if self.id != model_type.id:
            return "ModelType attribute 'id' not equal: %s  %s" % (str(self.id), str(model_type.id))
        if self.len != model_type.len:
            return "ModelType attribute 'len' not equal: %s  %s" % (str(self.len), str(model_type.len))
        if self.label != model_type.label:
            return "ModelType attribute 'label' not equal: %s  %s" % (str(self.label), str(model_type.label))
        if self.description != model_type.description:
            return "ModelType attribute 'description' not equal: %s  %s" % (str(self.description), str(model_type.description))
        if self.notes != model_type.notes:
            return "ModelType attribute 'notes' not equal: %s  %s" % (str(self.notes), str(model_type.notes))
        if self.fixed_block is not None:
            not_equal = self.fixed_block.not_equal(model_type.fixed_block)
            if not_equal:
                return not_equal
        elif model_type.fixed_block is not None:
            return "ModelType fixed block is None"
        if self.repeating_block is not None:
            not_equal = self.repeating_block.not_equal(model_type.repeating_block)
            if not_equal:
                return not_equal
        elif model_type.repeating_block is not None:
            return "ModelType repeating block is None"

        return False

    def __str__(self):

        s = 'ModelType: id = %s len = %s\n' % (self.id, self.len)
        if self.fixed_block:
            s += str(self.fixed_block)
        if self.repeating_block:
            s += str(self.repeating_block)
        return s

class BlockType(object):

    def __init__(self, btype=None, blen=0, name=None, model_type=None):
        self.model_type = model_type
        self.type = btype
        self.len = blen
        self.name = name
        self.points_list = []
        self.points = {}

    def from_smdx(self, element):

        btype = element.attrib.get(smdx.SMDX_ATTR_TYPE, smdx.SMDX_ATTR_TYPE_FIXED)

        if btype != smdx.SMDX_ATTR_TYPE_FIXED and btype != smdx.SMDX_ATTR_TYPE_REPEATING:
            raise SunSpecError('Invalid block type')

        self.type = smdx.smdx_block_types.get(btype)
        self.len = element.attrib.get(smdx.SMDX_ATTR_LEN)
        if self.len is None:
            raise SunSpecError('Block len error')
        self.name = element.attrib.get(smdx.SMDX_ATTR_NAME)
        if self.name is None:
            self.name = self.type

        # process points
        for e in element.findall(smdx.SMDX_POINT):
            pt = PointType(block_type=self)
            pt.from_smdx(e)

            if self.points.get(pt.id) is not None:
                ET.dump(e)
                raise SunSpecError('Duplicate point definition: %s' % (pt.id))

            self.points_list.append(pt)
            self.points[pt.id] = pt

    def not_equal(self, block_type):

        if self == block_type:
            return False

        if block_type is None:
            return "BlockType '%s' is none" % (str(self.type))
        if self.type != block_type.type:
            return "BlockType attribute 'type' not equal: %s  %s" % (str(self.type), str(block_type.type))
        if self.len != block_type.len:
            return "BlockType attribute 'len' not equal: %s  %s" % (str(self.len), str(block_type.len))
        if len(self.points) != len(block_type.points):
            return "BlockType '%s' point count not equal" % (str(self.type))
        for k, v in self.points.items():
            value = block_type.points.get(k)
            not_equal =  v.not_equal(value)
            if not_equal:
                return not_equal

        return False

    def __str__(self):

        s = 'BlockType: type = %s len = %s\n' % (self.type, self.len)
        for p in self.points_list:
            s += '  %s\n' % (str(p))
        return s

class PointType(object):

    def __init__(self, pid=None, offset=None, ptype=None, plen=None, mandatory=None, access=None, sf=None,
                 block_type=None):
        self.block_type = block_type
        self.id = pid
        self.offset = offset
        self.type = ptype
        self.len = plen
        self.mandatory = mandatory
        self.access = access
        self.units = None
        self.sf = sf
        self.label = None
        self.description = None
        self.notes = None
        self.value_default = None
        self.is_impl = None
        self.data_to = None
        self.to_data = None
        self.to_value = None
        self.symbols = []

    def from_smdx(self, element, strings=False):

        for e in element.findall('*'):
            if e.tag == smdx.SMDX_LABEL:
                self.label = e.text
            elif e.tag == smdx.SMDX_DESCRIPTION:
                self.description = e.text
            elif e.tag == smdx.SMDX_NOTES:
                self.notes = e.text
            elif e.tag == smdx.SMDX_SYMBOL:
                sid = e.attrib.get(smdx.SMDX_ATTR_ID)
                symbol = self.block_type.model_type.symbol_get(sid)
                if symbol is None:
                    symbol = Symbol()
                    symbol.from_smdx(e, strings)
                    self.block_type.model_type.symbol_add(symbol)
                if self.symbol_get(sid) is None:
                    self.symbols.append(symbol)
                symbol.from_smdx(e, strings)

        if strings is False:
            self.id = element.attrib.get(smdx.SMDX_ATTR_ID)
            self.offset = int(element.attrib.get(smdx.SMDX_ATTR_OFFSET))
            ptype = element.attrib.get(smdx.SMDX_ATTR_TYPE)
            plen = element.attrib.get(smdx.SMDX_ATTR_LEN)
            mandatory = element.attrib.get(smdx.SMDX_ATTR_MANDATORY, smdx.SMDX_MANDATORY_FALSE)
            access = element.attrib.get(smdx.SMDX_ATTR_ACCESS, smdx.SMDX_ACCESS_R)
            self.units = element.attrib.get(smdx.SMDX_ATTR_UNITS)

            if self.id is None:
                raise SunSpecError('Missing point id attribute')
            if self.offset is None:
                raise SunSpecError('Missing offset attribute for point: %s' % self.id)
            if ptype is None:
                raise SunSpecError('Missing type attribute for point: %s' % self.id)
            if ptype == smdx.SMDX_TYPE_STRING and plen is None:
                raise SunSpecError('Missing len attribute for point: %s' % self.id)

            self.type = smdx.smdx_point_types.get(ptype)
            if self.type is None:
                raise SunSpecError('Unknown point type: %s' % ptype)
            self.mandatory = smdx.smdx_mandatory_types.get(mandatory)
            if self.mandatory is None:
                raise SunSpecError('Unknown mandatory type: %s' % mandatory)
            self.access = smdx.smdx_access_types.get(access)
            if self.access is None:
                raise SunSpecError('Unknown access type: %s' % access)
            self.sf = element.attrib.get(smdx.SMDX_ATTR_SF)

            info = suns.suns_point_type_info.get(self.type)

            if info is not None:
                self.len, self.is_impl, self.data_to, self.to_data, self.to_value, self.value_default = info
                if plen is not None:
                    self.len = int(plen)

    def symbol_get(self, sid):
        for symbol in self.symbols:
            if symbol.id == sid:
                return symbol

    def not_equal(self, point_type):

        if self == point_type:
            return False

        if point_type is None:
            return "PointType '%s' is None" % (str(self.id))
        if len(self.__dict__) != len(point_type.__dict__):
            return "PointType '%s' attribute count not equal': %s  %s" % (str(self.id))
        for k, v in self.__dict__.items():
            if k != 'block_type':
                value = point_type.__dict__.get(k)
                if v is not None and value is not None:
                    if value is None or v != value:
                        return "PointType '%s' attribute '%s' not equal: %s  %s" % (str(self.id), str(k), str(v), str(value))

        return False

    def __str__(self):

        return 'PointType: id = %s offset = %d type = %s len = %d sf = %s access = %s mandatory = %s' % \
            (self.id, self.offset, self.type, self.len, self.sf, self.access, self.mandatory)

class Symbol(object):

    def __init__(self, sid=None):
        self.id = sid
        self.value = None
        self.label = None
        self.description = None
        self.notes = None

    def from_smdx(self, element, strings=False):

        for e in element.findall('*'):
            if e.tag == smdx.SMDX_LABEL:
                self.label = e.text
            elif e.tag == smdx.SMDX_DESCRIPTION:
                self.description = e.text
            elif e.tag == smdx.SMDX_NOTES:
                self.notes = e.text

        if strings is False:
            self.id = element.attrib.get(smdx.SMDX_ATTR_ID)
            self.value = element.text
            if self.id is None:
                raise SunSpecError('Missing point id attribute')

    def __str__(self):

        return 'Symbol: id = %s value = %s' % (self.id, self.value)
