# Created: 21.03.2011
# Copyright (C) 2011, Manfred Moitzi
# License: MIT License
from typing import TYPE_CHECKING, Union, List, Iterable, Tuple, Dict, Hashable, Sequence, Optional
from ezdxf.graphicsfactory import GraphicsFactory
from ezdxf.entityspace import EntitySpace
from ezdxf.query import EntityQuery
from ezdxf.groupby import groupby
from ezdxf.lldxf.const import STD_SCALES, DXFValueError

if TYPE_CHECKING:
    from ezdxf.eztypes import Drawing, EntityDB, LayoutType, DXFEntity, TagWriter, DXFFactoryType, EntitySpace
    from ezdxf.eztypes import KeyFunc, GenericLayoutType


class DXF12Layouts:
    """
    The Layout container.

    """

    def __init__(self, drawing: 'Drawing'):
        entities = drawing.sections.entities
        model_space = entities.model_space_entities()
        self._modelspace = DXF12Layout(model_space, drawing.dxffactory, 0)
        paper_space = entities.active_layout_entities()
        self._paperspace = DXF12Layout(paper_space, drawing.dxffactory, 1)
        entities.clear()  # remove entities for entities section -> stored in layouts

    def __iter__(self) -> 'LayoutType':
        yield self._modelspace
        yield self._paperspace

    def __len__(self) -> int:
        return 2

    def modelspace(self) -> 'LayoutType':
        return self._modelspace

    def get(self, name: str = "") -> 'LayoutType':
        # AC1009 supports only one paperspace/layout
        return self._paperspace

    def names(self) -> List[str]:
        return []

    def get_layout_for_entity(self, entity: 'DXFEntity') -> 'LayoutType':
        # paperspace attribute defaults to 0 if not present
        if entity in self._modelspace:
            return self._modelspace
        elif entity in self._paperspace:
            return self._paperspace
        else:
            return None

    def active_layout(self) -> 'LayoutType':
        return self._paperspace

    def write_entities_section(self, tagwriter: 'TagWriter') -> None:
        self._modelspace.write(tagwriter)
        self._paperspace.write(tagwriter)


class BaseLayout(GraphicsFactory):
    """
    Base class for DXF12Layout() and DXF12BlockLayout()

    Entities are wrapped into class GraphicEntity() or inherited.

    """

    def __init__(self, dxffactory: 'DXFFactoryType', entity_space: 'EnitySpace'):
        super(BaseLayout, self).__init__(dxffactory)
        self._entity_space = entity_space

    def __len__(self) -> int:
        """
        Entities count.

        """
        return len(self._entity_space)

    def __iter__(self) -> Iterable['DXFEntity']:
        """
        Iterate over all drawing entities in this layout.

        Returns: :class:`DXFEntity`

        """
        wrap = self._dxffactory.wrap_handle
        for handle in self._entity_space:
            yield wrap(handle)

    @property
    def entitydb(self) -> 'EnityDB':
        return self._dxffactory.entitydb

    @property
    def drawing(self) -> 'Drawing':
        return self._dxffactory.drawing

    def build_and_add_entity(self, type_: str, dxfattribs: dict) -> 'DXFEntity':
        """
        Create entity in drawing database and add entity to the entity space.

        Args:
            type_ (str): DXF type string, like 'LINE', 'CIRCLE' or 'LWPOLYLINE'
            dxfattribs (dict): DXF attributes for the new entity

        """
        entity = self.build_entity(type_, dxfattribs)
        self.add_entity(entity)
        return entity

    def build_entity(self, type_: str, dxfattribs: dict) -> 'DXFEntity':
        """
        Create entity in drawing database, returns a wrapper class inherited from GraphicEntity().

        Adds entity to the drawing database.

        Args:
            type_ (str): DXF type string, like 'LINE', 'CIRCLE' or 'LWPOLYLINE'
            dxfattribs(dict): DXF attributes for the new entity

        """
        entity = self._dxffactory.create_db_entry(type_, dxfattribs)
        self._set_paperspace(entity)
        return entity

    def add_entity(self, entity: 'DXFEntity') -> None:
        """
        Add an existing :class:`DXFEntity` to a layout, but be sure to unlink (:meth:`~Layout.unlink_entity()`) first the entity
        from the previous owner layout.

        """
        self._entity_space.append(entity.dxf.handle)
        self._set_paperspace(entity)
        for linked_entity in entity.linked_entities():
            self._set_paperspace(linked_entity)

    def unlink_entity(self, entity: 'DXFEntity') -> None:
        """
        Unlink `entity` from layout but does not delete entity from the drawing database.

        Removes `entity` just from  entity space but not from the drawing database.

        Args:
            entity: :class:`DXFEntity`

        """
        self._entity_space.delete_entity(entity)
        entity.dxf.paperspace = -1  # set invalid paper space
        if entity.supports_dxf_attrib('owner'):  # R2000
            entity.dxf.owner = '0'

    def delete_entity(self, entity: 'DXFEntity') -> None:
        """
        Delete `entity` from layout (entity space) and drawing database.

        Args:
            entity: :class:`DXFEntity`

        """
        self.entitydb.delete_entity(entity)  # 1. delete from drawing database
        self.unlink_entity(entity)  # 2. unlink from entity space

    def delete_all_entities(self) -> None:
        """
        Delete all entities from Layout (entity space) and from drawing database.

        """
        # noinspection PyTypeChecker
        for entity in list(self):  # temp list, because delete modifies the base data structure of the iterator
            self.delete_entity(entity)

    def _set_paperspace(self, entity: 'DXFEntity') -> None:
        pass  # only for DXF 2000 and later necessary

    def get_entity_by_handle(self, handle: str) -> 'DXFEntity':
        """
        Get entity by handle as GraphicEntity() or inherited.

        """
        return self._dxffactory.wrap_handle(handle)

    def query(self, query='*') -> EntityQuery:
        """
        Get all DXF entities matching the :ref:`entity query string`.

        Args:
            query: eintity query string

        Returns: :class:`EntityQuery`

        """
        return EntityQuery(iter(self), query)

    def groupby(self, dxfattrib: str = "", key: 'KeyFunc' = None) -> Dict[Hashable, List['DXFEntity']]:
        """
        Returns a dict of entity lists, where entities are grouped by a `dxfattrib` or a `key` function.

        Args:
            dxfattrib: grouping by DXF attribute like "layer"
            key: key function, which accepts a :class:`DXFEntity` as argument, returns grouping key of this entity or
                 None to ignore this object. Reason for ignoring: a queried DXF attribute is not supported by this
                 entity.

        """
        return groupby(iter(self), dxfattrib, key)

    def move_to_layout(self, entity: 'DXFEntity', layout: 'GenericLayoutType') -> None:
        """
        Move entity from block layout to another layout.

        Args:
            entity: DXF entity to move
            layout: any layout (model space, paper space, block)

        """
        if entity.dxf.handle in self._entity_space:
            self.unlink_entity(entity)
            layout.add_entity(entity)
        else:
            raise DXFValueError('Layout does not contain entity.')


class DXF12Layout(BaseLayout):
    """
    Layout representation

    """

    def __init__(self, entityspace: 'EntitySpace', dxffactory: 'DXFFactoryType', paperspace: int = 0):
        super(DXF12Layout, self).__init__(dxffactory, entityspace)
        self._paperspace = paperspace

    # start of public interface

    def __contains__(self, entity: Union[str, 'DXFEntity']):
        """
        Test if the layout contains the drawing element `entity` (aka `in` operator).

        """
        if not hasattr(entity, 'dxf'):  # entity is a handle and not a wrapper class
            entity = self.get_entity_by_handle(entity)
        return entity.dxf.paperspace == self._paperspace and entity.dxf.handle in self._entity_space

    # end of public interface

    def _set_paperspace(self, entity: 'DXFEntity'):
        entity.dxf.paperspace = self._paperspace

    def page_setup(self, size: Tuple[int, int] = (297, 210),
                   margins: Tuple[int, int, int, int] = (0, 0, 0, 0),
                   units: str = 'mm',
                   offset: Tuple[int, int] = (0, 0),
                   rotation: float = 0,
                   scale: int = 16) -> None:
        if self._paperspace == 0:
            raise DXFTypeError("No paper setup for model space.")

        # remove existing viewports
        for viewport in self.viewports():
            self.delete_entity(viewport)

        if int(rotation) not in (0, 1, 2, 3):
            raise DXFValueError("valid rotation values: 0-3")

        if isinstance(scale, int):
            scale = STD_SCALES.get(scale, (1, 1))

        if scale[0] == 0:
            raise DXFValueError("scale numerator can't be 0.")
        if scale[1] == 0:
            raise DXFValueError("scale denominator can't be 0.")

        scale_factor = scale[1] / scale[0]

        # TODO: don't know how to set inch or mm mode in R12
        units = units.lower()
        if units.startswith('inch'):
            units = 'Inches'
            plot_paper_units = 0
            unit_factor = 25.4  # inch to mm
        elif units == 'mm':
            units = 'MM'
            plot_paper_units = 1
            unit_factor = 1.0
        else:
            raise DXFValueError('Supported units: "mm" and "inch"')

        # all viewport parameters are scaled paper space units
        def paper_units(value):
            return value * scale_factor

        # TODO: don't know how paper setup in DXF R12 works
        paper_width, paper_height = size

        # TODO: don't know how margins setup in DXF R12 works
        margin_top, margin_right, margin_bottom, margin_left = margins

        paper_width = paper_units(size[0])
        paper_height = paper_units(size[1])

        plimmin = self.drawing.header['$PLIMMIN'] = (0, 0)
        plimmax = self.drawing.header['$PLIMMAX'] = (paper_width, paper_height)

        # TODO: don't know how paper setup in DXF R12 works

        pextmin = self.drawing.header['$PEXTMIN'] = (0, 0, 0)
        pextmax = self.drawing.header['$PEXTMAX'] = (paper_width, paper_height, 0)

        # printing area
        printable_width = paper_width - paper_units(margin_left) - paper_units(margin_right)
        printable_height = paper_height - paper_units(margin_bottom) - paper_units(margin_top)

        # AutoCAD viewport (window) size
        vp_width = paper_width * 1.1
        vp_height = paper_height * 1.1

        # center of printing area
        center = (printable_width / 2, printable_height / 2)

        # create 'main' viewport
        main_viewport = self.add_viewport(
            center=center,  # no influence to 'main' viewport?
            size=(vp_width, vp_height),  # I don't get it, just use paper size!
            view_center_point=center,  # same as center
            view_height=vp_height,  # view height in paper space units
        )
        main_viewport.dxf.id = 1  # set as main viewport
        main_viewport.dxf.status = 2  # AutoCAD default value
        with main_viewport.edit_data() as vpdata:
            vpdata.view_mode = 1000  # AutoDesk default

    def get_paper_limits(self) -> Tuple[float, float]:
        """
        Returns paper limits in plot paper units

        """
        limmin = self.drawing.header.get('$PLIMMIN', (0, 0))
        limmax = self.drawing.header.get('$PLIMMAX', (0, 0))
        return limmin, limmax

    @property
    def layout_key(self) -> int:
        return self._paperspace

    def viewports(self) -> List['DXFEntity']:
        """
        Get all VIEWPORT entities defined in the layout. Returns a list of Viewport() objects, sorted by id, the first
        entity is always the paper space view with the id=1.

        """
        vports = [entity for entity in self if entity.dxftype() == 'VIEWPORT']
        vports.sort(key=lambda e: e.dxf.id)
        return vports

    def renumber_viewports(self) -> None:
        for num, viewport in enumerate(self.viewports(), start=1):
            viewport.dxf.id = num

    def write(self, tagwriter: 'TagWriter') -> None:
        self._entity_space.write(tagwriter)

    def add_viewport(self,
                     center: Tuple[float, float],
                     size: Tuple[float, float],
                     view_center_point: Tuple[float, float],
                     view_height: float,
                     dxfattribs: dict = None) -> 'DXFEntity':
        if dxfattribs is None:
            dxfattribs = {}
        else:
            dxfattribs = dict(dxfattribs)
        width, height = size
        attribs = {
            'center': center,
            'width': width,
            'height': height,
            'status': 1,  # by default highest priority (stack order)
            'layer': 'VIEWPORTS',  # use separated layer to turn off for plotting
        }
        attribs.update(dxfattribs)
        # DXF R12 (AC1009): view_center_point and view_height (as many other viewport attributes) are not usual
        # DXF attributes, they are stored as extended DXF tags.
        viewport = self.build_and_add_entity('VIEWPORT', attribs)
        viewport.dxf.id = viewport.get_next_viewport_id()
        with viewport.edit_data() as vp_data:
            vp_data.view_center_point = view_center_point
            vp_data.view_height = view_height
        return viewport


class DXF12BlockLayout(BaseLayout):
    """
    BlockLayout has the same factory-function as Layout, but is managed
    in the BlocksSection() class. It represents a DXF Block definition.

    Attributes:
        _block_handle: db handle to BLOCK entity
        _endblk_handle: db handle to ENDBLK entity
        _entityspace: is the block content

    """

    def __init__(self, entitydb: 'EntityDB', dxffactory: 'DXFFactoryType', block_handle: str, endblk_handle: str):
        super(DXF12BlockLayout, self).__init__(dxffactory, EntitySpace(entitydb))
        self._block_handle = block_handle
        self._endblk_handle = endblk_handle

    # start of public interface

    def __contains__(self, entity: 'DXFEntity') -> bool:
        """
        Returns True if block contains entity else False. *entity* can be a handle-string, Tags(),
        ExtendedTags() or a wrapped entity.

        """
        if hasattr(entity, 'get_handle'):
            handle = entity.get_handle()
        elif hasattr(entity, 'dxf'):  # it's a wrapped entity
            handle = entity.dxf.handle
        else:
            handle = entity
        return handle in self._entity_space

    @property
    def block(self) -> 'DXFEntity':
        """ Get associated BLOCK entity. """
        return self.get_entity_by_handle(self._block_handle)

    @property
    def endblk(self) -> 'DXFEntity':
        """ Get associated ENDBLK entity. """
        return self.get_entity_by_handle(self._endblk_handle)

    @property
    def name(self) -> str:
        """ Get block name """
        return self.block.dxf.name

    @name.setter
    def name(self, new_name) -> None:
        """ Set block name """
        block = self.block
        block.dxf.name = new_name
        block.dxf.name2 = new_name

    @property
    def is_layout_block(self) -> bool:
        """
        True if block is a model space or paper space block definition.

        """
        return self.block.is_layout_block

    def add_attdef(self, tag: str, insert: Sequence[float] = (0, 0), text: str = '',
                   dxfattribs: dict = None) -> 'DXFEntity':
        """
        Add an :class:`Attdef` entity.

        Set position and alignment by the idiom::

            myblock.add_attdef('NAME').set_pos((2, 3), align='MIDDLE_CENTER')

        Args:
            tag: attribute name (tag) as string without spaces
            insert: attribute insert point relative to block origin (0, 0, 0)
            text: preset text for attribute

        """
        if dxfattribs is None:
            dxfattribs = {}
        dxfattribs['tag'] = tag
        dxfattribs['insert'] = insert
        dxfattribs['text'] = text
        return self.build_and_add_entity('ATTDEF', dxfattribs)

    def attdefs(self) -> Iterable['DXFEntity']:
        """
        Iterate for all :class:`Attdef` entities.

        """
        return (entity for entity in self if entity.dxftype() == 'ATTDEF')

    def has_attdef(self, tag: str) -> bool:
        """
        Returns `True` if an :class:`Attdef` for `tag` exists else `False`.

        Args:
            tag: tag name

        """
        return self.get_attdef(tag) is not None

    def get_attdef(self, tag: str) -> Optional['DXFEntity']:
        """
        Get attached :class:`Attdef` entity by `tag`.

        Args:
            tag: tag name

        Returns: :class:`Attdef`

        """
        for attdef in self.attdefs():
            if tag == attdef.dxf.tag:
                return attdef

    def get_attdef_text(self, tag: str, default: str = None) -> str:
        """
        Get content text for :class:`Attdef` `tag` as string or returns `default` if no :class:`Attdef` for `tag` exists.

        Args:
            tag: tag name
            default: default value if tag is absent

        """
        attdef = self.get_attdef(tag)
        if attdef is None:
            return default
        return attdef.dxf.text

    # end of public interface

    def add_entity(self, entity: 'DXFEntity') -> None:
        """
        Add an existing DXF entity to a layout, but be sure to unlink (:meth:`~Layout.unlink_entity()`) first the entity
        from the previous owner layout.

        Args:
            entity: :class:`DXFEntity`

        """
        self.add_handle(entity.dxf.handle)
        entity.dxf.paperspace = 0  # set a model space, because paper space layout is a different class
        for linked_entity in entity.linked_entities():
            linked_entity.dxf.paperspace = 0

    def add_handle(self, handle: str) -> None:
        """
        Add entity by handle to the block entity space.

        """
        self._entity_space.append(handle)

    def write(self, tagwriter: 'TagWriter') -> None:
        def write_tags(handle):
            tags = self._entity_space.get_tags_by_handle(handle)
            tagwriter.write_tags(tags)

        write_tags(self._block_handle)
        self._entity_space.write(tagwriter)
        write_tags(self._endblk_handle)

    def delete_all_entities(self) -> None:
        # 1. delete from database
        for handle in self._entity_space:
            del self.entitydb[handle]
        # 2. delete from entity space
        self._entity_space.delete_all_entities()

    def destroy(self) -> None:
        self.delete_all_entities()
        del self.entitydb[self._block_handle]
        del self.entitydb[self._endblk_handle]

    def get_const_attdefs(self) -> Iterable['DXFEntity']:
        """
        Returns a generator for constant ATTDEF entities.

        """
        return (attdef for attdef in self.attdefs() if attdef.is_const)
