# coding: utf-8
"""Window Parameters with instructions for generating windows."""
from __future__ import division
import math

from honeybee.typing import float_in_range, float_positive
from honeybee.boundarycondition import Surface
from honeybee.aperture import Aperture

from ladybug_geometry.geometry2d.pointvector import Point2D, Vector2D
from ladybug_geometry.geometry2d.polygon import Polygon2D
from ladybug_geometry.geometry3d.pointvector import Vector3D, Point3D
from ladybug_geometry.geometry3d.line import LineSegment3D
from ladybug_geometry.geometry3d.plane import Plane
from ladybug_geometry.geometry3d.face import Face3D

import sys
if (sys.version_info < (3, 0)):  # python 2
    from itertools import izip as zip  # python 2


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.01):
        """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:: python

            {
            "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.

    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 ribbon 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.

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

    def __init__(self, width, height, sill_height=1):
        """Initialize SingleWindow."""
        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.01):
        """Add Apertures to a Honeybee Face using these Window Parameters.

        Args:
            face: A honeybee-core Face object.
            tolerance: Optional tolerance value. Default: 0.01, suitable for
                objects in meters.
        """
        if self._width == 0 or self._height == 0:
            return None
        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):
                ids = face._boundary_condition.boundary_condition_objects
                adj_ap_id = '{}_Glz1'.format(ids[0])
                final_ids = (adj_ap_id,) + ids
                face.apertures[0].boundary_condition = Surface(final_ids, 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:: python

            {
            "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: [width: {}] [height: {}] [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

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

    def __init__(self, window_ratio):
        """Initialize SimpleWindowRatio."""
        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.01):
        """Add Apertures to a Honeybee Face using these Window Parameters.

        Args:
            face: A honeybee-core Face object.
            tolerance: Optional tolerance value. Default: 0.01, suitable for
                objects in meters.
        """
        if self._window_ratio == 0:
            return None
        scale_factor = self.window_ratio ** .5
        ap_face = face.geometry.scale(scale_factor, face.geometry.center)
        aperture = Aperture('{}_Glz1'.format(face.identifier), ap_face)
        aperture._parent = face
        face.add_aperture(aperture)
        # if the Aperture is interior, set adjacent boundary condition
        if isinstance(face._boundary_condition, Surface):
            ids = face._boundary_condition.boundary_condition_objects
            adj_ap_id = '{}_Glz1'.format(ids[0])
            final_ids = (adj_ap_id,) + ids
            aperture.boundary_condition = Surface(final_ids, True)

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

        .. code-block:: python

            {
            "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: [ratio: {}]'.format(self.window_ratio)


class RepeatingWindowRatio(SimpleWindowRatio):
    """Instructions for repeating windows derived 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.

    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):
        """Initialize RepeatingWindowRatio."""
        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.01):
        """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. Default: 0.01, suitable for
                objects in meters.
        """
        if self._window_ratio == 0:
            return None
        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):
                ids = face._boundary_condition.boundary_condition_objects
                adj_ap_id = '{}_Glz{}'.format(ids[0], num_aps - i - 1)
                final_ids = (adj_ap_id,) + ids
                ap.boundary_condition = Surface(final_ids, 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:: python

            {
            "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: [ratio: {}] [window_height: {}] [sill_height:' \
            ' {}] [horizontal: {}] [vertical: {}]'.format(
                self._window_ratio, self.window_height, self.sill_height,
                self.horizontal_separation, self.vertical_separation)


class RepeatingWindowWidthHeight(_WindowParameterBase):
    """Instructions for repeating rectangular windows of a fixed width and height.

    This class effectively fills a wall with windows at the specified width, height
    and separation.

    Args:
        window_height: A number for the target height of the windows.
            Note that, if the window_height is larger than the height of the wall,
            the generated windows will have a height equal to the wall height
            in order to avoid having windows extend outside the wall face.
        window_width: A number for the target width of the windows.
            Note that, if the window_width is larger than the width of the wall,
            the generated windows will have a width equal to the wall width
            in order to avoid having windows extend outside the wall face.
        sill_height: A number for the target height above the bottom edge of
            the wall to start the windows. If the window_height
            is too large for the sill_height to fit within the rectangle,
            the window_height will take precedence.
        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.

    Properties:
        * window_height
        * window_width
        * sill_height
        * horizontal_separation
    """
    __slots__ = ('_window_height', '_window_width', '_sill_height',
                 '_horizontal_separation')

    def __init__(self, window_height, window_width, sill_height, horizontal_separation):
        """Initialize RepeatingWindowWidthHeight."""
        self._window_height = float_positive(window_height, 'window height')
        self._window_width = float_positive(window_width, 'window width')
        self._sill_height = float_positive(sill_height, 'sill height')
        self._horizontal_separation = \
            float_positive(horizontal_separation, 'window horizontal separation')

    @property
    def window_height(self):
        """Get a number or the target height of the windows."""
        return self._window_height

    @property
    def window_width(self):
        """Get a number or the target width of the windows."""
        return self._window_width

    @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

    def add_window_to_face(self, face, tolerance=0.01):
        """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. Default: 0.01, suitable for
                objects in meters.
        """
        if self._window_width == 0 or self._window_height == 0:
            return None
        face.apertures_by_width_height_rectangle(
            self.window_height, self.window_width, self.sill_height,
            self.horizontal_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):
                ids = face._boundary_condition.boundary_condition_objects
                adj_ap_id = '{}_Glz{}'.format(ids[0], num_aps - i - 1)
                final_ids = (adj_ap_id,) + ids
                ap.boundary_condition = Surface(final_ids, 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 RepeatingWindowWidthHeight(
            self.window_height * factor, self.window_width * factor,
            self.sill_height * factor, self.horizontal_separation * factor)

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

        .. code-block:: python

            {
            "type": "RepeatingWindowWidthHeight",
            "window_height": 2,
            "window_width": 1.5,
            "sill_height": 0.8,
            "horizontal_separation": 4
            }
        """
        assert data['type'] == 'RepeatingWindowWidthHeight', 'Expected ' \
            'RepeatingWindowWidthHeight dictionary. Got {}.'.format(data['type'])
        return cls(data['window_height'], data['window_width'], data['sill_height'],
                   data['horizontal_separation'])

    def to_dict(self):
        """Get RepeatingWindowWidthHeight as a dictionary."""
        return {'type': 'RepeatingWindowWidthHeight',
                'window_height': self.window_height,
                'window_width': self.window_width,
                'sill_height': self.sill_height,
                'horizontal_separation': self.horizontal_separation}

    def __copy__(self):
        return RepeatingWindowWidthHeight(
            self._window_height, self._window_width, self._sill_height,
            self._horizontal_separation)

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

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

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

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

    def __repr__(self):
        return 'RepeatingWindowWidthHeight: [window_height: {}] [window_width: ' \
            '{}] [sill_height: {}] [horizontal: {}]'.format(
                self.window_height, self.window_width, self.sill_height,
                self.horizontal_separation)


class _AsymmetricBase(_WindowParameterBase):
    """Base class for WindowParameters that can make asymmetric windows on a wall.
    """

    def flip(self, seg_length):
        """Flip the direction of the windows along a wall segment.

        This is needed since windows can exist asymmetrically across the wall
        segment and operations like reflecting the Room2D across a plane will
        require the window parameters to be flipped. Reversing the Room2D vertices
        also requires flipping.

        Args:
            seg_length: The length of the segment along which the parameters are
                being flipped.
        """
        return self


class RectangularWindows(_AsymmetricBase):
    """Instructions for several rectangular windows, defined by origin, width and height.

    Note that, if these parameters are applied to a base wall that is too short
    or too narrow such that the windows fall outside the boundary of the wall, the
    generated windows will automatically be shortened or excluded. This way, a
    certain pattern of repeating rectangular windows can be encoded in a single
    RectangularWindows instance and applied to multiple Room2D segments.

    Args:
        origins: An array of ladybug_geometry Point2D objects within the plane
            of the wall for the origin of each window. The wall plane is assumed
            to have an origin at the first point of the wall segment and an
            X-axis extending along the length of the segment. The wall plane's
            Y-axis always points upwards.  Therefore, both X and Y values of
            each origin point should be positive.
        widths: An array of positive numbers for the window widths. The length
            of this list must match the length of the origins.
        heights: An array of positive numbers for the window heights. The length
            of this list must match the length of the origins.

    Properties:
        * origins
        * widths
        * heights
    """
    __slots__ = ('_origins', '_widths', '_heights')

    def __init__(self, origins, widths, heights):
        """Initialize RectangularWindows."""
        if not isinstance(origins, tuple):
            origins = tuple(origins)
        for point in origins:
            assert isinstance(point, Point2D), \
                'Expected Point2D for window origin. Got {}'.format(type(point))
        self._origins = origins

        self._widths = tuple(float_positive(width, 'window width') for width in widths)
        self._heights = tuple(float_positive(hgt, 'window height') for hgt in heights)

        assert len(self._origins) == len(self._widths) == len(self._heights), \
            'Number of window origins, widths, and heights must match.'
        assert len(self._origins) != 0, \
            'There must be at least one window to use RectangularWindows.'

    @property
    def origins(self):
        """Get an array of Point2Ds within the wall plane for the origin of each window.
        """
        return self._origins

    @property
    def widths(self):
        """Get an array of numbers for the window widths."""
        return self._widths

    @property
    def heights(self):
        """Get an array of numbers for the window heights."""
        return self._heights

    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.999
        max_height = floor_to_ceiling_height * 0.999

        areas = []
        for o, width, height in zip(self.origins, self.widths, self.heights):
            final_width = max_width - o.x if width + o.x > max_width else width
            final_height = max_height - o.y if height + o.y > max_height else height
            if final_height > 0 and final_height > 0:  # inside wall boundary
                areas.append(final_width * final_height)

        return sum(areas)

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

        Args:
            face: A honeybee-core Face object.
            tolerance: Optional tolerance value. Default: 0.01, suitable for
                objects in meters.
        """
        # collect the global properties of the face that set limits on apertures
        wall_plane = face.geometry.plane
        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

        # loop through each window and create its geometry
        for i, (o, wid, hgt) in enumerate(zip(self.origins, self.widths, self.heights)):
            final_width = max_width - o.x if wid + o.x > max_width else wid
            final_height = max_height - o.y if hgt + o.y > max_height else hgt
            if final_height > 0 and final_height > 0:  # inside wall boundary
                base_plane = Plane(wall_plane.n, wall_plane.xy_to_xyz(o), wall_plane.x)
                ap_face = Face3D.from_rectangle(final_width, final_height, base_plane)
                aperture = Aperture('{}_Glz{}'.format(face.identifier, i + 1), ap_face)
                aperture._parent = face
                face.add_aperture(aperture)
                # if the Aperture is interior, set adjacent boundary condition
                if isinstance(face._boundary_condition, Surface):
                    ids = face._boundary_condition.boundary_condition_objects
                    adj_ap_id = '{}_Glz{}'.format(ids[0], i + 1)
                    final_ids = (adj_ap_id,) + ids
                    aperture.boundary_condition = Surface(final_ids, 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 RectangularWindows(
            tuple(Point2D(pt.x * factor, pt.y * factor) for pt in self.origins),
            tuple(width * factor for width in self.widths),
            tuple(height * factor for height in self.heights))

    def flip(self, seg_length):
        """Flip the direction of the windows along a segment.

        This is needed since windows can exist asymmetrically across the wall
        segment and operations like reflecting the Room2D across a plane will
        require the window parameters to be flipped to remain in the same place.

        Args:
            seg_length: The length of the segment along which the parameters are
                being flipped.
        """
        new_origins = []
        new_widths = []
        new_heights = []
        for o, width, height in zip(self.origins, self.widths, self.heights):
            new_x = seg_length - o.x - width
            if new_x > 0:  # fully within the wall boundary
                new_origins.append(Point2D(new_x, o.y))
                new_widths.append(width)
                new_heights.append(height)
            elif new_x + width > seg_length * 0.001:  # partially within the boundary
                new_widths.append(width + new_x - (seg_length * 0.001))
                new_x = seg_length * 0.001
                new_origins.append(Point2D(new_x, o.y))
                new_heights.append(height)
        return RectangularWindows(new_origins, new_widths, new_heights)

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

        .. code-block:: python

            {
            "type": "RectangularWindows",
            "origins": [(1, 1), (3, 0.5)],  # array of (x, y) floats in wall plane
            "widths": [1, 3],  # array of floats for window widths
            "heights": [1, 2.5]  # array of floats for window heights
            }
        """
        assert data['type'] == 'RectangularWindows', \
            'Expected RectangularWindows. Got {}.'.format(data['type'])
        return cls(tuple(Point2D.from_array(pt) for pt in data['origins']),
                   data['widths'], data['heights'])

    def to_dict(self):
        """Get RectangularWindows as a dictionary."""
        return {'type': 'RectangularWindows',
                'origins': [pt.to_array() for pt in self.origins],
                'widths': self.widths,
                'heights': self.heights
                }

    def __copy__(self):
        return RectangularWindows(self.origins, self.widths, self.heights)

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

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

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

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

    def __repr__(self):
        return 'RectangularWindows: [{} windows]'.format(len(self.origins))


class DetailedWindows(_AsymmetricBase):
    """Instructions for detailed windows, defined by 2D Polygons (lists of 2D vertices).

    Note that these parameters are intended to represent windows that are specific
    to a particular segment and, unlike the other WindowParameters, this class
    performs no automatic checks to ensure that the windows lie within the
    boundary of the wall they have been assigned to.

    Args:
        polygons: An array of ladybug_geometry Polygon2D objects within the plane
            of the wall with one polygon for each window. The wall plane is
            assumed to have an origin at the first point of the wall segment
            and an X-axis extending along the length of the segment. The wall
            plane's Y-axis always points upwards.  Therefore, both X and Y
            values of each point in the polygon should always be positive.

    Properties:
        * polygons

    Usage:
        Note that, if you are starting from 3D vertices of windows, you can
        use this class to represent them. Below is some sample code to convert from
        vertices in the same 3D space as a vertical wall to vertices in the 2D
        plane of the wall (as this class interprets it).

        In the code, 'seg_p1' is the first point of a given wall segment and
        is assumed to be the origin of the wall plane. 'seg_p2' is the second
        point of the wall segment, and 'vertex' is a given vertex of the
        window that you want to translate from 3D coordinates into 2D. All
        input points are presented as arrays of 3 floats and the output is
        an array of 2 (x, y) coordinates.

    .. code-block:: python

        def dot_product(v1, v2):
            return v1[0] * v2[0] + v1[1] * v2[1] + v1[2] * v2[3]

        def normalize(v):
            d = (v[0] ** 2 + v[1] ** 2 + v[2] ** 2) ** 0.5
            return (v[0] / d, v[1] / d, v[2] / d)

        def xyz_to_xy(seg_p1, seg_p2, vertex):
            diff = (vertex[0] - seg_p1[0], vertex[1] - seg_p1[1], vertex[2] - seg_p1[2])
            axis = (seg_p2[0] - seg_p1[0], seg_p2[1] - seg_p1[1], seg_p2[2] - seg_p1[2])
            plane_x = normalize(axis)
            plane_y = (0, 0, 1)
            return (dot_product(plane_x , diff), dot_product(plane_y, diff))
    """
    __slots__ = ('_polygons',)

    def __init__(self, polygons):
        """Initialize DetailedWindows."""
        if not isinstance(polygons, tuple):
            polygons = tuple(polygons)
        for polygon in polygons:
            assert isinstance(polygon, Polygon2D), \
                'Expected Polygon2D for window polygon. Got {}'.format(type(polygon))
        assert len(polygons) != 0, \
            'There must be at least one polygon to use DetailedWindows.'
        self._polygons = polygons

    @classmethod
    def from_face3ds(cls, face3ds, segment):
        """Create DetailedWindows from Face3Ds and a segment they are assigned to.

        Args:
            face3ds: A list of Face3D objects for the detailed windows.
            segment: A LineSegment3D that sets the plane in which 3D vertices
                are located.
        """
        plane = Plane(Vector3D(segment.v.y, -segment.v.x, 0), segment.p, segment.v)
        pt3d = tuple(tuple(pt for pt in face.vertices) for face in face3ds)
        return cls(tuple(Polygon2D(tuple(plane.xyz_to_xy(pt) for pt in poly))
                         for poly in pt3d))

    @property
    def polygons(self):
        """Get an array of Polygon2Ds with one polygon for each window."""
        return self._polygons

    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 sum(polygon.area for polygon in self._polygons)

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

        Args:
            face: A honeybee-core Face object.
            tolerance: Optional tolerance value. Default: 0.01, suitable for
                objects in meters.
        """
        wall_plane = face.geometry.plane
        wall_plane = wall_plane.rotate(wall_plane.n, math.pi, wall_plane.o)

        # loop through each window and create its geometry
        for i, polygon in enumerate(self.polygons):
            pt3d = tuple(wall_plane.xy_to_xyz(pt) for pt in polygon)
            aperture = Aperture('{}_Glz{}'.format(face.identifier, i + 1), Face3D(pt3d))
            aperture._parent = face
            face.add_aperture(aperture)
            # if the Aperture is interior, set adjacent boundary condition
            if isinstance(face._boundary_condition, Surface):
                ids = face._boundary_condition.boundary_condition_objects
                adj_ap_id = '{}_Glz{}'.format(ids[0], i + 1)
                final_ids = (adj_ap_id,) + ids
                aperture.boundary_condition = Surface(final_ids, 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 DetailedWindows(
            tuple(polygon.scale(factor) for polygon in self.polygons))

    def flip(self, seg_length):
        """Flip the direction of the windows along a segment.

        This is needed since windows can exist asymmetrically across the wall
        segment and operations like reflecting the Room2D across a plane will
        require the window parameters to be flipped to remain in the same place.

        Args:
            seg_length: The length of the segment along which the parameters are
                being flipped.
        """
        # set values derived from the property of the segment
        normal = Vector2D(1, 0)
        origin = Point2D(seg_length / 2, 0)

        # loop through the polygons and reflect them across the midplane of the wall
        new_polygons = []
        for polygon in self.polygons:
            new_verts = tuple(pt.reflect(normal, origin) for pt in polygon.vertices)
            new_polygons.append(Polygon2D(tuple(reversed(new_verts))))
        return DetailedWindows(new_polygons)

    @classmethod
    def from_dict(cls, data, segment=None):
        """Create DetailedWindows from a dictionary.

        Args:
            data: A dictionary in the format below. The vertices of the "polygons"
                can either contain 2 values (indicating they are vertices within
                the plane of a parent wall segment) or they can contain 3 values
                (indicating they are 3D world coordinates). If 3 values are used,
                a segment must be specified below to convert them to the 2D format.
            segment: A LineSegment3D that sets the plane in which 3D vertices
                are located.

        .. code-block:: python

            {
            "type": "DetailedWindows",
            "polygons": [((0.5, 0.5), (2, 0.5), (2, 2), (0.5, 2)),
                         ((3, 1), (4, 1), (4, 2))]
            }
        """
        assert data['type'] == 'DetailedWindows', \
            'Expected DetailedWindows dictionary. Got {}.'.format(data['type'])
        if len(data['polygons'][0][0]) == 2:
            return cls(tuple(Polygon2D(tuple(Point2D.from_array(pt) for pt in poly))
                             for poly in data['polygons']))
        else:
            assert segment is not None, 'Segment must be specified when using 3D ' \
                'vertices for DetailedWindows.'
            plane = Plane(Vector3D(segment.v.y, -segment.v.x, 0), segment.p, segment.v)
            pt3d = tuple(tuple(Point3D.from_array(pt) for pt in poly)
                               for poly in data['polygons'])
            return cls(tuple(Polygon2D(tuple(plane.xyz_to_xy(pt) for pt in poly))
                             for poly in pt3d))

    def to_dict(self):
        """Get DetailedWindows as a dictionary."""
        return {
            'type': 'DetailedWindows',
            'polygons': [[pt.to_array() for pt in poly] for poly in self.polygons]
        }

    @staticmethod
    def is_face3d_in_segment_plane(
            face3d, segment, height, tolerance=0.01, angle_tolerance=1):
        """Check if a given Face3D is in the plane and range of a LineSegment3D.

        Args:
            face3d: A list of Face3D objects for the detailed windows.
            segment: A LineSegment3D that sets the plane in which 3D vertices
                are located.
            height: A number for the height of the 
            tolerance: The minimum difference between the coordinate values of two
                vertices at which they can be considered equivalent. Default: 0.01,
                suitable for objects in meters.
            angle_tolerance: The max angle in degrees that the plane normals can
                differ from one another in order for them to be considered coplanar.
                Default: 1 degree.
        
        Returns:
            True if the face3d is in the plane and range of the segment. False
            if it is not.
        """
        plane = Plane(Vector3D(segment.v.y, -segment.v.x, 0), segment.p, segment.v)
        # test whether the face3d is coplanar
        if not plane.is_coplanar_tolerance(face3d.plane, tolerance, angle_tolerance):
            return False
        seg_face = Face3D.from_rectangle(segment.length, height, plane)
        return seg_face.is_sub_face(face3d, tolerance, angle_tolerance)

    def __copy__(self):
        return DetailedWindows(self._polygons)

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

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

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

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

    def __repr__(self):
        return 'DetailedWindows: [{} windows]'.format(len(self._polygons))
