# encoding: utf-8

"""
Placeholder-related objects, specific to shapes having a `p:ph` element.
A placeholder has distinct behaviors depending on whether it appears on
a slide, layout, or master. Hence there is a non-trivial class inheritance
structure.
"""

from __future__ import absolute_import, division, print_function, unicode_literals

from .autoshape import Shape
from ..enum.shapes import MSO_SHAPE_TYPE, PP_PLACEHOLDER
from .graphfrm import GraphicFrame
from ..oxml.shapes.graphfrm import CT_GraphicalObjectFrame
from ..oxml.shapes.picture import CT_Picture
from .picture import Picture
from ..util import Emu


class _InheritsDimensions(object):
    """
    Mixin class that provides inherited dimension behavior. Specifically,
    left, top, width, and height report the value from the layout placeholder
    where they would have otherwise reported |None|. This behavior is
    distinctive to placeholders. :meth:`_base_placeholder` must be overridden
    by all subclasses to provide lookup of the appropriate base placeholder
    to inherit from.
    """

    @property
    def height(self):
        """
        The effective height of this placeholder shape; its directly-applied
        height if it has one, otherwise the height of its parent layout
        placeholder.
        """
        return self._effective_value("height")

    @height.setter
    def height(self, value):
        self._element.cy = value

    @property
    def left(self):
        """
        The effective left of this placeholder shape; its directly-applied
        left if it has one, otherwise the left of its parent layout
        placeholder.
        """
        return self._effective_value("left")

    @left.setter
    def left(self, value):
        self._element.x = value

    @property
    def shape_type(self):
        """
        Member of :ref:`MsoShapeType` specifying the type of this shape.
        Unconditionally ``MSO_SHAPE_TYPE.PLACEHOLDER`` in this case.
        Read-only.
        """
        return MSO_SHAPE_TYPE.PLACEHOLDER

    @property
    def top(self):
        """
        The effective top of this placeholder shape; its directly-applied
        top if it has one, otherwise the top of its parent layout
        placeholder.
        """
        return self._effective_value("top")

    @top.setter
    def top(self, value):
        self._element.y = value

    @property
    def width(self):
        """
        The effective width of this placeholder shape; its directly-applied
        width if it has one, otherwise the width of its parent layout
        placeholder.
        """
        return self._effective_value("width")

    @width.setter
    def width(self, value):
        self._element.cx = value

    @property
    def _base_placeholder(self):
        """
        Return the layout or master placeholder shape this placeholder
        inherits from. Not to be confused with an instance of
        |BasePlaceholder| (necessarily).
        """
        raise NotImplementedError("Must be implemented by all subclasses.")

    def _effective_value(self, attr_name):
        """
        The effective value of *attr_name* on this placeholder shape; its
        directly-applied value if it has one, otherwise the value on the
        layout placeholder it inherits from.
        """
        directly_applied_value = getattr(super(_InheritsDimensions, self), attr_name)
        if directly_applied_value is not None:
            return directly_applied_value
        return self._inherited_value(attr_name)

    def _inherited_value(self, attr_name):
        """
        Return the attribute value, e.g. 'width' of the base placeholder this
        placeholder inherits from.
        """
        base_placeholder = self._base_placeholder
        if base_placeholder is None:
            return None
        inherited_value = getattr(base_placeholder, attr_name)
        return inherited_value


class _BaseSlidePlaceholder(_InheritsDimensions, Shape):
    """
    Base class for placeholders on slides. Provides common behaviors such as
    inherited dimensions.
    """

    @property
    def is_placeholder(self):
        """
        Boolean indicating whether this shape is a placeholder.
        Unconditionally |True| in this case.
        """
        return True

    @property
    def shape_type(self):
        """
        Member of :ref:`MsoShapeType` specifying the type of this shape.
        Unconditionally ``MSO_SHAPE_TYPE.PLACEHOLDER`` in this case.
        Read-only.
        """
        return MSO_SHAPE_TYPE.PLACEHOLDER

    @property
    def _base_placeholder(self):
        """
        Return the layout placeholder this slide placeholder inherits from.
        Not to be confused with an instance of |BasePlaceholder|
        (necessarily).
        """
        layout, idx = self.part.slide_layout, self._element.ph_idx
        return layout.placeholders.get(idx=idx)

    def _replace_placeholder_with(self, element):
        """
        Substitute *element* for this placeholder element in the shapetree.
        This placeholder's `._element` attribute is set to |None| and its
        original element is free for garbage collection. Any attribute access
        (including a method call) on this placeholder after this call raises
        |AttributeError|.
        """
        element._nvXxPr.nvPr._insert_ph(self._element.ph)
        self._element.addprevious(element)
        self._element.getparent().remove(self._element)
        self._element = None


class BasePlaceholder(Shape):
    """
    NOTE: This class is deprecated and will be removed from a future release
    along with the properties *idx*, *orient*, *ph_type*, and *sz*. The *idx*
    property will be available via the .placeholder_format property. The
    others will be accessed directly from the oxml layer as they are only
    used for internal purposes.

    Base class for placeholder subclasses that differentiate the varying
    behaviors of placeholders on a master, layout, and slide.
    """

    @property
    def idx(self):
        """
        Integer placeholder 'idx' attribute, e.g. 0
        """
        return self._sp.ph_idx

    @property
    def orient(self):
        """
        Placeholder orientation, e.g. ST_Direction.HORZ
        """
        return self._sp.ph_orient

    @property
    def ph_type(self):
        """
        Placeholder type, e.g. PP_PLACEHOLDER.CENTER_TITLE
        """
        return self._sp.ph_type

    @property
    def sz(self):
        """
        Placeholder 'sz' attribute, e.g. ST_PlaceholderSize.FULL
        """
        return self._sp.ph_sz


class LayoutPlaceholder(_InheritsDimensions, Shape):
    """
    Placeholder shape on a slide layout, providing differentiated behavior
    for slide layout placeholders, in particular, inheriting shape properties
    from the master placeholder having the same type, when a matching one
    exists.
    """

    @property
    def _base_placeholder(self):
        """
        Return the master placeholder this layout placeholder inherits from.
        """
        base_ph_type = {
            PP_PLACEHOLDER.BODY: PP_PLACEHOLDER.BODY,
            PP_PLACEHOLDER.CHART: PP_PLACEHOLDER.BODY,
            PP_PLACEHOLDER.BITMAP: PP_PLACEHOLDER.BODY,
            PP_PLACEHOLDER.CENTER_TITLE: PP_PLACEHOLDER.TITLE,
            PP_PLACEHOLDER.ORG_CHART: PP_PLACEHOLDER.BODY,
            PP_PLACEHOLDER.DATE: PP_PLACEHOLDER.DATE,
            PP_PLACEHOLDER.FOOTER: PP_PLACEHOLDER.FOOTER,
            PP_PLACEHOLDER.MEDIA_CLIP: PP_PLACEHOLDER.BODY,
            PP_PLACEHOLDER.OBJECT: PP_PLACEHOLDER.BODY,
            PP_PLACEHOLDER.PICTURE: PP_PLACEHOLDER.BODY,
            PP_PLACEHOLDER.SLIDE_NUMBER: PP_PLACEHOLDER.SLIDE_NUMBER,
            PP_PLACEHOLDER.SUBTITLE: PP_PLACEHOLDER.BODY,
            PP_PLACEHOLDER.TABLE: PP_PLACEHOLDER.BODY,
            PP_PLACEHOLDER.TITLE: PP_PLACEHOLDER.TITLE,
        }[self._element.ph_type]
        slide_master = self.part.slide_master
        return slide_master.placeholders.get(base_ph_type, None)


class MasterPlaceholder(BasePlaceholder):
    """
    Placeholder shape on a slide master.
    """


class NotesSlidePlaceholder(_InheritsDimensions, Shape):
    """
    Placeholder shape on a notes slide. Inherits shape properties from the
    placeholder on the notes master that has the same type (e.g. 'body').
    """

    @property
    def _base_placeholder(self):
        """
        Return the notes master placeholder this notes slide placeholder
        inherits from, or |None| if no placeholder of the matching type is
        present.
        """
        notes_master = self.part.notes_master
        ph_type = self.element.ph_type
        return notes_master.placeholders.get(ph_type=ph_type)


class SlidePlaceholder(_BaseSlidePlaceholder):
    """
    Placeholder shape on a slide. Inherits shape properties from its
    corresponding slide layout placeholder.
    """


class ChartPlaceholder(_BaseSlidePlaceholder):
    """
    Placeholder shape that can only accept a chart.
    """

    def insert_chart(self, chart_type, chart_data):
        """
        Return a |PlaceholderGraphicFrame| object containing a new chart of
        *chart_type* depicting *chart_data* and having the same position and
        size as this placeholder. *chart_type* is one of the
        :ref:`XlChartType` enumeration values. *chart_data* is a |ChartData|
        object populated with the categories and series values for the chart.
        Note that the new |Chart| object is not returned directly. The chart
        object may be accessed using the
        :attr:`~.PlaceholderGraphicFrame.chart` property of the returned
        |PlaceholderGraphicFrame| object.
        """
        rId = self.part.add_chart_part(chart_type, chart_data)
        graphicFrame = self._new_chart_graphicFrame(
            rId, self.left, self.top, self.width, self.height
        )
        self._replace_placeholder_with(graphicFrame)
        return PlaceholderGraphicFrame(graphicFrame, self._parent)

    def _new_chart_graphicFrame(self, rId, x, y, cx, cy):
        """
        Return a newly created `p:graphicFrame` element having the specified
        position and size and containing the chart identified by *rId*.
        """
        id_, name = self.shape_id, self.name
        return CT_GraphicalObjectFrame.new_chart_graphicFrame(
            id_, name, rId, x, y, cx, cy
        )


class PicturePlaceholder(_BaseSlidePlaceholder):
    """
    Placeholder shape that can only accept a picture.
    """

    def insert_picture(self, image_file):
        """
        Return a |PlaceholderPicture| object depicting the image in
        *image_file*, which may be either a path (string) or a file-like
        object. The image is cropped to fill the entire space of the
        placeholder. A |PlaceholderPicture| object has all the properties and
        methods of a |Picture| shape except that the value of its
        :attr:`~._BaseSlidePlaceholder.shape_type` property is
        `MSO_SHAPE_TYPE.PLACEHOLDER` instead of `MSO_SHAPE_TYPE.PICTURE`.
        """
        pic = self._new_placeholder_pic(image_file)
        self._replace_placeholder_with(pic)
        return PlaceholderPicture(pic, self._parent)

    def _new_placeholder_pic(self, image_file):
        """
        Return a new `p:pic` element depicting the image in *image_file*,
        suitable for use as a placeholder. In particular this means not
        having an `a:xfrm` element, allowing its extents to be inherited from
        its layout placeholder.
        """
        rId, desc, image_size = self._get_or_add_image(image_file)
        shape_id, name = self.shape_id, self.name
        pic = CT_Picture.new_ph_pic(shape_id, name, desc, rId)
        pic.crop_to_fit(image_size, (self.width, self.height))
        return pic

    def _get_or_add_image(self, image_file):
        """
        Return an (rId, description, image_size) 3-tuple identifying the
        related image part containing *image_file* and describing the image.
        """
        image_part, rId = self.part.get_or_add_image_part(image_file)
        desc, image_size = image_part.desc, image_part._px_size
        return rId, desc, image_size


class PlaceholderGraphicFrame(GraphicFrame):
    """
    Placeholder shape populated with a table, chart, or smart art.
    """

    @property
    def is_placeholder(self):
        """
        Boolean indicating whether this shape is a placeholder.
        Unconditionally |True| in this case.
        """
        return True


class PlaceholderPicture(_InheritsDimensions, Picture):
    """
    Placeholder shape populated with a picture.
    """

    @property
    def _base_placeholder(self):
        """
        Return the layout placeholder this picture placeholder inherits from.
        """
        layout, idx = self.part.slide_layout, self._element.ph_idx
        return layout.placeholders.get(idx=idx)


class TablePlaceholder(_BaseSlidePlaceholder):
    """
    Placeholder shape that can only accept a picture.
    """

    def insert_table(self, rows, cols):
        """
        Return a |PlaceholderGraphicFrame| object containing a table of
        *rows* rows and *cols* columns. The position and width of the table
        are those of the placeholder and its height is proportional to the
        number of rows. A |PlaceholderGraphicFrame| object has all the
        properties and methods of a |GraphicFrame| shape except that the
        value of its :attr:`~._BaseSlidePlaceholder.shape_type` property is
        unconditionally `MSO_SHAPE_TYPE.PLACEHOLDER`. Note that the return
        value is not the new table but rather *contains* the new table. The
        table can be accessed using the
        :attr:`~.PlaceholderGraphicFrame.table` property of the returned
        |PlaceholderGraphicFrame| object.
        """
        graphicFrame = self._new_placeholder_table(rows, cols)
        self._replace_placeholder_with(graphicFrame)
        return PlaceholderGraphicFrame(graphicFrame, self._parent)

    def _new_placeholder_table(self, rows, cols):
        """
        Return a newly added `p:graphicFrame` element containing an empty
        table with *rows* rows and *cols* columns, positioned at the location
        of this placeholder and having its same width. The table's height is
        determined by the number of rows.
        """
        shape_id, name, height = self.shape_id, self.name, Emu(rows * 370840)
        return CT_GraphicalObjectFrame.new_table_graphicFrame(
            shape_id, name, rows, cols, self.left, self.top, self.width, height
        )
