#  Copyright (c) 2020, Manfred Moitzi
#  License: MIT License
from typing import TYPE_CHECKING, List, cast
from itertools import chain
from ezdxf.entities import factory
from ezdxf.math import Vec3, OCS
import logging

if TYPE_CHECKING:
    from ezdxf.entities import MLine, DXFGraphic, Hatch, LWPolyline

__all__ = ['virtual_entities']

logger = logging.getLogger('ezdxf')


# The MLINE geometry stored in vertices, is the final geometry,
# scaling factor, justification and MLineStyle settings are already
# applied.

def _dxfattribs(mline):
    attribs = mline.graphic_properties()
    # True color value of MLINE is ignored by CAD applications:
    if 'true_color' in attribs:
        del attribs['true_color']
    return attribs


def virtual_entities(mline: 'MLine') -> List['DXFGraphic']:
    """ Yields 'virtual' parts of MLINE as LINE, ARC and HATCH entities.

    This entities are located at the original positions, but are not stored
    in the entity database, have no handle and are not assigned to any
    layout.
    """

    def filling():
        attribs = _dxfattribs(mline)
        attribs['color'] = style.dxf.fill_color
        attribs['elevation'] = ocs.from_wcs(bottom_border[0]).replace(x=0, y=0)
        attribs['extrusion'] = mline.dxf.extrusion
        hatch = cast('Hatch', factory.new('HATCH', dxfattribs=attribs, doc=doc))
        bulges: List[float] = [0.0] * (len(bottom_border) * 2)
        points = chain(
            ocs.points_from_wcs(bottom_border),
            ocs.points_from_wcs(reversed(top_border))
        )
        if not closed:
            if style.get_flag_state(style.END_ROUND):
                bulges[len(bottom_border) - 1] = 1.0
            if style.get_flag_state(style.START_ROUND):
                bulges[-1] = 1.0
        lwpoints = ((v.x, v.y, bulge) for v, bulge in zip(points, bulges))
        hatch.paths.add_polyline_path(lwpoints, is_closed=True)
        return hatch

    def start_cap():
        entities = []
        if style.get_flag_state(style.START_SQUARE):
            entities.extend(create_miter(miter_points[0]))
        if style.get_flag_state(style.START_ROUND):
            entities.extend(round_caps(0, top_index, bottom_index))
        if style.get_flag_state(style.START_INNER_ARC) and \
                len(style.elements) > 3:
            start_index = ordered_indices[-2]
            end_index = ordered_indices[1]
            entities.extend(round_caps(0, start_index, end_index))
        return entities

    def end_cap():
        entities = []
        if style.get_flag_state(style.END_SQUARE):
            entities.extend(create_miter(miter_points[-1]))
        if style.get_flag_state(style.END_ROUND):
            entities.extend(round_caps(-1, bottom_index, top_index))
        if style.get_flag_state(style.END_INNER_ARC) and \
                len(style.elements) > 3:
            start_index = ordered_indices[1]
            end_index = ordered_indices[-2]
            entities.extend(round_caps(-1, start_index, end_index))
        return entities

    def round_caps(miter_index: int, start_index: int, end_index:int):
        color1 = style.elements[start_index].color
        color2 = style.elements[end_index].color
        start = ocs.from_wcs(miter_points[miter_index][start_index])
        end = ocs.from_wcs(miter_points[miter_index][end_index])
        return _arc_caps(start, end, color1, color2)

    def _arc_caps(start: Vec3, end: Vec3, color1: int, color2: int):
        attribs = _dxfattribs(mline)
        center = start.lerp(end)
        radius = (end - start).magnitude / 2.0
        angle = (start - center).angle_deg
        attribs['center'] = center
        attribs['radius'] = radius
        attribs['color'] = color1
        attribs['start_angle'] = angle
        attribs['end_angle'] = angle + (180 if color1 == color2 else 90)
        arc1 = factory.new('ARC', dxfattribs=attribs, doc=doc)
        if color1 == color2:
            return arc1,
        attribs['start_angle'] = angle + 90
        attribs['end_angle'] = angle + 180
        attribs['color'] = color2
        arc2 = factory.new('ARC', dxfattribs=attribs, doc=doc)
        return arc1, arc2

    def lines():
        prev = None
        _lines = []
        attribs = _dxfattribs(mline)

        for miter in miter_points:
            if prev is not None:
                for index, element in enumerate(style.elements):
                    attribs['start'] = prev[index]
                    attribs['end'] = miter[index]
                    attribs['color'] = element.color
                    attribs['linetype'] = element.linetype
                    _lines.append(factory.new(
                        'LINE', dxfattribs=attribs, doc=doc))
            prev = miter
        return _lines

    def display_miter():
        _lines = []
        skip = set()
        skip.add(len(miter_points) - 1)
        if not closed:
            skip.add(0)
        for index, miter in enumerate(miter_points):
            if index not in skip:
                _lines.extend(create_miter(miter))
        return _lines

    def create_miter(miter):
        _lines = []
        attribs = _dxfattribs(mline)
        top = miter[top_index]
        bottom = miter[bottom_index]
        zero = bottom.lerp(top)
        element = style.elements[top_index]
        attribs['start'] = top
        attribs['end'] = zero
        attribs['color'] = element.color
        attribs['linetype'] = element.linetype
        _lines.append(factory.new(
            'LINE', dxfattribs=attribs, doc=doc))
        element = style.elements[bottom_index]
        attribs['start'] = bottom
        attribs['end'] = zero
        attribs['color'] = element.color
        attribs['linetype'] = element.linetype
        _lines.append(factory.new(
            'LINE', dxfattribs=attribs, doc=doc))
        return _lines

    entities = []
    if not mline.is_alive or mline.doc is None or len(mline.vertices) < 2:
        return entities

    style = mline.style
    if style is None:
        return entities

    doc = mline.doc
    ocs = OCS(mline.dxf.extrusion)
    element_count = len(style.elements)
    closed = mline.is_closed
    ordered_indices = style.ordered_indices()
    bottom_index = ordered_indices[0]
    top_index = ordered_indices[-1]
    bottom_border: List[Vec3] = []
    top_border: List[Vec3] = []
    miter_points: List[List[Vec3]] = []

    for vertex in mline.vertices:
        offsets = vertex.line_params
        if len(offsets) != element_count:
            logger.debug(
                f'Invalid line parametrization for vertex {len(miter_points)} '
                f'in {str(mline)}.'
            )
            return entities
        location = vertex.location
        miter_direction = vertex.miter_direction
        miter = []
        for offset in offsets:
            try:
                length = offset[0]
            except IndexError:  # DXFStructureError?
                length = 0
            miter.append(location + miter_direction * length)
        miter_points.append(miter)
        top_border.append(miter[top_index])
        bottom_border.append(miter[bottom_index])

    if closed:
        miter_points.append(miter_points[0])
        top_border.append(top_border[0])
        bottom_border.append(bottom_border[0])

    if not closed:
        entities.extend(start_cap())

    entities.extend(lines())

    if style.get_flag_state(style.MITER):
        entities.extend(display_miter())

    if not closed:
        entities.extend(end_cap())

    if style.get_flag_state(style.FILL):
        entities.insert(0, filling())

    return entities
