local RandomShapes = require('Legacy.Systems.Gen.ShapeLib.RandomShapes')
local BasicShapes  = require('Legacy.Systems.Gen.ShapeLib.BasicShapes')
local MathUtil     = require('Legacy.Systems.Gen.MathUtil')
local Joint        = require('Legacy.Systems.Gen.ShapeLib.Joint')
local JointField   = require('Legacy.Systems.Gen.ShapeLib.JointField')
local Shape        = require('Legacy.Systems.Gen.ShapeLib.Shape')
local Parametric   = require('Legacy.Systems.Gen.ShapeLib.Parametric')
local Style        = require('Legacy.Systems.Gen.ShapeLib.Style')
require('Legacy.Systems.Gen.ShapeLib.Warp')

local Cluster = {}

-- GenerateLinear (
-- Style style,
-- int numShapes [optional],
-- float spacing [optional],
-- Shape shape [optional],
-- Joint joint [optional]
-- )
-- Generates on z axis
function Cluster.GenerateLinear(style, numShapes, spacing, shape, joint)
    numShapes = numShapes or style.rng:getInt(10, 30)
    spacing = spacing or style.rng:getUniformRange(0.0, 3.0)

    local field = JointField()
    field:generateFromFunction(
        function(i) return Vec3d(0, 0, i * spacing) end, -- pos
        function(i) return Vec3d(0, 0, 1) end,           -- dir
        function(i) return Vec3d(0, 1, 0) end,           -- up
        function(i) return Vec3d(1, 1, 1) end,           -- scale
        numShapes, 1
    )

    shape = shape or style:getShape()
    if joint == nil then
        joint = Joint()
        joint:generateFromPoly(shape, shape:getRandomPoly(style.rng))
    end

    return field:createShape(shape, joint)
end

function Cluster.GenerateParametricSafe(style, r)
    return Cluster.GenerateParametric(style, r, style:getParametricSafe())
end

-- GenerateParametric (
-- Style style, float r,
-- Shape shape [optional],
-- function* fn [optional],
-- Joint joint [optional]
--)
-- Generates on x,y plane
function Cluster.GenerateParametric(style, r, fn, shape, joint)
    local fn = fn or style:getParametric()
    local numShapes = style.rng:getExp() * 150 + 10
    local scale = (2 * math.pi * r) / numShapes

    local step = (2 * math.pi) / numShapes
    local field = JointField()
    field:generateFromFunction(
        function(t) return Vec3d(fn(t).x * r, fn(t).y * r, 0) end, -- pos
        function(t) return Vec3d(fn(t).x * r, fn(t).y * r, 0) end, -- dir
        function(t) return Vec3d(0, 0, 1) end,                     -- up
        function(t) return Vec3d(scale, scale, scale) end,         -- scale
        numShapes, step
    )

    shape = shape or style:getShape()
    if joint == nil then
        joint = Joint()
        joint:generateFromPoly(shape, shape:getRandomPoly(style.rng))
    end

    local final = field:createShape(shape, joint)
    return final
end

-- GenerateCurve (
-- Style style, float length,
-- Shape shape [optional]
-- )
-- Generates on x,z plane
function Cluster.GenerateCurve(style, length, shape)
    -- p0, p3 = endpoints
    local p0 = Vec2d(0, 0)
    local p3 = Vec2d(length, 0)
    -- p1, p2 = points between p0, p4
    local p1 = Vec2d(style.rng:getUniformRange(0, length / 2.0),
        style.rng:getUniformRange(0, length))
    local p2 = Vec2d(style.rng:getUniformRange(length / 2.0, length),
        style.rng:getUniformRange(0, length))
    local numShapes = style.rng:getInt(7, 30)
    local scale = length / numShapes

    -- bezier function
    local fnbez = function(t)
        return Vec3d(
            ((1 - t) ^ 3) * p0.x + 3 * (1 - t) * (1 - t) * t * p1.x + 3 * (1 - t) * t * t * p2.x + (t ^ 3) * p3.x,
            0,
            ((1 - t) ^ 3) * p0.y + 3 * (1 - t) * (1 - t) * t * p1.y + 3 * (1 - t) * t * t * p2.y + (t ^ 3) * p3.y)
    end

    -- create joint field
    local step = 1.0 / numShapes
    local field = JointField()
    field:generateFromFunction(
        fnbez,                                             -- pos
        fnbez,                                             -- dir
        function(i) return Vec3d(0, 1, 0) end,             -- up
        function(i) return Vec3d(scale, scale, scale) end, -- scale
        numShapes, step
    )

    -- create shape
    local shape = shape or style:getShape()
    if joint == nil then
        joint = Joint()
        joint:generateFromPoly(shape, shape:getRandomPoly(style.rng))
    end

    return field:createShape(shape, joint)
end

-- GeneratePlane (Style style,
-- int numShapes - x,
-- float spacing - x,
-- int numShapes - y,
-- float spacing - y,
-- Shape shape, Joint joint
-- )
-- Generates on the x,y plane
function Cluster.GeneratePlane(style, nX, sX, nY, sY, shape, joint)
    local linearX = Cluster.GenerateLinear(style, nX, sX, shape, joint)
    linearX:rotate(math.pi / 2.0, 0, 0)

    local joint2 = Joint()
    joint2.pos = Vec3d(0, 0, 0)
    joint2.dir = Vec3d(0, 0, 1)
    joint2.up = Vec3d(0, 1, 0)
    joint2.scale = Vec3d(1, 1, 1)

    local shape = Cluster.GenerateLinear(style, nY, sY, linearX, joint2)
    shape:rotate(0, math.pi / 2.0, 0)

    return shape
end

-- RandomRing (Style style, float r [optional])
-- Generates on x,y plane
-- Guaruntees safe parametric fn
function Cluster.GenerateRingSafe(style, r)
    r = r or 1
    local ring
    local ringType = style.rng:getInt(1, 2)
    if ringType == 1 then
        ring = style:getTorusSafe()
    else
        ring = Cluster.GenerateParametricSafe(style, r)
    end
    return ring
end

function Cluster.GenerateRing(style, r, addConnector)
    r = r or 1
    local ring
    local ringType = style.rng:getInt(1, 2)
    if ringType == 1 then
        ring = style:getTorus()
    else
        local p = style:getParametric()
        ring = Cluster.GenerateParametric(style, r, p)
        if addConnector then
            ring:add(
                BasicShapes.Torus(p, style:getParametricSafe(), r, 0.1)
            )
        end
    end
    return ring
end

-- RandomColumn (Style style, float l)
-- Generates on z axis
function Cluster.GenerateColumn(style, l)
    l = l or 1
    local col

    local colType = style.rng:getInt(1, 4)
    if colType == 1 then
        col = BasicShapes.Box(10)
        col:scale(1, 1, l)
    elseif colType == 2 then
        col = BasicShapes.IrregularPrism(style.rng, 10)
        col:scale(1, l, 1)
        col:rotate(0, math.pi / 2.0, 0)
    elseif colType == 3 then
        col = BasicShapes.Prism(10, style.rng:choose({ 3, 4, 5, 6, 8, 20, 25, 30 }))
        col:scale(1, l, 1)
        col:rotate(0, math.pi / 2.0, 0)
    else
        col = Cluster.GenerateLinear(style, l, 1)
    end

    return col
end

-- GenerateSpherical
function Cluster.GenerateSpherical(style, r, shape, joint)
    r = r or 0.5

    -- create field
    -- can't use JointField:generateFromFunction()
    -- because of the random pos on spehere, which determines
    -- both position and direction
    local numShapes = style.rng:getInt(5, 30)
    local field = JointField()
    local ind = #field.joints + 1
    for i = 1, numShapes do
        local v = style.rng:getSphere() -- needed for both fnd and fnp
        local p = Vec3d(v.x * r, v.y * r, v.z * r)
        local dir = Vec3d(p.x, p.y, p.z)
        local up = Vec3d(1, 0, 0)
        local valid = dir:length() > 1e-6 and up:length() > 1e-6
        assert(valid)
        if valid then
            dir:inormalize()
            up:inormalize()
            field.joints[ind] = Joint()
            field.joints[ind].pos = p
            field.joints[ind].dir = dir
            field.joints[ind].up = up
            field.joints[ind].scale = Vec3d(1, 1, 1)
            ind = ind + 1
        end
    end
    assert(field:valid())

    -- create shape
    local shape = shape or style:getShape()
    if joint == nil then
        joint = Joint()
        joint:generateFromPoly(shape, shape:getRandomPoly(style.rng))
    end
    local cluster = field:createShape(shape, joint)

    return cluster
end

return Cluster
