# Created: 24.05.2015
# Copyright (c) 2015-2018, Manfred Moitzi
# License: MIT License
from typing import TYPE_CHECKING, Iterable, List, Sequence, Tuple, Union, Dict
from contextlib import contextmanager
import array
from itertools import chain

from ezdxf.lldxf.attributes import DXFAttr, DXFAttributes, DefSubclass
from ezdxf.lldxf.types import DXFTag
from ezdxf.lldxf.extendedtags import ExtendedTags
from ezdxf.lldxf.const import DXFStructureError, DXFValueError
from ezdxf.lldxf.packedtags import TagArray, VertexArray, TagList
from ezdxf.tools import take2
from ezdxf.lldxf import loader

from .graphics import none_subclass, entity_subclass, ModernGraphicEntity

if TYPE_CHECKING:
    from ezdxf.eztypes import Tags, Vertex


class MeshVertexArray(VertexArray):
    code = -92

    def dxftags(self) -> Iterable[DXFTag]:
        yield DXFTag(92, len(self))
        # python 2.7 compatible
        for tag in super(MeshVertexArray, self).dxftags():
            yield tag

    def set_data(self, vertices: Iterable['Vertex']) -> None:
        self.value = array.array('d', chain.from_iterable(vertices))


def create_vertex_array(tags: 'Tags', start_index: int) -> 'MeshVertexArray':
    vertex_tags = tags.collect_consecutive_tags(codes=(10,), start=start_index)
    return MeshVertexArray(data=chain.from_iterable(t.value for t in vertex_tags))


class FaceList(TagList):
    code = -93

    def __len__(self) -> int:
        return len(self.value)

    def __iter__(self) -> Iterable[array.array]:
        return iter(self.value)

    def dxftags(self) -> List[DXFTag]:
        # count = count of tags not faces!
        yield DXFTag(93, self.tag_count())
        for face in self.value:
            yield DXFTag(90, len(face))
            for index in face:
                yield DXFTag(90, index)

    def tag_count(self) -> int:
        return len(self.value) + sum(len(f) for f in self.value)

    def set_data(self, faces: Iterable[Sequence[int]]) -> None:
        _faces = []
        for face in faces:
            _faces.append(face_to_array(face))
        self.value = _faces


def face_to_array(face: Sequence[int]) -> array.array:
    max_index = max(face)
    if max_index < 256:
        dtype = 'B'
    elif max_index < 65536:
        dtype = 'I'
    else:
        dtype = 'L'
    return array.array(dtype, face)


def create_face_list(tags: 'Tags', start_index: int) -> 'FaceList':
    faces = FaceList()
    faces_list = faces.value
    face = []
    counter = 0
    for tag in tags.collect_consecutive_tags(codes=(90,), start=start_index):
        if not counter:
            # leading counter tag
            counter = tag.value
            if face:
                # group code 90 = 32 bit integer
                faces_list.append(face_to_array(face))
                face = []
        else:
            # followed by count face tags
            counter -= 1
            face.append(tag.value)

    # add last face
    if face:
        # group code 90 = 32 bit integer
        faces_list.append(face_to_array(face))

    return faces


class EdgeArray(TagArray):
    code = -94
    VALUE_CODE = 90  # 32 bit integer
    DTYPE = 'L'

    def dxftags(self) -> Iterable[DXFTag]:
        # count = count of edges not tags!
        yield DXFTag(94, len(self.value) // 2)
        # python 2.7 compatible
        for v in super(EdgeArray, self).dxftags():
            yield v

    def __len__(self) -> int:
        return len(self.value) // 2

    def __iter__(self) -> Iterable[Tuple[int, int]]:
        for edge in take2(self.value):
            yield edge

    def set_data(self, edges: Iterable[Tuple[int, int]]) -> None:
        self.value = array.array('L', chain.from_iterable(edges))


def create_edge_array(tags: 'Tags', start_index: int) -> 'EdgeArray':
    return EdgeArray(data=collect_values(tags, start_index, code=90))  # int values


def collect_values(tags: 'Tags', start_index: int, code: int) -> Iterable[Union[float, int]]:
    values = tags.collect_consecutive_tags(codes=(code,), start=start_index)
    return (t.value for t in values)


def create_crease_array(tags: 'Tags', start_index: int) -> 'CreaseArray':
    return CreaseArray(data=collect_values(tags, start_index, code=140))  # float values


class CreaseArray(TagArray):
    code = -95
    VALUE_CODE = 140  # double precision
    DTYPE = 'd'

    def dxftags(self) -> Iterable[DXFTag]:
        yield DXFTag(95, len(self.value))
        # python 2.7 compatible
        for v in super(CreaseArray, self).dxftags():
            yield v

    def __len__(self) -> int:
        return len(self.value)

    def __iter__(self) -> Iterable[float]:
        return iter(self.value)

    def set_data(self, creases: Iterable[float]) -> None:
        self.value = array.array('d', creases)


COUNT_ERROR_MSG = "'MESH (#{}) without {} count.'"


def convert_and_replace_tags(tags: 'Tags', handle: str) -> None:
    def process_vertices():
        try:
            vertex_count_index = tags.tag_index(92)
        except DXFValueError:
            raise DXFStructureError(COUNT_ERROR_MSG.format(handle, 'vertex'))
        vertices = create_vertex_array(tags, vertex_count_index + 1)
        # replace vertex count tag and all vertex tags by MeshVertexArray()
        end_index = vertex_count_index + 1 + len(vertices)
        tags[vertex_count_index:end_index] = [vertices]

    def process_faces():
        try:
            face_count_index = tags.tag_index(93)
        except DXFValueError:
            raise DXFStructureError(COUNT_ERROR_MSG.format(handle, 'face'))
        else:
            # replace face count tag and all face tags by FaceList()
            faces = create_face_list(tags, face_count_index + 1)
            end_index = face_count_index + 1 + faces.tag_count()
            tags[face_count_index:end_index] = [faces]

    def process_edges():
        try:
            edge_count_index = tags.tag_index(94)
        except DXFValueError:
            raise DXFStructureError(COUNT_ERROR_MSG.format(handle, 'edge'))
        else:
            edges = create_edge_array(tags, edge_count_index + 1)
            # replace edge count tag and all edge tags by EdgeArray()
            end_index = edge_count_index + 1 + len(edges.value)
            tags[edge_count_index:end_index] = [edges]

    def process_creases():
        try:
            crease_count_index = tags.tag_index(95)
        except DXFValueError:
            raise DXFStructureError(COUNT_ERROR_MSG.format(handle, 'crease'))
        else:
            creases = create_crease_array(tags, crease_count_index + 1)
            # replace crease count tag and all crease tags by CreaseArray()
            end_index = crease_count_index + 1 + len(creases.value)
            tags[crease_count_index:end_index] = [creases]

    process_vertices()
    process_faces()
    process_edges()
    process_creases()


@loader.register('MESH', legacy=False)
def tag_processor(tags: ExtendedTags) -> ExtendedTags:
    subclass = tags.get_subclass('AcDbSubDMesh')
    handle = tags.get_handle()
    convert_and_replace_tags(subclass, handle)
    return tags


_MESH_TPL = """0
MESH
5
0
330
1F
100
AcDbEntity
8
0
100
AcDbSubDMesh
71
2
72
0
91
0
92
0
93
0
94
0
95
0
90
0
"""

mesh_subclass = DefSubclass('AcDbSubDMesh', {
    'version': DXFAttr(71),
    'blend_crease': DXFAttr(72),  # 0 = off, 1 = on
    'subdivision_levels': DXFAttr(91),  # int >= 0, 0 is no smoothing
    # 92: Vertex count of level 0
    # 10: Vertex position, multiple entries
    # 93: Size of face list of level 0
    # 90: Face list item, >=3 possible
    #     90: length of face list
    #     90: 1st vertex index
    #     90: 2nd vertex index ...
    # 94: Edge count of level 0
    #     90: Vertex index of 1st edge
    #     90: Vertex index of 2nd edge
    # 95: Edge crease count of level 0
    #     95 same as 94, or how is the 'edge create value' associated to edge index
    # 140: Edge create value
    #
    # Overriding properties: how does this work?
    # 90: Count of sub-entity which property has been overridden
    # 91: Sub-entity marker
    # 92: Count of property was overridden
    # 90: Property type
    #     0 = Color
    #     1 = Material
    #     2 = Transparency
    #     3 = Material mapper
})


class Mesh(ModernGraphicEntity):
    __slots__ = ()
    TEMPLATE = tag_processor(ExtendedTags.from_text(_MESH_TPL))
    DXFATTRIBS = DXFAttributes(none_subclass, entity_subclass, mesh_subclass)

    @property
    def AcDbSubDMesh(self) -> 'Tags':
        return self.tags.subclasses[2]

    @property
    def vertices(self) -> MeshVertexArray:
        return self.AcDbSubDMesh.get_first_tag(MeshVertexArray.code)

    @property
    def faces(self) -> FaceList:
        return self.AcDbSubDMesh.get_first_tag(FaceList.code)

    @property
    def edges(self) -> EdgeArray:
        return self.AcDbSubDMesh.get_first_tag(EdgeArray.code)

    @property
    def creases(self) -> CreaseArray:
        return self.AcDbSubDMesh.get_first_tag(CreaseArray.code)

    def get_data(self) -> 'MeshData':
        return MeshData(self)

    def set_data(self, data: 'MeshData') -> None:
        self.vertices.set_data(data.vertices)
        self.faces.set_data(data.faces)
        self.edges.set_data(data.edges)
        self.creases.set_data(data.edge_crease_values)

    @contextmanager
    def edit_data(self) -> 'MeshData':
        data = self.get_data()
        yield data
        self.set_data(data)


class MeshData:
    def __init__(self, mesh):
        self.vertices = list(mesh.vertices)  # type: List[array.array]
        self.faces = list(mesh.faces)  # type: List[array.array]
        self.edges = list(mesh.edges)  # type: List[Tuple[int, int]]
        self.edge_crease_values = list(mesh.creases)  # type: List[float]

    def add_face(self, vertices: Iterable[Sequence[float]]) -> Sequence[int]:
        return self.add_entity(vertices, self.faces)

    def add_edge(self, vertices: Sequence[Sequence[float]]) -> Sequence[int]:
        if len(vertices) != 2:
            raise DXFValueError("Parameter vertices has to be a list/tuple of 2 vertices [(x1, y1, z1), (x2, y2, z2)].")
        return self.add_entity(vertices, self.edges)

    def add_entity(self, vertices: Iterable[Sequence[float]], entity_list: List) -> Sequence[int]:
        indices = [self.add_vertex(vertex) for vertex in vertices]
        entity_list.append(indices)
        return indices

    def add_vertex(self, vertex: Sequence[float]) -> int:
        if len(vertex) != 3:
            raise DXFValueError('Parameter vertex has to be a 3-tuple (x, y, z).')
        index = len(self.vertices)
        self.vertices.append(vertex)
        return index

    def optimize(self, precision: int = 6):
        def remove_doublette_vertices() -> Dict[int, int]:
            def prepare_vertices() -> Iterable[Tuple[float, float, float]]:
                for index, vertex in enumerate(self.vertices):
                    x, y, z = vertex
                    yield round(x, precision), round(y, precision), round(z, precision), index

            sorted_vertex_list = list(sorted(prepare_vertices()))
            original_vertices = self.vertices
            self.vertices = []
            index_map = {}  # type: Dict[int, int]
            cmp_vertex = None
            index = 0
            while len(sorted_vertex_list):
                vertex_entry = sorted_vertex_list.pop()
                original_index = vertex_entry[3]
                vertex = original_vertices[original_index]
                if vertex != cmp_vertex:  # this is not a doublette
                    index = len(self.vertices)
                    self.vertices.append(vertex)
                    index_map[original_index] = index
                    cmp_vertex = vertex
                else:  # it is a doublette
                    index_map[original_index] = index
            return index_map

        def remap_faces() -> None:
            self.faces = remap_indices(self.faces)

        def remap_edges() -> None:
            self.edges = remap_indices(self.edges)

        def remap_indices(entity_list: Sequence[Sequence[int]]) -> List[Tuple]:
            mapped_indices = []  # type: List[Tuple]
            for entity in entity_list:
                index_list = [index_map[index] for index in entity]
                mapped_indices.append(tuple(index_list))
            return mapped_indices

        index_map = remove_doublette_vertices()
        remap_faces()
        remap_edges()
