#!/usr/bin/env python
# -*- coding: utf-8 -*-

# ***************************************************************************
# *                                                                         *
# *   Copyright (c) 2016 execuc                                             *
# *                                                                         *
# *   This file is part of LCInterlocking module.                           *
# *   LCInterlocking module is free software; you can redistribute it and/or*
# *   modify it under the terms of the GNU Lesser General Public            *
# *   License as published by the Free Software Foundation; either          *
# *   version 2.1 of the License, or (at your option) any later version.    *
# *                                                                         *
# *   This module is distributed in the hope that it will be useful,       *
# *   but WITHOUT ANY WARRANTY; without even the implied warranty of        *
# *   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU     *
# *   Lesser General Public License for more details.                       *
# *                                                                         *
# *   You should have received a copy of the GNU Lesser General Public      *
# *   License along with this library; if not, write to the Free Software   *
# *   Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,            *
# *   MA  02110-1301  USA                                                   *
# *                                                                         *
# ***************************************************************************

import Part
import FreeCAD
import math
from lasercut.helper import transform, compare_freecad_vector, compare_value, Segment, assemble_list_element


def complete_hinges_properties(hinge, face_1, face_2, storeAll = False):
    edge1, edge2, extrusion_vector = get_coplanar_edge(face_1, face_2)
    seg_face_1, seg_face_2 = get_segment_from_edge(edge1, edge2)
    intersection_point = do_intersection(seg_face_1,seg_face_2)

    #print "intersection point : => " + str(intersection_point)
    #print "seg1 " +str(seg_face_1)
    #print "seg2 " +str(seg_face_2)

    diff_length_test = compare_value(intersection_point.sub(seg_face_1.B).Length,
                                     intersection_point.sub(seg_face_2.B).Length,
                                     10e-3)
    if diff_length_test is False :
        raise ValueError("Not an arc %f %f" %(intersection_point.sub(seg_face_1.B).Length, intersection_point.sub(seg_face_2.B).Length))
    inner_arc_radius = intersection_point.sub(seg_face_1.B).Length
    outer_arc_radius = intersection_point.sub(seg_face_1.A).Length
    mid_arc_radius = intersection_point.sub(seg_face_1.mid_point()).Length

    mid_point_b = seg_face_1.B.add(seg_face_2.B)
    mid_point_b.scale(0.5, 0.5, 0.5)
    dir_mid_point = mid_point_b.sub(intersection_point)
    dir_mid_point.normalize()

    inner_arc_point = dir_mid_point * inner_arc_radius
    outter_arc_point = dir_mid_point * outer_arc_radius
    if hinge.reversed_angle:
        inner_arc_point = intersection_point.sub(inner_arc_point)
        outter_arc_point = intersection_point.sub(outter_arc_point)
    else:    
        inner_arc_point = inner_arc_point.add(intersection_point)
        outter_arc_point = outter_arc_point.add(intersection_point)
    arc_middle_segment = Segment(outter_arc_point, inner_arc_point)
    
    hinge.seg_face_1 = seg_face_1
    hinge.seg_face_2 = seg_face_2
    hinge.arc_middle_segment = arc_middle_segment
    hinge.extrustion_vector = extrusion_vector
    hinge.rad_angle = seg_face_1.get_angle(seg_face_2)
    if hinge.reversed_angle:
        hinge.rad_angle= math.pi*2 - hinge.rad_angle
    hinge.deg_angle = hinge.rad_angle * 180. / math.pi 
    hinge.rotation_vector = seg_face_1.vector().cross(seg_face_2.vector())
    if hinge.reversed_angle:
        hinge.rotation_vector*=-1
    hinge.arc_length = mid_arc_radius * hinge.rad_angle
    hinge.arc_inner_radius = inner_arc_radius
    hinge.arc_outer_radius = outer_arc_radius
    hinge.thickness = seg_face_1.length()

    if storeAll is False:
        hinge.arc_middle_segment = None
        hinge.extrustion_vector = None
        hinge.seg_face_1 = None
        hinge.seg_face_2 = None
        hinge.rotation_vector = None

    return True


def create_solid_corner(hinge):
    inner_arc_point = hinge.arc_middle_segment.B
    outter_arc_point = hinge.arc_middle_segment.A

    l1 = Part.makeLine(hinge.seg_face_1.A, hinge.seg_face_1.B)
    a2 = Part.Arc(hinge.seg_face_1.B, inner_arc_point, hinge.seg_face_2.B).toShape()
    l3 = Part.makeLine(hinge.seg_face_2.B, hinge.seg_face_2.A)
    a4 = Part.Arc(hinge.seg_face_2.A, outter_arc_point, hinge.seg_face_1.A).toShape()
    wire = Part.Wire([l1, a2, l3, a4])
    face = Part.Face(wire)

    hinge.solid = face.extrude(hinge.extrustion_vector)
    #Part.show(hinge.solid)
    return


def get_coplanar_edge(face1, face2):
    face1_edges = get_thickness_edge(face1)
    face2_edges = get_thickness_edge(face2)

    midpoint1_a = face1_edges[0].CenterOfMass
    midpoint2_a = face2_edges[0].CenterOfMass
    midpoint2_b = face2_edges[1].CenterOfMass

    edge2 = face2_edges[0]
    distance = midpoint1_a.sub(midpoint2_a).Length
    if midpoint1_a.sub(midpoint2_b).Length < distance:
        edge2 = face2_edges[1]

    return face1_edges[0], edge2, face1_edges[1].CenterOfMass.sub(face1_edges[0].CenterOfMass)


def get_segment_from_edge(edge1, edge2):
    length = edge1.Vertexes[0].Point.sub(edge2.Vertexes[0].Point).Length
    case = 1

    test_length = edge1.Vertexes[0].Point.sub(edge2.Vertexes[1].Point).Length
    if test_length < length:
        case = 2
        length = test_length

    test_length = edge1.Vertexes[1].Point.sub(edge2.Vertexes[0].Point).Length
    if test_length < length:
        case = 3
        length = test_length

    test_length =  edge1.Vertexes[1].Point.sub(edge2.Vertexes[1].Point).Length
    if test_length < length:
        case = 4
        length = test_length

    if case == 1:
        first = Segment(edge1.Vertexes[1].Point, edge1.Vertexes[0].Point)
        second = Segment(edge2.Vertexes[1].Point, edge2.Vertexes[0].Point)
    elif case == 2:
        first = Segment(edge1.Vertexes[1].Point, edge1.Vertexes[0].Point)
        second = Segment(edge2.Vertexes[0].Point, edge2.Vertexes[1].Point)
    elif case == 3:
        first = Segment(edge1.Vertexes[0].Point, edge1.Vertexes[1].Point)
        second = Segment(edge2.Vertexes[1].Point, edge2.Vertexes[0].Point)
    elif case == 4:
        first = Segment(edge1.Vertexes[0].Point, edge1.Vertexes[1].Point)
        second = Segment(edge2.Vertexes[0].Point, edge2.Vertexes[1].Point)
    else:
        raise ValueError("Impossible exception")
    return first, second


def get_thickness_edge(face):
    list_edges = Part.__sortEdges__(face.Edges)
    small_edge_list = [list_edges[0]]

    for edge in list_edges[1:]:
        if edge.Length == small_edge_list[0].Length:
            small_edge_list.append(edge)
        elif edge.Length < small_edge_list[0].Length:
            small_edge_list = [edge]

    return small_edge_list


def get_width_edge(face):
    list_edges = Part.__sortEdges__(face.Edges)
    long_edge_list = [list_edges[0]]

    for edge in list_edges[1:]:
        if edge.Length == long_edge_list[0].Length:
            long_edge_list.append(edge)
        elif edge.Length > long_edge_list[0].Length:
            long_edge_list = [edge]

    return long_edge_list


#http# s://gist.github.com/hanigamal/6556506
def do_intersection(seg1, seg2):
    da = seg1.B - seg1.A
    db = seg2.B - seg2.A
    dc = seg2.A - seg1.A

    coplanar_val = dc.dot(da.cross(db))
    if not compare_value(coplanar_val, 0.):
        raise ValueError("Not coplanar (seg1: %s, seg2: %s) => res: %s" %(str(seg1), str(seg2), str(coplanar_val)))

    a = dc.cross(db).dot(da.cross(db))
    s = dc.cross(db).dot(da.cross(db)) / da.cross(db).dot(da.cross(db))
    if s >= 10e-6:
        da.scale(s, s, s)
        ip = seg1.A.add(da)
        return ip
    else:
        raise ValueError("Wrong scale")


# http://www.deferredprocrastination.co.uk/blog/2012/minimum-bend-radius/
def estimate_min_link(rad_angle, thickness, clearance_width):
    tmp = (clearance_width + thickness) / (2. * math.sqrt(thickness * thickness / 2.))
    min_link = rad_angle / (math.pi / 4.0 - math.acos(tmp))
    return math.ceil(min_link)


def create_linked_part(hinges_list, material_properties):
    if len(hinges_list) == 0:
        raise ValueError("No hinge defined")

    if material_properties.laser_beam_diameter > material_properties.link_clearance:
        raise ValueError("Laser beam diameter is greater than clearance width")
    elif material_properties.link_clearance < 2. * material_properties.laser_beam_diameter:
        FreeCAD.Console.PrintMessage( \
              "Caution : link clearance is less than twice the laser beam diameter. Exported svg " + \
              "will contains lines too close and the laser will almost go twice in the same position.\n" + \
              "It is advisable to choose a clearance at least twice the kerf. You could also set clearance " + \
              "equals to kerf if you want laser have one passage. But in the exported SVG, hinges apertures " + \
              "are in fact square with 10e-3 width, so you have to remove manually the three others sides " + \
              "for all square to avoid laser returning to same place.")

    parts_to_fuse = [hinges_list[0].freecad_object_1.Shape.copy()]
    hinges_to_removes = []
    last_face = hinges_list[0].freecad_face_1
    sum_angle = 0.
    rotation_vector = None
    for index, hinge in enumerate(hinges_list):
        if hinge.nb_link < hinge.min_links_nb:
            FreeCAD.Console.PrintError("Min. link is not respected for living hinges named " + str(hinge.name))

        flat_connection = create_flat_connection(hinge, last_face)
        parts_to_fuse.append(flat_connection)
        hinges_to_removes.append(make_hinges(hinge, material_properties, last_face))
        last_face = find_same_normal_face(flat_connection, last_face)

        sum_angle += hinge.deg_angle
        if rotation_vector is None:
            rotation_vector = hinge.rotation_vector

        second_shape_transformed = assemble_shape(last_face, hinge.freecad_object_2.Shape, hinge.freecad_face_2,
                                                  rotation_vector, -sum_angle)
        parts_to_fuse.append(second_shape_transformed)
        if index < (len(hinges_list) - 1):
            last_face = find_same_normal_face(second_shape_transformed, last_face)

    flat_part = assemble_list_element(parts_to_fuse)
    for hinge_to_remove in hinges_to_removes:
        flat_part = flat_part.cut(hinge_to_remove)

    solid = hinges_list[0].solid.copy()
    for hinge in hinges_list[1:]:
        solid = solid.fuse(hinge.solid)

    return flat_part, solid


def create_flat_connection(hinge_properties, referentiel_face):
    box_x_size = hinge_properties.arc_length
    box_y_size = hinge_properties.extrustion_vector.Length
    box_z_size = hinge_properties.thickness
    box = Part.makeBox(box_x_size, box_y_size, box_z_size, FreeCAD.Vector(0., -box_y_size/2.0, -box_z_size/2.0))

    flat_connection = transform(box, referentiel_face)

    return flat_connection


def get_hinges_x_positions(nb_hinges, x_length):
    hinges_list = []
    interval_length = x_length / float(nb_hinges - 1)

    for i in range(int(nb_hinges)):
        hinges_list.append(float(i) * interval_length)

    return hinges_list


def get_hinges_y_positions(nb_holes_by_column, hole_length, hole_space):
    y_pos_list = []
    half_hole_number = int(float(nb_holes_by_column) / 2.0)
    interval_length = hole_length + hole_space

    if nb_holes_by_column % 2 == 1:
        y_pos_list.append(0.0)
        for i in range(half_hole_number):
            y_pos_list.append(float(i+1) * interval_length)
            y_pos_list.append(float(-i-1) * interval_length)
    else:
        for i in range(half_hole_number):
            y_pos_list.append(float(i) * interval_length + interval_length / 2.0)
            y_pos_list.append(float(-i) * interval_length - interval_length / 2.0)

    return y_pos_list


def create_hole_hinge(hinge_clearance, hinge_length, thickness, kerf_diameter):
    height = thickness * 2.0
    hinge_width = max(10e-3, hinge_clearance - kerf_diameter)
    if hinge_clearance < kerf_diameter:
        raise ValueError("Hinge clearance is less than kerf diameter")
    elif hinge_clearance < 2. * kerf_diameter:
        box_length = hinge_length - kerf_diameter
        hinge = Part.makeBox(hinge_width, box_length, height, FreeCAD.Vector(-hinge_width/2.0, -box_length/2.0, -height/2.0))
    else:
        box_length = hinge_length - hinge_width - kerf_diameter # hinge_width is for the two corner radius
        hinge = draw_rounded_hinge(hinge_width, box_length, height)

    return hinge


def draw_rounded_hinge(hinge_width, hinge_length, height):
    half_w = hinge_width/2.0
    half_l = hinge_length/2.0
    half_h = height / 2.0
    z_plane = -half_h
    v1 = FreeCAD.Vector(-half_w, -half_l, z_plane)
    v2 = FreeCAD.Vector(-half_w, half_l, z_plane)
    v3 = FreeCAD.Vector(half_w, half_l, z_plane)
    v4 = FreeCAD.Vector(half_w, -half_l, z_plane)
    vc1 = FreeCAD.Vector(0, -(half_l + half_w), z_plane)
    vc2 = FreeCAD.Vector(0, half_l+half_w, z_plane)
    c1 = Part.Arc(v1, vc1, v4).toShape()
    c2 = Part.Arc(v2, vc2, v3).toShape()
    l1 = Part.makeLine(v1, v2)
    l2 = Part.makeLine(v3, v4)
    wire = Part.Wire([c1, l1, c2, l2])
    hinge = wire.extrude(FreeCAD.Vector(0.0, 0.0, height))
    hinge_solid = Part.makeSolid(hinge)
    return hinge_solid


def make_hinges(hinge_properties, global_hinges_properties, referentiel_face):
    x_hinges_positions = get_hinges_x_positions(hinge_properties.nb_link, hinge_properties.arc_length)
    hinges_list = []
    y_length = hinge_properties.extrustion_vector.Length
    thickness = hinge_properties.thickness
    length_ratio = global_hinges_properties.occupancy_ratio

    nb_holes_by_column = int(global_hinges_properties.alternate_nb_hinge)
    hole_length = (y_length * length_ratio) / float(nb_holes_by_column)
    hole_space = y_length * (1. - length_ratio) /float(nb_holes_by_column + 1)
    y_pos_list = get_hinges_y_positions(nb_holes_by_column, hole_length, hole_space)
    nb_holes_by_column = int(nb_holes_by_column + 1)
    y_pos_list_2 = get_hinges_y_positions(nb_holes_by_column, hole_length, hole_space)

    index = 0
    for hinge_x in x_hinges_positions:

        if index % 2 == 0:
            pos_list = y_pos_list
        else:
            pos_list = y_pos_list_2

        for pos_y in pos_list:
            new_hinge = create_hole_hinge(global_hinges_properties.link_clearance, hole_length, thickness,
                                          global_hinges_properties.laser_beam_diameter)
            new_hinge.translate(FreeCAD.Vector(hinge_x, pos_y, 0))
            hinges_list.append(new_hinge)
        index += 1

    hinges_to_remove = assemble_list_element(hinges_list)
    hinges_to_remove_transformed = transform(hinges_to_remove, referentiel_face)
    return hinges_to_remove_transformed


def assemble_shape(face1, shape, face2, rotation_vector, rotation_angle):
    new_shape = shape.copy()
    new_shape.rotate(face2.CenterOfMass, rotation_vector, rotation_angle)
    new_shape.translate(face1.CenterOfMass.sub(face2.CenterOfMass))
    return new_shape


def find_same_normal_face(obj, ref_face):
    ref_normal = ref_face.normalAt(0, 0)
    found_face = None
    for face in obj.Faces:
        normal = face.normalAt(0, 0)

        if compare_freecad_vector(ref_normal, normal) is True and compare_value(ref_face.Area, face.Area) is True:
            found_face = face
            break
    if found_face is None:
        raise ValueError("Unable to find face with same normal")

    return found_face