//
//  Lol Engine
//
//  Copyright © 2010—2015 Sam Hocevar <sam@hocevar.net>
//            © 2013—2015 Benjamin "Touky" Huet <huet.benjamin@gmail.com>
//
//  This library is free software. It comes without any warranty, to
//  the extent permitted by applicable law. You can redistribute it
//  and/or modify it under the terms of the Do What the Fuck You Want
//  to Public License, Version 2, as published by the WTFPL Task Force.
//  See http://www.wtfpl.net/ for more details.
//

#pragma once

#include <lol/base/array.h>
#include <lol/debug/lines.h>
#include <lol/image/color.h>

#include <cfloat> /* for FLT_MAX */

namespace lol
{

//------ PORTAL SYSTEM --------
template <typename TE> class PortalRoom;
template <typename TE> class PortalDoor;
template <typename TE> class PortalSet;

//--
namespace Debug {
template <typename TE, typename TV = void>
void Draw(PortalDoor<TE>& port, vec4 color)
{
    vec3 points[4]; port.GetPoints(points);

    // Draw normal
    vec3 p = port.m_center + port.m_up * port.m_size.y * .5f;
    Debug::DrawLine(p, p + port.m_normal, Color::red);
    Debug::DrawLine(p, p + port.m_up, Color::green);
    // Draw door
    for (int l = 0; l < 4; l++)
        Debug::DrawLine(points[l], points[(l + 1) % 4], color);
    Debug::DrawLine(points[0], points[2], color);
    Debug::DrawLine(points[1], points[3], color);
}
}

//PortalDoor base class
template <typename TE>
class PortalDoor
{
    friend class PortalSet<TE>;
    friend class PortalRoom<TE>;
    friend void Debug::Draw<TE,void>(PortalDoor<TE>& port, vec4 color);

private:
    void Init()
    {
        m_center    = vec3::zero;
        m_normal    = vec3::zero;
        m_up        = vec3::zero;
        m_size      = vec2::zero;

        m_view      = mat4::identity;
        m_proj      = mat4::identity;

        m_rooms[0]  = nullptr;
        m_rooms[1]  = nullptr;
    }

public:
    //Normal portal
    PortalDoor(vec3 center, vec3 normal, vec3 up, vec2 size)
    {
        Init();

        m_center = center;
        m_normal = normal;
        m_up     = up;
        m_size   = size;
    }
    //Camera  portal
    PortalDoor(mat4 view, mat4 proj)
    {
        Init();

        m_view = view;
        m_proj = proj;
    }
    //D.Tor
    ~PortalDoor()
    {
        ConnectRooms(nullptr, nullptr);
    }

    //Connect door to room
    void ConnectRooms(class PortalRoom<TE>* front_room, class PortalRoom<TE>* back_room)
    {
        for (int i = 0; i < 2; i++)
            if (m_rooms[i] != nullptr)
                *m_rooms[i] >> this;

        m_rooms[0] = back_room;
        m_rooms[1] = front_room;

        for (int i = 0; i < 2; i++)
            if (m_rooms[i] != nullptr)
               *m_rooms[i] << this;
    }

    //--
    void DisconnectRoom(class PortalRoom<TE>* room)
    {
        for (int i = 0; i < 2; i++)
            if (m_rooms[i] != nullptr && m_rooms[i] == room)
                m_rooms[i] = nullptr;
    }

    //--
    PortalRoom<TE>* GetRoom(bool front) { return m_rooms[(int)front]; }
    PortalRoom<TE>* GetRoom(PortalRoom<TE>* room) { return (m_rooms[0] == room) ? m_rooms[1] : m_rooms[0]; }

    //Get Four portal point
    void GetPoints(vec3 *points) const
    {
        vec3 right = cross(m_normal, m_up);
        points[0] = m_center + right * m_size.x *  .5f + m_up * m_size.y;
        points[1] = m_center + right * m_size.x *  .5f;
        points[2] = m_center + right * m_size.x * -.5f;
        points[3] = m_center + right * m_size.x * -.5f + m_up * m_size.y;
    }
    //Builds the portal view proj.
    //Returns false if portal is out of the view or points are on each others.
    bool BuildViewProj(mat4 view, mat4 proj)
    {
        mat4 cam_mx = proj * view;
        mat4 inv_proj_mx = inverse(proj);

        // First: Check normal dot
        if (lol::abs(dot(mat3(inverse(view)) * vec3(0.f, 0.f, 1.f), m_normal)) < .00001f)
            return false;

        // Second: convert to screen coordinates
        vec3 port_2d[2] = { vec3(FLT_MAX), vec3(-FLT_MAX) };
        vec3 door_points[4];
        vec4 proj_p[4];

        GetPoints(door_points);

        for (int i = 0; i < 4; i++)
        {
            //W to S calculations
            proj_p[i] = cam_mx * vec4(door_points[i], 1.f);
            proj_p[i] /= proj_p[i].w;

            //Clamp points within screen
            port_2d[0] = lol::min(proj_p[i].xyz, port_2d[0]);
            port_2d[1] = lol::max(proj_p[i].xyz, port_2d[1]);
            port_2d[0] = vec3(lol::clamp(port_2d[0].xy, vec2(-1.f), vec2(1.f)), port_2d[0].z);
            port_2d[1] = vec3(lol::clamp(port_2d[1].xy, vec2(-1.f), vec2(1.f)), port_2d[1].z);
        }

        //Quit if door not within the screen
        for (int i = 0; i < 3; i++)
            if (port_2d[0][i] == port_2d[1][i])
                return false;

        //Third: Convert back to view
        ivec2 j[4] = { ivec2(0), ivec2(0, 1), ivec2(1), ivec2(1, 0) };
        vec3 frust[2] = { vec3(FLT_MAX), vec3(-FLT_MAX) };
        for (int i = 0; i < 5; i++)
        {
            int k = i % 4;
            //world calculations
            proj_p[k] = inv_proj_mx * vec4(port_2d[j[k].x].x, port_2d[j[k].y].y, (i<4)?(port_2d[0].z):(1.f), 1.f);
            proj_p[k] /= proj_p[k].w;
            proj_p[k].z = lol::abs(proj_p[k].z);

            for (int h = 0; h < 3; h++)
            {
                if (i < 4 || h > 1)
                {
                    frust[0][h] = lol::min(frust[0][h], proj_p[k][h]);
                    frust[1][h] = lol::max(frust[1][h], proj_p[k][h]);
                }
            }
        }

        //Fourth: Create frustum
        m_proj = mat4::frustum(frust[0].x, frust[1].x, frust[0].y, frust[1].y, frust[0].z, frust[1].z);
        m_view = view;

        return true;
    }

    //View proj getter (doesn't check matrix validity)
    mat4 GetViewProj() { return m_proj * m_view; }

    //--
    bool TestCollide(const vec3& point)
    {
        return TestPointVsFrustum(point, GetViewProj());
    }

    //--
    bool TestCollide(const PortalDoor& door)
    {
        vec3 door_points[4];
        vec3 res_points[4];
        ivec3 pos_test = ivec3::zero;
        bool is_in = false;

        //Get points and test them on frustum
        door.GetPoints(door_points);
        for (int i = 0; i < 4; i++)
        {
            is_in = is_in || TestPointVsFrustum(door_points[i], GetViewProj(), &res_points[i]);

            if (is_in)
                return true;

            //Add points on test stuff
            pos_test += ivec3(lol::clamp(res_points[i], vec3(-1.1f), vec3(1.1f)));
        }

        return false;

        //Check if at least one point is not on the same side as the others
        for (int i = 0; i < 3; i++)
            if (lol::abs(pos_test[i]) == 4)
                return false;

        return true;
    }

private:
    mat4                        m_view;
    mat4                        m_proj;
    vec3                        m_center;
    vec3                        m_normal;
    vec3                        m_up;
    vec2                        m_size;
    PortalRoom<TE>*             m_rooms[2]; //0: Back, 1: Front
};

//--
template <typename TE>
class PortalRoom
{
    friend class PortalSet<TE>;
    friend class PortalDoor<TE>;

public:
    PortalRoom(TE* element=nullptr)
    {
        m_element = element;
    }
    ~PortalRoom()
    {
        for (auto door : m_doors)
            door->DisconnectRoom(this);
        m_doors.Empty();
    }

    PortalRoom& operator<<(class PortalDoor<TE>* door)
    {
        m_doors.PushUnique(door);
        return *this;
    }
    PortalRoom& operator>>(class PortalDoor<TE>* door)
    {
        m_doors.RemoveSwapItem(door);
        return *this;
    }

    int GetDoorCount()              { return m_doors.Count(); }
    PortalDoor<TE>* GetDoor(int i)  { return m_doors[i]; }

private:
    //Portals associated with this room.
    array<PortalDoor<TE>*>      m_doors;
    TE*                         m_element;
};

//--
template <typename TE>
class PortalSet
{
public:
    ~PortalSet()
    {
        for (auto door : m_doors)
            delete door;
        for (auto room : m_rooms)
            delete room;
        m_doors.Empty();
        m_rooms.Empty();
    }

    //Visible room getter
    void GetVisibleRooms(PortalDoor<TE>* see_through, PortalRoom<TE>* start_room, array<PortalRoom<TE>*>& visible_rooms)
    {
        array<PortalDoor<TE>*> ignore_doors;
        GetVisibleRooms(see_through, start_room, visible_rooms, ignore_doors);
    #if LOL_BUILD_DEBUG
        for (auto room : visible_rooms)
        {
            vec4 tmp = vec4::zero;
            for (auto port : room->m_doors)
            {
                Debug::Draw(*port, Color::cyan);
                tmp += vec4(port->m_center, 1.f);
            }
            tmp /= tmp.w;
            Debug::DrawBox(tmp.xyz - vec3(1.f), tmp.xyz + vec3(1.f), Color::yellow);
        }
        for (auto port : ignore_doors)
        {
            Debug::Draw(*port, Color::magenta);
            Debug::DrawViewProj(port->m_view, port->m_proj, Color::magenta);
        }
    #endif //LOL_BUILD_DEBUG
    }
private:
    void GetVisibleRooms(PortalDoor<TE>* see_through, PortalRoom<TE>* start_room, array<PortalRoom<TE>*>& visible_rooms, array<PortalDoor<TE>*>& ignore_doors)
    {
        for (auto door : start_room->m_doors)
        {
            if (door == see_through || ignore_doors.Find(door) != INDEX_NONE)
                continue;

            if (see_through->TestCollide(*door))
            {
                PortalRoom<TE>* other_room = door->GetRoom(start_room);
                if (visible_rooms.Find(other_room) != INDEX_NONE)
                    continue;

                ignore_doors.PushUnique(door);
                visible_rooms.PushUnique(other_room);
                door->BuildViewProj(see_through->m_view, see_through->m_proj);
                GetVisibleRooms(door, other_room, visible_rooms, ignore_doors);
            }
        }
    }
public:

    //Operator
    PortalSet<TE>& operator<<(class PortalRoom<TE>* room)
    {
        m_rooms.PushUnique(room);
        for (auto door : room->m_doors)
            m_doors.PushUnique(door);
        return *this;
    }
    //--
    PortalSet<TE>& operator>>(class PortalRoom<TE>* room)
    {
        for (auto door : room->m_doors)
            *this >> door;
        m_rooms.RemoveItem(room);
        return *this;
    }
    //--
    PortalSet<TE>& operator<<(class PortalDoor<TE>* door)
    {
        m_doors.PushUnique(door);
        return *this;
    }
    //--
    PortalSet<TE>& operator>>(class PortalDoor<TE>* door)
    {
        m_doors.RemoveItem(door);
        return *this;
    }

    //--
    int GetDoorCount()              { return m_doors.Count(); }
    PortalDoor<TE>* GetDoor(int i)  { return m_doors[i]; }
    int GetRoomCount()              { return m_rooms.Count(); }
    PortalRoom<TE>* GetRoom(int i)  { return m_rooms[i]; }

private:
    //Portals associated with this room.
    array<PortalRoom<TE>*>          m_rooms;
    array<PortalDoor<TE>*>          m_doors;
};

} /* namespace lol */

