import numpy as np

from string import Template

from ..arc import to_threepoint
from ..entities import Line, Arc, BSpline, Text

from ... import resources
from ...constants import log
from ...constants import tol_path as tol

from ... import util
from ... import grouping
from ... import transformations as tf

# stuff for DWG loading
import os
import shutil
import tempfile
import subprocess

from distutils.spawn import find_executable


# unit codes
_DXF_UNITS = {1: 'inches',
              2: 'feet',
              3: 'miles',
              4: 'millimeters',
              5: 'centimeters',
              6: 'meters',
              7: 'kilometers',
              8: 'microinches',
              9: 'mils',
              10: 'yards',
              11: 'angstroms',
              12: 'nanometers',
              13: 'microns',
              14: 'decimeters',
              15: 'decameters',
              16: 'hectometers',
              17: 'gigameters',
              18: 'AU',
              19: 'light years',
              20: 'parsecs'}
# backwards, for reference
_UNITS_TO_DXF = {v: k for k, v in _DXF_UNITS.items()}

# a string which we will replace spaces with temporarily
_SAFESPACE = '|<^>|'

# save metadata to a DXF Xrecord starting here
# Valid values are 1-369 (except 5 and 105)
XRECORD_METADATA = 134
# the sentinel string for trimesh metadata
# this should be seen at XRECORD_METADATA
XRECORD_SENTINEL = 'TRIMESH_METADATA:'
# the maximum line length before we split lines
XRECORD_MAX_LINE = 200
# the maximum index of XRECORDS
XRECORD_MAX_INDEX = 368


def load_dxf(file_obj, **kwargs):
    """
    Load a DXF file to a dictionary containing vertices and
    entities.

    Parameters
    ----------
    file_obj: file or file- like object (has object.read method)

    Returns
    ----------
    result: dict, keys are  entities, vertices and metadata
    """

    # in a DXF file, lines come in pairs,
    # a group code then the next line is the value
    # we are removing all whitespace then splitting with the
    # splitlines function which uses the universal newline method
    raw = file_obj.read()
    # if we've been passed bytes
    if hasattr(raw, 'decode'):
        # search for the sentinel string indicating binary DXF
        # do it by encoding sentinel to bytes and subset searching
        if raw[:22].find(b'AutoCAD Binary DXF') != -1:
            if _teigha is None:
                # no converter to ASCII DXF available
                raise ValueError('binary DXF not supported!')
            else:
                # convert binary DXF to R14 ASCII DXF
                raw = _teigha_convert(raw, extension='dxf')
        else:
            # we've been passed bytes that don't have the
            # header for binary DXF so try decoding as UTF-8
            raw = raw.decode('utf-8', errors='ignore')

    # remove trailing whitespace
    raw = str(raw).strip()
    # without any spaces and in upper case
    cleaned = raw.replace(' ', '').strip().upper()

    # blob with spaces and original case
    blob_raw = np.array(str.splitlines(raw)).reshape((-1, 2))
    # if this reshape fails, it means the DXF is malformed
    blob = np.array(str.splitlines(cleaned)).reshape((-1, 2))

    # get the section which contains the header in the DXF file
    endsec = np.nonzero(blob[:, 1] == 'ENDSEC')[0]

    # store metadata
    metadata = {}

    # try reading the header, which may be malformed
    header_start = np.nonzero(blob[:, 1] == 'HEADER')[0]
    if len(header_start) > 0:
        header_end = endsec[np.searchsorted(endsec, header_start[0])]
        header_blob = blob[header_start[0]:header_end]

        # store some properties from the DXF header
        metadata['DXF_HEADER'] = {}
        for key, group in [('$ACADVER', '1'),
                           ('$DIMSCALE', '40'),
                           ('$DIMALT', '70'),
                           ('$DIMALTF', '40'),
                           ('$DIMUNIT', '70'),
                           ('$INSUNITS', '70'),
                           ('$LUNITS', '70')]:
            value = get_key(header_blob,
                            key,
                            group)
            if value is not None:
                metadata['DXF_HEADER'][key] = value

        # store unit data pulled from the header of the DXF
        # prefer LUNITS over INSUNITS
        # I couldn't find a table for LUNITS values but they
        # look like they are 0- indexed versions of
        # the INSUNITS keys, so for now offset the key value
        for offset, key in [(-1, '$LUNITS'),
                            (0, '$INSUNITS')]:
            # get the key from the header blob
            units = get_key(header_blob, key, '70')
            # if it exists add the offset
            if units is None:
                continue
            metadata[key] = units
            units += offset
            # if the key is in our list of units store it
            if units in _DXF_UNITS:
                metadata['units'] = _DXF_UNITS[units]
        # warn on drawings with no units
        if 'units' not in metadata:
            log.debug('DXF doesn\'t have units specified!')

    # get the section which contains entities in the DXF file
    entity_start = np.nonzero(blob[:, 1] == 'ENTITIES')[0][0]
    entity_end = endsec[np.searchsorted(endsec, entity_start)]

    blocks = None
    # only load blocks if an entity references them via an INSERT
    if (blob[entity_start:entity_end] == ['0', 'INSERT']).all(axis=1).any():
        try:
            # which part of the raw file contains blocks
            block_start = np.nonzero(blob[:, 1] == 'BLOCKS')[0][0]
            block_end = endsec[np.searchsorted(endsec, block_start)]

            blob_block = blob[block_start:block_end]
            blob_block_raw = blob_raw[block_start:block_end]
            block_infl = np.nonzero((blob_block == ['0', 'BLOCK']).all(axis=1))[0]

            # collect blocks by name
            blocks = {}
            for index in np.array_split(np.arange(len(blob_block)), block_infl):
                try:
                    v, e, name = convert_entities(
                        blob_block[index],
                        blob_block_raw[index],
                        return_name=True)
                    if len(e) > 0:
                        blocks[name] = (v, e)
                except BaseException:
                    pass
        except BaseException:
            pass

    # actually load referenced entities
    vertices, entities = convert_entities(
        blob[entity_start:entity_end],
        blob_raw[entity_start:entity_end],
        blocks=blocks)

    # return result as kwargs for trimesh.path.Path2D constructor
    result = {'vertices': vertices,
              'entities': entities,
              'metadata': metadata}

    return result


def convert_entities(
        blob, blob_raw=None, blocks=None, return_name=False):
    """
    Convert a chunk of entities into trimesh entities.

    Parameters
    ------------
    blob : (n, 2) str
      Blob of entities uppercased
    blob_raw : (n, 2) str
      Blob of entities not uppercased
    blocks : None or dict
      Blocks referenced by INSERT entities
    return_name : bool
      If True return the first '2' value

    Returns
    ----------
    """
    if blob_raw is None:
        blob_raw = blob

    def info(e):
        """
        Pull metadata based on group code, and return as a dict.
        """
        # which keys should we extract from the entity data
        # DXF group code : our metadata key
        get = {'8': 'layer'}
        # replace group codes with names and only
        # take info from the entity dict if it is in cand
        renamed = {get[k]: util.make_sequence(v)[0] for k,
                   v in e.items() if k in get}
        return renamed

    def convert_line(e):
        """
        Convert DXF LINE entities into trimesh Line entities.
        """
        # create a single Line entity
        entities.append(Line(points=len(vertices) + np.arange(2),
                             **info(e)))
        # add the vertices to our collection
        vertices.extend(np.array([[e['10'], e['20']],
                                  [e['11'], e['21']]],
                                 dtype=np.float64))

    def convert_circle(e):
        """
        Convert DXF CIRCLE entities into trimesh Circle entities
        """
        R = float(e['40'])
        C = np.array([e['10'],
                      e['20']]).astype(np.float64)
        points = to_threepoint(center=C[:2],
                               radius=R)
        entities.append(Arc(points=(len(vertices) + np.arange(3)),
                            closed=True,
                            **info(e)))
        vertices.extend(points)

    def convert_arc(e):
        """
        Convert DXF ARC entities into into trimesh Arc entities.
        """
        # the radius of the circle
        R = float(e['40'])
        # the center point of the circle
        C = np.array([e['10'],
                      e['20']], dtype=np.float64)
        # the start and end angle of the arc, in degrees
        # this may depend on an AUNITS header data
        A = np.radians(np.array([e['50'],
                                 e['51']], dtype=np.float64))
        # convert center/radius/angle representation
        # to three points on the arc representation
        points = to_threepoint(
            center=C[:2],
            radius=R,
            angles=A)
        # add a single Arc entity
        entities.append(Arc(points=len(vertices) + np.arange(3),
                            closed=False,
                            **info(e)))
        # add the three vertices
        vertices.extend(points)

    def convert_polyline(e):
        """
        Convert DXF LWPOLYLINE entities into trimesh Line entities.
        """
        # load the points in the line
        lines = np.column_stack((
            e['10'], e['20'])).astype(np.float64)

        # save entity info so we don't have to recompute
        polyinfo = info(e)

        # 70 is the closed flag for polylines
        # if the closed flag is set make sure to close
        is_closed = '70' in e and int(e['70'][0]) & 1
        if is_closed:
            lines = np.vstack((lines, lines[:1]))

        # 42 is the vertex bulge flag for LWPOLYLINE entities
        # "bulge" is autocad for "add a stupid arc using flags
        # in my otherwise normal polygon", it's like SVG arc
        # flags but somehow even more annoying
        if '42' in e:
            # get the actual bulge float values
            bulge = np.array(e['42'], dtype=np.float64)
            # what position were vertices stored at
            vid = np.nonzero(chunk[:, 0] == '10')[0]
            # what position were bulges stored at in the chunk
            bid = np.nonzero(chunk[:, 0] == '42')[0]
            # filter out endpoint bulge if we're not closed
            if not is_closed:
                bid_ok = bid < vid.max()
                bid = bid[bid_ok]
                bulge = bulge[bid_ok]
            # which vertex index is bulge value associated with
            bulge_idx = np.searchsorted(vid, bid)
            # convert stupid bulge to Line/Arc entities
            v, e = bulge_to_arcs(lines=lines,
                                 bulge=bulge,
                                 bulge_idx=bulge_idx,
                                 is_closed=is_closed)
            for i in e:
                # offset added entities by current vertices length
                i.points += len(vertices)
            vertices.extend(v)
            entities.extend(e)
            # done with this polyline
            return

        # we have a normal polyline so just add it
        # as single line entity and vertices
        entities.append(Line(
            points=np.arange(len(lines)) + len(vertices),
            **polyinfo))
        vertices.extend(lines)

    def convert_bspline(e):
        """
        Convert DXF Spline entities into trimesh BSpline entities.
        """
        # in the DXF there are n points and n ordered fields
        # with the same group code

        points = np.column_stack((e['10'],
                                  e['20'])).astype(np.float64)
        knots = np.array(e['40']).astype(np.float64)

        # if there are only two points, save it as a line
        if len(points) == 2:
            # create a single Line entity
            entities.append(Line(points=len(vertices) +
                                 np.arange(2),
                                 **info(e)))
            # add the vertices to our collection
            vertices.extend(points)
            return

        # check bit coded flag for closed
        # closed = bool(int(e['70'][0]) & 1)
        # check euclidean distance to see if closed
        closed = np.linalg.norm(points[0] -
                                points[-1]) < tol.merge

        # create a BSpline entity
        entities.append(BSpline(
            points=np.arange(len(points)) + len(vertices),
            knots=knots,
            closed=closed,
            **info(e)))
        # add the vertices
        vertices.extend(points)

    def convert_text(e):
        """
        Convert a DXF TEXT entity into a native text entity.
        """
        # text with leading and trailing whitespace removed
        text = e['1'].strip()
        # try getting optional height of text
        try:
            height = float(e['40'])
        except BaseException:
            height = None
        try:
            # rotation angle converted to radians
            angle = np.radians(float(e['50']))
        except BaseException:
            # otherwise no rotation
            angle = 0.0
        # origin point
        origin = np.array(
            [e['10'], e['20']], dtype=np.float64)
        # an origin-relative point (so transforms work)
        vector = origin + [np.cos(angle), np.sin(angle)]
        # try to extract a (horizontal, vertical) text alignment
        align = ['center', 'center']
        try:
            align[0] = ['left', 'center', 'right'][int(e['72'])]
        except BaseException:
            pass
        # append the entity
        entities.append(Text(origin=len(vertices),
                             vector=len(vertices) + 1,
                             height=height,
                             text=text,
                             align=align))
        # append the text origin and direction
        vertices.append(origin)
        vertices.append(vector)

    def convert_insert(e):
        """
        Convert an INSERT entity, which inserts a named group of
        entities (i.e. a "BLOCK") at a specific location.
        """
        if blocks is None:
            return
        # name of block to insert
        name = e['2']
        # if we haven't loaded the block skip
        if name not in blocks:
            return
        # angle to rotate the block by
        angle = float(e.get('50', 0.0))
        # the insertion point of the block
        offset = np.array([e.get('10', 0.0),
                           e.get('20', 0.0)],
                          dtype=np.float64)
        # what to scale the block by
        scale = np.array([e.get('41', 1.0),
                          e.get('42', 1.0)],
                         dtype=np.float64)

        # the current entities and vertices of the referenced block.
        cv, ce = blocks[name]
        for i in ce:
            # copy the referenced entity as it may be included multiple times
            entities.append(i.copy())
            # offset its vertices to the current index
            entities[-1].points += len(vertices)
        # transform the block's vertices based on the entity settings
        vertices.extend(tf.transform_points(cv, tf.planar_matrix(
            offset=offset, theta=np.radians(angle), scale=scale)))

    # find the start points of entities
    # DXF object to trimesh object converters
    loaders = {'LINE': (dict, convert_line),
               'LWPOLYLINE': (util.multi_dict, convert_polyline),
               'ARC': (dict, convert_arc),
               'CIRCLE': (dict, convert_circle),
               'SPLINE': (util.multi_dict, convert_bspline),
               'INSERT': (dict, convert_insert)}

    # store loaded vertices
    vertices = []
    # store loaded entities
    entities = []
    # an old-style polyline entity strings its data across
    # multiple vertex entities like a real asshole
    polyline = None
    # chunks of entities are divided by group-code-0
    inflection = np.nonzero(blob[:, 0] == '0')[0]

    # loop through chunks of entity information
    for index in np.array_split(
            np.arange(len(blob)), inflection):
        # if there is only a header continue
        if len(index) < 1:
            continue
        # chunk will be an (n, 2) array of (group code, data) pairs
        chunk = blob[index]
        # the string representing entity type
        entity_type = chunk[0][1]

        # special case old- style polyline entities
        if entity_type == 'POLYLINE':
            polyline = [dict(chunk)]
        # if we are collecting vertex entities
        elif polyline is not None and entity_type == 'VERTEX':
            polyline.append(dict(chunk))
        # the end of a polyline
        elif polyline is not None and entity_type == 'SEQEND':
            # pull the geometry information for the entity
            lines = np.array([[i['10'], i['20']]
                              for i in polyline[1:]],
                             dtype=np.float64)

            is_closed = False
            # check for a closed flag on the polyline
            if '70' in polyline[0]:
                # flag is bit- coded integer
                flag = int(polyline[0]['70'])
                # first bit represents closed
                is_closed = bool(flag & 1)
                if is_closed:
                    lines = np.vstack((lines, lines[:1]))

            # get the index of each bulged vertices
            bulge_idx = np.array([i for i, e in enumerate(polyline)
                                  if '42' in e],
                                 dtype=np.int64)
            # get the actual bulge value
            bulge = np.array([float(e['42'])
                              for i, e in enumerate(polyline)
                              if '42' in e],
                             dtype=np.float64)
            # convert bulge to new entities
            cv, ce = bulge_to_arcs(
                lines=lines,
                bulge=bulge,
                bulge_idx=bulge_idx,
                is_closed=is_closed)
            for i in ce:
                # offset entities by existing vertices
                i.points += len(vertices)
            vertices.extend(cv)
            entities.extend(ce)
            # we no longer have an active polyline
            polyline = None
        elif entity_type == 'TEXT':
            # text entities need spaces preserved so take
            # group codes from clean representation (0- column)
            # and data from the raw representation (1- column)
            chunk_raw = blob_raw[index]
            # if we didn't use clean group codes we wouldn't
            # be able to access them by key as whitespace
            # is random and crazy, like: '  1 '
            chunk_raw[:, 0] = blob[index][:, 0]
            try:
                convert_text(dict(chunk_raw))
            except BaseException:
                log.debug('failed to load text entity!',
                          exc_info=True)
        # if the entity contains all relevant data we can
        # cleanly load it from inside a single function
        elif entity_type in loaders:
            # the chunker converts an (n,2) list into a dict
            chunker, loader = loaders[entity_type]
            # convert data to dict
            entity_data = chunker(chunk)
            # append data to the lists we're collecting
            loader(entity_data)
        else:
            log.debug('Entity type %s not supported',
                      entity_type)
    # stack vertices into single array
    vertices = util.vstack_empty(vertices).astype(np.float64)
    if return_name:
        name = blob_raw[blob[:, 0] == '2'][0][1]
        return vertices, entities, name

    return vertices, entities


def export_dxf(path, layers=None):
    """
    Export a 2D path object to a DXF file.

    Parameters
    ----------
    path : trimesh.path.path.Path2D
      Input geometry to export
    layers : None, set or iterable
      If passed only export the layers specified

    Returns
    ----------
    export : str
      Path formatted as a DXF file
    """
    # get the TEMPLATES for exporting DXF files
    TEMPLATES = {k: Template(v) for k, v in
                 resources.get('dxf.json.template',
                               decode_json=True).items()}

    def format_points(points,
                      as_2D=False,
                      increment=True):
        """
        Format points into DXF- style point string.

        Parameters
        -----------
        points : (n,2) or (n,3) float
          Points in space
        as_2D : bool
          If True only output 2 points per vertex
        increment : bool
          If True increment group code per point
          Example:
            [[X0, Y0, Z0], [X1, Y1, Z1]]
          Result, new lines replaced with spaces:
            True  -> 10 X0 20 Y0 30 Z0 11 X1 21 Y1 31 Z1
            False -> 10 X0 20 Y0 30 Z0 10 X1 20 Y1 30 Z1

        Returns
        -----------
        packed : str
          Points formatted with group code
        """
        points = np.asanyarray(points, dtype=np.float64)
        # get points in 3D
        three = util.stack_3D(points)
        if increment:
            group = np.tile(
                np.arange(len(three), dtype=np.int).reshape((-1, 1)),
                (1, 3))
        else:
            group = np.zeros((len(three), 3), dtype=np.int)
        group += [10, 20, 30]

        if as_2D:
            group = group[:, :2]
            three = three[:, :2]
        # join into result string
        packed = '\n'.join('{:d}\n{:.12f}'.format(g, v)
                           for g, v in zip(group.reshape(-1),
                                           three.reshape(-1)))

        return packed

    def entity_info(entity):
        """
        Pull layer, color, and name information about an entity

        Parameters
        -----------
        entity : entity object
          Source entity to pull metadata

        Returns
        ----------
        subs : dict
          Has keys 'COLOR', 'LAYER', 'NAME'
        """
        # TODO : convert RGBA entity.color to index
        subs = {'COLOR': 255,  # default is ByLayer
                'LAYER': 0,
                'NAME': str(id(entity))[:16]}
        if hasattr(entity, 'layer'):
            # make sure layer name is forced into ASCII
            subs['LAYER'] = util.to_ascii(entity.layer)
        return subs

    def convert_line(line, vertices):
        """
        Convert an entity to a discrete polyline

        Parameters
        -------------
        line : entity
          Entity which has 'e.discrete' method
        vertices : (n, 2) float
          Vertices in space

        Returns
        -----------
        as_dxf : str
          Entity exported as a DXF
        """
        # get a discrete representation of entity
        points = line.discrete(vertices)
        # if one or fewer points return nothing
        if len(points) <= 1:
            return ''

        # generate a substitution dictionary for template
        subs = entity_info(line)
        subs['POINTS'] = format_points(points,
                                       as_2D=True,
                                       increment=False)
        subs['TYPE'] = 'LWPOLYLINE'
        subs['VCOUNT'] = len(points)
        # 1 is closed
        # 0 is default (open)
        subs['FLAG'] = int(bool(line.closed))

        result = TEMPLATES['line'].substitute(subs)
        return result

    def convert_arc(arc, vertices):
        # get the center of arc and include span angles
        info = arc.center(vertices, return_angle=True, return_normal=False)
        subs = entity_info(arc)
        center = info['center']
        if len(center) == 2:
            center = np.append(center, 0.0)
        data = '10\n{:.12f}\n20\n{:.12f}\n30\n{:.12f}'.format(*center)
        data += '\n40\n{:.12f}'.format(info['radius'])

        if arc.closed:
            subs['TYPE'] = 'CIRCLE'
        else:
            subs['TYPE'] = 'ARC'
            # an arc is the same as a circle, with an added start
            # and end angle field
            data += '\n100\nAcDbArc'
            data += '\n50\n{:.12f}\n51\n{:.12f}'.format(
                *np.degrees(info['angles']))
        subs['DATA'] = data

        result = TEMPLATES['arc'].substitute(subs)

        return result

    def convert_bspline(spline, vertices):
        # points formatted with group code
        points = format_points(vertices[spline.points],
                               increment=False)

        # (n,) float knots, formatted with group code
        knots = ('40\n{:.12f}\n' * len(spline.knots)
                 ).format(*spline.knots)[:-1]

        # bit coded
        flags = {'closed': 1,
                 'periodic': 2,
                 'rational': 4,
                 'planar': 8,
                 'linear': 16}

        flag = flags['planar']
        if spline.closed:
            flag = flag | flags['closed']

        normal = [0.0, 0.0, 1.0]
        n_code = [210, 220, 230]
        n_str = '\n'.join('{:d}\n{:.12f}'.format(i, j)
                          for i, j in zip(n_code, normal))

        subs = entity_info(spline)
        subs.update({'TYPE': 'SPLINE',
                     'POINTS': points,
                     'KNOTS': knots,
                     'NORMAL': n_str,
                     'DEGREE': 3,
                     'FLAG': flag,
                     'FCOUNT': 0,
                     'KCOUNT': len(spline.knots),
                     'PCOUNT': len(spline.points)})
        # format into string template
        result = TEMPLATES['bspline'].substitute(subs)

        return result

    def convert_text(txt, vertices):
        """
        Convert a Text entity to DXF string.
        """
        # start with layer info
        sub = entity_info(txt)
        # get the origin point of the text
        sub['ORIGIN'] = format_points(
            vertices[[txt.origin]], increment=False)
        # rotation angle in degrees
        sub['ANGLE'] = np.degrees(txt.angle(vertices))
        # actual string of text with spaces escaped
        # force into ASCII to avoid weird encoding issues
        sub['TEXT'] = txt.text.replace(' ', _SAFESPACE).encode(
            'ascii', errors='ignore').decode('ascii')
        # height of text
        sub['HEIGHT'] = txt.height
        result = TEMPLATES['text'].substitute(sub)
        return result

    def convert_generic(entity, vertices):
        """
        For entities we don't know how to handle, return their
        discrete form as a polyline
        """
        return convert_line(entity, vertices)

    # make sure we're not losing a ton of
    # precision in the string conversion
    np.set_printoptions(precision=12)
    # trimesh entity to DXF entity converters
    conversions = {'Line': convert_line,
                   'Text': convert_text,
                   'Arc': convert_arc,
                   'Bezier': convert_generic,
                   'BSpline': convert_bspline}
    collected = []
    for e, layer in zip(path.entities, path.layers):
        name = type(e).__name__
        # only export specified layers
        if layers is not None:
            if layer not in layers:
                continue
        if name in conversions:
            converted = conversions[name](e, path.vertices).strip()
            if len(converted) > 0:
                # only save if we converted something
                collected.append(converted)
        else:
            log.debug('Entity type %s not exported!', name)

    # join all entities into one string
    entities_str = '\n'.join(collected)
    hsub = {'BOUNDS_MIN': format_points([path.bounds[0]]),
            'BOUNDS_MAX': format_points([path.bounds[1]]),
            'LUNITS': '1'}
    if path.units in _UNITS_TO_DXF:
        hsub['LUNITS'] = _UNITS_TO_DXF[path.units]
    # sections of the DXF
    header = TEMPLATES['header'].substitute(hsub)
    # entities section
    entities = TEMPLATES['entities'].substitute({
        'ENTITIES': entities_str})
    footer = TEMPLATES['footer'].substitute()

    # filter out empty sections
    # random whitespace causes AutoCAD to fail to load
    # although Draftsight, LibreCAD, and Inkscape don't care
    # what a giant legacy piece of shit
    # strip out all leading and trailing whitespace
    sections = [i.strip() for i in
                [header, entities, footer]
                if len(i) > 0]
    # create the joined string blob
    blob = '\n'.join(sections).replace(_SAFESPACE, ' ')
    # run additional self- checks
    if tol.strict:
        # check that every line pair is (group code, value)
        lines = str.splitlines(str(blob))
        # should be even number of lines
        assert (len(lines) % 2) == 0
        # group codes should all be convertible to int and positive
        assert all(int(i) >= 0 for i in lines[::2])
        # make sure we didn't slip any unicode in there
        blob.encode('ascii')

    return blob


def load_dwg(file_obj, **kwargs):
    """
    Load DWG files by converting them to DXF files using
    TeighaFileConverter.

    Parameters
    -------------
    file_obj : file- like object

    Returns
    -------------
    loaded : dict
        kwargs for a Path2D constructor
    """
    # read the DWG data into a bytes object
    data = file_obj.read()
    # convert data into R14 ASCII DXF
    converted = _teigha_convert(data)
    # load into kwargs for Path2D constructor
    result = load_dxf(util.wrap_as_stream(converted))

    return result


def bulge_to_arcs(lines,
                  bulge,
                  bulge_idx,
                  is_closed=False,
                  metadata=None):
    """
    Polylines can have "vertex bulge," which means the polyline
    has an arc tangent to segments, rather than meeting at a
    vertex.

    From Autodesk reference:
    The bulge is the tangent of one fourth the included
    angle for an arc segment, made negative if the arc
    goes clockwise from the start point to the endpoint.
    A bulge of 0 indicates a straight segment, and a
    bulge of 1 is a semicircle.

    Parameters
    ----------------
    lines : (n, 2) float
      Polyline vertices in order
    bulge : (m,) float
      Vertex bulge value
    bulge_idx : (m,) float
      Which index of lines is bulge associated with
    is_closed : bool
      Is segment closed
    metadata : None, or dict
      Entity metadata to add

    Returns
    ---------------
    vertices : (a, 2) float
      New vertices for poly-arc
    entities : (b,) entities.Entity
      New entities, either line or arc
    """
    # make sure lines are 2D array
    lines = np.asanyarray(lines, dtype=np.float64)

    # make sure inputs are numpy arrays
    bulge = np.asanyarray(bulge, dtype=np.float64)
    bulge_idx = np.asanyarray(bulge_idx, dtype=np.int64)

    # filter out zero- bulged polylines
    ok = np.abs(bulge) > 1e-5
    bulge = bulge[ok]
    bulge_idx = bulge_idx[ok]

    # metadata to apply to new entities
    if metadata is None:
        metadata = {}

    # if there's no bulge, just return the input curve
    if len(bulge) == 0:
        index = np.arange(len(lines))
        # add a single line entity and vertices
        entities = [Line(index, **metadata)]
        return lines, entities

    # use bulge to calculate included angle of the arc
    angle = np.arctan(bulge) * 4.0
    # the indexes making up a bulged segment
    tid = np.column_stack((bulge_idx, bulge_idx - 1))
    # if it's a closed segment modulus to start vertex
    if is_closed:
        tid %= len(lines)

    # the vector connecting the two ends of the arc
    vector = lines[tid[:, 0]] - lines[tid[:, 1]]

    # the length of the connector segment
    length = (np.linalg.norm(vector, axis=1))

    # perpendicular vectors by crossing vector with Z
    perp = np.cross(
        np.column_stack((vector, np.zeros(len(vector)))),
        np.ones((len(vector), 3)) * [0, 0, 1])
    # strip the zero Z
    perp = util.unitize(perp[:, :2])

    # midpoint of each line
    midpoint = lines[tid].mean(axis=1)

    # calculate the signed radius of each arc segment
    radius = (length / 2.0) / np.sin(angle / 2.0)

    # offset magnitude to point on arc
    offset = radius - np.cos(angle / 2) * radius

    # convert each arc to three points:
    # start, any point on arc, end
    three = np.column_stack((
        lines[tid[:, 0]],
        midpoint + perp * offset.reshape((-1, 1)),
        lines[tid[:, 1]])).reshape((-1, 3, 2))

    # if we're in strict mode make sure our arcs
    # have the same magnitude as the input data
    if tol.strict:
        from ..arc import arc_center
        check_angle = [arc_center(i)['span']
                       for i in three]
        assert np.allclose(np.abs(angle),
                           np.abs(check_angle))

        check_radii = [arc_center(i)['radius']
                       for i in three]
        assert np.allclose(check_radii, np.abs(radius))

    # collect new entities and vertices
    entities, vertices = [], []
    # add the entities for each new arc
    for arc_points in three:
        entities.append(Arc(
            points=np.arange(3) + len(vertices),
            **metadata))
        vertices.extend(arc_points)

    # if there are unconsumed line
    # segments add them to drawing
    if (len(lines) - 1) > len(bulge):
        # indexes of line segments
        existing = util.stack_lines(np.arange(len(lines)))
        # remove line segments replaced with arcs
        for line_idx in grouping.boolean_rows(
                existing,
                np.sort(tid, axis=1),
                np.setdiff1d):
            # add a single line entity and vertices
            entities.append(Line(
                points=np.arange(2) + len(vertices),
                **metadata))
            vertices.extend(lines[line_idx].copy())

    # make sure vertices are clean numpy array
    vertices = np.array(vertices, dtype=np.float64)

    return vertices, entities


def get_key(blob, field, code):
    """
    Given a loaded (n, 2) blob and a field name
    get a value by code.
    """
    try:
        line = blob[np.nonzero(blob[:, 1] == field)[0][0] + 1]
    except IndexError:
        return None
    if line[0] == code:
        try:
            return int(line[1])
        except ValueError:
            return line[1]
    else:
        return None


def _teigha_convert(data, extension='dwg'):
    """
    Convert any DXF/DWG to R14 ASCII DXF using Teigha Converter.

    Parameters
    ---------------
    data : str or bytes
       The contents of a DXF or DWG file
    extension : str
       The format of data: 'dwg' or 'dxf'

    Returns
    --------------
    converted : str
       Result as R14 ASCII DXF
    """
    # temp directory for DWG file
    dir_dwg = tempfile.mkdtemp()
    # temp directory for DXF output
    dir_out = tempfile.mkdtemp()

    # put together the subprocess command
    cmd = [_xvfb_run,  # suppress the GUI QT status bar
           '-a',      # use an automatic screen
           _teigha,   # run the converter
           dir_dwg,   # the directory containing DWG files
           dir_out,   # the directory for output DXF files
           'ACAD14',  # the revision of DXF
           'DXF',     # the output format
           '1',       # recurse input folder
           '1']       # audit each file

    # if Xvfb is already running it probably
    # has a working configuration so use it
    running = b'Xvfb' in subprocess.check_output(['ps', '-eaf'])
    # chop off XVFB if it isn't installed or is running
    if running or _xvfb_run is None:
        cmd = cmd[2:]

    # create file in correct mode for data
    if hasattr(data, 'encode'):
        # data is a string which can be encoded to bytes
        mode = 'w'
    else:
        # data is already bytes
        mode = 'wb'

    # write the file_obj in the temp directory
    dwg_name = os.path.join(dir_dwg, 'drawing.' + extension)
    with open(dwg_name, mode) as f:
        f.write(data)

    # run the conversion
    output = subprocess.check_output(cmd)

    # load the ASCII DXF produced from the conversion
    name_result = os.path.join(dir_out, 'drawing.dxf')
    # if the conversion failed log things before failing
    if not os.path.exists(name_result):
        log.error('teigha convert failed!\nls {}: {}\n\n {}'.format(
            dir_out,
            os.listdir(dir_out),
            output))
        raise ValueError('conversion using Teigha failed!')

    # load converted file into a string
    with open(name_result, 'rb') as f:
        converted = f.read().decode(errors='ignore')

    # remove the temporary directories
    shutil.rmtree(dir_out)
    shutil.rmtree(dir_dwg)

    return converted


# the DWG to DXF converter
# they renamed it at some point but it is the same
for _name in ['ODAFileConverter',
              'TeighaFileConverter']:
    _teigha = find_executable(_name)
    if _teigha is not None:
        break

# suppress X11 output
_xvfb_run = find_executable('xvfb-run')

# store the loaders we have available
_dxf_loaders = {'dxf': load_dxf}
# DWG is only available if teigha converter is installed
if _teigha is not None:
    _dxf_loaders['dwg'] = load_dwg
