#include <stdio.h>
#include <stdlib.h>
#include <assert.h>
#include <math.h>
#include <stdbool.h>
#include <unistd.h>
#include <termios.h>
#include <limits.h>
#include <time.h>

#include "hash.h"
#include "base.h"
#include "entities.h"
#include "proj.h"
#include "move.h"

// ---------------------------------------------------------------------------------------------------- //
double sensitivity = 0.06;
double fov = 90.0;
double creative_speed = 0.5;
double speed = 6.0;
double vtmult = 2.25;
double min_dist = 0.4;
double friction = 0.3;
double gravity_factor = 25.0;
// ---------------------------------------------------------------------------------------------------- //

bool is_clipping = false;
int clip_dps = 500;

int njumps;
double fx;
double fy;
double fz;
bool updateForces = false;

int player_hp;

bool stop_evetything;

double camx;
double camy;
double camz;

double camvx;
double camvy;
double camvz;
double rot_hz;
double rot_vt;

double tan_fov;

int draw_type;
int fade_dmg;

bool has_changed;
int collisionVal = 0;

double room_width;
double room_depth;

double sq2;

float dmgCD;
int lastDmg;

bool buttonSwitch[16];
float buttonTimes[16];

void init_csts() {
    camx = 2.0;
    camy = 5.0;
    camz = 2.0;
    player_chx = 0;
    player_chy = 0;
    camvx = 0.0;
    camvy = 0.0;
    camvz = 0.0;
    rot_hz = 0.0;
    rot_vt = 0.0;

    room_width = 16.0;
    room_depth = 16.0;

    coins = 0;
    player_hp = 1000;
    njumps = 1;

    draw_type = 0;
    fade_dmg = 0;

    for(int k = 0; k < 16; k++) {
        buttonTimes[k] = 0.0f;
        buttonSwitch[k] = false;
    }

    dmgCD = 0.0f;
    lastDmg = player_hp;

    stop_evetything = false;
    tan_fov = tan((fov * 3.14159 / 180.0) / 2.0);
    sq2 = sqrt(2);
}

void update_buttons(float dtime) {
    for(int k = 0; k < 16; k++) {
        if(buttonSwitch[k]) {
            if(buttonTimes[k] <= dtime) {
                buttonTimes[k] = 0.0f;
                buttonSwitch[k] = false;
            } else {
                buttonTimes[k] -= dtime;
            }
        }
    }
}

void set_player_coords(int old_chx, int old_chy) {
    for(int k = 0; k < current_room->tps_size; k++) {
        if(current_room->tps[k]->dest_chx == old_chx && current_room->tps[k]->dest_chy == old_chy) {
            if(true) {
                camx = current_room->tps[k]->hitbox->x + current_room->tps[k]->hitbox->w/2.0;
                camy = current_room->tps[k]->hitbox->y + current_room->tps[k]->hitbox->h +1.5;
                camz = current_room->tps[k]->hitbox->z + current_room->tps[k]->hitbox->d/2.0;
            }
            return;
        }
    }
    // no TP has been found, placing to the middle of the room
    camx = 0.0;
    camy = 10.0;
    camz = 0.0;
}

pt_2d surface[3];
pt_2d directors[2];
pt_2d normal;
double p1, p2, p3;
void getSF(cube_0* cb, int sf) {
    if(sf == 0 || sf == 1) {
        project_to_cube(cb->x + (sf==0)*(cb->w), cb->y        , cb->z        , &p1, &p2, &p3, cb);
                                                                                                                                                                surface[0] = (pt_2d){.x = p1, .y = p2, .z = p3};
        project_to_cube(cb->x + (sf==0)*(cb->w), cb->y + cb->h, cb->z        , &p1, &p2, &p3, cb);
                                                                                                                                                                surface[1] = (pt_2d){.x = p1, .y = p2, .z = p3};
        project_to_cube(cb->x + (sf==0)*(cb->w), cb->y        , cb->z + cb->d, &p1, &p2, &p3, cb);
                                                                                                                                                                surface[2] = (pt_2d){.x = p1, .y = p2, .z = p3};
    } else if(sf == 2 || sf == 3) {
        project_to_cube(cb->x        , cb->y + (sf==2)*(cb->h), cb->z        , &p1, &p2, &p3, cb);
                                                                                                                                                                surface[0] = (pt_2d){.x = p1, .y = p2, .z = p3};
        project_to_cube(cb->x + cb->w, cb->y + (sf==2)*(cb->h), cb->z        , &p1, &p2, &p3, cb);
                                                                                                                                                                surface[1] = (pt_2d){.x = p1, .y = p2, .z = p3};
        project_to_cube(cb->x        , cb->y + (sf==2)*(cb->h), cb->z + cb->d, &p1, &p2, &p3, cb);
                                                                                                                                                                surface[2] = (pt_2d){.x = p1, .y = p2, .z = p3};
    } else {
        project_to_cube(cb->x        , cb->y        , cb->z + (sf==4)*(cb->d), &p1, &p2, &p3, cb);
                                                                                                                                                                surface[0] = (pt_2d){.x = p1, .y = p2, .z = p3};
        project_to_cube(cb->x + cb->w, cb->y        , cb->z + (sf==4)*(cb->d), &p1, &p2, &p3, cb);
                                                                                                                                                                surface[1] = (pt_2d){.x = p1, .y = p2, .z = p3};
        project_to_cube(cb->x        , cb->y + cb->h, cb->z + (sf==4)*(cb->d), &p1, &p2, &p3, cb);
                                                                                                                                                                surface[2] = (pt_2d){.x = p1, .y = p2, .z = p3};
    }
}

void getDirectors() {
    directors[0] = (pt_2d){.x = surface[1].x - surface[0].x, .y = surface[1].y - surface[0].y, .z = surface[1].z - surface[0].z};
    directors[1] = (pt_2d){.x = surface[2].x - surface[0].x, .y = surface[2].y - surface[0].y, .z = surface[2].z - surface[0].z};
}

void getNormal() {
    normal.x =  directors[0].y*directors[1].z - directors[1].y*directors[0].z;
    normal.y = -directors[0].z*directors[1].x + directors[1].z*directors[0].x;
    normal.z =  directors[0].x*directors[1].y - directors[1].x*directors[0].y;
    double norm = sqrt(normal.x*normal.x + normal.y*normal.y + normal.z*normal.z);
    normal.x /= norm;
    normal.y /= norm;
    normal.z /= norm;
}

void normalize(pt_2d* p) {
    double norm = sqrt(p->x*p->x + p->y*p->y + p->z*p->z);
    p->x /= norm;
    p->y /= norm;
    p->z /= norm;
}

void updateF(cube_0* cb, double dtime) {
    for(int d = 0; d < 6; d++) {
        cb->x -= min_dist; 
        cb->y -= min_dist;
        cb->z -= min_dist;
        cb->w += 2*min_dist;
        cb->h += 2*min_dist;
        cb->d += 2*min_dist;
        getSF(cb, d);
        cb->x += min_dist;
        cb->y += min_dist;
        cb->z += min_dist;
        cb->w -= 2*min_dist;
        cb->h -= 2*min_dist;
        cb->d -= 2*min_dist;
        getDirectors();
        getNormal();
        if(d%2==1) {
            normal.x *= -1.0;
            normal.y *= -1.0;
            normal.z *= -1.0;
        }
        pt_2d vt = (pt_2d){.x = camx-camvx*dtime - surface[0].x, .y = camy-camvy*dtime - surface[0].y, .z = camz-camvz*dtime - surface[0].z};
        pt_2d vtdt = (pt_2d){.x = camx - surface[0].x, .y = camy - surface[0].y, .z = camz - surface[0].z};
        normalize(&vtdt);
        normalize(&vt);
        if(
            (dot3D(vt, normal) <= 0.0 && dot3D(vtdt, normal) >= 0.0) || 
            (dot3D(vt, normal) >= 0.0 && dot3D(vtdt, normal) <= 0.0)
        ) {
            //printf("%d\n", d);
            double normv = sqrt(camvx*camvx + camvy*camvy + camvz*camvz);

            double alpha = acos(dot3D(normal, (pt_2d){.x = camvx, .y = camvy, .z = camvz})/normv);
            double beta = 3.1415926535 - 2*alpha;
            double nu = normv*sqrt(1-cos(beta));

            pt_2d u = (pt_2d){.x = normal.x*nu, .y = normal.y*nu, .z = normal.z*nu};

            camvx += u.x;
            camvy += u.y;
            camvz += u.z;

            //camvx /= 1.41;
            //camvy /= 1.41;
            //camvz /= 1.41;

            is_clipping = false;
        }
    }
}

bool is_colliding(float dtime) {
    bool globalCollision = false;
    for(int w = -1; w <= 1; w++) {
        for(int h = -1; h <= 1; h++) {
            room* vstd = hashtbl_find_opt(visited, player_chx+w, player_chy+h);
            if(vstd != NULL) {
                for(int k = 0; k < vstd->map_size; k++) {
                    double dist = distance_pt_cube_0_3d_infinite(camx-2*room_width*w, camy, camz-2*room_depth*h, vstd->map[k]);
                    if(dist <= min_dist) {
                        if(updateForces) {
                            updateF(vstd->map[k], (double)dtime);
                        }
                        globalCollision = true;
                    }
                }

                for(int k = 0; k < vstd->tps_size; k++) {
                    double dist = distance_pt_cube_0_3d_infinite(camx-2*room_width*w, camy, camz-2*room_depth*h, vstd->tps[k]->hitbox);
                    if(dist <= min_dist) {
                        if(updateForces) {
                            updateF(vstd->tps[k]->hitbox, (double)dtime);
                        }
                        int old_chx = player_chx;
                        int old_chy = player_chy;
                        player_chx = vstd->tps[k]->dest_chx;
                        player_chy = vstd->tps[k]->dest_chy;
                        vstd = hashtbl_find_opt(visited, player_chx, player_chy);
                        set_player_coords(old_chx, old_chy);
                        resetProj();
                        globalCollision = true;
                    }
                }

                for(int k = 0; k < vstd->ent_len; k++) {
                    //printf("%d -> %d\n", k, vstd->ents[k]->entity_type);
                    if(    
                        (vstd->ents[k]->entity_type != 12 || xor(buttonSwitch[vstd->ents[k]->metai1], vstd->ents[k]->metai2)) &&
                        (vstd->ents[k]->entity_type != 9 || vstd->ents[k]->metai1)
                    ) {
                        double dist = distance_pt_cube_0_3d_infinite(camx-2*room_width*w, camy, camz-2*room_depth*h, vstd->ents[k]->pos);
                        //printf("%lf vs %lf\n", dist, min_dist);
                        if(dist <= min_dist) {
                            bool exists = true;
                            if(vstd->ents[k]->onHit != NULL) {
                                (*vstd->ents[k]->onHit)(dtime, vstd->ents[k]->hitpoints, &vstd->ents[k]->damage, vstd->ents[k], &(*(vstd->ents[k]->pos)));
                                if(*(vstd->ents[k]->hitpoints) <= 0) {
                                    if(vstd->ents[k]->onDeath != NULL) {
                                        (*vstd->ents[k]->onDeath)(dtime);
                                    }
                                    remove_entity(vstd->ents, &vstd->ent_memlen, &vstd->ent_len, k);
                                    is_clipping = false;
                                    exists = false;
                                    k =- 1;
                                }
                            }
                            if(exists && updateForces && vstd->ents[k]->entity_type != 0) {
                                updateF(vstd->ents[k]->pos, (double)dtime);
                            }
                            if(exists && (vstd->ents[k]->entity_type == 9 || vstd->ents[k]->entity_type == 4 || vstd->ents[k]->entity_type == 5)) {
                                is_clipping = false;
                            }
                            globalCollision = true;
                        }
                    }
                }
            }
        }
    }
    return globalCollision;
}

void movePlayerG(float dtime) {
    updateForces = true;
    fx = 0.0;
    fy = 0.0;
    fz = 0.0;

    camvy -= gravity_factor*dtime;
    bool isfalling = camvy < 0.0;

    double delx = camvx*dtime;
    double dely = camvy*dtime;
    double delz = camvz*dtime;
    camx += delx;
    camy += dely;
    camz += delz;
    is_clipping = true;
    if(is_colliding(dtime)) {
        if(is_clipping) {
            player_hp -= (dtime)*clip_dps;
        }
        //printf("HIT\n");
        //printf("[%lf, %lf, %lf]\n{%lf, %lf, %lf}\n\n", fx, fy, fz, camvx, camvy, camvz);
    }
    camx -= delx;
    camy -= dely;
    camz -= delz;
    //printf("%lf | %lf | %lf\n", fx, fy, fz);

    updateForces = false;
    camvx += fx*dtime;
    camvy += fy*dtime;
    camvz += fz*dtime;
    //printf("%lf | %lf | %lf\n\n", camvx, camvy, camvz);
    if(isfalling && camvy > 0.0) {
        njumps = 1;
    }
    camx += camvx*dtime;
    camy += camvy*dtime;
    camz += camvz*dtime;

    camvx *= (1.0 - friction*((double)(dtime)));
    camvy *= (1.0 - friction*((double)(dtime)));
    camvz *= (1.0 - friction*((double)(dtime)));

    if(camy <= -48) {
        camx = 0.0;
        camz = 0.0;
        camy = 5.0;
        camvx = 0.0;
        camvy = 0.0;
        camvz = 0.0;
        player_chx = 0;
        player_chy = 0;
        player_hp -= 250;
        current_room = hashtbl_find_opt(visited, player_chx, player_chy);
        resetProj();
    }
}

void teleport_on_edge() {
    if(camx >= room_width) {
        if(!is_one_room) {
            camx -= 2.0*room_width;
            player_chx += 1;
            current_room = hashtbl_find_opt(visited, player_chx, player_chy);
            resetProj();
            for(int k = 0; k < 16; k++) {
                buttonSwitch[k] = false;
                buttonTimes[k] = 0.0f;
            }
        }
    } else if(camx <= -room_width) {
        if(!is_one_room) {
            camx += 2.0*room_width;
            player_chx -= 1;
            current_room = hashtbl_find_opt(visited, player_chx, player_chy);
            resetProj();
            for(int k = 0; k < 16; k++) {
                buttonSwitch[k] = false;
                buttonTimes[k] = 0.0f;
            }
        }
    } else if(camz >= room_depth) {
        if(!is_one_room) {
            camz -= 2.0*room_depth;
            player_chy += 1;
            current_room = hashtbl_find_opt(visited, player_chx, player_chy);
            resetProj();
            for(int k = 0; k < 16; k++) {
                buttonSwitch[k] = false;
                buttonTimes[k] = 0.0f;
            }
        }
    } else if(camz <= -room_depth) {
        if(!is_one_room) {
            camz += 2.0*room_depth;
            player_chy -= 1;
            current_room = hashtbl_find_opt(visited, player_chx, player_chy);
            resetProj();
            for(int k = 0; k < 16; k++) {
                buttonSwitch[k] = false;
                buttonTimes[k] = 0.0f;
            }
        }
    }
}