# coding: utf-8
"""Glazing Parameters with instructions for generating windows."""
from honeybee.typing import float_in_range, float_positive
from honeybee.boundarycondition import Surface
from honeybee.aperture import Aperture

from ladybug_geometry.geometry3d.line import LineSegment3D


class _GlazingParameterBase(object):
    """Base object for all glazing parameters.

    This object records all of the methods that must be overwritten on a glazing
    parameter object for it to be successfully be applied in dragonfly workflows.
    """
    __slots__ = ()

    def __init__(self):
        pass

    def area_from_segment(self, segment, floor_to_ceiling_height):
        """Get the glazing area generated by these parameters from a LineSegment3D."""
        return 0

    def add_glazing_to_face(self, face, tolerance=0):
        """Add Apertures to a Honeybee Face using these Glazing Parameters."""
        pass

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

        .. code-block:: json

            {
            "type": "GlazingParameterBase"
            }
        """
        assert data['type'] == 'GlazingParameterBase', \
            'Expected GlazingParameterBase dictionary. Got {}.'.format(data['type'])
        return cls()

    def to_dict(self):
        """Get GlazingParameterBase as a dictionary."""
        return {'type': 'GlazingParameterBase'}

    def duplicate(self):
        """Get a copy of this object."""
        return self.__copy__()

    def ToString(self):
        return self.__repr__()

    def __copy__(self):
        return _GlazingParameterBase()

    def __repr__(self):
        return 'GlazingParameterBase'


class SingleWindow(_GlazingParameterBase):
    """Instructions for a single window in the face center defined by a width x height.

    Properties:
        * width
        * height
        * sill_height
    """
    __slots__ = ('_width', '_height', '_sill_height')

    def __init__(self, width, height, sill_height=1):
        """Instructions for a single window in the face center defined by a width*height.

        Note that, if these parameters are applied to a base face that is too short
        or too narrow for the input width and/or height, the generated window will
        automatically be shortened when it is applied to the face. In this way,
        setting the width to be `float('inf')` will create parameters that always
        generate a ribboin window of the input height.

        Args:
            width: A number for the window width.
            height: A number for the window height.
            sill_height: A number for the window sill height. Default: 1.
        """
        self._width = float_positive(width, 'glazing width')
        self._height = float_positive(height, 'glazing height')
        self._sill_height = float_positive(sill_height, 'glazing sill height')

    @property
    def width(self):
        """Get a number for the window width."""
        return self._width

    @property
    def height(self):
        """Get a number for the window height."""
        return self._height

    @property
    def sill_height(self):
        """Get a number for the sill height."""
        return self._sill_height

    def area_from_segment(self, segment, floor_to_ceiling_height):
        """Get the glazing area generated by these parameters from a LineSegment3D.

        Args:
            segment: A LineSegment3D to which these parameters are applied.
            floor_to_ceiling_height: The floor-to-ceiling height of the Room2D
                to which the segment belongs.
        """
        max_width = segment.length * 0.99
        max_height = (floor_to_ceiling_height * 0.99) - self.sill_height
        final_width = max_width if self.width > max_width else self.width
        final_height = max_height if self.height > max_height else self.height
        if final_height < 0:
            return 0
        else:
            return final_width * final_height

    def add_glazing_to_face(self, face, tolerance=0):
        """Add Apertures to a Honeybee Face using these Glazing Parameters.

        Args:
            face: A honeybee-core Face object.
            tolerance: Optional tolerance value.
        """
        width_seg = LineSegment3D.from_end_points(face.geometry[0], face.geometry[1])
        height_seg = LineSegment3D.from_end_points(face.geometry[1], face.geometry[2])
        max_width = width_seg.length * 0.99
        max_height = (height_seg.length * 0.99) - self.sill_height
        final_width = max_width if self.width > max_width else self.width
        final_height = max_height if self.height > max_height else self.height
        if final_height > 0:
            face.aperture_by_width_height(final_width, final_height, self.sill_height)
            # if the Aperture is interior, set adjacent boundary condition
            if isinstance(face._boundary_condition, Surface):
                names = face._boundary_condition.boundary_condition_objects
                adj_ap_name = '{}_Glz1'.format(names[0])
                final_names = (adj_ap_name,) + names
                face.apertures[0].boundary_condition = Surface(final_names, True)

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

        .. code-block:: json

            {
            "type": "SingleWindow",
            "width": 100,
            "height": 1.5,
            "sill_height": 0.8
            }
        """
        assert data['type'] == 'SingleWindow', \
            'Expected SingleWindow dictionary. Got {}.'.format(data['type'])
        sill = data['sill_height'] if 'sill_height' in data else 1
        return cls(data['width'], data['height'], sill)

    def to_dict(self):
        """Get SingleWindow as a dictionary."""
        return {'type': 'SingleWindow',
                'width': self.width,
                'height': self.height,
                'sill_height': self.sill_height}

    def __copy__(self):
        return SingleWindow(self.width, self.height, self.sill_height)

    def __key(self):
        """A tuple based on the object properties, useful for hashing."""
        return (self.width, self.height, self.sill_height)

    def __hash__(self):
        return hash(self.__key())

    def __eq__(self, other):
        return isinstance(other, SingleWindow) and self.__key() == other.__key()

    def __ne__(self, other):
        return not self.__eq__(other)

    def __repr__(self):
        return 'SingleWindow:\n width: {}\n height: {}\n sill_height: {}'.format(
            self.width, self.height, self.sill_height)


class SimpleGlazingRatio(_GlazingParameterBase):
    """Instructions for a single window using an area ratio with the base surface.

    Properties:
        * glazing_ratio
    """
    __slots__ = ('_glazing_ratio',)

    def __init__(self, glazing_ratio):
        """Instructions for a single window using an area ratio with the base surface.

        Args:
            glazing_ratio: A number between 0 and 1 for the ratio between the glazing
                area and the parent wall surface area.
        """
        self._glazing_ratio = float_in_range(glazing_ratio, 0, 1, 'glazing ratio')

    @property
    def glazing_ratio(self):
        """Get a number between 0 and 1 for the glazing ratio."""
        return self._glazing_ratio

    def area_from_segment(self, segment, floor_to_ceiling_height):
        """Get the glazing area generated by these parameters from a LineSegment3D.

        Args:
            segment: A LineSegment3D to which these parameters are applied.
            floor_to_ceiling_height: The floor-to-ceiling height of the Room2D
                to which the segment belongs.
        """
        return segment.length * floor_to_ceiling_height * self._glazing_ratio

    def add_glazing_to_face(self, face, tolerance=0):
        """Add Apertures to a Honeybee Face using these Glazing Parameters.

        Args:
            face: A honeybee-core Face object.
            tolerance: Optional tolerance value.
        """
        scale_factor = self.glazing_ratio ** .5
        ap_face = face.geometry.scale(scale_factor, face.geometry.center)
        aperture = Aperture('{}_Glz1'.format(face.display_name), ap_face)
        aperture._parent = face
        face._apertures.append(aperture)
        # if the Aperture is interior, set adjacent boundary condition
        if isinstance(face._boundary_condition, Surface):
            names = face._boundary_condition.boundary_condition_objects
            adj_ap_name = '{}_Glz1'.format(names[0])
            final_names = (adj_ap_name,) + names
            aperture.boundary_condition = Surface(final_names, True)

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

        .. code-block:: json

            {
            "type": "SimpleGlazingRatio",
            "glazing_ratio": 0.4
            }
        """
        assert data['type'] == 'SimpleGlazingRatio', \
            'Expected SimpleGlazingRatio dictionary. Got {}.'.format(data['type'])
        return cls(data['glazing_ratio'])

    def to_dict(self):
        """Get SimpleGlazingRatio as a dictionary."""
        return {'type': 'SimpleGlazingRatio',
                'glazing_ratio': self.glazing_ratio}

    def __copy__(self):
        return SimpleGlazingRatio(self.glazing_ratio)

    def __key(self):
        """A tuple based on the object properties, useful for hashing."""
        return self._glazing_ratio

    def __hash__(self):
        return hash(self.__key())

    def __eq__(self, other):
        return isinstance(other, SimpleGlazingRatio) and self.__key() == other.__key()

    def __ne__(self, other):
        return not self.__eq__(other)

    def __repr__(self):
        return 'SimpleGlazingRatio:\n ratio: {}'.format(self.glazing_ratio)


class RepeatingGlazingRatio(SimpleGlazingRatio):
    """Instructions for repeating windows derived from an area ratio with the base surface.

    Properties:
        * glazing_ratio
        * glazing_height
        * sill_height
        * horizontal_separation
        * vertical_separation
    """
    __slots__ = ('_glazing_height', '_sill_height',
                 '_horizontal_separation', '_vertical_separation')

    def __init__(self, glazing_ratio, glazing_height, sill_height,
                 horizontal_separation, vertical_separation=0):
        """Instructions for repeating windows from an area ratio with the base surface.

        Args:
            glazing_ratio: A number between 0 and 1 for the ratio between the glazing
                area and the total facade area.
            glazing_height: A number for the target height of the apertures.
                Note that, if the glazing ratio is too large for the height, the
                ratio will take precedence and the actual aperture_height will
                be larger than this value.
            sill_height: A number for the target height above the bottom edge of
                the rectangle to start the apertures. Note that, if the
                ratio is too large for the height, the ratio will take precedence
                and the sill_height will be smaller than this value.
            horizontal_separation: A number for the target separation between
                individual aperture centerlines.  If this number is larger than
                the parent rectangle base, only one aperture will be produced.
            vertical_separation: An optional number to create a single vertical
                separation between top and bottom apertures. Default: 0.
        """
        self._glazing_ratio = float_in_range(glazing_ratio, 0, 0.95, 'glazing ratio')
        self._glazing_height = float_positive(glazing_height, 'glazing height')
        self._sill_height = float_positive(sill_height, 'sill height')
        self._horizontal_separation = \
            float_positive(horizontal_separation, 'glazing horizontal separation')
        self._vertical_separation = \
            float_positive(vertical_separation, 'glazing vertical separation')

    @property
    def glazing_height(self):
        """Get a number or the target height of the apertures."""
        return self._glazing_height

    @property
    def sill_height(self):
        """Get a number for the height above the bottom edge of the floor."""
        return self._sill_height

    @property
    def horizontal_separation(self):
        """Get a number for the separation between individual aperture centerlines."""
        return self._horizontal_separation

    @property
    def vertical_separation(self):
        """Get a number for a vertical separation between top/bottom apertures."""
        return self._vertical_separation

    def add_glazing_to_face(self, face, tolerance=0):
        """Add Apertures to a Honeybee Face using these Glazing Parameters.

        Args:
            face: A honeybee-core Face object.
            tolerance: The maximum difference between point values for them to be
                considered a part of a rectangle.
        """
        face.apertures_by_ratio_rectangle(
            self.glazing_ratio, self.glazing_height, self.sill_height,
            self.horizontal_separation, self.vertical_separation, tolerance)
        # if the Aperture is interior, set adjacent boundary condition
        if isinstance(face._boundary_condition, Surface):
            num_aps = face.apertures
            for i, ap in enumerate(face.apertures):
                names = face._boundary_condition.boundary_condition_objects
                adj_ap_name = '{}_Glz{}'.format(names[0], num_aps - i - 1)
                final_names = (adj_ap_name,) + names
                ap.boundary_condition = Surface(final_names, True)

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

        .. code-block:: json

            {
            "type": "RepeatingGlazingRatio",
            "glazing_ratio": 0.4,
            "glazing_height": 2,
            "sill_height": 0.8,
            "horizontal_separation": 4,
            "vertical_separation": 0.5
            }
        """
        assert data['type'] == 'RepeatingGlazingRatio', \
            'Expected RepeatingGlazingRatio dictionary. Got {}.'.format(data['type'])
        vert = data['vertical_separation'] if 'vertical_separation' in data else 0
        return cls(data['glazing_ratio'], data['glazing_height'], data['sill_height'],
                   data['horizontal_separation'], vert)

    def to_dict(self):
        """Get RepeatingGlazingRatio as a dictionary."""
        base = {'type': 'RepeatingGlazingRatio',
                'glazing_ratio': self.glazing_ratio,
                'glazing_height': self.glazing_height,
                'sill_height': self.sill_height,
                'horizontal_separation': self.horizontal_separation}
        if self.vertical_separation != 0:
            base['vertical_separation'] = self.vertical_separation
        return base

    def __copy__(self):
        return RepeatingGlazingRatio(
            self._glazing_ratio, self._glazing_height, self._sill_height,
            self._horizontal_separation, self._vertical_separation)

    def __key(self):
        """A tuple based on the object properties, useful for hashing."""
        return (self._glazing_ratio, self._glazing_height, self._sill_height,
                self._horizontal_separation, self._vertical_separation)

    def __hash__(self):
        return hash(self.__key())

    def __eq__(self, other):
        return isinstance(other, RepeatingGlazingRatio) and self.__key() == other.__key()

    def __ne__(self, other):
        return not self.__eq__(other)

    def __repr__(self):
        return 'RepeatingGlazingRatio:\n ratio: {}\n glazing_height: {}\n sill_height:' \
            ' {}\n horizontal: {}\n vertical: {}'.format(
                self._glazing_ratio, self.glazing_height, self.sill_height,
                self.horizontal_separation, self.vertical_separation)
