"""Functions to bake entire Ladybug objects into the Rhino scene.

The methods here are intended to help translate groups of geometry that are
commonly generated by several objects in Ladybug core (ie. legends, compasses,
visualization sets, etc.)
"""
import json

from ladybug_geometry.geometry2d import Vector2D, Point2D, Ray2D, LineSegment2D, \
    Arc2D, Polyline2D, Polygon2D, Mesh2D
from ladybug_geometry.geometry3d import Vector3D, Point3D, Ray3D, LineSegment3D, \
    Arc3D, Polyline3D, Plane, Mesh3D, Face3D, Polyface3D, Sphere, Cone, Cylinder
from ladybug.graphic import GraphicContainer
from ladybug_display.geometry2d import DisplayVector2D, DisplayPoint2D, \
    DisplayRay2D, DisplayLineSegment2D, DisplayPolyline2D, DisplayArc2D, \
    DisplayPolygon2D, DisplayMesh2D
from ladybug_display.geometry3d import DisplayVector3D, DisplayPoint3D, \
    DisplayRay3D, DisplayPlane, DisplayLineSegment3D, DisplayPolyline3D, DisplayArc3D, \
    DisplayFace3D, DisplayMesh3D, DisplayPolyface3D, DisplaySphere, DisplayCone, \
    DisplayCylinder, DisplayText3D
from ladybug_display.visualization import AnalysisGeometry

from .config import units_system
from .color import color_to_color
from .bakegeometry import bake_point2d, bake_vector2d, bake_ray2d, \
    bake_linesegment2d, bake_arc2d, bake_polygon2d, bake_polyline2d, bake_mesh2d, \
    bake_point3d, bake_vector3d, bake_ray3d, bake_linesegment3d, bake_plane, \
    bake_arc3d, bake_polyline3d, bake_mesh3d, bake_face3d, bake_polyface3d, \
    bake_sphere, bake_cone, bake_cylinder, _get_layer, _get_attributes
from .bakedisplay import bake_display_point2d, bake_display_vector2d, \
    bake_display_ray2d, bake_display_linesegment2d, bake_display_arc2d, \
    bake_display_polygon2d, bake_display_polyline2d, bake_display_mesh2d, \
    bake_display_text3d, bake_display_vector3d, bake_display_point3d, \
    bake_display_ray3d, bake_display_linesegment3d, bake_display_arc3d, \
    bake_display_polyline3d, bake_display_plane, bake_display_mesh3d, \
    bake_display_face3d, bake_display_polyface3d, bake_display_sphere, \
    bake_display_cone, bake_display_cylinder

try:
    import System
except ImportError as e:
    raise ImportError("Failed to import Windows/.NET libraries\n{}".format(e))

try:
    import Rhino.DocObjects as docobj
    import Rhino.RhinoDoc as rhdoc
    doc = rhdoc.ActiveDoc
except ImportError as e:
    raise ImportError("Failed to import Rhino document attributes.\n{}".format(e))

try:
    import Grasshopper as gh
except ImportError:
    print('Failed to import Grasshopper.Grasshopper Baking disabled.')
    gh = None

BAKE_MAPPER = {
    Vector2D: bake_vector2d,
    Point2D: bake_point2d,
    Ray2D: bake_ray2d,
    LineSegment2D: bake_linesegment2d,
    Arc2D: bake_arc2d,
    Polyline2D: bake_polyline2d,
    Polygon2D: bake_polygon2d,
    Mesh2D: bake_mesh2d,
    Vector3D: bake_vector3d,
    Point3D: bake_point3d,
    Ray3D: bake_ray3d,
    LineSegment3D: bake_linesegment3d,
    Arc3D: bake_arc3d,
    Polyline3D: bake_polyline3d,
    Plane: bake_plane,
    Mesh3D: bake_mesh3d,
    Face3D: bake_face3d,
    Polyface3D: bake_polyface3d,
    Sphere: bake_sphere,
    Cone: bake_cone,
    Cylinder: bake_cylinder,
    DisplayVector2D: bake_display_vector2d,
    DisplayPoint2D: bake_display_point2d,
    DisplayRay2D: bake_display_ray2d,
    DisplayLineSegment2D: bake_display_linesegment2d,
    DisplayPolyline2D: bake_display_polyline2d,
    DisplayArc2D: bake_display_arc2d,
    DisplayPolygon2D: bake_display_polygon2d,
    DisplayMesh2D: bake_display_mesh2d,
    DisplayVector3D: bake_display_vector3d,
    DisplayPoint3D: bake_display_point3d,
    DisplayRay3D: bake_display_ray3d,
    DisplayPlane: bake_display_plane,
    DisplayLineSegment3D: bake_display_linesegment3d,
    DisplayPolyline3D: bake_display_polyline3d,
    DisplayArc3D: bake_display_arc3d,
    DisplayFace3D: bake_display_face3d,
    DisplayMesh3D: bake_display_mesh3d,
    DisplayPolyface3D: bake_display_polyface3d,
    DisplaySphere: bake_display_sphere,
    DisplayCone: bake_display_cone,
    DisplayCylinder: bake_display_cylinder,
    DisplayText3D: bake_display_text3d
}


def bake_legend(legend, layer_name=None):
    """Add a Ladybug Legend object to the Rhino scene.

    Args:
        legend: A Ladybug Legend object to be added to the Rhino scene.
        layer_name: Optional text string for the layer name on which to place the
            legend. If None, text will be added to the current layer.

    Returns:
        A list of IDs that point to the objects in the Rhino scene in the following
        order:

        -   legend_mesh -- A colored mesh for the legend.

        -   legend_title -- A text object for the legend title.

        -   legend_text -- Text objects for the rest of the legend text.
    """
    # bake the legend mesh
    legend_mesh = bake_mesh3d(legend.segment_mesh, layer_name)
    # translate the legend text
    _height = legend.legend_parameters.text_height
    _font = legend.legend_parameters.font
    if legend.legend_parameters.continuous_legend is False:
        legend_text = [
            DisplayText3D(txt, loc, _height, None, _font, 'Left', 'Bottom')
            for txt, loc in zip(legend.segment_text, legend.segment_text_location)]
    elif legend.legend_parameters.vertical is True:
        legend_text = [
            DisplayText3D(txt, loc, _height, None, _font, 'Left', 'Center')
            for txt, loc in zip(legend.segment_text, legend.segment_text_location)]
    else:
        legend_text = [
            DisplayText3D(txt, loc, _height, None, _font, 'Center', 'Bottom')
            for txt, loc in zip(legend.segment_text, legend.segment_text_location)]
    legend_title = DisplayText3D(
        legend.title, legend.title_location, _height, None, _font)
    legend_text.insert(0, legend_title)
    # bake the text objects
    legend_text_guids = []
    for txt_obj in legend_text:
        legend_text_guids.append(bake_display_text3d(txt_obj, layer_name))
    return [legend_mesh] + legend_text_guids


def bake_analysis(analysis, layer_name=None, bake_3d_legend=False,
                  min_point=None, max_point=None):
    """Add a Ladybug Display AnalysisGeometry object to the Rhino scene.

    Args:
        analysis: A Ladybug Display AnalysisGeometry object to be added to
            the Rhino scene.
        layer_name: Optional text string for the parent layer name on which to
            place the AnalysisGeometry. The actual layer of the context will
            always have a name that aligns with the AnalysisGeometry.display_name.
        bake_3d_legend: A Boolean to note whether the AnalysisGeometry should
            be baked with 3D legends for any AnalysisGeometries it
            includes. (Default: False).
        min_point: An optional Point3D to override the default min point
            that are used to generate the legend. (Default: None).
        max_point: An optional Point3D to override the default max point
            that are used to generate the legend. (Default: None).

    Returns:
        A list of IDs that point to the objects in the Rhino scene.
    """
    # get attributes corresponding to the layer
    layer_name = analysis.display_name if layer_name is None else \
        '{}::{}'.format(layer_name, analysis.display_name)
    min_pt = analysis.min_point if min_point is None else min_point
    max_pt = analysis.max_point if max_point is None else max_point
    # generate the colors that correspond to the values
    obj_ids = []
    for i, data in enumerate(analysis.data_sets):
        # get properties used for all analysis geometries
        objs_to_group = []
        graphic = GraphicContainer(
            data.values, min_pt, max_pt,
            data.legend_parameters, data.data_type, data.unit)
        colors = graphic.value_colors
        sub_layer_name = layer_name if data.data_type is None else \
            '{}::{}'.format(layer_name, data.data_type.name)
        layer_index = _get_layer(sub_layer_name)
        # translate the analysis geometry using the matching method
        if analysis.matching_method == 'faces':
            c_count = 0
            for mesh in analysis.geometry:
                mesh.colors = colors[c_count:c_count + len(mesh.faces)]
                c_count += len(mesh.faces)
                bake_func = bake_mesh3d if isinstance(mesh, Mesh3D) else bake_mesh2d
                objs_to_group.append(bake_func(mesh, layer_name=layer_index))
        elif analysis.matching_method == 'vertices':
            c_count = 0
            for mesh in analysis.geometry:
                mesh.colors = colors[c_count:c_count + len(mesh.vertices)]
                c_count += len(mesh.vertices)
                bake_func = bake_mesh3d if isinstance(mesh, Mesh3D) else bake_mesh2d
                objs_to_group.append(bake_func(mesh, layer_name=layer_index))
        else:  # one color per geometry object
            bake_func = BAKE_MAPPER[analysis.geometry[0].__class__]
            for geo_obj, col in zip(analysis.geometry, colors):
                attrib = _get_attributes(layer_index)
                attrib.ColorSource = docobj.ObjectColorSource.ColorFromObject
                attrib.ObjectColor = color_to_color(col)
                objs_to_group.append(bake_func(geo_obj, attributes=attrib))
        # group the objects, and add JSON of values to layer user data
        group_table = doc.Groups  # group table
        group_table.Add(sub_layer_name, objs_to_group)
        layer_table = doc.Layers  # layer table
        layer_obj = layer_table[layer_index]
        layer_obj.UserDictionary.Set('vis_data', json.dumps(data.to_dict()))
        layer_obj.UserDictionary.Set('guids', System.Array[System.Guid](objs_to_group))
        if i != analysis.active_data:  # hide the inactive data layer
            layer_obj.IsVisible = False
        # add geometry to the global list and bake the legend if requested
        obj_ids.extend(objs_to_group)
        if bake_3d_legend:
            obj_ids.extend(bake_legend(graphic.legend, layer_index))
    # hide the layer if it is hidden
    if analysis.hidden:
        layer_table = doc.Layers  # layer table
        layer_index = _get_layer(layer_name)
        layer_obj = layer_table[layer_index]
        layer_obj.IsVisible = False
    return obj_ids


def bake_context(context, layer_name=None):
    """Add a Ladybug Display ContextGeometry object to the Rhino scene.

    Args:
        context: A Ladybug Display ContextGeometry object to be added to
            the Rhino scene.
        layer_name: Optional text string for the parent layer name on which to
            place the ContextGeometry. The actual layer of the context will
            always have a name that aligns with the ContextGeometry.display_name.

    Returns:
        A list of IDs that point to the objects in the Rhino scene.
    """
    # get attributes corresponding to the layer
    layer_name = context.display_name if layer_name is None else \
        '{}::{}'.format(layer_name, context.display_name)
    layer_index = _get_layer(layer_name)
    # hide the layer if it is hidden
    if context.hidden:
        layer_table = doc.Layers  # layer table
        layer_obj = layer_table[layer_index]
        layer_obj.IsVisible = False
    # loop through the objects and add them to the scene
    obj_ids = []
    for geo_obj in context.geometry:
        bake_func = BAKE_MAPPER[geo_obj.__class__]
        obj_ids.append(bake_func(geo_obj, layer_index))
    return obj_ids


def bake_visualization_set(vis_set, bake_3d_legend=False):
    """Add a Ladybug Display VisualizationSet object to the Rhino scene.

    Args:
        context_geometry: A Ladybug VisualizationSet object to be added to
            the Rhino scene.
        bake_3d_legend: A Boolean to note whether the VisualizationSet should
            be baked with 3D legends for any AnalysisGeometries it
            includes. (Default: False).

    Returns:
        A list of IDs that point to the objects in the Rhino scene.
    """
    # convert the visualization set to model units if necessary
    units_sys = units_system()
    if vis_set.units is not None and vis_set.units != units_sys:
        vis_set.convert_to_units(units_sys)
    # bake all of the geometries
    obj_ids = []
    for geo in vis_set.geometry:
        if isinstance(geo, AnalysisGeometry):  # translate it as AnalysisGeometry
            a_objs = bake_analysis(
                geo, vis_set.display_name, bake_3d_legend,
                vis_set.min_point, vis_set.max_point)
            obj_ids.extend(a_objs)
        else:  # translate it as ContextGeometry
            obj_ids.extend(bake_context(geo, vis_set.display_name))
    return obj_ids


class VisSetGoo(gh.Kernel.IGH_BakeAwareData):
    """A Bake-able version of the VisualizationSet for Grasshopper.

    Args:
        visualization_set: A Ladybug Display VisualizationSet object to be bake-able
            in the Rhino scene.
    """

    def __init__(self, visualization_set):
        self.vis_set = visualization_set

    def BakeGeometry(self, doc, att, id):
        try:
            if self.vis_set is not None:
                guids = bake_visualization_set(self.vis_set, True)
                return True, guids
        except Exception as e:
            System.Windows.Forms.MessageBox.Show(str(e), 'script error')
            return False, System.Guid.Empty

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

    def __repr__(self):
        return str(self.vis_set)
