# Copyright (c) 2019-2021 Manfred Moitzi
# License: MIT License
import pytest
import ezdxf

from ezdxf.entities.insert import Insert
from ezdxf.lldxf.const import DXF12, DXF2000
from ezdxf.lldxf.tagwriter import TagCollector, basic_tags_from_text
from ezdxf.math import Matrix44
from ezdxf.entities import factory
from ezdxf.protocols import SupportsVirtualEntities, query_virtual_entities

ENTITY_R12 = """0
INSERT
5
0
8
0
2
BLOCKNAME
10
0.0
20
0.0
30
0.0
41
1.0
42
1.0
43
1.0
50
0.0
"""

ENTITY_R2000 = """0
INSERT
5
0
330
0
100
AcDbEntity
8
0
100
AcDbBlockReference
2
BLOCKNAME
10
0.0
20
0.0
30
0.0
41
1.0
42
1.0
43
1.0
50
0.0
"""


@pytest.fixture(scope="module")
def doc():
    return ezdxf.new()


@pytest.fixture(scope="module")
def layout(doc):
    return doc.modelspace()


@pytest.fixture(params=[ENTITY_R12, ENTITY_R2000])
def entity(request):
    return Insert.from_text(request.param)


def test_registered():
    from ezdxf.entities.factory import ENTITY_CLASSES

    assert "INSERT" in ENTITY_CLASSES


def test_default_constructor():
    insert = Insert()
    assert insert.dxftype() == "INSERT"
    assert insert.is_virtual
    assert insert.seqend is None, "SEQEND must not exist"


def test_new_constructor():
    insert = Insert.new(
        handle="ABBA",
        owner="0",
        dxfattribs={
            "color": "7",
            "insert": (1, 2, 3),
        },
    )
    assert insert.is_virtual is True, "Has no assigned document"
    assert insert.dxf.layer == "0"
    assert insert.dxf.color == 7
    assert insert.dxf.linetype == "BYLAYER"
    assert insert.dxf.insert == (1, 2, 3)
    assert insert.dxf.insert.x == 1, "is not Vec3 compatible"
    assert insert.dxf.insert.y == 2, "is not Vec3 compatible"
    assert insert.dxf.insert.z == 3, "is not Vec3 compatible"
    assert insert.has_scaling is False
    assert insert.has_uniform_scaling is True
    # can set DXF R2007 value
    insert.dxf.shadow_mode = 1
    assert insert.dxf.shadow_mode == 1


def test_has_scaling():
    entity = Insert.new(handle="ABBA", owner="0", dxfattribs={"xscale": 2})
    assert entity.has_scaling is True
    assert entity.has_uniform_scaling is False
    entity = Insert.new(handle="ABBA", owner="0", dxfattribs={"yscale": 2})
    assert entity.has_scaling is True
    assert entity.has_uniform_scaling is False
    entity = Insert.new(handle="ABBA", owner="0", dxfattribs={"zscale": 2})
    assert entity.has_scaling is True
    assert entity.has_uniform_scaling is False

    # reflections are under control, so (-2, 2, 2) is a uniform scaling
    entity = Insert.new(
        handle="ABBA",
        owner="0",
        dxfattribs={
            "xscale": -2,
            "yscale": 2,
            "zscale": 2,
        },
    )
    assert entity.has_uniform_scaling is True


def test_load_from_text(entity):
    assert entity.dxf.layer == "0"
    assert entity.dxf.color == 256, "default color is 256 (by layer)"
    assert entity.dxf.insert == (0, 0, 0)
    assert entity.has_scaling is False


@pytest.mark.parametrize("txt,ver", [(ENTITY_R2000, DXF2000), (ENTITY_R12, DXF12)])
def test_write_dxf(txt, ver):
    expected = basic_tags_from_text(txt)
    vertex = Insert.from_text(txt)
    collector = TagCollector(dxfversion=ver, optional=True)
    vertex.export_dxf(collector)
    assert collector.tags == expected

    collector2 = TagCollector(dxfversion=ver, optional=False)
    vertex.export_dxf(collector2)
    assert collector.has_all_tags(collector2)


def test_add_attribs():
    insert = Insert()
    assert insert.attribs_follow is False
    insert.add_attrib("T1", "value1", (0, 0))
    assert len(insert.attribs) == 1
    assert insert.attribs_follow is True


def test_get_block(doc):
    blk = doc.blocks.new("TEST-2020-02-29")
    msp = doc.modelspace()
    insert = msp.add_blockref("TEST-2020-02-29", (0, 0))
    assert insert.block() is blk
    insert = msp.add_blockref("NOT-EXIST-2020-02-29", (0, 0))
    assert insert.block() is None
    insert = Insert()
    assert insert.block() is None


def test_clone_with_insert():
    # difference of clone() to copy_entity() is:
    # - clone returns and unassigned entity without handle, owner or reactors
    # - copy_entity clones the entity and assigns the new entity to the same owner as the source and adds the entity
    #   and it linked entities (ATTRIB & VERTEX) to the entity database, but does not adding entity to a layout, setting
    #   owner tag is not enough to assign an entity to a layout, use Layout.add_entity()
    insert = Insert()
    insert.add_attrib("T1", "value1", (0, 0))
    clone = insert.copy()
    assert clone.dxf.handle is None
    assert clone.dxf.owner is None
    assert len(clone.attribs) == 1
    attrib = clone.attribs[0]
    assert attrib.dxf.handle is None
    assert attrib.dxf.tag == "T1"
    assert attrib.dxf.text == "value1"
    # change cloned attrib
    attrib.dxf.tag = "T2"
    attrib.dxf.text = "value2"
    # source attrib is unchanged
    assert insert.attribs[0].dxf.tag == "T1"
    assert insert.attribs[0].dxf.text == "value1"


def test_export_without_sub_entities_to_dxf(doc):
    from ezdxf.lldxf.tagwriter import TagCollector

    blk = doc.blocks.new("INSERT_WITHOUT_ATTRIBS")
    blk.add_blockref("TEST", (0, 0))
    writer = TagCollector()
    blk.entity_space.export_dxf(tagwriter=writer)
    structure_tags = [tag for tag in writer.tags if tag[0] == 0]
    assert len(structure_tags) == 1
    assert structure_tags[0] == (0, "INSERT")


def test_export_with_sub_entities_to_dxf(doc):
    from ezdxf.lldxf.tagwriter import TagCollector

    blk = doc.blocks.new("INSERT_WITH_ATTRIBS")
    insert = blk.add_blockref("TEST", (0, 0))
    insert.add_attrib("TAG", "TEXT", (0, 0))
    writer = TagCollector()
    blk.entity_space.export_dxf(tagwriter=writer)
    structure_tags = [tag for tag in writer.tags if tag[0] == 0]
    assert structure_tags[0] == (0, "INSERT")
    assert structure_tags[1] == (0, "ATTRIB")
    assert structure_tags[2] == (0, "SEQEND")


def test_copy_with_insert(doc):
    msp = doc.modelspace()
    msp_count = len(msp)
    db = doc.entitydb
    db_len = len(doc.entitydb)

    insert = msp.add_blockref("Test", insert=(0, 0))
    assert (
        insert.seqend.dxf.owner == insert.dxf.handle
    ), "INSERT is owner of the SEQEND entity"
    assert insert.seqend.dxf.handle is not None
    assert insert.seqend.dxf.handle in doc.entitydb

    insert.add_attrib("T1", "value1")

    # linked attribs not stored in the entity space
    assert len(msp) == msp_count + 1
    # added INSERT + SEQEND
    assert len(db) == db_len + 3, "New ATTRIBS automatically stored in db"

    copy = doc.entitydb.duplicate_entity(insert)

    # not duplicated in entity space
    assert len(msp) == msp_count + 1
    # duplicated in entity database (2x SEQEND)
    assert len(doc.entitydb) == db_len + 6

    # get 1. paperspace in tab order
    psp = doc.layout()
    psp.add_entity(copy)
    assert len(psp) == 1

    assert copy.dxf.handle is not None
    assert copy.dxf.handle != insert.dxf.handle
    assert copy.dxf.owner == psp.layout_key
    assert (
        copy.attribs[0].dxf.owner == copy.dxf.handle
    ), "INSERT is owner of ATTRIB entities"


def test_matrix44_no_transform():
    insert = Insert.new(handle="ABBA", owner="0")
    m = insert.matrix44()
    assert m.transform((0, 0, 0)) == (0, 0, 0)
    assert m.transform_direction((1, 0, 0)) == (1, 0, 0)


def test_matrix44_insert():
    insert = Insert.new(
        handle="ABBA",
        owner="0",
        dxfattribs={
            "insert": (1, 2, 3),
        },
    )
    m = insert.matrix44()
    assert m.transform((0, 0, 0)) == (1, 2, 3)
    assert m.transform_direction((1, 0, 0)) == (1, 0, 0)


def test_matrix44_insert_and_base_point(doc):
    doc.blocks.new("Matrix44_001", base_point=(2, 2, 2))
    insert = doc.modelspace().add_blockref("Matrix44_001", insert=(1, 2, 3))
    m = insert.matrix44()
    assert m.transform((0, 0, 0)) == (-1, 0, 1)
    assert m.transform_direction((1, 0, 0)) == (1, 0, 0)


def test_matrix44_rotation():
    insert = Insert.new(
        handle="ABBA",
        owner="0",
        dxfattribs={
            "insert": (0, 0, 0),
            "rotation": 90,
        },
    )
    m = insert.matrix44()
    for v1, v2 in zip(
        m.transform_vertices([(1, 0, 0), (0, 0, 1)]), [(0, 1, 0), (0, 0, 1)]
    ):
        assert v1.isclose(v2)
    assert m.transform_direction((1, 0, 0)).isclose((0, 1, 0))


def test_matrix44_scaled():
    insert = Insert.new(
        handle="ABBA",
        owner="0",
        dxfattribs={
            "xscale": 2,
            "yscale": 3,
            "zscale": 4,
        },
    )
    m = insert.matrix44()
    assert m.transform((1, 1, 1)) == (2, 3, 4)
    assert m.transform_direction((1, 0, 0)) == (
        2,
        0,
        0,
    ), "scaling has to be applied for directions"


def test_matrix44_direction():
    insert = Insert.new(
        handle="ABBA",
        owner="0",
        dxfattribs={
            "insert": (1, 2, 3),
            "xscale": 2,
        },
    )
    m = insert.matrix44()
    assert m.transform((1, 0, 0)) == (3, 2, 3)
    assert m.transform_direction((1, 0, 0)) == (
        2,
        0,
        0,
    ), "only scaling has to be applied for directions"


def test_insert_transform_interface():
    insert = Insert()
    insert.dxf.insert = (1, 0, 0)

    insert.transform(Matrix44.translate(1, 2, 3))
    assert insert.dxf.insert == (2, 2, 3)

    # optimized translate implementation
    insert.translate(-1, -2, -3)
    assert insert.dxf.insert == (1, 0, 0)


def test_insert_scaling():
    # Insert.transform() changes the extrusion vector
    # sign of scaling factors depend from new extrusion vector
    # just compare absolute values
    insert = Insert()
    insert.dxf.insert = (0, 0, 0)

    insert.scale(2, 3, 4)
    assert abs(insert.dxf.xscale) == 2
    assert abs(insert.dxf.yscale) == 3
    assert abs(insert.dxf.zscale) == 4

    insert.scale(-1, 1, 1)
    assert abs(insert.dxf.xscale) == 2
    assert abs(insert.dxf.yscale) == 3
    assert abs(insert.dxf.zscale) == 4

    insert.scale(-1, -1, 1)
    assert abs(insert.dxf.xscale) == 2
    assert abs(insert.dxf.yscale) == 3
    assert abs(insert.dxf.zscale) == 4

    insert.scale(-2, -2, -2)
    assert abs(insert.dxf.xscale) == 4
    assert abs(insert.dxf.yscale) == 6
    assert abs(insert.dxf.zscale) == 8


def test_add_virtual_insert_with_attribs_to_layout(doc):
    doc.blocks.new("TestAddVirtualInsert")
    msp = doc.modelspace()
    insert = Insert.new(dxfattribs={"name": "TestAddVirtualInsert"})
    insert.add_attrib("TAG", "TEXT", (0, 0))
    msp.add_entity(insert)

    assert factory.is_bound(insert, doc) is True
    assert (
        factory.is_bound(insert.seqend, doc) is True
    ), "SEQEND must be bound to document"

    assert insert.attribs_follow is True
    assert (
        factory.is_bound(insert.attribs[0], doc) is True
    ), "ATTRIB must be bound to document"


class TestSupportsVirtualEntitiesProtocol:
    @pytest.fixture(scope="class")
    def doc(self):
        doc = ezdxf.new()
        blk = doc.blocks.new("POINT")
        blk.add_point(location=(0, 0))
        return doc

    @pytest.fixture
    def insert(self, doc):
        msp = doc.modelspace()
        return msp.add_blockref("POINT", (0, 0))

    def test_supports_virtual_entities_protocol(self, insert):
        assert isinstance(insert, SupportsVirtualEntities) is True

    def test_query_virtual_entities(self, insert):
        entities = query_virtual_entities(insert)
        assert len(entities) == 1
        assert entities[0].dxftype() == "POINT"


MALFORMED_INSERT = """0
INSERT
2
BLOCKNAME
5
0
62
7
330
0
6
LT_EZDXF
8
LY_EZDXF
100
AcDbEntity
100
AcDbArc
2
BLOCKNAME
10
1.0
20
2.0
30
3.0
41
1.0
42
1.0
43
1.0
100
AcDbBlockReference
50
0.0
"""


def test_load_malformed_insert():
    insert = Insert.from_text(MALFORMED_INSERT)
    assert insert.dxf.layer == "LY_EZDXF"
    assert insert.dxf.linetype == "LT_EZDXF"
    assert insert.dxf.color == 7
    assert insert.dxf.insert.isclose((1, 2, 3))
    assert insert.dxf.name == "BLOCKNAME"
