# coding=utf-8
from __future__ import division

from .legend import Legend, LegendParameters, LegendParametersCategorized

from .datatype.base import DataTypeBase

from ladybug_geometry.geometry2d.pointvector import Point2D
from ladybug_geometry.geometry3d.pointvector import Point3D
from ladybug_geometry.geometry3d.plane import Plane
from ladybug_geometry.geometry2d.mesh import Mesh2D
from ladybug_geometry.geometry3d.mesh import Mesh3D
from ladybug_geometry.geometry3d.polyface import Polyface3D
from ladybug_geometry.dictutil import geometry_dict_to_object


class GraphicContainer(object):
    """Graphic container used to get legends, title locations, and colors for any graphic.

    Args:
        values: A List or Tuple of numerical values that will be used to
            generate the legend and colors.
        min_point: A Point3D object for the minimum of the bounding box around
            the graphic geometry. If None, then there must be an input for geometry
            and the bounding box around this geometry will be used to set up the
            graphic container. (Default: None).
        max_point: A Point3D object for the maximum of the  bounding box around
            the graphic geometry. If None, then there must be an input for geometry
            and the bounding box around this geometry will be used to set up the
            graphic container. (Default: None).
        legend_parameters: An Optional LegendParameter object to override default
            parameters of the legend. None indicates that default legend parameters
            will be used. (Default: None).
        data_type: Optional DataType from the ladybug datatype subpackage (ie.
            Temperature()) , which will be used to assign default legend properties.
            If None, the legend associated with this object will contain no units
            unless a unit below is specified. (Default: None).
        unit: Optional text string for the units of the values. (ie. 'C'). If None,
            the default units of the data_type will be used. (Default: None).
        geometry: An optional ladybug-geometry object (or list of ladybug-geometry
            objects) that's aligned with the input values. If a Mesh or Polyface
            is specified here, it is expected that the number of values match the
            number of faces or the number of vertices. If a list of geometry objects
            is specified (ie. a list of Point3Ds), it is expected that the length
            of this list align with the number of values. (Default: None).

    Properties:
        * values
        * min_point
        * max_point
        * legend_parameters
        * data_type
        * unit
        * geometry
        * legend
        * value_colors
        * lower_title_location
        * upper_title_location
    """
    __slots__ = ('_legend', '_min_point', '_max_point',
                 '_data_type', '_unit', '_geometry')

    def __init__(self, values, min_point=None, max_point=None,
                 legend_parameters=None, data_type=None, unit=None, geometry=None):
        """Initialize graphic container.
        """
        # check the input points and legend information
        if min_point is None or max_point is None:
            assert isinstance(geometry, (Mesh2D, Mesh3D, Polyface3D)), \
                'If min_point or max_point are unspecified, then geometry must be a ' \
                'Mesh or Polyface to determine the bounding box.'
            min_point = geometry.min if min_point is None else min_point
            max_point = geometry.max if max_point is None else max_point
            if isinstance(min_point, Point2D):
                min_point = Point3D(min_point.x, min_point.y)
            if isinstance(max_point, Point2D):
                max_point = Point3D(max_point.x, max_point.y)
        assert isinstance(min_point, Point3D), \
            'min_point should be a ladybug Point3D. Got {}'.format(type(min_point))
        assert isinstance(max_point, Point3D), \
            'max_point should be a ladybug Point3D. Got {}'.format(type(max_point))
        self._legend = Legend(values, legend_parameters)
        self._min_point = min_point
        self._max_point = max_point

        # process the geometry if it is specified
        if geometry is not None:
            if isinstance(geometry, (list, tuple)):
                assert len(values) == len(geometry), 'Expected one value per geometry ' \
                    'when geometry is a list. Number of values ({}) does not match ' \
                    'number of geometries ({}).'.format(len(values), len(geometry))
                if isinstance(geometry, list):
                    geometry = tuple(geometry)
            elif isinstance(geometry, (Mesh2D, Mesh3D, Polyface3D)):
                assert len(values) == len(geometry.faces) or len(values) == \
                    len(geometry.vertices), 'Expected number of values ({}) to match ' \
                    'number of faces ({}) or number of vertices ({}).'.format(
                        len(values), len(geometry.faces), len(geometry.vertices))
            else:
                msg = 'Expected geometry to be a list of geometries, a Mesh or a ' \
                    'Polyface. Got {}.'.format(type(geometry))
                raise ValueError(msg)
        self._geometry = geometry

        # set default legend parameters based on input data_type and unit
        self._data_type = data_type
        self._unit = unit
        if data_type is not None:
            assert isinstance(data_type, DataTypeBase), \
                'data_type should be a ladybug DataType. Got {}'.format(type(data_type))
            if self.legend_parameters.is_title_default:
                unit = unit if unit else data_type.units[0]
                data_type.is_unit_acceptable(unit)
                self.legend_parameters.title = unit if \
                    self.legend_parameters.vertical \
                    else '{} ({})'.format(data_type.name, unit)
            if data_type.unit_descr is not None and \
                    self.legend_parameters.ordinal_dictionary is None and not \
                    isinstance(self.legend_parameters, LegendParametersCategorized):
                self.legend_parameters.ordinal_dictionary = data_type.unit_descr
                sorted_keys = sorted(data_type.unit_descr.keys())
                if self.legend.is_min_default:
                    self.legend_parameters._min = sorted_keys[0]
                if self.legend.is_max_default:
                    self.legend_parameters._max = sorted_keys[-1]
                assert self.legend_parameters._min <= self.legend_parameters._max, \
                    'Legend min is greater than legend max. {} > {}.'.format(
                        self.legend_parameters._min, self.legend_parameters._max)
                if self.legend_parameters.is_segment_count_default:
                    try:  # try to set the number of segments to align with ordinal text
                        min_i = sorted_keys.index(self.legend_parameters.min)
                        max_i = sorted_keys.index(self.legend_parameters.max)
                        self.legend_parameters.segment_count = \
                            len(sorted_keys[min_i:max_i + 1])
                    except IndexError:
                        pass
        elif unit and self.legend_parameters.is_title_default:
            assert isinstance(unit, str), \
                'Expected string for unit. Got {}.'.format(type(unit))
            self.legend_parameters.title = unit

        # set the default segment_height
        if self.legend_parameters.is_segment_height_default:
            s_count = self.legend_parameters.segment_count
            denom = s_count if s_count >= 8 else 8
            if self.legend_parameters.vertical:
                seg_height = float((self._max_point.y - self._min_point.y) / denom)
                if seg_height == 0:
                    seg_height = float((self._max_point.x - self._min_point.x) / denom)
            else:
                seg_height = float((self._max_point.x - self._min_point.x) / (denom * 2))
                if seg_height == 0:
                    seg_height = float((self._max_point.y - self._min_point.y) / denom)
            self.legend_parameters.segment_height = seg_height
            self.legend_parameters._is_segment_height_default = True

        # set the default segment_width
        if self.legend_parameters.is_segment_width_default:
            if self.legend_parameters.vertical:
                seg_width = self.legend_parameters.segment_height / 2
            else:
                seg_width = self.legend_parameters.text_height * \
                    (len(str(int(self.legend_parameters.max))) +
                     self.legend_parameters.decimal_count + 2)
            self.legend_parameters.segment_width = seg_width
            self.legend_parameters._is_segment_width_default = True

        # set the default base point
        if self.legend_parameters.is_base_plane_default:
            if self.legend_parameters.vertical:
                base_pt = Point3D(
                    self._max_point.x + self.legend_parameters.segment_width,
                    self._min_point.y, self._min_point.z)
            else:
                base_pt = Point3D(
                    self._max_point.x,
                    self._max_point.y + 3 * self.legend_parameters.text_height,
                    self._min_point.z)
            self.legend_parameters.base_plane = Plane(o=base_pt)
            self.legend_parameters._is_base_plane_default = True

    @classmethod
    def from_dict(cls, data):
        """Create a graphic container from a dictionary.

        Args:
            data: A python dictionary in the following format

        .. code-block:: python

            {
            "values": [0, 10],
            "min_point": {"x": 0, "y": 0, "z": 0},
            "max_point": {"x": 10, "y": 10, "z": 0},
            "legend_parameters": {},  # optional LegendParameter specification
            "data_type": {},  # optional DataType object
            "unit": "C",  # optional text for the units
            "geometry": {}  # optional geometry associated with the values
            }
        """
        legend_parameters = None
        if 'legend_parameters' in data and data['legend_parameters'] is not None:
            if data['legend_parameters']['type'] == 'LegendParametersCategorized':
                legend_parameters = LegendParametersCategorized.from_dict(
                    data['legend_parameters'])
            else:
                legend_parameters = LegendParameters.from_dict(data['legend_parameters'])

        data_type = None
        if 'data_type' in data and data['data_type'] is not None:
            data_type = DataTypeBase.from_dict(data['data_type'])
        unit = data['unit'] if 'unit' in data else None

        geometry = None
        if 'geometry' in data and data['geometry'] is not None:
            if isinstance(data['geometry'], (list, tuple)):
                geometry = [geometry_dict_to_object(g) for g in data['geometry']]
            else:
                geometry = geometry_dict_to_object(data['geometry'])

        return cls(data['values'], Point3D.from_dict(data['min_point']),
                   Point3D.from_dict(data['max_point']),
                   legend_parameters, data_type, unit, geometry)

    @property
    def values(self):
        """The assigned data set of values."""
        return self._legend.values

    @property
    def min_point(self):
        """Point3D for the minimum of the bounding box around referenced geometry."""
        return self._min_point

    @property
    def max_point(self):
        """Point3D for the maximum of the bounding box around referenced geometry."""
        return self._max_point

    @property
    def legend_parameters(self):
        """The legend parameters assigned to this graphic."""
        return self._legend._legend_par

    @property
    def data_type(self):
        """The data_type input to this object (if it exists)."""
        return self._data_type

    @property
    def unit(self):
        """The unit input to this object (if it exists)."""
        return self._unit

    @property
    def geometry(self):
        """Get a list of geometries, mesh or polyface associated with the values."""
        return self._geometry

    @property
    def legend(self):
        """The legend assigned to this graphic."""
        return self._legend

    @property
    def value_colors(self):
        """A List of colors associated with the assigned values."""
        return self._legend.value_colors

    @property
    def lower_title_location(self):
        """A Plane for the lower location of title text."""
        return Plane(o=Point3D(
            self._min_point.x,
            self._min_point.y - 2.5 * self._legend.legend_parameters.text_height,
            self._min_point.z))

    @property
    def upper_title_location(self):
        """A Plane for the upper location of title text."""
        return Plane(o=Point3D(
            self._min_point.x,
            self._max_point.y + self._legend.legend_parameters.text_height,
            self._min_point.z))

    def to_dict(self):
        """Get graphic container as a dictionary."""
        base = {
            'type': 'GraphicContainer',
            'values': self.values,
            'min_point': self.min_point.to_dict(),
            'max_point': self.max_point.to_dict(),
            'legend_parameters': self.legend_parameters.to_dict()
        }
        if self.data_type is not None:
            base['data_type'] = self.data_type.to_dict()
        if self.unit is not None:
            base['unit'] = self.unit
        if self.geometry is not None:
            base['geometry'] = [g.to_dict() for g in self.geometry] if \
                isinstance(self.geometry, tuple) else self.geometry.to_dict()
        return base

    def __len__(self):
        """Return length of values on the object."""
        return len(self._legend._values)

    def __getitem__(self, key):
        """Return one of the values."""
        return self._legend._values[key]

    def __iter__(self):
        """Iterate through the values."""
        return iter(self._legend._values)

    def ToString(self):
        """Overwrite .NET ToString."""
        return self.__repr__()

    def __repr__(self):
        """ResultMesh representation."""
        return 'Ladybug Result Mesh ({} values)'.format(len(self))
