local function prequire(name) local success, result = pcall(require, name); return success and result end
local bench = script and require(script.Parent.bench_support) or prequire("bench_support") or require("../../bench_support")

local PI = 3.141592653589793
local SOLAR_MASS = 4 * PI * PI
local DAYS_PER_YEAR = 365.24

local Body = {}
Body.__index = Body

function Body.new(x, y, z, vx, vy, vz, mass)
    local self = {}
    self.x = x
    self.y = y
    self.z = z
    self.vx = vx
    self.vy = vy
    self.vz = vz
    self.mass = mass
    return setmetatable(self, Body)
end

function Body:offsetMomentum(px, py, pz)
    self.vx = -px / SOLAR_MASS
    self.vy = -py / SOLAR_MASS
    self.vz = -pz / SOLAR_MASS

    return self
end

local function Jupiter()
    return Body.new(
      4.841431442464721e0,
      -1.1603200440274284e0,
      -1.036220444711231e-1,
      1.660076642744037e-3 * DAYS_PER_YEAR,
      7.6990111841974045e-3 * DAYS_PER_YEAR,
      -6.90460016972063e-5 * DAYS_PER_YEAR,
      9.547919384243267e-4 * SOLAR_MASS
    )
end
local function Saturn()
    return Body.new(8.34336671824458e0, 4.124798564124305e0, -4.035234171143213e-1, -2.767425107268624e-3 * DAYS_PER_YEAR, 4.998528012349173e-3 * DAYS_PER_YEAR, 2.3041729757376395e-5 * DAYS_PER_YEAR, 2.8588598066613082e-4 * SOLAR_MASS)
end
local function Uranus()
    return Body.new(1.2894369562139132e1, -1.511115140169863e1, -2.2330757889265573e-1, 2.964601375647616e-3 * DAYS_PER_YEAR, 2.3784717395948096e-3 * DAYS_PER_YEAR, -2.9658956854023755e-5 * DAYS_PER_YEAR, 4.366244043351563e-5 * SOLAR_MASS)
end
local function Neptune()
    return Body.new(1.5379697114850917e1, -2.5919314609987962e1, 1.7925877295037118e-1, 2.680677724903893e-3 * DAYS_PER_YEAR, 1.628241700382423e-3 * DAYS_PER_YEAR, -9.515922545197158e-5 * DAYS_PER_YEAR, 5.151389020466114e-5 * SOLAR_MASS)
end
local function Sun()
    return Body.new(0, 0, 0, 0, 0, 0, SOLAR_MASS)
end

local NBodySystem = {}
NBodySystem.__index = NBodySystem


function NBodySystem.new(bodies)
    local self = {}
    self.bodies = bodies

    local px = 0
    local py = 0
    local pz = 0
    local size = #self.bodies

    for i=1, size do
        local b = self.bodies[i]
        local m = b.mass

        px = px + b.vx * m
        py = py + b.vy * m
        pz = pz + b.vz * m
    end

    self.bodies[1]:offsetMomentum(px, py, pz)

    return setmetatable(self, NBodySystem)
end

function NBodySystem:advance(dt)
    local dx, dy, dz, distance, mag
    local size = #self.bodies

    for i=1, size do
      local bodyi = self.bodies[i]
      for j=i+1, size do
         local bodyj = self.bodies[j]
         dx = bodyi.x - bodyj.x
         dy = bodyi.y - bodyj.y
         dz = bodyi.z - bodyj.z

         distance = math.sqrt(dx*dx + dy*dy + dz*dz)
         mag = dt / (distance * distance * distance)

         bodyi.vx -= dx * bodyj.mass * mag
         bodyi.vy -= dy * bodyj.mass * mag
         bodyi.vz -= dz * bodyj.mass * mag

         bodyj.vx += dx * bodyi.mass * mag
         bodyj.vy += dy * bodyi.mass * mag
         bodyj.vz += dz * bodyi.mass * mag
      end
    end
    for i=1, size do
        local body = self.bodies[i]

        body.x = body.x + dt * body.vx
        body.y = body.y + dt * body.vy
        body.z = body.z + dt * body.vz
    end
end

function NBodySystem:energy()
    local dx, dy, dz, distance
    local e = 0.0
    local size = #self.bodies

    for i=1, size do
        local bodyi = self.bodies[i]

        e = e + 0.5 * bodyi.mass * (bodyi.vx * bodyi.vx + bodyi.vy * bodyi.vy + bodyi.vz * bodyi.vz)

      for j=i+1, size do
         local bodyj = self.bodies[j]
         dx = bodyi.x - bodyj.x
         dy = bodyi.y - bodyj.y
         dz = bodyi.z - bodyj.z

         distance = math.sqrt(dx*dx + dy*dy + dz*dz)
         e -= (bodyi.mass * bodyj.mass) / distance
      end
    end

    return e
end

local function run()
    local ret = 0
    local n = 3
    while n <= 24 do
        (function()
            local bodies = NBodySystem.new({ 
               Sun(),Jupiter(),Saturn(),Uranus(),Neptune()
            })
            local max = n * 100
            
            ret += bodies:energy()
            for i=1, max do
                bodies:advance(0.01)
            end 
            ret += bodies:energy()
        end)()
        n *= 2
    end
    local expected = -1.3524862408537381

    if ret ~= expected then
        error('ERROR: bad result: expected ' .. expected .. ' but got ' .. ret)
    end
end

function runIteration()
    for i=1, 5 do
        run()
    end
end

bench.runCode(runIteration, "n-body-oop")
