// Copyright 2014 Google Inc. All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//     http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

#include "lobster/stdafx.h"

#include "lobster/natreg.h"

#include "lobster/glinterface.h"

#include "Box2D/Box2D.h"

using namespace lobster;

struct Renderable : Textured {
    float4 color = float4_1;
    Shader *sh;

    Renderable(const char *shname) : sh(LookupShader(shname)) { assert(sh); }

    void Set() {
        sh->Set();
        sh->SetTextures(textures);
    }
};

b2World *world = nullptr;
b2ParticleSystem *particlesystem = nullptr;
Renderable *particlematerial = nullptr;

b2Vec2 Float2ToB2(const float2 &v) { return b2Vec2(v.x, v.y); }
float2 B2ToFloat2(const b2Vec2 &v) { return float2(v.x, v.y); }

b2Vec2 PopB2(VM &vm) {
    auto v = vm.PopVec<float2>();
    return Float2ToB2(v);
}

struct PhysicsObject {
    Renderable r;
    b2Fixture *fixture;
    vector<int> *particle_contacts;

    PhysicsObject(const Renderable &_r, b2Fixture *_f)
        : r(_r), fixture(_f), particle_contacts(nullptr) {}
    ~PhysicsObject() {
        auto body = fixture->GetBody();
        body->DestroyFixture(fixture);
        if (!body->GetFixtureList()) world->DestroyBody(body);
        if (particle_contacts) delete particle_contacts;
    }
    float2 Pos() { return B2ToFloat2(fixture->GetBody()->GetPosition()); }
};

static ResourceType physics_type = { "physical", [](void *v) { delete ((PhysicsObject *)v); } };

PhysicsObject &GetObject(VM &vm, const Value &res) {
    return *GetResourceDec<PhysicsObject *>(vm, res, &physics_type);
}

void CleanPhysics() {
    if (world) delete world;
    world = nullptr;
    particlesystem = nullptr;
    delete particlematerial;
    particlematerial = nullptr;
}

void InitPhysics(const float2 &gv) {
    // FIXME: check that shaders are initialized, since renderables depend on that
    CleanPhysics();
    world = new b2World(b2Vec2(gv.x, gv.y));
}

void CheckPhysics() {
    if (!world) InitPhysics(float2(0, -10));
}

void CheckParticles(float size = 0.1f) {
    CheckPhysics();
    if (!particlesystem) {
        b2ParticleSystemDef psd;
        psd.radius = size;
        particlesystem = world->CreateParticleSystem(&psd);
        particlematerial = new Renderable("color_attr");
    }
}

b2Body &GetBody(VM &vm, Value &id, float2 wpos) {
    CheckPhysics();
    b2Body *body = id.True() ? GetObject(vm, id).fixture->GetBody() : nullptr;
    if (!body) {
        b2BodyDef bd;
        bd.type = b2_staticBody;
        bd.position.Set(wpos.x, wpos.y);
        body = world->CreateBody(&bd);
    }
    return *body;
}

Value CreateFixture(VM &vm, b2Body &body, b2Shape &shape) {
    auto fixture = body.CreateFixture(&shape, 1.0f);
    auto po = new PhysicsObject(Renderable("color"), fixture);
    fixture->SetUserData(po);
    return Value(vm.NewResource(po, &physics_type));
}

b2Vec2 OptionalOffset(VM &vm) {
    return vm.Top().True() ? PopB2(vm) : (vm.Pop(), b2Vec2_zero);
}

Renderable &GetRenderable(VM &vm, const Value &id) {
    CheckPhysics();
    return id.True() ? GetObject(vm, id).r : *particlematerial;
}

extern int GetSampler(VM &vm, Value &i);  // from graphics

void AddPhysics(NativeRegistry &nfr) {

nfr("ph_initialize", "gravityvector", "F}:2", "",
    "initializes or resets the physical world, gravity typically [0, -10].",
    [](VM &vm) {
        InitPhysics(vm.PopVec<float2>());
    });

nfr("ph_create_box", "position,size,offset,rotation,attachto", "F}:2F}:2F}:2?F?R?", "R",
    "creates a physical box shape in the world at position, with size the half-extends around"
    " the center, offset from the center if needed, at a particular rotation (in degrees)."
    " attachto is a previous physical object to attach this one to, to become a combined"
    " physical body.",
    [](VM &vm) {
        auto other_id = vm.Pop();
        auto rot = vm.Pop().fltval();
        auto offset = OptionalOffset(vm);
        auto sz = vm.PopVec<float2>();
        auto &body = GetBody(vm, other_id, vm.PopVec<float2>());
        b2PolygonShape shape;
        shape.SetAsBox(sz.x, sz.y, offset, rot * RAD);
        vm.Push(CreateFixture(vm, body, shape));
    });

nfr("ph_create_circle", "position,radius,offset,attachto", "F}:2FF}:2?R?", "R",
    "creates a physical circle shape in the world at position, with the given radius, offset"
    " from the center if needed. attachto is a previous physical object to attach this one to,"
    " to become a combined physical body.",
    [](VM &vm) {
        auto other_id = vm.Pop();
        auto offset = OptionalOffset(vm);
        auto radius = vm.Pop().fltval();
        auto &body = GetBody(vm, other_id, vm.PopVec<float2>());
        b2CircleShape shape;
        shape.m_p.Set(offset.x, offset.y);
        shape.m_radius = radius;
        vm.Push(CreateFixture(vm, body, shape));
    });

nfr("ph_create_polygon", "position,vertices,attachto", "F}:2F}:2]R?", "R",
    "creates a polygon circle shape in the world at position, with the given list of vertices."
    " attachto is a previous physical object to attach this one to, to become a combined"
    " physical body.",
    [](VM &vm) {
        auto other_id = vm.Pop();
        auto vertices = vm.Pop().vval();
        auto &body = GetBody(vm, other_id, vm.PopVec<float2>());
        b2PolygonShape shape;
        auto verts = new b2Vec2[vertices->len];
        for (int i = 0; i < vertices->len; i++) {
            auto vert = ValueToFLT<2>(vertices->AtSt(i), vertices->width);
            verts[i] = Float2ToB2(vert);
        }
        shape.Set(verts, (int)vertices->len);
        delete[] verts;
        vm.Push(CreateFixture(vm, body, shape));
    });

nfr("ph_dynamic", "shape,on", "RB", "",
    "makes a shape dynamic (on = true) or not.",
    [](VM &vm, Value &fixture_id, Value &on) {
        CheckPhysics();
        GetObject(vm, fixture_id)
            .fixture->GetBody()
            ->SetType(on.ival() ? b2_dynamicBody : b2_staticBody);
        return Value();
    });

nfr("ph_set_color", "id,color", "R?F}:4", "",
    "sets a shape (or nil for particles) to be rendered with a particular color.",
    [](VM &vm) {
        auto c = vm.PopVec<float4>();
        auto &r = GetRenderable(vm, vm.Pop());
        r.color = c;
    });

nfr("ph_set_shader", "id,shadername", "R?S", "",
    "sets a shape (or nil for particles) to be rendered with a particular shader.",
    [](VM &vm, Value &fixture_id, Value &shader) {
        auto &r = GetRenderable(vm, fixture_id);
        auto sh = LookupShader(shader.sval()->strv());
        if (sh) r.sh = sh;
        return Value();
    });

nfr("ph_set_texture", "id,tex,texunit", "R?RI?", "",
    "sets a shape (or nil for particles) to be rendered with a particular texture"
    " (assigned to a texture unit, default 0).",
    [](VM &vm, Value &fixture_id, Value &tex, Value &tex_unit) {
        auto &r = GetRenderable(vm, fixture_id);
        extern Texture GetTexture(VM &vm, const Value &res);
        r.Get(GetSampler(vm, tex_unit)) = GetTexture(vm, tex);
        return Value();
    });

nfr("ph_get_position", "id", "R", "F}:2",
    "gets a shape's position.",
    [](VM &vm) {
        vm.PushVec(GetObject(vm, vm.Pop()).Pos());
    });

nfr("ph_create_particle", "position,velocity,color,flags", "F}:2F}:2F}:4I?", "I",
    "creates an individual particle. For flags, see include/physics.lobster",
    [](VM &vm) {
        CheckParticles();
        b2ParticleDef pd;
        pd.flags = vm.Pop().intval();
        auto c = vm.PopVec<float3>();
        pd.color.Set(b2Color(c.x, c.y, c.z));
        pd.velocity = PopB2(vm);
        pd.position = PopB2(vm);
        vm.Push(particlesystem->CreateParticle(pd));
    });

nfr("ph_create_particle_circle", "position,radius,color,flags", "F}:2FF}:4I?", "",
    "creates a circle filled with particles. For flags, see include/physics.lobster",
    [](VM &vm) {
        CheckParticles();
        b2ParticleGroupDef pgd;
        b2CircleShape shape;
        pgd.shape = &shape;
        pgd.flags = vm.Pop().intval();
        auto c = vm.PopVec<float3>();
        pgd.color.Set(b2Color(c.x, c.y, c.z));
        shape.m_radius = vm.Pop().fltval();
        pgd.position = PopB2(vm);
        particlesystem->CreateParticleGroup(pgd);
    });

nfr("ph_initialize_particles", "radius", "F", "",
    "initializes the particle system with a given particle radius.",
    [](VM &, Value &size) {
        CheckParticles(size.fltval());
        return Value();
    });

nfr("ph_step", "seconds,viter,piter", "FII", "",
    "simulates the physical world for the given period (try: gl_delta_time()). You can specify"
    " the amount of velocity/position iterations per step, more means more accurate but also"
    " more expensive computationally (try 8 and 3).",
    [](VM &, Value &delta, Value &viter, Value &piter) {
        CheckPhysics();
        world->Step(min(delta.fltval(), 0.1f), viter.intval(), piter.intval());
        if (particlesystem) {
            for (b2Body *body = world->GetBodyList(); body; body = body->GetNext()) {
                for (b2Fixture *fixture = body->GetFixtureList(); fixture;
                     fixture = fixture->GetNext()) {
                    auto pc = ((PhysicsObject *)fixture->GetUserData())->particle_contacts;
                    if (pc) pc->clear();
                }
            }
            auto contacts = particlesystem->GetBodyContacts();
            for (int i = 0; i < particlesystem->GetBodyContactCount(); i++) {
                auto &c = contacts[i];
                auto pc = ((PhysicsObject *)c.fixture->GetUserData())->particle_contacts;
                if (pc) pc->push_back(c.index);
            }
        }
        return Value();
    });

nfr("ph_particle_contacts", "id", "R", "I]",
    "gets the particle indices that are currently contacting a giving physics object."
    " Call after step(). Indices may be invalid after next step().",
    [](VM &vm, Value &id) {
        CheckPhysics();
        auto &po = GetObject(vm, id);
        if (!po.particle_contacts) po.particle_contacts = new vector<int>();
        auto numelems = (int)po.particle_contacts->size();
        auto v = vm.NewVec(numelems, numelems, TYPE_ELEM_VECTOR_OF_INT);
        for (int i = 0; i < numelems; i++) v->At(i) = Value((*po.particle_contacts)[i]);
        return Value(v);
    });

nfr("ph_raycast", "p1,p2,n", "F}:2F}:2I", "I]",
    "returns a vector of the first n particle ids that intersect a ray from p1 to p2,"
    " not including particles that overlap p1.",
    [](VM &vm) {
        CheckPhysics();
        auto n = vm.Pop().ival();
        auto p2v = PopB2(vm);
        auto p1v = PopB2(vm);
        auto v = vm.NewVec(0, max(n, (intp)1), TYPE_ELEM_VECTOR_OF_INT);
        if (!particlesystem) { vm.Push(v); return; }
        struct callback : b2RayCastCallback {
            LVector *v;
            VM &vm;
            float ReportParticle(const b2ParticleSystem *, int i, const b2Vec2 &, const b2Vec2 &,
                                 float) {
                v->Push(vm, Value(i));
                return v->len == v->maxl ? -1.0f : 1.0f;
            }
            float ReportFixture(b2Fixture *, const b2Vec2 &, const b2Vec2 &, float) {
                return -1.0f;
            }
            callback(LVector *_v, VM &vm) : v(_v), vm(vm) {}
        } cb(v, vm);
        particlesystem->RayCast(&cb, p1v, p2v);
        vm.Push(v);
    });

nfr("ph_delete_particle", "i", "I", "",
    "deletes given particle. Deleting particles causes indices to be invalidated at next"
    " step().",
    [](VM &, Value &i) {
        CheckPhysics();
        particlesystem->DestroyParticle(i.intval());
        return Value();
    });

nfr("ph_getparticle_position", "i", "I", "F}:2",
    "gets a particle's position.",
    [](VM &vm) {
        CheckPhysics();
        auto pos = B2ToFloat2(particlesystem->GetPositionBuffer()[vm.Pop().ival()]);
        vm.PushVec(pos);
    });

nfr("ph_render", "", "", "",
    "renders all rigid body objects.",
    [](VM &) {
        CheckPhysics();
        auto oldobject2view = otransforms.object2view;
        auto oldcolor = curcolor;
        for (b2Body *body = world->GetBodyList(); body; body = body->GetNext()) {
            auto pos = body->GetPosition();
            auto mat = translation(float3(pos.x, pos.y, 0)) * rotationZ(body->GetAngle());
            otransforms.object2view = oldobject2view * mat;
            for (b2Fixture *fixture = body->GetFixtureList(); fixture;
                 fixture = fixture->GetNext()) {
                auto shapetype = fixture->GetType();
                auto &r = ((PhysicsObject *)fixture->GetUserData())->r;
                curcolor = r.color;
                switch (shapetype) {
                    case b2Shape::e_polygon: {
                        r.Set();
                        auto polyshape = (b2PolygonShape *)fixture->GetShape();
                        RenderArraySlow(
                            PRIM_FAN, make_span(polyshape->m_vertices, polyshape->m_count), "pn",
                            span<int>(), make_span(polyshape->m_normals, polyshape->m_count));
                        break;
                    }
                    case b2Shape::e_circle: {
                        r.sh->SetTextures(r.textures);  // FIXME
                        auto polyshape = (b2CircleShape *)fixture->GetShape();
                        Transform2D(translation(float3(B2ToFloat2(polyshape->m_p), 0)), [&]() {
                            geomcache->RenderCircle(r.sh, PRIM_FAN, 20, polyshape->m_radius);
                        });
                        break;
                    }
                    case b2Shape::e_edge:
                    case b2Shape::e_chain:
                    case b2Shape::e_typeCount: assert(0); break;
                }
            }
        }
        otransforms.object2view = oldobject2view;
        curcolor = oldcolor;
        return Value();
    });

nfr("ph_render_particles", "scale", "F", "",
    "render all particles, with the given scale.",
    [](VM &, Value &particlescale) {
        CheckPhysics();
        if (!particlesystem) return Value();
        // LOG_DEBUG("rendering particles: ", particlesystem->GetParticleCount());
        auto verts = (float2 *)particlesystem->GetPositionBuffer();
        auto colors = (byte4 *)particlesystem->GetColorBuffer();
        auto scale = length(otransforms.object2view[0].xy());
        SetPointSprite(scale * particlesystem->GetRadius() * particlescale.fltval());
        particlematerial->Set();
        RenderArraySlow(PRIM_POINT, make_span(verts, particlesystem->GetParticleCount()), "pC",
                        span<int>(), make_span(colors, particlesystem->GetParticleCount()));
        return Value();
    });

}  // AddPhysics
