# coding: utf-8
"""Window 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 _WindowParameterBase(object):
    """Base object for all window parameters.

    This object records all of the methods that must be overwritten on a window
    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 window area generated by these parameters from a LineSegment3D."""
        return 0

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

    def scale(self, factor):
        """Get a scaled version of these WindowParameters.
        
        This method is called within the scale methods of the Room2D.
        
        Args:
            factor: A number representing how much the object should be scaled.
        """
        return self

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

        .. code-block:: json

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

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

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

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

    def __copy__(self):
        return _WindowParameterBase()

    def __repr__(self):
        return 'WindowParameterBase'


class SingleWindow(_WindowParameterBase):
    """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 wall 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, 'window width')
        self._height = float_positive(height, 'window height')
        self._sill_height = float_positive(sill_height, 'window 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 window 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_window_to_face(self, face, tolerance=0):
        """Add Apertures to a Honeybee Face using these Window 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)

    def scale(self, factor):
        """Get a scaled version of these WindowParameters.
        
        This method is called within the scale methods of the Room2D.
        
        Args:
            factor: A number representing how much the object should be scaled.
        """
        return SingleWindow(
            self.width * factor, self.height * factor, self.sill_height * factor)

    @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 SimpleWindowRatio(_WindowParameterBase):
    """Instructions for a single window defined by an area ratio with the base wall.

    Properties:
        * window_ratio
    """
    __slots__ = ('_window_ratio',)

    def __init__(self, window_ratio):
        """Instructions for a single window defined by an area ratio with the base wall.

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

    @property
    def window_ratio(self):
        """Get a number between 0 and 1 for the window ratio."""
        return self._window_ratio

    def area_from_segment(self, segment, floor_to_ceiling_height):
        """Get the window 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._window_ratio

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

        Args:
            face: A honeybee-core Face object.
            tolerance: Optional tolerance value.
        """
        scale_factor = self.window_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 SimpleWindowRatio from a dictionary.

        .. code-block:: json

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

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

    def __copy__(self):
        return SimpleWindowRatio(self.window_ratio)

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

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

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

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

    def __repr__(self):
        return 'SimpleWindowRatio:\n ratio: {}'.format(self.window_ratio)


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

    Properties:
        * window_ratio
        * window_height
        * sill_height
        * horizontal_separation
        * vertical_separation
    """
    __slots__ = ('_window_height', '_sill_height',
                 '_horizontal_separation', '_vertical_separation')

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

        Args:
            window_ratio: A number between 0 and 1 for the ratio between the window
                area and the total facade area.
            window_height: A number for the target height of the windows.
                Note that, if the window ratio is too large for the height, the
                ratio will take precedence and the actual window_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 windows. 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 window centerlines.  If this number is larger than
                the parent rectangle base, only one window will be produced.
            vertical_separation: An optional number to create a single vertical
                separation between top and bottom windows. Default: 0.
        """
        self._window_ratio = float_in_range(window_ratio, 0, 0.95, 'window ratio')
        self._window_height = float_positive(window_height, 'window height')
        self._sill_height = float_positive(sill_height, 'sill height')
        self._horizontal_separation = \
            float_positive(horizontal_separation, 'window horizontal separation')
        self._vertical_separation = \
            float_positive(vertical_separation, 'window vertical separation')

    @property
    def window_height(self):
        """Get a number or the target height of the windows."""
        return self._window_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 window centerlines."""
        return self._horizontal_separation

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

    def add_window_to_face(self, face, tolerance=0):
        """Add Apertures to a Honeybee Face using these Window 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.window_ratio, self.window_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)

    def scale(self, factor):
        """Get a scaled version of these WindowParameters.
        
        This method is called within the scale methods of the Room2D.
        
        Args:
            factor: A number representing how much the object should be scaled.
        """
        return RepeatingWindowRatio(
            self.window_ratio, self.window_height * factor, self.sill_height * factor,
            self.horizontal_separation * factor, self.vertical_separation * factor)

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

        .. code-block:: json

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

    def to_dict(self):
        """Get RepeatingWindowRatio as a dictionary."""
        base = {'type': 'RepeatingWindowRatio',
                'window_ratio': self.window_ratio,
                'window_height': self.window_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 RepeatingWindowRatio(
            self._window_ratio, self._window_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._window_ratio, self._window_height, self._sill_height,
                self._horizontal_separation, self._vertical_separation)

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

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

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

    def __repr__(self):
        return 'RepeatingWindowRatio:\n ratio: {}\n window_height: {}\n sill_height:' \
            ' {}\n horizontal: {}\n vertical: {}'.format(
                self._window_ratio, self.window_height, self.sill_height,
                self.horizontal_separation, self.vertical_separation)
