# encoding: utf-8
# Purpose: entity classes
# Created: 21.07.2012, parts taken from my ezdxf project
# Copyright (C) 2012, Manfred Moitzi
# License: MIT License
from __future__ import unicode_literals
__author__ = "mozman <mozman@gmx.at>"

from . import dxf12, dxf13
from . import const
from .juliandate import calendar_date
from datetime import datetime
from .color import TrueColor
import math
import sys

from .styles import default_text_style

SPECIAL_CHARS = {
    'd': '°'
}

NEEDS_ENCODING = sys.version_info[0] == 2


class SeqEnd(object):
    def __init__(self, wrapper):
        self.dxftype = wrapper.dxftype()


class Entity(SeqEnd):
    def __init__(self, wrapper):
        super(Entity, self).__init__(wrapper)
        self.paperspace = bool(wrapper.paperspace())

    def __str__(self):
        return super(Entity, self).__repr__()

    def __repr__(self):
        """
        Same as __str__ for every entity except the dxftype is added
        :return: a string of the format {dxftype}[str({entity})]
        """
        return "%s[%s]" % (self.dxftype, str(self))


class Shape(Entity):
    def __init__(self, wrapper):
        super(Shape, self).__init__(wrapper)
        get_dxf = wrapper.get_dxf_attrib
        self.layer = get_dxf('layer', '0')
        self.linetype = get_dxf('linetype', None)  # None=BYLAYER
        self.thickness = get_dxf('thickness', 0.0)
        self.extrusion = get_dxf('extrusion', (0., 0., 1.))
        self.ltscale = get_dxf('ltscale', 1.0)
        self.invisible = get_dxf('invisible', 0)  # 0=visible
        self.color = get_dxf('color', const.BYLAYER)  # 256=BYLAYER, 0=BYBLOCK
        self.true_color = get_dxf('true_color', None)  # 0x00RRGGBB
        if self.true_color is not None:
            self.true_color = TrueColor(self.true_color)
        self.transparency = get_dxf('transparency', None)  # 0x020000TT
        if self.transparency is not None:
            # 0.0 = opaque & 1.0 if fully transparent
            self.transparency = 1. - float(self.transparency & 0xFF) / 255.
        self.shadow_mode = get_dxf('shadow_mode', None)
        # 0 = Casts and receives shadows
        # 1 = Casts shadows
        # 2 = Receives shadows
        # 3 = Ignores shadows

        # if adding additional DXF attributes, do it also for PolyShape


class PolyShape(object):
    """ Base class for Polyface and Polymesh, both are special cases of POLYLINE.
    """
    def __init__(self, polyline, dxftype):
        self.dxftype = dxftype
        self.paperspace = polyline.paperspace
        self.layer = polyline.layer
        self.linetype = polyline.linetype
        self.ltscale = polyline.ltscale
        self.invisible = polyline.invisible
        self.color = polyline.color
        self.true_color = polyline.true_color
        self.transparency = polyline.transparency
        self.shadow_mode = polyline.shadow_mode

    def __str__(self):
        return super(PolyShape, self).__repr__()

    def __repr__(self):
        """
        Same as __str__ for every entity except the dxftype is added
        :return: a string of the format {dxftype}[str({entity})]
        """
        return "%s[%s]" % (self.dxftype, str(self))


class Line(Shape):
    def __init__(self, wrapper):
        super(Line, self).__init__(wrapper)
        self.start = wrapper.get_dxf_attrib('start')
        self.end = wrapper.get_dxf_attrib('end')

    def __str__(self):
        try:
            start = _coordinate_format(self.start)
            end = _coordinate_format(self.end)
            return "%s to %s" % (start, end)
        except (AttributeError, IndexError):
            return super(Line, self).__str__()


class Point(Shape):
    def __init__(self, wrapper):
        super(Point, self).__init__(wrapper)
        self.point = wrapper.get_dxf_attrib('point')

    def __str__(self):
        try:
            return _coordinate_format(self.point)
        except AttributeError:
            return super(Point, self).__str__()


class Circle(Shape):
    def __init__(self, wrapper):
        super(Circle, self).__init__(wrapper)
        self.center = wrapper.get_dxf_attrib('center')
        self.radius = wrapper.get_dxf_attrib('radius')

    def __str__(self):
        return _coordinate_format(self.center)

    def __repr__(self):
        return "%s[center=%s; radius=%s]" % (self.dxftype, _coordinate_format(self.center), self.radius)


class Arc(Shape):
    def __init__(self, wrapper):
        super(Arc, self).__init__(wrapper)
        get_dxf = wrapper.get_dxf_attrib
        self.center = get_dxf('center')
        self.radius = get_dxf('radius')
        self.startangle = get_dxf('startangle')
        self.endangle = get_dxf('endangle')

    def __str__(self):
        return _coordinate_format(self.center)

    def __repr__(self):
        return "%s[center=%s; radius=%s]" % (self.dxftype, _coordinate_format(self.center), self.radius)


class Trace(Shape):
    def __init__(self, wrapper):
        super(Trace, self).__init__(wrapper)
        get_dxf = wrapper.get_dxf_attrib
        self.points = [
            get_dxf(vname) for vname in const.VERTEXNAMES
        ]

    def __str__(self):
        return "(%s)" % ', '.join([_coordinate_format(p) for p in self.points])

Solid = Trace


class Face(Trace):
    def __init__(self, wrapper):
        super(Face, self).__init__(wrapper)
        self.invisible_edge = wrapper.get_dxf_attrib('invisible_edge', 0)

    def is_edge_invisible(self, edge):
        # edges 0 .. 3
        return bool(self.invisible_edge & (1 << edge))


class Text(Shape):
    def __init__(self, wrapper):
        super(Text, self).__init__(wrapper)
        get_dxf = wrapper.get_dxf_attrib
        self.insert = get_dxf('insert')
        self.text = get_dxf('text')
        self.height = get_dxf('height', 0)
        self.width = get_dxf('width', 0)
        self.oblique = get_dxf('oblique', None)
        self.rotation = get_dxf('rotation', 0.)
        self.style = get_dxf('style', "")
        self.halign = get_dxf('halign', 0)
        self.valign = get_dxf('valign', 0)
        self.alignpoint = get_dxf('alignpoint', None)
        if get_dxf('textgenerationflag', None) is not None:
            self.is_backwards = bool(get_dxf('textgenerationflag', 0) & 2)
            self.is_upside_down = bool(get_dxf('textgenerationflag', 0) & 4)
        else:
            self.is_backwards = None
            self.is_upside_down = None
        self.font = ""
        self.bigfont = ""

    def resolve_text_style(self, text_styles):
        style = text_styles.get(self.style, None)
        if style is None:
            style = default_text_style
        if self.height == 0:
            self.height = style.height
        if self.width == 0:
            self.width = style.width
        if self.oblique is None:
            self.oblique = style.oblique
        if self.is_backwards is None:
            self.is_backwards = style.is_backwards
        if self.is_upside_down is None:
            self.is_upside_down = style.is_upside_down
        if self.font is None:
            self.font = style.font
        if self.bigfont is None:
            self.bigfont = style.bigfont

    def plain_text(self):
        chars = []
        raw_chars = list(reversed(self.text))  # text splitted into chars, in reversed order for efficient pop()
        while len(raw_chars):
            char = raw_chars.pop()
            if char == '%':  # formatting codes and special characters
                if len(raw_chars) and raw_chars[-1] == '%':
                    raw_chars.pop()  # '%'
                    if len(raw_chars):
                        special_char = raw_chars.pop()  # command char
                        chars.append(SPECIAL_CHARS.get(special_char, ""))
                else:  # char is just a single '%'
                    chars.append(char)
            else:  # char is what it is, a character
                chars.append(char)
        return "".join(chars)

    def __str__(self):
        text = self.plain_text()
        return text.encode('utf-8', 'ignore') if NEEDS_ENCODING else text

    def __repr__(self):
        text = "Text('%s')" % self.plain_text().strip()
        return text.encode('utf-8', 'ignore') if NEEDS_ENCODING else text



class Insert(Shape):
    def __init__(self, wrapper):
        super(Insert, self).__init__(wrapper)
        get_dxf = wrapper.get_dxf_attrib
        self.name = get_dxf('name')
        self.insert = get_dxf('insert')
        self.rotation = get_dxf('rotation', 0.)
        self.scale = get_dxf('xscale', 1.), get_dxf('yscale', 1.), get_dxf('zscale', 1.)
        self.row_count = get_dxf('rowcount', 1)
        self.row_spacing = get_dxf('rowspacing', 0.)
        self.col_count = get_dxf('colcount', 1)
        self.col_spacing = get_dxf('colspacing', 0.)
        self.attribsfollow = bool(get_dxf('attribsfollow', 0))
        self.attribs = []

    def find_attrib(self, attrib_tag):
        for attrib in self.attribs:
            if attrib.tag == attrib_tag:
                return attrib
        return None

    def append_data(self, attribs):
        self.attribs = attribs

    def __str__(self):
        return "'%s' at %s" % (self.name, _coordinate_format(self.insert))


class Attrib(Text):  # also ATTDEF
    def __init__(self, wrapper):
        super(Attrib, self).__init__(wrapper)
        self.tag = wrapper.get_dxf_attrib('tag')

_LINE_TYPES = frozenset(('spline2d', 'polyline2d', 'polyline3d'))


class Polyline(Shape):
    def __init__(self, wrapper):
        super(Polyline, self).__init__(wrapper)
        self.vertices = []  # set in append data
        self.points = []  # set in append data
        self.controlpoints = []  # set in append data
        self.width = []  # set in append data
        self.bulge = []  # set in append data
        self.tangents = []  # set in append data
        self.flags = wrapper.flags
        self.mode = wrapper.get_mode()
        get_dxf = wrapper.get_dxf_attrib
        self.mcount = get_dxf('mcount', 0)
        self.ncount = get_dxf('ncount', 0)
        self.default_start_width = get_dxf('defaultstartwidth', 0.)
        self.default_end_width = get_dxf('defaultendwidth', 0.)
        self.is_mclosed = wrapper.is_mclosed()
        self.is_nclosed = wrapper.is_nclosed()
        self.elevation = get_dxf('elevation', (0., 0., 0.))
        self.m_smooth_density = get_dxf('msmoothdensity', 0.)
        self.n_smooth_density = get_dxf('nsmoothdensity', 0.)
        self.smooth_type = get_dxf('smoothtype', 0)
        self.spline_type = None
        if self.mode == 'spline2d':
            if self.smooth_type == const.POLYMESH_CUBIC_BSPLINE:
                self.spline_type = 'cubic_bspline'
            elif self.smooth_type == const.POLYMESH_QUADRIC_BSPLINE:
                self.spline_type = 'quadratic_bspline'
            elif self.smooth_type == const.POLYMESH_BEZIER_SURFACE:
                self.spline_type = 'bezier_curve'  # is this a valid spline type for DXF12?

    def __len__(self):
        return len(self.vertices)

    def __getitem__(self, item):
        return self.vertices[item]

    def __iter__(self):
        return iter(self.vertices)

    def __str__(self):
        return "(%s)" % ", ".join([_coordinate_format(p) for p in self.points])

    @property
    def is_closed(self):
        return self.is_mclosed

    @is_closed.setter
    def is_closed(self, status):
        self.is_mclosed = status

    def append_data(self, vertices):
        def default_width(start_width, end_width):
            if start_width == 0.:
                start_width = self.default_start_width
            if end_width == 0.:
                end_width = self.default_end_width
            return start_width, end_width

        self.vertices = vertices
        if self.mode in _LINE_TYPES:
            for vertex in self.vertices:
                if vertex.flags & const.VTX_SPLINE_FRAME_CONTROL_POINT:
                    self.controlpoints.append(vertex.location)
                else:
                    self.points.append(vertex.location)
                    self.width.append(default_width(vertex.start_width, vertex.end_width))
                    self.bulge.append(vertex.bulge)
                    self.tangents.append(vertex.tangent if vertex.flags & const.VTX_CURVE_FIT_TANGENT else None)

    def cast(self):
        if self.mode == 'polyface':
            return Polyface(self)
        elif self.mode == 'polymesh':
            return Polymesh(self)
        else:
            return self


class SubFace(object):
    def __init__(self, face_record, vertices):
        self._vertices = vertices
        self.face_record = face_record

    def __len__(self):
        return len(self.face_record.vtx)

    def __getitem__(self, item):
        return self._vertices[self._vertex_index(item)]

    def __iter__(self):
        return (self._vertices[index].location for index in self.indices())

    def _vertex_index(self, pos):
        return abs(self.face_record.vtx[pos]) - 1

    def indices(self):
        return tuple(abs(i)-1 for i in self.face_record.vtx if i != 0)

    def is_edge_visible(self, pos):
        return self.face_record.vtx[pos] > 0


class Polyface(PolyShape):
    def __init__(self, polyline):
        VERTEX_FLAGS = const.VTX_3D_POLYFACE_MESH_VERTEX + const.VTX_3D_POLYGON_MESH_VERTEX

        def is_vertex(flags):
            return flags & VERTEX_FLAGS == VERTEX_FLAGS

        super(Polyface, self).__init__(polyline, 'POLYFACE')
        vertices = []
        face_records = []
        for vertex in polyline.vertices:
            (vertices if is_vertex(vertex.flags) else face_records).append(vertex)

        self.vertices = vertices
        self._face_records = face_records

    def __getitem__(self, item):
        return SubFace(self._face_records[item], self.vertices)

    def __len__(self):
        return len(self._face_records)

    def __iter__(self):
        return (SubFace(f, self.vertices) for f in self._face_records)
    
    def __str__(self):
        return ", ".join([str(v) for v in self.vertices])


class Polymesh(PolyShape):
    def __init__(self, polyline):
        super(Polymesh, self).__init__(polyline, 'POLYMESH')
        self.mcount = polyline.mcount
        self.ncount = polyline.ncount
        self.is_mclosed = polyline.is_mclosed
        self.is_nclosed = polyline.is_nclosed
        self._vertices = polyline.vertices
        self.m_smooth_density = polyline.m_smooth_density
        self.n_smooth_density = polyline.n_smooth_density
        self.smooth_type = polyline.smooth_type

    def __iter__(self):
        return iter(self._vertices)

    def get_location(self, pos):
        return self.get_vertex(pos).location

    def get_vertex(self, pos):
        mcount = self.mcount
        ncount = self.ncount
        m, n = pos
        if 0 <= m < mcount and 0 <= n < ncount:
            pos = m * ncount + n
            return self._vertices[pos]
        else:
            raise IndexError(repr(pos))


class Vertex(Shape):
    def __init__(self, wrapper):
        super(Vertex, self).__init__(wrapper)
        get_dxf = wrapper.get_dxf_attrib
        self.location = get_dxf('location')
        self.flags = get_dxf('flags', 0)
        self.start_width = get_dxf('startwidth', 0)
        self.end_width = get_dxf('endwidth', 0)
        self.bulge = get_dxf('bulge', 0)
        self.tangent = get_dxf('tangent', None)
        self.vtx = self._get_vtx(wrapper)

    def _get_vtx(self, wrapper):
        vtx = []
        get_dxf = wrapper.get_dxf_attrib
        for vname in const.VERTEXNAMES:
            try:
                vtx.append(get_dxf(vname))
            except ValueError:
                pass
        return tuple(vtx)

    def __getitem__(self, item):
        return self.location[item]

    def __iter__(self):
        return iter(self.location)

    def __str__(self):
        return _coordinate_format(self.location)


class LWPolyline(Shape):
    def __init__(self, wrapper):
        super(LWPolyline, self).__init__(wrapper)
        self.points, self.width, self.bulge = wrapper.data()
        self.const_width = wrapper.get_dxf_attrib('const_width', 0)
        self.is_closed = wrapper.is_closed()
        self.elevation = wrapper.get_dxf_attrib('elevation', (0., 0., 0.))

    def __len__(self):
        return len(self.points)

    def __getitem__(self, item):
        return self.points[item]

    def __iter__(self):
        return iter(self.points)

    def __str__(self):
        return "(%s)" % ", ".join([_coordinate_format(p) for p in self.points])


class Ellipse(Shape):
    def __init__(self, wrapper):
        super(Ellipse, self).__init__(wrapper)
        get_dxf = wrapper.get_dxf_attrib
        self.center = get_dxf('center')
        self.majoraxis = get_dxf('majoraxis')
        self.ratio = get_dxf('ratio', 1.0)  # circle
        self.startparam = get_dxf('startparam', 0.)
        self.endparam = get_dxf('endparam', 6.283185307179586)  # 2*pi

    def __str__(self):
        return _coordinate_format(self.center)


class Ray(Shape):
    def __init__(self, wrapper):
        super(Ray, self).__init__(wrapper)
        self.start = wrapper.get_dxf_attrib('start')
        self.unitvector = wrapper.get_dxf_attrib('unitvector')

XLine = Ray


class Spline(Shape):
    def __init__(self, wrapper):
        super(Spline, self).__init__(wrapper)
        get_dxf = wrapper.get_dxf_attrib
        self.normalvector = get_dxf('normalvector', None)
        self.flags = get_dxf('flags', 0)
        self.degree = get_dxf('degree', 3)
        self.starttangent = get_dxf('starttangent', None)
        self.endtangent = get_dxf('endtangent', None)
        self.knots = tuple(wrapper.knots())
        self.weights = tuple(wrapper.weights())
        self.tol_knot = get_dxf('knot_tolernace', .0000001)
        self.tol_controlpoint = get_dxf('controlpoint_tolerance', .0000001)
        self.tol_fitpoint = get_dxf('fitpoint_tolerance', .0000000001)
        self.controlpoints = tuple(wrapper.controlpoints())
        self.fitpoints = tuple(wrapper.fitpoints())
        if len(self.weights) == 0:
            self.weights = tuple([1.0] * len(self.controlpoints))

    @property
    def is_closed(self):
        return bool(self.flags & const.SPLINE_CLOSED)

    @property
    def is_periodic(self):
        return bool(self.flags & const.SPLINE_PERIODIC)

    @property
    def is_rational(self):
        return bool(self.flags & const.SPLINE_RATIONAL)

    @property
    def is_planar(self):
        return bool(self.flags & const.SPLINE_PLANAR)

    @property
    def is_linear(self):
        return bool(self.flags & const.SPLINE_LINEAR)


class Helix(Spline):
    def __init__(self, wrapper):
        super(Helix, self).__init__(wrapper)
        get_dxf = wrapper.get_dxf_attrib
        self.helix_version = (get_dxf('helix_major_version', 1),
                              get_dxf('helix_maintainance_version', 1))
        self.axis_base_point = get_dxf('axis_base_point', None)
        self.start_point = get_dxf('start_point', None)
        self.axis_vector = get_dxf('axis_vector', None)
        self.radius = get_dxf('radius', 0)
        self.turns = get_dxf('turns', 0)
        self.turn_height = get_dxf('turn_height', 0)
        self.handedness = get_dxf('handedness', 0)  # 0 = left, 1 = right
        self.constrain = get_dxf('constrain', 0)
        # 0 = Constrain turn height;
        # 1 = Constrain turns;
        # 2 = Constrain height


def deg2vec(deg):
    rad = float(deg) * math.pi / 180.0
    return math.cos(rad), math.sin(rad), 0.


def normalized(vector):
    x, y, z = vector
    m = (x**2 + y**2 + z**2)**0.5
    return x/m, y/m, z/m

##################################################
# MTEXT inline codes
# \L	Start underline
# \l	Stop underline
# \O	Start overstrike
# \o	Stop overstrike
# \K	Start strike-through
# \k	Stop strike-through
# \P	New paragraph (new line)
# \pxi	Control codes for bullets, numbered paragraphs and columns
# \X	Paragraph wrap on the dimension line (only in dimensions)
# \Q	Slanting (obliquing) text by angle - e.g. \Q30;
# \H	Text height - e.g. \H3x;
# \W	Text width - e.g. \W0.8x;
# \F	Font selection
#
#     e.g. \Fgdt;o - GDT-tolerance
#     e.g. \Fkroeger|b0|i0|c238|p10 - font Kroeger, non-bold, non-italic, codepage 238, pitch 10
#
# \S	Stacking, fractions
#
#     e.g. \SA^B:
#     A
#     B
#     e.g. \SX/Y:
#     X
#     -
#     Y
#     e.g. \S1#4:
#     1/4
#
# \A	Alignment
#
#     \A0; = bottom
#     \A1; = center
#     \A2; = top
#
# \C	Color change
#
#     \C1; = red
#     \C2; = yellow
#     \C3; = green
#     \C4; = cyan
#     \C5; = blue
#     \C6; = magenta
#     \C7; = white
#
# \T	Tracking, char.spacing - e.g. \T2;
# \~	Non-wrapping space, hard space
# {}	Braces - define the text area influenced by the code
# \	Escape character - e.g. \\ = "\", \{ = "{"
#
# Codes and braces can be nested up to 8 levels deep

ESCAPED_CHARS = "\\{}"
GROUP_CHARS = "{}"
ONE_CHAR_COMMANDS = "PLlOoKkX"


class MText(Shape):
    def __init__(self, wrapper):

        super(MText, self).__init__(wrapper)
        self.insert = wrapper.get_dxf_attrib('insert')
        self.rawtext = wrapper.rawtext()
        get_dxf = wrapper.get_dxf_attrib
        self.height = get_dxf('height', 0)
        self.rect_width = get_dxf('reference_rectangle_width', None)
        self.horizontal_width = get_dxf('horizontal_width', None)
        self.vertical_height = get_dxf('vertical_height', None)
        self.linespacing = get_dxf('linespacing', 1.0)
        self.attachmentpoint = get_dxf('attachmentpoint', 1)
        self.style = get_dxf('style', 'STANDARD')
        self.extrusion = get_dxf('extrusion', (0., 0., 1.))
        try:
            xdir = wrapper.get_dxf_attrib('xdirection')
        except ValueError:
            xdir = deg2vec(get_dxf('rotation', 0.0))
        self.xdirection = normalized(xdir)
        self.font = None
        self.bigfont = None

    def lines(self):
        return self.rawtext.split('\P')

    def plain_text(self, split=False):
        chars = []
        raw_chars = list(reversed(self.rawtext))  # text splitted into chars, in reversed order for efficient pop()
        while len(raw_chars):
            char = raw_chars.pop()
            if char == '\\':  # is a formatting command
                try:
                    char = raw_chars.pop()
                except IndexError:
                    break  # premature end of text - just ignore

                if char in ESCAPED_CHARS:  # \ { }
                    chars.append(char)
                elif char in ONE_CHAR_COMMANDS:
                    if char == 'P':  # new line
                        chars.append('\n')
                    # discard other commands
                else:  # more character commands are terminated by ';'
                    stacking = char == 'S'  # stacking command surrounds user data
                    try:
                        while char != ';':  # end of format marker
                            char = raw_chars.pop()
                            if stacking and char != ';':
                                chars.append(char)  # append user data of stacking command
                    except IndexError:
                        break  # premature end of text - just ignore
            elif char in GROUP_CHARS:  # { }
                pass  # discard group markers
            elif char == '%':  # special characters
                if len(raw_chars) and raw_chars[-1] == '%':
                    raw_chars.pop()  # discard next '%'
                    if len(raw_chars):
                        special_char = raw_chars.pop()
                        # replace or discard formatting code
                        chars.append(SPECIAL_CHARS.get(special_char, ""))
                else:  # char is just a single '%'
                    chars.append(char)
            else:  # char is what it is, a character
                chars.append(char)

        plain_text = "".join(chars)
        return plain_text.split('\n') if split else plain_text

    def resolve_text_style(self, text_styles):
        style = text_styles.get(self.style, None)
        if style is None:
            style = default_text_style
        if self.height == 0:
            self.height = style.height
        if self.font is None:
            self.font = style.font
        if self.bigfont is None:
            self.bigfont = style.font

    def __str__(self):
        text = self.plain_text()
        return text.encode('utf-8', 'ignore') if NEEDS_ENCODING else text


    def __repr__(self):
        splitlines = self.plain_text().split('\n', 1)
        text = splitlines[0].strip()
        if len(splitlines) > 1:
            text += '...'
        text = "MText('%s')" % text
        return text.encode('utf-8', 'ignore') if NEEDS_ENCODING else text


class Block(Shape):
    def __init__(self, wrapper):
        super(Block, self).__init__(wrapper)
        self.basepoint = wrapper.get_dxf_attrib('basepoint', (0, 0, 0))
        self.name = wrapper.get_dxf_attrib('name')
        self.flags = wrapper.get_dxf_attrib('flags', 0)
        self.xrefpath = wrapper.get_dxf_attrib('xrefpath', "")
        self._entities = list()

    @property
    def is_xref(self):
        return bool(self.flags & const.BLK_XREF)

    @property
    def is_xref_overlay(self):
        return bool(self.flags & const.BLK_XREF_OVERLAY)

    @property
    def is_anonymous(self):
        return bool(self.flags & const.BLK_ANONYMOUS)

    def set_entities(self, entities):
        self._entities = entities

    def __iter__(self):
        return iter(self._entities)

    def __getitem__(self, item):
        return self._entities[item]

    def __len__(self):
        return len(self._entities)

    def __str__(self):
        try:
            return self.name
        except AttributeError:
            return self.dxftype


class BlockEnd(SeqEnd):
    pass


def unpack_seconds(seconds):
    seconds = int(seconds / 1000)  # remove 1/1000 part
    hours = int(seconds / 3600)
    seconds = int(seconds % 3600)
    minutes = int(seconds / 60)
    seconds = int(seconds % 60)
    return hours, minutes, seconds


class Sun(Entity):
    def __init__(self, wrapper):
        super(Sun, self).__init__(wrapper)
        get_dxf = wrapper.get_dxf_attrib
        self.version = get_dxf('version', 1)
        self.status = bool(get_dxf('status', 0))  # on/off ?
        self.sun_color = get_dxf('sun_color', None)  # None is unset
        self.intensity = get_dxf('intensity', 0)
        self.shadows = bool(get_dxf('shadows', 0))
        julian_date = get_dxf('date', 0.)
        if julian_date > 0.:
            date = calendar_date(julian_date)
        else:
            date = datetime.now()
        hours, minutes, seconds = unpack_seconds(get_dxf('time', 0))
        self.date = datetime(date.year, date.month, date.day, hours, minutes, seconds)
        self.daylight_savings_time = bool(get_dxf('daylight_savings_time', 0))
        self.shadow_type = get_dxf('shadows_type', 0)
        self.shadow_map_size = get_dxf('shadow_map_size', 0)
        self.shadow_softness = get_dxf('shadow_softness', 0)


class Mesh(Shape):
    def __init__(self, wrapper):
        super(Mesh, self).__init__(wrapper)
        get_dxf = wrapper.get_dxf_attrib
        self.version = get_dxf('version', 2)
        self.blend_crease = bool(get_dxf('blend_crease', 0))
        self.subdivision_levels = get_dxf('subdivision_levels', 1)
        # rest are mostly positional tags
        self.vertices = []
        self.faces = []
        self.edges = []
        self.edge_crease_list = []

        subdmesh_tags = wrapper.tags.get_subclass('AcDbSubDMesh')
        # for all blocks I ignore the count values, perhaps they are wrong,
        # but I use the count tags as indicator for the begin of the list
        try:
            pos = subdmesh_tags.tag_index(92)
        except ValueError:  # no vertices???
            return
        else:
            self.vertices = Mesh.get_vertices(subdmesh_tags, pos+1)
        try:
            pos = subdmesh_tags.tag_index(93)
        except ValueError:  # no faces???
            pass
        else:
            self.faces = Mesh.get_faces(subdmesh_tags, pos+1)
        try:
            pos = subdmesh_tags.tag_index(94)
        except ValueError:  # no edges
            pass
        else:
            self.edges = Mesh.get_edges(subdmesh_tags, pos+1)
        try:
            pos = subdmesh_tags.tag_index(95)
        except ValueError:  # no edges crease values
            pass
        else:
            self.edge_crease_list = Mesh.get_edge_crease_list(subdmesh_tags, pos+1)

    def get_face(self, index):
        return tuple(self.vertices[vertex_index] for vertex_index in self.faces[index])

    def get_edge(self, index):
        return tuple(self.vertices[vertex_index] for vertex_index in self.edges[index])

    @staticmethod
    def get_vertices(tags, pos):
        vertices = []
        itags = iter(tags[pos:])
        while True:
            try:
                tag = next(itags)
            except StopIteration:  # premature end of tags, return what you got
                break
            if tag.code == 10:
                vertices.append(tag.value)
            else:
                break
        return vertices

    @staticmethod
    def get_faces(tags, pos):
        faces = []
        face = []
        itags = iter(tags[pos:])
        try:
            while True:
                tag = next(itags)
                # loop until first tag.code != 90
                if tag.code != 90:
                    break
                count = tag.value  # count of vertex indices
                while count > 0:
                    tag = next(itags)
                    face.append(tag.value)
                    count -= 1
                faces.append(tuple(face))
                del face[:]
        except StopIteration:  # premature end of tags, return what you got
            pass
        return faces

    @staticmethod
    def get_edges(tags, pos):
        edges = []
        start_index = None
        for index in Mesh.get_raw_list(tags, pos, code=90):
            if start_index is None:
                start_index = index
            else:
                edges.append((start_index, index))
                start_index = None
        return edges

    @staticmethod
    def get_edge_crease_list(tags, pos):
        return Mesh.get_raw_list(tags, pos, code=140)

    @staticmethod
    def get_raw_list(tags, pos, code):
        raw_list = []
        itags = iter(tags[pos:])
        while True:
            try:
                tag = next(itags)
            except StopIteration:
                break
            if tag.code == code:
                raw_list.append(tag.value)
            else:
                break
        return raw_list


class Light(Shape):
    def __init__(self, wrapper):
        super(Light, self).__init__(wrapper)
        get_dxf = wrapper.get_dxf_attrib
        self.version = get_dxf('version', 1)
        self.name = get_dxf('name', "")
        self.light_type = get_dxf('light_type', 1)  # distant = 1; point = 2; spot = 3
        self.status = bool(get_dxf('status', 0))  # on/off ?
        self.light_color = get_dxf('light_color', None)  # 0 is unset
        self.true_color = get_dxf('true_color', None)  # None is unset
        self.plot_glyph = bool(get_dxf('plot_glyph', 0))
        self.intensity = get_dxf('intensity', 0)
        self.position = get_dxf('position', (0, 0, 1))
        self.target = get_dxf('target', (0, 0, 0))
        self.attenuation_type = get_dxf('attenuation_type', 0)  # 0 = None; 1 = Inverse Linear; 2 = Inverse Square
        self.use_attenuation_limits = bool(get_dxf('use_attenuation_limits', 0))
        self.attenuation_start_limit = get_dxf('attenuation_start_limit', 0)
        self.attenuation_end_limit = get_dxf('attenuation_end_limit', 0)
        self.hotspot_angle = get_dxf('hotspot_angle', 0)
        self.fall_off_angle = get_dxf('fall_off_angle', 0)
        self.cast_shadows = bool(get_dxf('cast_shadows', 0))
        self.shadow_type = get_dxf('shadow_type', 0)  # 0 = Ray traced shadows; 1 = Shadow maps
        self.shadow_map_size = get_dxf('shadow_map_size', 0)
        self.shadow_softness = get_dxf('shadow_softness', 0)


class Body(Shape):
    def __init__(self, wrapper):
        super(Body, self).__init__(wrapper)
        # need handle to get SAB data in DXF version AC1027 and later
        self.handle = wrapper.get_dxf_attrib('handle', None)
        self.version = wrapper.get_dxf_attrib('version', 1)
        self.acis = wrapper.get_acis_data()

    def set_sab_data(self, sab_data):
        self.acis = sab_data

    @property
    def is_sat(self):
        return isinstance(self.acis, list)  # but could be an empty list

    @property
    def is_sab(self):
        return not self.is_sat  # has binary encoded ACIS data

Solid3d = Body
# perhaps reading creation history is needed


class Surface(Body):
    def __init__(self, wrapper):
        super(Surface, self).__init__(wrapper)
        self.u_isolines = wrapper.get_dxf_attrib('u_isolines', 0)
        self.v_isolines = wrapper.get_dxf_attrib('v_isolines', 0)


EntityTable = {
    'LINE': (Line, dxf12.Line, dxf13.Line),
    'POINT': (Point, dxf12.Point, dxf13.Point),
    'CIRCLE': (Circle, dxf12.Circle, dxf13.Circle),
    'ARC': (Arc, dxf12.Arc, dxf13.Arc),
    'TRACE': (Trace, dxf12.Trace, dxf13.Trace),
    'SOLID': (Solid, dxf12.Solid, dxf13.Solid),
    '3DFACE': (Face, dxf12.Face, dxf13.Face),
    'TEXT': (Text, dxf12.Text, dxf13.Text),
    'INSERT': (Insert, dxf12.Insert, dxf13.Insert),
    'SEQEND': (SeqEnd, dxf12.SeqEnd, dxf13.SeqEnd),
    'ATTRIB': (Attrib, dxf12.Attrib, dxf13.Attrib),
    'ATTDEF': (Attrib, dxf12.Attrib, dxf13.Attdef),
    'POLYLINE': (Polyline, dxf12.Polyline, dxf13.Polyline),
    'VERTEX': (Vertex, dxf12.Vertex, dxf13.Vertex),
    'BLOCK': (Block, dxf12.Block, dxf13.Block),
    'ENDBLK': (BlockEnd, dxf12.EndBlk, dxf13.EndBlk),
    'LWPOLYLINE': (LWPolyline, None, dxf13.LWPolyline),
    'ELLIPSE': (Ellipse, None, dxf13.Ellipse),
    'RAY': (Ray, None, dxf13.Ray),
    'XLINE': (XLine, None, dxf13.XLine),
    'SPLINE': (Spline, None, dxf13.Spline),
    'HELIX': (Helix, None, dxf13.Helix),
    'MTEXT': (MText, None, dxf13.MText),
    'SUN': (Sun, None, dxf13.Sun),
    'MESH': (Mesh, None, dxf13.Mesh),
    'LIGHT': (Light, None, dxf13.Light),
    'BODY': (Body, None, dxf13.Body),
    'REGION': (Body, None, dxf13.Body),
    '3DSOLID': (Solid3d, None, dxf13.Solid3d),
    'SURFACE': (Surface, None, dxf13.Surface),
    'PLANESURFACE': (Surface, None, dxf13.Surface),
}


def entity_factory(tags, dxfversion):
    dxftype = tags.get_type()
    cls, dxf12wrapper, dxf13wrapper = EntityTable[dxftype]
    wrapper = dxf12wrapper(tags) if dxfversion == "AC1009" else dxf13wrapper(tags)
    wrapper.post_read_correction()
    shape = cls(wrapper)
    return shape


def _coordinate_format(coords):
        """
        Given a tuple of length 3 (X, Y, Z), rounds the X and Y component to nearest 2 decimal places and drops the Z
        component if it is zero.

        :param coords: a tuple of length 3
        :return: a string formatted to make the points more succinct
        """
        try:
            if coords[2] == 0:
                coords = coords[:2]
        except IndexError:
            pass
        rounded = tuple([round(i, 3) for i in coords])
        return repr(rounded)  # gives us the (x, y) or (x, y, z) we want
