local MeshUtil = require('Legacy.Systems.Gen.MeshUtil')
local Shape    = require('Legacy.Systems.Gen.ShapeLib.Shape')
require('Legacy.Systems.Gen.ShapeLib.Warp')
local Parametric  = require('Legacy.Systems.Gen.ShapeLib.Parametric')
local MathUtil    = require('Legacy.Systems.Gen.MathUtil')
local Warp        = require('Legacy.Systems.Gen.ShapeLib.Warp')

local BasicShapes = {}

-- BAD INPUT PROTECTION POLICY
-- Because we use so much randomness,
-- sometimes the input to the shape functions may be bad,
-- even in release.
-- For example, we may call Prism() and ask for fewer than 3 verts.
-- If any input is invalid, the warp function MUST
-- 1. ASSERT, so that devs can debug the bad input AND
-- 2. Set the input to a valid default value,
-- so that we prevent junk shapes & crashes in release.

-- Box(float resolution)
function BasicShapes.Box(res)
    -- default values
    res = res or 1
    -- bad input protection
    if res < 0 then
        assert(res > 0)
        res = 0
    end

    local self = Shape()
    self:addVertex(-1, 1, -1)  -- 0
    self:addVertex(1, 1, -1)   -- 1
    self:addVertex(-1, 1, 1)   -- 2
    self:addVertex(1, 1, 1)    -- 3

    self:addVertex(-1, -1, -1) -- 4
    self:addVertex(1, -1, -1)  -- 5
    self:addVertex(-1, -1, 1)  -- 6
    self:addVertex(1, -1, 1)   -- 7

    self:addQuad(0, 2, 3, 1)   -- top
    self:addQuad(4, 5, 7, 6)   -- bottom
    self:addQuad(3, 7, 5, 1)
    self:addQuad(0, 4, 6, 2)
    self:addQuad(2, 6, 7, 3)
    self:addQuad(0, 1, 5, 4)

    return self:tessellate(res)
end

-- Prism(int stacks, int slices)
function BasicShapes.Prism(stacks, slices)
    -- default values
    stacks = stacks or 2
    slices = slices or 3
    -- bad input protection
    if stacks < 2 then
        assert(stacks >= 2)
        stacks = 2
    end
    if slices < 3 then
        assert(slices >= 3)
        slices = 3
    end

    local self = Shape()

    -- Verts
    for i = 0, stacks - 1 do
        local y = i / (stacks - 1) - 0.5
        for j = 0, slices - 1 do
            local t = 2 * j * math.pi / slices
            self:addVertex(math.cos(t) * 0.5, y, math.sin(t) * 0.5)
        end
    end

    -- Polys
    do -- Top Cap
        local poly = {}
        for i = 0, slices - 1 do
            poly[#poly + 1] = i % slices
        end
        self:addPoly(poly)
    end

    do -- Bottom Cap
        local poly = {}
        local offset = (stacks - 1) * slices
        -- NOTE : Bottom winding must be opposite top
        for i = slices - 1, 0, -1 do
            poly[#poly + 1] = offset + i % slices
        end
        self:addPoly(poly)
    end

    -- Side Quads
    for stack = 0, stacks - 2 do
        for slice = 0, slices - 1 do
            self:addQuad(
                stack * slices + slice,
                (stack + 1) * slices + slice,
                (stack + 1) * slices + (slice + 1) % slices,
                stack * slices + (slice + 1) % slices)
        end
    end

    return self
end

-- Pyramid (5 faced, rectangle bottom)
function BasicShapes.Pyramid()
    local self = Shape()

    self
        :addVertex(0, 0, 0)      -- center bottom
        :addVertex(-0.5, 0, 0.5) -- bottom 4 verticies
        :addVertex(-0.5, 0, -0.5)
        :addVertex(0.5, 0, -0.5)
        :addVertex(0.5, 0, 0.5)
        :addVertex(0, 0.5, 0) -- top

    self
        :addQuad(1, 2, 3, 4) -- bottom
        :addTri(5, 2, 1)     -- 4 sides
        :addTri(5, 1, 4)
        :addTri(5, 4, 3)
        :addTri(5, 3, 2)

    return self
end

-- Icosahedron (20 sides)
function BasicShapes.Icosahedron()
    local phi = (1.0 + math.sqrt(5)) / 2.0
    local self = Shape()

    -- YZ
    self
        :addVertex(0, phi, 1)
        :addVertex(0, phi, -1)
        :addVertex(0, -phi, 1)
        :addVertex(0, -phi, -1)

    -- XZ
    self
        :addVertex(-1, 0, -phi)
        :addVertex(1, 0, -phi)
        :addVertex(-1, 0, phi)
        :addVertex(1, 0, phi)

    -- XY
    self
        :addVertex(-phi, 1, 0)
        :addVertex(phi, 1, 0)
        :addVertex(-phi, -1, 0)
        :addVertex(phi, -1, 0)

    -- Cap 1
    self
        :addTri(1, 0, 9)
        :addTri(1, 9, 5)
        :addTri(1, 5, 4)
        :addTri(1, 4, 8)
        :addTri(1, 8, 0)

    -- cap 2
    self
        :addTri(7, 6, 2)
        :addTri(11, 7, 2)
        :addTri(3, 11, 2)
        :addTri(10, 3, 2)
        :addTri(6, 10, 2)

    -- sides
    self
        :addTri(9, 0, 7)
        :addTri(9, 7, 11)
        :addTri(5, 9, 11)
        :addTri(5, 11, 3)
        :addTri(5, 3, 4)
        :addTri(4, 3, 10)
        :addTri(4, 10, 8)
        :addTri(8, 10, 6)
        :addTri(0, 8, 6)
        :addTri(0, 6, 7)

    self:sphereize()
    return self
end

-- Ellipsoid (int res)
function BasicShapes.Ellipsoid(res)
    -- default values
    res = res or 20
    -- bad input protection
    if res < 3 then
        assert(res >= 3)
        res = 3
    end

    local self = Shape()

    local thetastack = math.pi / res
    local thetaslice = (2 * math.pi) / res

    -- verts
    self:addVertex(0, 1, 0)
    for stack = 1, res - 1 do
        local rx = math.sin(thetastack * stack)
        local rz = math.sin(thetastack * stack)
        for slice = 0, res - 1 do
            local x = math.cos(thetaslice * slice) * rx
            local y = math.cos(thetastack * stack)
            local z = math.sin(thetaslice * slice) * rz
            self:addVertex(x, y, z)
        end
    end
    self:addVertex(0, -1, 0)

    -- top tris
    for slice = 0, res - 2 do
        self:addTri(0, slice + 2, slice + 1)
    end
    self:addTri(0, 1, res)

    -- side quads
    for stack = 0, res - 3 do
        for slice = 0, res - 2 do
            local i1 = 1 + slice + (res * stack)
            local i2 = i1 + 1
            local i3 = 1 + slice + (res * (stack + 1))
            local i4 = i3 + 1
            self:addQuad(i1, i2, i4, i3)
        end
        local i1 = res * (stack + 1)
        local i2 = 1 + (res * stack)
        local i3 = res * (stack + 2)
        local i4 = 1 + (res * (stack + 1))
        self:addQuad(i1, i2, i4, i3)
    end

    -- bottom tris
    local lvi = self:getVertexCount() - 1
    for slice = 0, res - 2 do
        local i1 = lvi
        local i2 = (res - 2) * res + slice + 1
        local i3 = (res - 2) * res + slice + 2
        self:addTri(i1, i2, i3)
    end
    self:addTri(lvi, (res - 1) * res, (res - 2) * res + 1)

    return self:center()
end

-- Torus (fn* outer shape function (torus shape),
-- fn* inner shape function (segment shape),
-- float rOuter (torus radius),
-- float rInner (segment radius),
-- int stacks,
-- int slices)
-- Unliked the rest of the shape fns, the Torus actually takes
-- size input because warps won't change the ratio
-- of the size of the segments to the whole torus radius
function BasicShapes.Torus(outerFn, innerFn, rOuter, rInner, stacks, slices)
    -- default values
    outerFn = outerFn or Parametric.Circle()
    innerFn = innerFn or Parametric.Circle()
    rOuter = rOuter or 1
    rInner = rInner or 0.2
    stacks = stacks or 20
    slices = slices or 20
    -- bad input protection
    assert(rInner > 1e-6 and rOuter > 1e-6 and stacks >= 3 and slices >= 3)
    if rInner < 1e-6 then rInner = 0.2 end
    if rOuter < 1e-6 then rOuter = 1 end
    if stacks < 3 then stacks = 3 end
    if slices < 3 then slices = 3 end

    local self = Shape()
    do                                                       -- Vertices
        for stack = 0, stacks - 1 do
            local theta = (2.0 * math.pi) * (stack / stacks) -- Outer angle (stacks)
            local pOuter = outerFn(theta)
            for slice = 0, slices - 1 do
                local phi = (2.0 * math.pi) * (slice / slices) -- Inner angle (slices)
                local pInner = innerFn(phi)
                local x = pOuter.x * (rOuter + rInner * pInner.x)
                local y = pOuter.y * (rOuter + rInner * pInner.x)
                local z = rInner * pInner.y
                self:addVertex(x, y, z)
            end
        end
    end

    do -- Polys
        for stack = 0, stacks - 1 do
            -- Index offsets for this stack (stack1) and next stack (stack2)
            local stack1 = stack * slices
            local stack2 = ((stack + 1) % stacks) * slices
            for slice = 0, slices - 1 do
                -- Index offsets for this slice (slice1) and next slice (slice2)
                local slice1 = slice
                local slice2 = (slice + 1) % slices
                self:addQuad(
                    stack1 + slice1,
                    stack2 + slice1,
                    stack2 + slice2,
                    stack1 + slice2)
            end
        end
    end

    return self
end

-- IrregularPrism (RNG rng, int res, int verts [optional])
function BasicShapes.IrregularPrism(rng, res, numV)
    -- default values
    res = res or 2
    numV = numV or rng:getInt(4, 6)
    -- bad input protection
    assert(rng)
    if res < 2 then
        assert(res >= 2)
        res = 2
    end
    if numV < 3 then
        assert(numV >= 3)
        numV = 4
    end

    local self = Shape()

    local maxRadius = 1
    local height = 0.2
    local halfV = numV / 2

    -- Generate TWO lists of angles between 0-180 and then add them together
    -- to guaruntee that no angle > 180, which creates ugly shapes
    local angles = MathUtil.GenerateNumsThatAddToSum(halfV, math.pi, rng)
    if numV % 2 ~= 0 then halfV = halfV + 1 end
    local angles2 = MathUtil.GenerateNumsThatAddToSum(halfV, math.pi, rng)

    local j = 1
    for i = #angles + 1, #angles + #angles2 do
        table.insert(angles, angles2[j])
        j = j + 1
    end
    local radii = {}
    for i = 1, #angles do
        radii[i] = rng:getUniformRange(maxRadius / 2.0, maxRadius)
    end

    -- create vertex list for base face
    local lastAngle = 0
    local dh = height / (res - 1)
    for j = 1, numV do
        local r = radii[j]
        local theta = 0
        if j ~= 1 then
            theta = lastAngle + angles[j]
        end
        local x = math.cos(theta) * r
        local z = math.sin(theta) * r
        self:addVertex(x, 0, z)
        lastAngle = theta
    end

    -- create poly for base face
    local face = {}
    for i = 0, #self.verts - 1 do
        face[#face + 1] = i
    end
    self.polys[1] = face

    -- extrude face along y 'res' times
    local heightPerStack = 1.0 / res
    for i = 1, res do
        local pi = self:getPolyWithNormal(Vec3d(0, -1, 0))
        self:extrudePoly(pi, heightPerStack, Vec3d(1, 1, 1), Vec3d(0, -1, 0), true)
    end

    return self:center()
end

return BasicShapes
