import numpy as np
from collections import deque, defaultdict

try:
    # `pip install pillow`
    # optional: used for textured meshes
    import PIL.Image as Image
except BaseException as E:
    # if someone tries to use Image re-raise
    # the import error so they can debug easily
    from ..exceptions import ExceptionModule
    Image = ExceptionModule(E)

from .. import util
from ..visual.color import to_float
from ..visual.texture import unmerge_faces, TextureVisuals
from ..visual.material import SimpleMaterial

from ..constants import log, tol


def load_obj(file_obj,
             resolver=None,
             split_object=False,
             group_material=True,
             skip_materials=False,
             maintain_order=False,
             **kwargs):
    """
    Load a Wavefront OBJ file into kwargs for a trimesh.Scene
    object.

    Parameters
    --------------
    file_obj : file like object
      Contains OBJ data
    resolver : trimesh.visual.resolvers.Resolver
      Allow assets such as referenced textures and
      material files to be loaded
    split_object : bool
      Split meshes at each `o` declared in file
    group_material : bool
      Group faces that share the same material
      into the same mesh.
    skip_materials : bool
      Don't load any materials.
    maintain_order : bool or None
      Do not reorder faces or vertices which may result
      in visual artifacts.

    Returns
    -------------
    kwargs : dict
      Keyword arguments which can be loaded by
      trimesh.exchange.load.load_kwargs into a trimesh.Scene
    """
    # get text as bytes or string blob
    text = file_obj.read()

    # if text was bytes decode into string
    text = util.decode_text(text)

    # add leading and trailing newlines so we can use the
    # same logic even if they jump directly in to data lines
    text = '\n{}\n'.format(text.strip().replace('\r\n', '\n'))

    # remove backslash continuation characters and merge them into the same line
    text = text.replace('\\\n', '')

    # Load Materials
    materials = {}
    mtl_position = text.find('mtllib')
    if not skip_materials and mtl_position >= 0:
        # take the line of the material file after `mtllib`
        # which should be the file location of the .mtl file
        mtl_path = text[mtl_position + 6:text.find('\n', mtl_position)].strip()
        try:
            # use the resolver to get the data
            material_kwargs = parse_mtl(resolver[mtl_path],
                                        resolver=resolver)
            # turn parsed kwargs into material objects
            materials = {k: SimpleMaterial(**v)
                         for k, v in material_kwargs.items()}
        except IOError:
            # usually the resolver couldn't find the asset
            log.warning('unable to load materials from: {}'.format(mtl_path))
        except BaseException:
            # something else happened so log a warning
            log.warning('unable to load materials from: {}'.format(mtl_path),
                        exc_info=True)

    # extract vertices from raw text
    v, vn, vt, vc = _parse_vertices(text=text)

    # get relevant chunks that have face data
    # in the form of (material, object, chunk)
    face_tuples = _preprocess_faces(
        text=text, split_object=split_object)

    # combine chunks that have the same material
    # some meshes end up with a LOT of components
    # and will be much slower if you don't do this
    if group_material:
        face_tuples = _group_by_material(face_tuples)

    # no faces but points given
    # return point cloud
    if not len(face_tuples) and v is not None:
        pc = {'vertices': v}
        if vn is not None:
            pc['vertex_normals'] = vn
        if vc is not None:
            pc['vertex_colors'] = vc
        return pc

    # Load Faces
    # now we have clean- ish faces grouped by material and object
    # so now we have to turn them into numpy arrays and kwargs
    # for trimesh mesh and scene objects
    geometry = {}
    while len(face_tuples) > 0:
        # consume the next chunk of text
        material, current_object, chunk = face_tuples.pop()
        # do wangling in string form
        # we need to only take the face line before a newline
        # using builtin functions in a list comprehension
        # is pretty fast relative to other options
        # this operation is the only one that is O(len(faces))
        # slower due to the tight-loop conditional:
        # face_lines = [i[:i.find('\n')]
        #              for i in chunk.split('\nf ')[1:]
        #              if i.rfind('\n') >0]
        # maxsplit=1 means that it can stop working
        # after it finds the first newline
        # passed as arg as it's not a kwarg in python2
        face_lines = [i.split('\n', 1)[0]
                      for i in chunk.split('\nf ')[1:]]
        # then we are going to replace all slashes with spaces
        joined = ' '.join(face_lines).replace('/', ' ')

        # the fastest way to get to a numpy array
        # processes the whole string at once into a 1D array
        # also wavefront is 1-indexed (vs 0-indexed) so offset
        array = np.fromstring(joined, sep=' ', dtype=np.int64) - 1

        # get the number of raw 2D columns in a sample line
        columns = len(face_lines[0].strip().replace('/', ' ').split())

        # make sure we have the right number of values for vectorized
        if len(array) == (columns * len(face_lines)):
            # everything is a nice 2D array
            faces, faces_tex, faces_norm = _parse_faces_vectorized(
                array=array,
                columns=columns,
                sample_line=face_lines[0])
        else:
            # if we had something annoying like mixed in quads
            # or faces that differ per-line we have to loop
            # i.e. something like:
            #  '31407 31406 31408',
            #  '32303/2469 32304/2469 32305/2469',
            log.debug('faces have mixed data, using slow fallback!')
            faces, faces_tex, faces_norm = _parse_faces_fallback(face_lines)

        # TODO: name usually falls back to something useless
        name = current_object
        if name is None or len(name) == 0 or name in geometry:
            name = '{}_{}'.format(name, util.unique_id())

        # try to get usable texture
        mesh = kwargs.copy()
        if faces_tex is not None:
            # convert faces referencing vertices and
            # faces referencing vertex texture to new faces
            # where each face
            if faces_norm is not None and len(faces_norm) == len(faces):
                new_faces, mask_v, mask_vt, mask_vn = unmerge_faces(
                    faces, faces_tex, faces_norm, maintain_faces=maintain_order)
            else:
                mask_vn = None
                # no face normals but face texturre
                new_faces, mask_v, mask_vt = unmerge_faces(
                    faces, faces_tex, maintain_faces=maintain_order)

            if tol.strict:
                # we should NOT have messed up the faces
                # note: this is EXTREMELY slow due to all the
                # float comparisons so only run this in unit tests
                assert np.allclose(v[faces], v[mask_v][new_faces])
                # faces should all be in bounds of vertives
                assert new_faces.max() < len(v[mask_v])
            try:
                # survive index errors as sometimes we
                # want materials without UV coordinates
                uv = vt[mask_vt]
            except BaseException:
                log.warning('index failed on UV coordinates, skipping!')
                uv = None

            # mask vertices and use new faces
            mesh.update({'vertices': v[mask_v].copy(),
                         'faces': new_faces})

        else:
            # otherwise just use unmasked vertices
            uv = None

            # check to make sure indexes are in bounds
            if tol.strict:
                assert faces.max() < len(v)

            if vn is not None and np.shape(faces_norm) == faces.shape:
                # do the crazy unmerging logic for split indices
                new_faces, mask_v, mask_vn = unmerge_faces(
                    faces, faces_norm, maintain_faces=maintain_order)
            else:
                # generate the mask so we only include
                # referenced vertices in every new mesh
                mask_v = np.zeros(len(v), dtype=bool)
                mask_v[faces] = True

                # reconstruct the faces with the new vertex indices
                inverse = np.zeros(len(v), dtype=np.int64)
                inverse[mask_v] = np.arange(mask_v.sum())
                new_faces = inverse[faces]
                # no normals
                mask_vn = None

            # start with vertices and faces
            mesh.update({'faces': new_faces,
                         'vertices': v[mask_v].copy()})

            # if colors and normals are OK save them
            if vc is not None:
                try:
                    # may fail on a malformed color mask
                    mesh['vertex_colors'] = vc[mask_v]
                except BaseException:
                    log.warning('failed to load vertex_colors',
                                exc_info=True)
            if mask_vn is not None:
                try:
                    # may fail on a malformed mask
                    mesh['vertex_normals'] = vn[mask_vn]
                except BaseException:
                    log.warning('failed to load vertex_normals',
                                exc_info=True)
        visual = None
        if material in materials:
            # use the material with the UV coordinates
            visual = TextureVisuals(
                uv=uv, material=materials[material])
        elif uv is not None and len(uv) == len(mesh['vertices']):
            # create a texture with an empty materials
            visual = TextureVisuals(uv=uv)
        elif material is not None:
            # case where material is specified but not available
            log.warning('specified material ({})  not loaded!'.format(
                material))
        # assign the visual
        mesh['visual'] = visual
        # store geometry by name
        geometry[name] = mesh

    if len(geometry) == 1:
        # TODO : should this be removed to always return a scene?
        return next(iter(geometry.values()))

    # add an identity transform for every geometry
    graph = [{'geometry': k, 'frame_to': k, 'matrix': np.eye(4)}
             for k in geometry.keys()]

    # convert to scene kwargs
    result = {'geometry': geometry,
              'graph': graph}

    return result


def parse_mtl(mtl, resolver=None):
    """
    Parse a loaded MTL file.

    Parameters
    -------------
    mtl : str or bytes
      Data from an MTL file
    resolver : trimesh.Resolver
      Fetch assets by name from file system, web, or other

    Returns
    ------------
    mtllibs : list of dict
      Each dict has keys: newmtl, map_Kd, Kd
    """
    # decode bytes into string if necessary
    mtl = util.decode_text(mtl)

    # current material
    material = None
    # materials referenced by name
    materials = {}
    # use universal newline splitting
    lines = str.splitlines(str(mtl).strip())

    # remap OBJ property names to kwargs for SimpleMaterial
    mapped = {'Kd': 'diffuse',
              'Ka': 'ambient',
              'Ks': 'specular',
              'Ns': 'glossiness'}

    for line in lines:
        # split by white space
        split = line.strip().split()
        # needs to be at least two values
        if len(split) <= 1:
            continue
        # the first value is the parameter name
        key = split[0]
        # start a new material
        if key == 'newmtl':
            # material name extracted from line like:
            # newmtl material_0
            if material is not None:
                # save the old material by old name and remove key
                materials[material.pop('newmtl')] = material
            # start a fresh new material
            material = {'newmtl': ' '.join(split[1:])}

        elif key == 'map_Kd':
            # represents the file name of the texture image
            try:
                file_data = resolver.get(split[1])
                # load the bytes into a PIL image
                # an image file name
                material['image'] = Image.open(
                    util.wrap_as_stream(file_data))
            except BaseException:
                log.warning('failed to load image', exc_info=True)

        elif key in mapped.keys():
            try:
                # diffuse, ambient, and specular float RGB
                value = [float(x) for x in split[1:]]
                # if there is only one value return that
                if len(value) == 1:
                    value = value[0]
                # store the key by mapped value
                material[mapped[key]] = value
                # also store key by OBJ name
                material[key] = value
            except BaseException:
                log.warning('failed to convert color!', exc_info=True)
        # pass everything as kwargs to material constructor
        elif material is not None:
            # save any other unspecified keys
            material[key] = split[1:]
    # reached EOF so save any existing materials
    if material:
        materials[material.pop('newmtl')] = material

    return materials


def _parse_faces_vectorized(array, columns, sample_line):
    """
    Parse loaded homogeneous (tri/quad) face data in a
    vectorized manner.

    Parameters
    ------------
    array : (n,) int
      Indices in order
    columns : int
      Number of columns in the file
    sample_line : str
      A single line so we can assess the ordering

    Returns
    --------------
    faces : (n, d) int
      Faces in space
    faces_tex : (n, d) int or None
      Texture for each vertex in face
    faces_norm : (n, d) int or None
      Normal index for each vertex in face
    """
    # reshape to columns
    array = array.reshape((-1, columns))
    # how many elements are in the first line of faces
    # i.e '13/1/13 14/1/14 2/1/2 1/2/1' is 4
    group_count = len(sample_line.strip().split())
    # how many elements are there for each vertex reference
    # i.e. '12/1/13' is 3
    per_ref = int(columns / group_count)
    # create an index mask we can use to slice vertex references
    index = np.arange(group_count) * per_ref
    # slice the faces out of the blob array
    faces = array[:, index]

    # TODO: probably need to support 8 and 12 columns for quads
    # or do something more general
    faces_tex, faces_norm = None, None
    if columns == 6:
        # if we have two values per vertex the second
        # one is index of texture coordinate (`vt`)
        # count how many delimiters are in the first face line
        # to see if our second value is texture or normals
        # do splitting to clip off leading/trailing slashes
        count = ''.join(i.strip('/') for i in sample_line.split()).count('/')
        if count == columns:
            # case where each face line looks like:
            # ' 75//139 76//141 77//141'
            # which is vertex/nothing/normal
            faces_norm = array[:, index + 1]
        elif count == int(columns / 2):
            # case where each face line looks like:
            # '75/139 76/141 77/141'
            # which is vertex/texture
            faces_tex = array[:, index + 1]
        else:
            log.warning('face lines are weird: {}'.format(
                sample_line))
    elif columns == 9:
        # if we have three values per vertex
        # second value is always texture
        faces_tex = array[:, index + 1]
        # third value is reference to vertex normal (`vn`)
        faces_norm = array[:, index + 2]
    return faces, faces_tex, faces_norm


def _parse_faces_fallback(lines):
    """
    Use a slow but more flexible looping method to process
    face lines as a fallback option to faster vectorized methods.

    Parameters
    -------------
    lines : (n,) str
      List of lines with face information

    Returns
    -------------
    faces : (m, 3) int
      Clean numpy array of face triangles
    """

    # collect vertex, texture, and vertex normal indexes
    v, vt, vn = [], [], []

    # loop through every line starting with a face
    for line in lines:
        # remove leading newlines then
        # take first bit before newline then split by whitespace
        split = line.strip().split('\n')[0].split()
        # split into: ['76/558/76', '498/265/498', '456/267/456']
        len_split = len(split)
        if len_split == 3:
            pass
        elif len_split == 4:
            # triangulate quad face
            split = [split[0],
                     split[1],
                     split[2],
                     split[2],
                     split[3],
                     split[0]]
        elif len_split > 4:
            # triangulate polygon, as a triangles fan
            r_split = []
            r_split_append = r_split.append
            for i in range(len(split) - 2):
                r_split_append(split[0])
                r_split_append(split[i + 1])
                r_split_append(split[i + 2])
            split = r_split
        else:
            log.warning(
                'face need at least 3 elements (got {})! skipping!'.format(
                    len(split)))
            continue

        # f is like: '76/558/76'
        for f in split:
            # vertex, vertex texture, vertex normal
            split = f.split('/')
            # we always have a vertex reference
            v.append(int(split[0]))

            # faster to try/except than check in loop
            try:
                vt.append(int(split[1]))
            except BaseException:
                pass
            try:
                # vertex normal is the third index
                vn.append(int(split[2]))
            except BaseException:
                pass

    # shape into triangles and switch to 0-indexed
    faces = np.array(v, dtype=np.int64).reshape((-1, 3)) - 1
    faces_tex, normals = None, None
    if len(vt) == len(v):
        faces_tex = np.array(vt, dtype=np.int64).reshape((-1, 3)) - 1
    if len(vn) == len(v):
        normals = np.array(vn, dtype=np.int64).reshape((-1, 3)) - 1

    return faces, faces_tex, normals


def _parse_vertices(text):
    """
    Parse raw OBJ text into vertices, vertex normals,
    vertex colors, and vertex textures.

    Parameters
    -------------
    text : str
      Full text of an OBJ file

    Returns
    -------------
    v : (n, 3) float
      Vertices in space
    vn : (m, 3) float or None
      Vertex normals
    vt : (p, 2) float or None
      Vertex texture coordinates
    vc : (n, 3) float or None
      Per-vertex color
    """

    # the first position of a vertex in the text blob
    # we only really need to search from the start of the file
    # up to the location of out our first vertex but we
    # are going to use this check for "do we have texture"
    # determination later so search the whole stupid file
    starts = {k: text.find('\n{} '.format(k)) for k in
              ['v', 'vt', 'vn']}

    # no valid values so exit early
    if not any(v >= 0 for v in starts.values()):
        return None, None, None, None

    # find the last position of each valid value
    ends = {k: text.find(
        '\n', text.rfind('\n{} '.format(k)) + 2 + len(k))
        for k, v in starts.items() if v >= 0}

    # take the first and last position of any vertex property
    start = min(s for s in starts.values() if s >= 0)
    end = max(e for e in ends.values() if e >= 0)
    # get the chunk of test that contains vertex data
    chunk = text[start:end].replace('+e', 'e').replace('-e', 'e')

    # get the clean-ish data from the file as python lists
    data = {k: [i.split('\n', 1)[0]
                for i in chunk.split('\n{} '.format(k))[1:]]
            for k, v in starts.items() if v >= 0}

    # count the number of data values per row on a sample row
    per_row = {k: len(v[0].split()) for k, v in data.items()}

    # convert data values into numpy arrays
    result = defaultdict(lambda: None)
    for k, value in data.items():
        # use joining and fromstring to get as numpy array
        array = np.fromstring(
            ' '.join(value), sep=' ', dtype=np.float64)
        # what should our shape be
        shape = (len(value), per_row[k])
        # check shape of flat data
        if len(array) == np.product(shape):
            # we have a nice 2D array
            result[k] = array.reshape(shape)
        else:
            # try to recover with a slightly more expensive loop
            count = per_row[k]
            try:
                # try to get result through reshaping
                result[k] = np.fromstring(
                    ' '.join(i.split()[:count] for i in value),
                    sep=' ', dtype=np.float64).reshape(shape)
            except BaseException:
                pass

    # vertices
    v = result['v']
    # vertex colors are stored next to vertices
    vc = None
    if v is not None and v.shape[1] >= 6:
        # vertex colors are stored after vertices
        v, vc = v[:, :3], v[:, 3:6]
    elif v is not None and v.shape[1] > 3:
        # we got a lot of something unknowable
        v = v[:, :3]

    # vertex texture or None
    vt = result['vt']
    if vt is not None:
        # sometimes UV coordinates come in as UVW
        vt = vt[:, :2]
    # vertex normals or None
    vn = result['vn']

    # check will generally only be run in unit tests
    # so we are allowed to do things that are slow
    if tol.strict:
        # check to make sure our subsetting
        # didn't miss any vertices or data
        assert len(v) == text.count('\nv ')
        # make sure optional data matches file too
        if vn is not None:
            assert len(vn) == text.count('\nvn ')
        if vt is not None:
            assert len(vt) == text.count('\nvt ')

    return v, vn, vt, vc


def _group_by_material(face_tuples):
    """
    For chunks of faces split by material group
    the chunks that share the same material.

    Parameters
    ------------
    face_tuples : (n,) list of (material, obj, chunk)
      The data containing faces

    Returns
    ------------
    grouped : (m,) list of (material, obj, chunk)
      Grouped by material
    """

    # store the chunks grouped by material
    grouped = defaultdict(lambda: ['', '', []])
    # loop through existring
    for material, obj, chunk in face_tuples:
        grouped[material][0] = material
        grouped[material][1] = obj
        # don't do a million string concatenations in loop
        grouped[material][2].append(chunk)
    # go back and do a join to make a single string
    for k in grouped.keys():
        grouped[k][2] = '\n'.join(grouped[k][2])
    # return as list
    return list(grouped.values())


def _preprocess_faces(text, split_object=False):
    # Pre-Process Face Text
    # Rather than looking at each line in a loop we're
    # going to split lines by directives which indicate
    # a new mesh, specifically 'usemtl' and 'o' keys
    # search for materials, objects, faces, or groups
    starters = ['\nusemtl ', '\no ', '\nf ', '\ng ', '\ns ']
    f_start = len(text)
    # first index of material, object, face, group, or smoother
    for st in starters:
        search = text.find(st, 0, f_start)
        # if not contained find will return -1
        if search < 0:
            continue
        # subtract the length of the key from the position
        # to make sure it's included in the slice of text
        if search < f_start:
            f_start = search
    # index in blob of the newline after the last face
    f_end = text.find('\n', text.rfind('\nf ') + 3)
    # get the chunk of the file that has face information
    if f_end >= 0:
        # clip to the newline after the last face
        f_chunk = text[f_start:f_end]
    else:
        # no newline after last face
        f_chunk = text[f_start:]

    if tol.strict:
        # check to make sure our subsetting didn't miss any faces
        assert f_chunk.count('\nf ') == text.count('\nf ')

    # start with undefined objects and material
    current_object = None
    current_material = None
    # where we're going to store result tuples
    # containing (material, object, face lines)
    face_tuples = []

    # two things cause new meshes to be created: objects and materials
    # first divide faces into groups split by material and objects
    # face chunks using different materials will be treated
    # as different meshes
    for m_chunk in f_chunk.split('\nusemtl '):
        # if empty continue
        if len(m_chunk) == 0:
            continue

        # find the first newline in the chunk
        # everything before it will be the usemtl direction
        new_line = m_chunk.find('\n')
        # if the file contained no materials it will start with a newline
        if new_line == 0:
            current_material = None
        else:
            # remove internal double spaces because why wouldn't that be OK
            current_material = ' '.join(m_chunk[:new_line].strip().split())

        # material chunk contains multiple objects
        if split_object:
            o_split = m_chunk.split('\no ')
        else:
            o_split = [m_chunk]
        if len(o_split) > 1:
            for o_chunk in o_split:
                # set the object label
                current_object = o_chunk[:o_chunk.find('\n')].strip()
                # find the first face in the chunk
                f_idx = o_chunk.find('\nf ')
                # if we have any faces append it to our search tuple
                if f_idx >= 0:
                    face_tuples.append(
                        (current_material,
                         current_object,
                         o_chunk[f_idx:]))
        else:
            # if there are any faces in this chunk add them
            f_idx = m_chunk.find('\nf ')
            if f_idx >= 0:
                face_tuples.append(
                    (current_material,
                     current_object,
                     m_chunk[f_idx:]))
    return face_tuples


def export_obj(mesh,
               include_normals=True,
               include_color=True,
               include_texture=True,
               return_texture=False,
               write_texture=True,
               resolver=None,
               digits=8):
    """
    Export a mesh as a Wavefront OBJ file.
    TODO: scenes with textured meshes

    Parameters
    -----------
    mesh : trimesh.Trimesh
      Mesh to be exported
    include_normals : bool
      Include vertex normals in export
    include_color : bool
      Include vertex color in export
    include_texture : bool
      Include `vt` texture in file text
    return_texture : bool
      If True, return a dict with texture files
    write_texture : bool
      If True and a writable resolver is passed
      write the referenced texture files with resolver
    resolver : None or trimesh.resolvers.Resolver
      Resolver which can write referenced text objects
    digits : int
      Number of digits to include for floating point

    Returns
    -----------
    export : str
      OBJ format output
    texture : dict
      [OPTIONAL]
      Contains files that need to be saved in the same
      directory as the exported mesh: {file name : bytes}
    """
    # store the multiple options for formatting
    # vertex indexes for faces
    face_formats = {('v',): '{}',
                    ('v', 'vn'): '{}//{}',
                    ('v', 'vt'): '{}/{}',
                    ('v', 'vn', 'vt'): '{}/{}/{}'}

    # check the input
    if util.is_instance_named(mesh, 'Trimesh'):
        meshes = [mesh]
    elif util.is_instance_named(mesh, 'Scene'):
        meshes = mesh.dump()
    elif util.is_instance_named(mesh, 'PointCloud'):
        meshes = [mesh]
    else:
        raise ValueError('must be Trimesh or Scene!')

    objects = deque([])

    counts = {'v': 0, 'vn': 0, 'vt': 0}

    tex_data = None

    for mesh in meshes:
        # we are going to reference face_formats with this
        face_type = ['v']
        # OBJ includes vertex color as RGB elements on the same line
        if (include_color and
            mesh.visual.kind in ['vertex', 'face'] and
                len(mesh.visual.vertex_colors)):

            # create a stacked blob with position and color
            v_blob = np.column_stack((
                mesh.vertices,
                to_float(mesh.visual.vertex_colors[:, :3])))
        else:
            # otherwise just export vertices
            v_blob = mesh.vertices

            # add the first vertex key and convert the array
        # add the vertices
        export = deque(
            ['v ' + util.array_to_string(
                v_blob,
                col_delim=' ',
                row_delim='\nv ',
                digits=digits)])
        # only include vertex normals if they're already stored
        if include_normals and mesh._cache.cache.get('vertex_normals') is not None:

            try:
                converted = util.array_to_string(
                    mesh.vertex_normals,
                    col_delim=' ',
                    row_delim='\nvn ',
                    digits=digits)
                # if vertex normals are stored in cache export them
                face_type.append('vn')
                export.append('vn ' + converted)
            except BaseException:
                log.debug('failed to convert vertex normals',
                          exc_info=True)

        if include_texture and hasattr(mesh.visual, 'uv'):
            try:
                material = mesh.visual.material
                if hasattr(material, 'to_simple'):
                    material = material.to_simple()
                (tex_data,
                 tex_name,
                 mtl_name) = material.to_obj()
                converted = util.array_to_string(
                    mesh.visual.uv,
                    col_delim=' ',
                    row_delim='\nvt ',
                    digits=digits)
                # if vertex texture exists and is the right shape
                face_type.append('vt')
                # add the uv coordinates
                export.append('vt ' + converted)
                # add the reference to the MTL file
                objects.appendleft('mtllib {}'.format(mtl_name))
                # add the directive to use the exported material
                export.appendleft('usemtl {}'.format(tex_name))
            except BaseException:
                log.debug('failed to convert UV coordinates',
                          exc_info=True)

        # the format for a single vertex reference of a face
        face_format = face_formats[tuple(face_type)]
        # add the exported faces to the export if available
        if hasattr(mesh, 'faces'):
            export.append('f ' + util.array_to_string(
                mesh.faces + 1 + counts['v'],
                col_delim=' ',
                row_delim='\nf ',
                value_format=face_format))
        # offset our vertex position
        counts['v'] += len(mesh.vertices)

        # add object name if found in metadata
        if 'name' in mesh.metadata:
            export.appendleft(
                '\no {}'.format(mesh.metadata['name']))
        # add this object
        objects.append('\n'.join(export))

    # add a created-with header to the top of the file
    objects.appendleft('# https://github.com/mikedh/trimesh')
    # combine elements into a single string
    text = '\n'.join(objects)

    # if we have a resolver and have asked to write texture
    if write_texture and resolver is not None and tex_data is not None:
        # not all resolvers have a write method
        [resolver.write(k, v) for k, v in tex_data.items()]

    # if we exported texture it changes returned values
    if return_texture:
        return text, tex_data

    return text


_obj_loaders = {'obj': load_obj}
