# ##### BEGIN GPL LICENSE BLOCK #####
#
#  This program is free software; you can redistribute it and/or
#  modify it under the terms of the GNU General Public License
#  as published by the Free Software Foundation; either version 2
#  of the License, or (at your option) any later version.
#
#  This program 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 General Public License for more details.
#
#  You should have received a copy of the GNU General Public License
#  along with this program; if not, write to the Free Software Foundation,
#  Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
#
# ##### END GPL LICENSE BLOCK #####

# <pep8-80 compliant>

# Script copyright (C) Campbell Barton

bl_info = {
    "name": "Grease Scatter Objects",
    "author": "Campbell Barton",
    "version": (0, 1),
    "blender": (2, 5, 8),
    "location": "3D View, Add Mesh",
    "description": "Scatter a group of objects onto the active mesh using "
                   "the grease pencil lines",
    "warning": "",
    "wiki_url": "http://wiki.blender.org/index.php/Extensions:2.5/Py/"
                "Scripts/Object/Grease_Scatter",
    "tracker_url": "https://projects.blender.org/tracker/index.php?"
                   "func=detail&aid=TODO",
    "support": 'OFFICIAL',
    "category": "Object"}

from mathutils import Vector, Matrix, Quaternion
from random import uniform, shuffle
import bpy


def _main(self,
          obj,
          group,
          DENSITY=1.0,
          SCALE=0.6,
          RAND_LOC=0.8,
          RAND_ALIGN=0.75,
          ):

    from math import radians, pi

    # OFS = 0.2
    SEEK = 2.0  # distance for ray to seek
    BAD_NORMAL = Vector((0.0, 0.0, -1.0))
    WALL_LIMIT = radians(45.0)

    mats = (Matrix.Rotation(radians(-45), 3, 'X'),
            Matrix.Rotation(radians(+45), 3, 'X'),
            Matrix.Rotation(radians(-45), 3, 'Y'),
            Matrix.Rotation(radians(+45), 3, 'Y'),
            Matrix.Rotation(radians(-45), 3, 'Z'),
            Matrix.Rotation(radians(+45), 3, 'Z'),
            )

    Z_UP = Vector((0.0, 0.0, 1.0))
    Y_UP = Vector((0.0, 1.0, 0.0))

    if not group:
        self.report({'WARNING'}, "Group '%s' not found" % obj.name)
        return

    def faces_from_hits(hit_list):
        def from_pydata(self, verts, edges, faces):
            """
            Make a mesh from a list of verts/edges/faces
            Until we have a nicer way to make geometry, use this.
            """
            self.add_geometry(len(verts), len(edges), len(faces))

            verts_flat = [f for v in verts for f in v]
            self.verts.foreach_set("co", verts_flat)
            del verts_flat

            edges_flat = [i for e in edges for i in e]
            self.edges.foreach_set("verts", edges_flat)
            del edges_flat

            def treat_face(f):
                if len(f) == 3:
                    return f[0], f[1], f[2], 0
                elif f[3] == 0:
                    return f[3], f[0], f[1], f[2]
                return f

            faces_flat = [v for f in faces for v in treat_face(f)]
            self.faces.foreach_set("verts_raw", faces_flat)
            del faces_flat

    def debug_edge(v1, v2):
        mesh = bpy.data.meshes.new("Retopo")
        mesh.from_pydata([v1, v2], [(0.0, 1.0)], [])

        scene = bpy.context.scene
        mesh.update()
        obj_new = bpy.data.objects.new("Torus", mesh)
        scene.objects.link(obj_new)

    ray = obj.ray_cast
    closest_point_on_mesh = obj.closest_point_on_mesh

    obj_mat = obj.matrix_world.copy()
    obj_mat_inv = obj_mat.inverted()
    # obj_quat = obj_mat.to_quaternion()
    # obj_quat_inv = obj_mat_inv.to_quaternion()

    DEBUG = False

    def fix_point(p):
        hit, no, ind = closest_point_on_mesh(obj_mat_inv * p)
        if ind != -1:
            if DEBUG:
                return [p, no, None]
            else:
                # print("good", hit, no)
                return [hit, no, None]

        # worry!
        print("bad!", p, BAD_NORMAL)

        return [p, BAD_NORMAL, None]

    def get_points(stroke):
        return [fix_point(point.co) for point in stroke.points]

    def get_splines(gp):
        if gp.layers.active:
            frame = gp.layers.active.active_frame
            return [get_points(stroke) for stroke in frame.strokes]
        else:
            return []

    def main():
        scene = bpy.context.scene
        obj = bpy.context.object

        gp = None

        if obj:
            gp = obj.grease_pencil

        if not gp:
            gp = scene.grease_pencil

        if not gp:
            self.report({'WARNING'}, "No grease pencil layer found")
            return

        splines = get_splines(gp)

        for s in splines:
            for pt in s:
                p = pt[0]
                n = pt[1]
                # print(p, n)
                if n is BAD_NORMAL:
                    continue

                # # dont self intersect
                best_nor = None
                #best_hit = None
                best_dist = 10000000.0
                pofs = p + n * 0.01

                n_seek = n * SEEK
                m_alt_1 = Matrix.Rotation(radians(22.5), 3, n)
                m_alt_2 = Matrix.Rotation(radians(-22.5), 3, n)
                for _m in mats:
                    for m in (_m, m_alt_1 * _m, m_alt_2 * _m):
                        hit, nor, ind = ray(pofs, pofs + (m * n_seek))
                        if ind != -1:
                            dist = (pofs - hit).length
                            if dist < best_dist:
                                best_dist = dist
                                best_nor = nor
                                #best_hit = hit

                if best_nor:
                    pt[1].length = best_dist
                    best_nor.negate()
                    pt[2] = best_nor

                    #scene.cursor_location[:] = best_hitnyway
                    # bpy.ops.wm.redraw_timer(type='DRAW_WIN_SWAP',
                    #                         iterations=1)
                    # debug_edge(p, best_hit)
                    # p[:] = best_hit

        # Now we need to do scattering.
        # first corners
        hits = []
        nors = []
        oris = []
        for s in splines:
            # point, normal, n_other the closest hit normal
            for p, n, n_other in s:
                if n is BAD_NORMAL:
                    continue
                if n_other:
                    # cast vectors twice as long as the distance
                    # needed just in case.
                    n_down = (n * -SEEK)
                    l = n_down.length
                    n_other.length = l

                    vantage = p + n
                    if DEBUG:
                        p[:] = vantage

                    # We should cast rays between n_down and n_other
                    #for f in (0.0, 0.2, 0.4, 0.6, 0.8, 1.0):
                    TOT = int(10 * DENSITY)
                    #for i in list(range(TOT)):
                    for i in list(range(TOT))[int(TOT / 1.5):]:  # second half
                        f = i / (TOT - 1)

                        # focus on the center
                        '''
                        f -= 0.5
                        f = f*f
                        f += 0.5
                        '''

                        ntmp = f * n_down + (1.0 - f) * n_other
                        # randomize
                        ntmp.x += uniform(-l, l) * RAND_LOC
                        ntmp.y += uniform(-l, l) * RAND_LOC
                        ntmp.z += uniform(-l, l) * RAND_LOC

                        hit, hit_no, ind = ray(vantage, vantage + ntmp)
                        # print(hit, hit_no)
                        if ind != -1:
                            if hit_no.angle(Z_UP) < WALL_LIMIT:
                                hits.append(hit)
                                nors.append(hit_no)
                                oris.append(n_other.cross(hit_no))
                                #oris.append(n_other)

        if 0:
            mesh = bpy.data.meshes.new("ScatterDupliFace")
            mesh.from_pydata(hits, [], [])

            scene = bpy.context.scene
            mesh.update()
            obj_new = bpy.data.objects.new("ScatterPar", mesh)
            scene.objects.link(obj_new)
            obj_new.layers[:] = obj.layers

            # Now setup dupli-faces
            obj_new.dupli_type = 'VERTS'
            ob_child = bpy.data.objects["trash"]
            ob_child.location = obj_new.location
            ob_child.parent = obj_new
        else:

            def apply_faces(triples):
                # first randomize the faces
                shuffle(triples)

                obs = group.objects[:]
                tot = len(obs)
                tot_div = int(len(triples) / tot)

                for inst_ob in obs:
                    triple_sub = triples[0:tot_div]
                    triples[0:tot_div] = []

                    vv = [tuple(v) for f in triple_sub for v in f]

                    mesh = bpy.data.meshes.new("ScatterDupliFace")
                    mesh.from_pydata(vv, [], [(i * 3, i * 3 + 1, i * 3 + 2)
                                              for i in range(len(triple_sub))])

                    scene = bpy.context.scene
                    mesh.update()
                    obj_new = bpy.data.objects.new("ScatterPar", mesh)

                    scene.objects.link(obj_new)
                    obj_new.layers[:] = obj.layers

                    # Now setup dupli-faces
                    obj_new.dupli_type = 'FACES'
                    obj_new.use_dupli_faces_scale = True
                    obj_new.dupli_faces_scale = 100.0

                    inst_ob.location = 0.0, 0.0, 0.0
                    inst_ob.parent = obj_new

                    # align the object with worldspace
                    obj_new.matrix_world = obj_mat

                    # BGE settings for testiing
                    '''
                    inst_ob.game.physics_type = 'RIGID_BODY'
                    inst_ob.game.use_collision_bounds = True
                    inst_ob.game.collision_bounds = 'TRIANGLE_MESH'
                    inst_ob.game.collision_margin = 0.1
                    obj_new.select = True
                    '''

            # build faces from vert/normals
            tri = (Vector((0.0, 0.0, 0.01)),
                   Vector((0.0, 0.0, 0.0)),
                   Vector((0.0, 0.01, 0.01)))

            coords = []
            # face_ind = []
            for i in range(len(hits)):
                co = hits[i]
                no = nors[i]
                ori = oris[i]
                quat = no.to_track_quat('X', 'Z')

                # make 2 angles and blend
                angle = uniform(-pi, pi)
                angle_aligned = -(ori.angle(quat * Y_UP, pi))

                quat = Quaternion(no,
                                  (angle * (1.0 - RAND_ALIGN)) +
                                  (angle_aligned * RAND_ALIGN)
                                  ).cross(quat)

                f = uniform(0.1, 1.2) * SCALE

                coords.append([co + (quat * (tri[0] * f)),
                               co + (quat * (tri[1] * f)),
                               co + (quat * (tri[2] * f)),
                               ])

            apply_faces(coords)

    main()


from bpy.props import FloatProperty,  StringProperty


class Scatter(bpy.types.Operator):
    ''''''
    bl_idname = "object.scatter"
    bl_label = "Grease Pencil Scatter"

    density = FloatProperty(
            name="Density",
            description="Multiplier for the density of items",
            default=1.0, min=0.01, max=10.0,
            )
    scale = FloatProperty(
            name="Scale",
            description="Size multiplier for duplifaces",
            default=1.0, min=0.01, max=10.0,
            )
    rand_align = FloatProperty(
            name="Random Align",
            description="Randomize alignment with the walls",
            default=0.75, min=0.0, max=1.0,
            )
    rand_loc = FloatProperty(
            name="Random Loc",
            description="Randomize placement",
            default=0.75, min=0.0, max=1.0,
            )
    # XXX, should not be a string - TODO, add a way for scritps to select ID's
    group = StringProperty(
            name="Group",
            description=("Group name to use for object placement, "
                         "defaults to object name when that matches a group"))

    def execute(self, context):
        obj = bpy.context.object
        group = bpy.data.groups.get(self.group)

        if not group:
            self.report({'ERROR'}, "Group %r not found", self.group)
            return {'CANCELLED'}

        _main(self,
              obj,
              group,
              DENSITY=self.density,
              SCALE=self.scale,
              RAND_LOC=self.rand_loc,
              RAND_ALIGN=self.rand_align,
              )
        return {'FINISHED'}

    def check(self, context):
        if self.group not in bpy.data.groups:
            self.group = ""
            return True
        return False

    def invoke(self, context, event):

        # useful to initialize, take a guess
        if not self.group and context.object.name in bpy.data.groups:
            self.group = context.object.name

        wm = context.window_manager
        wm.invoke_props_dialog(self, width=180)
        return {'RUNNING_MODAL'}


def menu_func(self, context):
    self.layout.operator(Scatter.bl_idname, icon='AUTO')


def register():
    bpy.utils.register_class(Scatter)
    bpy.types.INFO_MT_mesh_add.append(menu_func)


def unregister():
    bpy.utils.unregister_class(Scatter)
    bpy.types.INFO_MT_mesh_add.remove(menu_func)

#if __name__ == "__main__":
#    _main()
