# Copyright (c) 2020-2022, Manfred Moitzi
# License: MIT License
from typing import List, Set, Sequence
import pytest
import ezdxf
from ezdxf.addons.drawing import Frontend, RenderContext
from ezdxf.addons.drawing.properties import Properties, LayerProperties, set_layers_state

from ezdxf.addons.drawing.backend import Backend
from ezdxf.addons.drawing.debug_backend import BasicBackend, PathBackend
from ezdxf.document import Drawing
from ezdxf.entities import DXFGraphic
from ezdxf.render.forms import cube
from ezdxf.path import from_vertices


@pytest.fixture
def doc():
    d = ezdxf.new()
    d.layers.new("Test1")
    return d


@pytest.fixture
def msp(doc):
    return doc.modelspace()


@pytest.fixture
def ctx(doc):
    return RenderContext(doc)


@pytest.fixture
def basic(doc, ctx):
    return Frontend(ctx, BasicBackend())


@pytest.fixture
def path_backend(doc, ctx):
    return Frontend(ctx, PathBackend())


def unique_types(result):
    return {e[0] for e in result}


def test_basic_frontend_init(basic):
    assert isinstance(basic.out, BasicBackend)


def test_backend_default_draw_path():
    backend = BasicBackend()
    path = from_vertices([(0, 0), (1, 0), (2, 0)])
    backend.draw_path(path, Properties())
    result = backend.collector
    assert len(result) == 2
    assert result[0][0] == "line"


def test_draw_layout(msp, basic):
    msp.add_point((0, 0))
    msp.add_point((0, 0))
    basic.draw_layout(msp)
    result = basic.out.collector
    assert len(result) == 3
    assert result[0][0] == "bgcolor"
    assert result[1][0] == "point"
    assert result[2][0] == "point"


def test_draw_entities(msp, basic):
    msp.add_point((0, 0))
    msp.add_point((0, 0))

    basic.draw_entities(msp)
    result = basic.out.collector
    assert len(result) == 2
    assert result[0][0] == "point"
    assert result[1][0] == "point"


def test_filter_draw_entities(msp, basic):
    def filter_layer_l1(e: DXFGraphic) -> bool:
        return e.dxf.layer == "L1"

    msp.add_point((0, 0), dxfattribs={"layer": "L1"})
    msp.add_point((0, 0), dxfattribs={"layer": "L2"})

    basic.draw_entities(msp, filter_func=filter_layer_l1)
    result = basic.out.collector
    assert len(result) == 1
    assert result[0][2].layer == "L1"


def test_point_and_layers(msp, basic):
    msp.add_point((0, 0), dxfattribs={"layer": "Test1"})
    # a non-existing layer shouldn't be a problem
    msp.add_point((0, 0), dxfattribs={"layer": "fail"})
    basic.draw_entities(msp)
    result = basic.out.collector
    assert len(result) == 2
    assert result[0][0] == "point"
    assert result[0][-1].layer == "Test1"
    assert result[1][0] == "point"
    assert result[1][-1].layer == "fail"


def test_line(msp, basic):
    msp.add_line((0, 0), (1, 0))
    basic.draw_entities(msp)
    result = basic.out.collector
    assert len(result) == 1
    assert result[0][0] == "line"


def test_lwpolyline_basic(msp, basic):
    msp.add_lwpolyline([(0, 0), (1, 0), (2, 0)])
    basic.draw_entities(msp)
    result = basic.out.collector
    assert len(result) == 2
    assert unique_types(result) == {"line"}


def test_lwpolyline_path(msp, path_backend):
    msp.add_lwpolyline([(0, 0), (1, 0), (2, 0)])
    path_backend.draw_entities(msp)
    result = path_backend.out.collector
    assert len(result) == 1
    assert unique_types(result) == {"path"}


def test_banded_lwpolyline(msp, basic):
    msp.add_lwpolyline(
        [(0, 0), (1, 0), (2, 0)], dxfattribs={"const_width": 0.1}
    )
    basic.draw_entities(msp)
    result = basic.out.collector
    assert len(result) == 1
    assert unique_types(result) == {"filled_polygon"}


def test_polyline_2d(msp, basic):
    msp.add_polyline2d([(0, 0), (1, 0), (2, 0)])
    basic.draw_entities(msp)
    result = basic.out.collector
    assert len(result) == 2
    assert result[0][0] == "line"
    assert result[1][0] == "line"


def test_banded_polyline_2d(msp, basic):
    msp.add_polyline2d(
        [(0, 0, 0.1, 0.2), (1, 0, 0.2, 0.1), (2, 0, 0.1, 0.5)], format="xyse"
    )
    basic.draw_entities(msp)
    result = basic.out.collector
    assert len(result) == 1
    assert result[0][0] == "filled_polygon"


def test_polyline_3d_basic(msp, basic):
    msp.add_polyline3d([(0, 0, 0), (1, 0, 1), (2, 0, 5)])
    basic.draw_entities(msp)
    result = basic.out.collector
    assert len(result) == 2
    assert unique_types(result) == {"line"}


def test_polyline_3d_path(msp, path_backend):
    msp.add_polyline3d([(0, 0, 0), (1, 0, 1), (2, 0, 5)])
    path_backend.draw_entities(msp)
    result = path_backend.out.collector
    assert len(result) == 1
    assert unique_types(result) == {"path"}


def test_2d_arc_basic(msp, basic):
    msp.add_circle((0, 0), radius=2)
    msp.add_arc(
        (0, 0),
        radius=2,
        start_angle=30,
        end_angle=60,
        dxfattribs={"layer": "Test1"},
    )
    msp.add_ellipse(
        (0, 0),
        major_axis=(1, 0, 0),
        ratio=0.5,
        start_param=1,
        end_param=2,
        dxfattribs={"layer": "Test1"},
    )
    basic.draw_entities(msp)
    result = basic.out.collector
    assert len(result) > 3
    assert unique_types(result) == {"line"}


def test_3d_circle_basic(msp, basic):
    msp.add_circle((0, 0), radius=2, dxfattribs={"extrusion": (0, 1, 1)})
    basic.draw_entities(msp)
    result = basic.out.collector
    assert len(result) > 30
    assert unique_types(result) == {"line"}


def test_3d_circle_path(msp, path_backend):
    msp.add_circle((0, 0), radius=2, dxfattribs={"extrusion": (0, 1, 1)})
    path_backend.draw_entities(msp)
    result = path_backend.out.collector
    assert len(result) == 1
    assert unique_types(result) == {"path"}


def test_3d_arc_basic(msp, basic):
    msp.add_arc(
        (0, 0),
        radius=2,
        start_angle=30,
        end_angle=60,
        dxfattribs={"extrusion": (0, 1, 1)},
    )
    basic.draw_entities(msp)
    result = basic.out.collector
    assert len(result) > 10
    assert unique_types(result) == {"line"}


def test_3d_arc_path(msp, path_backend):
    msp.add_arc(
        (0, 0),
        radius=2,
        start_angle=30,
        end_angle=60,
        dxfattribs={"extrusion": (0, 1, 1)},
    )
    path_backend.draw_entities(msp)
    result = path_backend.out.collector
    assert len(result) == 1
    assert unique_types(result) == {"path"}


def test_3d_ellipse_basic(msp, basic):
    msp.add_ellipse(
        (0, 0),
        major_axis=(1, 0, 0),
        ratio=0.5,
        start_param=1,
        end_param=2,
        dxfattribs={"extrusion": (0, 1, 1)},
    )
    basic.draw_entities(msp)
    result = basic.out.collector
    assert len(result) > 10
    assert unique_types(result) == {"line"}


def test_3d_ellipse_path(msp, path_backend):
    msp.add_ellipse(
        (0, 0),
        major_axis=(1, 0, 0),
        ratio=0.5,
        start_param=1,
        end_param=2,
        dxfattribs={"extrusion": (0, 1, 1)},
    )
    path_backend.draw_entities(msp)
    result = path_backend.out.collector
    assert len(result) == 1
    assert unique_types(result) == {"path"}


def test_2d_text(msp, basic):
    msp.add_text("test\ntest")  # \n shouldn't be  problem. Will be ignored
    basic.draw_entities(msp)
    result = basic.out.collector
    assert len(result) == 1
    assert result[0][0] == "text"
    assert result[0][1] == "testtest"


def test_ignore_3d_text(msp, basic):
    msp.add_text("test", dxfattribs={"extrusion": (0, 1, 1)})
    basic.draw_entities(msp)
    result = basic.out.collector
    assert len(result) == 0


def test_mtext(msp, basic):
    msp.add_mtext("line1\nline2")
    basic.draw_entities(msp)
    result = basic.out.collector
    assert len(result) == 2
    assert result[0][0] == "text"
    assert result[0][1] == "line1"
    assert result[1][0] == "text"
    assert result[1][1] == "line2"


def test_hatch(msp, path_backend):
    hatch = msp.add_hatch()
    hatch.paths.add_polyline_path([(0, 0), (1, 0), (1, 1), (0, 1)])
    path_backend.draw_entities(msp)
    result = path_backend.out.collector
    assert len(result) == 1
    assert result[0][0] == "filled_polygon"  # default implementation


def test_basic_spline(msp, basic):
    msp.add_spline(fit_points=[(0, 0), (3, 2), (4, 5), (6, 4), (12, 0)])
    basic.draw_entities(msp)
    result = basic.out.collector
    assert len(result) > 1
    entities = {e[0] for e in result}
    assert entities == {"line"}


def test_mesh(msp, basic):
    # draw mesh as wire frame
    c = cube()
    c.render_mesh(msp)
    basic.draw_entities(msp)
    result = basic.out.collector
    assert len(result) == 24
    assert unique_types(result) == {"line"}


def test_polyface(msp, basic):
    # draw mesh as wire frame
    c = cube()
    c.render_polyface(msp)
    basic.draw_entities(msp)
    result = basic.out.collector
    assert len(result) == 24
    entities = {e[0] for e in result}
    assert entities == {"line"}


def _add_text_block(doc: Drawing):
    doc.layers.new(name="Layer1")
    doc.layers.new(name="Layer2")

    text_block = doc.blocks.new(name="text-block")
    text_block.add_text(
        text="L0",
        dxfattribs={
            "layer": "0",
            "insert": (0, 0, 0),
            "height": 5.0,
        },
    )
    text_block.add_text(
        text="L1",
        dxfattribs={
            "layer": "Layer1",
            "insert": (0, 0, 0),
            "height": 5.0,
        },
    )


def _get_text_visible_when(doc: Drawing, active_layers: Set[str]) -> List[str]:
    def update_layers_state(layers: Sequence[LayerProperties]):
        # set given layer to ON, others to OFF
        set_layers_state(layers, active_layers, state=True)

    ctx = RenderContext(doc)
    ctx.set_layer_properties_override(update_layers_state)

    backend = BasicBackend()
    Frontend(ctx, backend).draw_layout(doc.modelspace())
    visible_text = [x[1] for x in backend.collector if x[0] == "text"]
    return visible_text


def test_visibility_insert_0():
    """see notes/drawing.md 'Layers and Draw Order'"""
    doc = ezdxf.new()
    _add_text_block(doc)
    layout = doc.modelspace()
    layout.add_blockref(
        name="text-block",
        insert=(0, 0, 0),
        dxfattribs={"layer": "0"},
    )
    # INSERT on '0'
    # 'L0' on '0'
    # 'L1' on 'Layer1'
    assert _get_text_visible_when(doc, {"0", "Layer1", "Layer2"}) == [
        "L0",
        "L1",
    ]
    assert _get_text_visible_when(doc, {"0", "Layer2"}) == ["L0"]
    assert _get_text_visible_when(doc, {"0", "Layer1"}) == ["L0", "L1"]
    assert _get_text_visible_when(doc, {"Layer1", "Layer2"}) == [
        "L1"
    ]  # result: []
    assert _get_text_visible_when(doc, {"Layer2"}) == []
    assert _get_text_visible_when(doc, {"Layer1"}) == ["L1"]  # result = []
    assert _get_text_visible_when(doc, set()) == []


def test_visibility_insert_2():
    """see notes/drawing.md 'Layers and Draw Order'"""
    doc = ezdxf.new()
    _add_text_block(doc)
    layout = doc.modelspace()
    layout.add_blockref(
        name="text-block",
        insert=(0, 0, 0),
        dxfattribs={"layer": "Layer2"},
    )
    # 'L0' on '0'
    # 'L1' on 'Layer1'
    # text-block on 'Layer2' -> 'L0' on '0' acts like on 'Layer2'
    assert _get_text_visible_when(doc, {"0", "Layer1", "Layer2"}) == [
        "L0",
        "L1",
    ]
    assert _get_text_visible_when(doc, {"0", "Layer2"}) == ["L0"]
    assert _get_text_visible_when(doc, {"0", "Layer1"}) == ["L1"]  # result = []
    assert _get_text_visible_when(doc, {"Layer1", "Layer2"}) == ["L0", "L1"]
    assert _get_text_visible_when(doc, {"Layer2"}) == ["L0"]
    assert _get_text_visible_when(doc, {"Layer1"}) == ["L1"]  # result = []
    assert _get_text_visible_when(doc, set()) == []


def test_override_filter(msp, ctx):
    class FrontendWithOverride(Frontend):
        def __init__(self, ctx: RenderContext, out: Backend):
            super().__init__(ctx, out)
            self.override_enabled = True

        def override_properties(
            self, entity: DXFGraphic, properties: Properties
        ) -> None:
            if not self.override_enabled:
                return
            if properties.layer == "T1":
                properties.layer = "Tx"
            properties.color = "#000000"
            if entity.dxf.text == "T2":
                properties.is_visible = False

    backend = BasicBackend()
    frontend = FrontendWithOverride(ctx, backend)

    msp.delete_all_entities()
    msp.add_text("T0", dxfattribs={"layer": "T0", "color": 7})
    msp.add_text("T1", dxfattribs={"layer": "T1", "color": 6})
    msp.add_text("T2", dxfattribs={"layer": "T2", "color": 5})
    frontend.draw_entities(msp)
    frontend.override_enabled = False
    frontend.draw_entities(msp)

    assert len(backend.collector) == 5

    # can modify color property
    result = backend.collector[0]
    assert result[0] == "text"
    assert result[1] == "T0"
    assert result[3].color == "#000000"

    # can modify layer property
    result = backend.collector[1]
    assert result[0] == "text"
    assert result[1] == "T1"
    assert result[3].layer == "Tx"

    # with override disabled

    result = backend.collector[2]
    assert result[0] == "text"
    assert result[1] == "T0"
    assert result[3].color == "#ffffff"

    result = backend.collector[3]
    assert result[0] == "text"
    assert result[1] == "T1"
    assert result[3].layer == "T1"

    result = backend.collector[4]
    assert result[0] == "text"
    assert result[1] == "T2"
    assert result[3].layer == "T2"


if __name__ == "__main__":
    pytest.main([__file__])
