#include "twn_game_api.h"
#include "state.h"
#include "twn_vec.h"

#include <SDL.h>

/* planned features: */
/* grid-based, bounded space (65536 / POINTS_PER_METER meters), which allows for efficient storage */
/* 65534 point limit, 16 object limit, 2048 faces per object, 32 textures per object */
/* triangles and quads only */
/* texture painting */
/* bones with mesh animations, snapping to grid, with no weights */
/* billboard render to specified angles */
/* 1 point light primitive lighting */

static State state;
static bool init;


static uint8_t new_object(const char *name) {
    if (state.objects_sz >= OBJECT_LIMIT)
        return INVALID_OBJECT;
    state.objects_sz++;
    state.objects = SDL_realloc(state.objects, state.objects_sz * sizeof (*state.objects));
    SDL_memset(&state.objects[state.objects_sz-1], 0, sizeof (Object));
    state.objects[state.objects_sz-1].name = SDL_strdup(name);
    return state.objects_sz-1;
}


static uint16_t new_point(int16_t x, int16_t y, int16_t z) {
    if (state.points_sz >= POINT_LIMIT)
        return INVALID_POINT;
    state.points_sz++;
    state.points[state.points_sz-1] = (Point){x, y, z};
    return state.points_sz-1;
}


static uint16_t push_face(uint8_t object, uint16_t p0, uint16_t p1, uint16_t p2, uint16_t p3, uint8_t texture) {
    Object *o = &state.objects[object];
    o->faces_sz++;
    o->faces[o->faces_sz-1] = (Face) {.p = {p0, p1, p2, p3}, .texture = texture};
    return o->faces_sz-1;
}


static uint8_t push_texture(uint8_t object, char *texture) {
    /* TODO: search and combine if it already exists */
    Object *o = &state.objects[object];
    o->textures_sz++;
    o->textures[o->textures_sz-1] = SDL_strdup(texture);
    return o->textures_sz-1;
}


static void push_operation(Operation operation, bool active) {
    state.op_stack_ptr++;
    uint8_t op = state.op_stack_ptr % UNDO_STACK_SIZE;
    state.op_stack[op] = operation;
    state.op_active = active;
}


static inline Vec3 point_to_vec3(uint8_t object, uint16_t point) {
    Object *o = &state.objects[object];
    return (Vec3){ (o->position.x + state.points[point].x) / (float)POINTS_PER_METER,
                   (o->position.y + state.points[point].y) / (float)POINTS_PER_METER,
                   (o->position.z + state.points[point].z) / (float)POINTS_PER_METER };
}


static void render_object(uint8_t object) {
    Object *o = &state.objects[object];

    if (o->is_invisible)
        return;

    for (uint16_t fi = 0; fi < o->faces_sz; ++fi) {
        Face *f = &o->faces[fi];
        /* quads */
        if (f->p[3] != INVALID_POINT) {
            Vec3 p0 = point_to_vec3(object, f->p[0]);
            Vec3 p1 = point_to_vec3(object, f->p[1]);
            Vec3 p2 = point_to_vec3(object, f->p[2]);
            Vec3 p3 = point_to_vec3(object, f->p[3]);

            if (state.solid_display_mode)
                draw_quad(o->textures[f->texture],
                          p0, p1, p2, p3,
                          (Rect){0,0,32,32},
                          (Color){255,255,255,255} );
            else {
                draw_line_3d(p0, p1, 1, (Color){255,255,255,255});
                draw_line_3d(p1, p2, 1, (Color){255,255,255,255});
                draw_line_3d(p2, p3, 1, (Color){255,255,255,255});
                draw_line_3d(p3, p0, 1, (Color){255,255,255,255});
            }
        } else {
            /* triangles */
            /* TODO: */
            SDL_assert_always(false);
        }
    }
}


static uint8_t new_cube(Point pos, Point size) {
    uint8_t object = new_object("cube");

    uint16_t p0 = new_point(pos.x - size.x / 2, pos.y - size.y / 2, pos.z - size.z / 2);
    uint16_t p1 = new_point(pos.x - size.x / 2, pos.y + size.y / 2, pos.z - size.z / 2);
    uint16_t p2 = new_point(pos.x + size.x / 2, pos.y + size.y / 2, pos.z - size.z / 2);
    uint16_t p3 = new_point(pos.x + size.x / 2, pos.y - size.y / 2, pos.z - size.z / 2);
    uint16_t p4 = new_point(pos.x - size.x / 2, pos.y - size.y / 2, pos.z + size.z / 2);
    uint16_t p5 = new_point(pos.x - size.x / 2, pos.y + size.y / 2, pos.z + size.z / 2);
    uint16_t p6 = new_point(pos.x + size.x / 2, pos.y + size.y / 2, pos.z + size.z / 2);
    uint16_t p7 = new_point(pos.x + size.x / 2, pos.y - size.y / 2, pos.z + size.z / 2);

    uint8_t tex = push_texture(object, "/data/placeholder.png");
    push_face(object, p0, p1, p2, p3, tex);
    push_face(object, p7, p6, p5, p4, tex);
    push_face(object, p4, p5, p1, p0, tex);
    push_face(object, p6, p7, p3, p2, tex);
    push_face(object, p2, p1, p5, p6, tex);
    push_face(object, p0, p3, p7, p4, tex);

    return object;
}


static void process_camera_movement(void) {
    input_action("camera_rotate_left", "A");
    input_action("camera_rotate_right", "D");
    input_action("camera_rotate_up", "W");
    input_action("camera_rotate_down", "S");

    float horizontal_rotation = 0;
    float vertical_rotation = 0;

    if (input_action_pressed("camera_rotate_left"))
        horizontal_rotation -= CAMERA_ROTATION_SPEED;
    if (input_action_pressed("camera_rotate_right"))
        horizontal_rotation += CAMERA_ROTATION_SPEED;
    if (input_action_pressed("camera_rotate_up"))
        vertical_rotation -= CAMERA_ROTATION_SPEED;
    if (input_action_pressed("camera_rotate_down"))
        vertical_rotation += CAMERA_ROTATION_SPEED;

    Vec3 front = vec3_cross(state.camera_direction, (Vec3){0,1,0});

    state.camera_position = vec3_rotate(state.camera_position, horizontal_rotation, (Vec3){0,1,0});
    state.camera_direction = vec3_rotate(state.camera_direction, horizontal_rotation, (Vec3){0,1,0});

    Vec3 new_rot = vec3_rotate(state.camera_direction, vertical_rotation, front);

    /* only apply if it's in limits */
    float d = vec3_dot(new_rot, (Vec3){0,-1,0});
    if (fabsf(d) <= 0.999f) {
        state.camera_position = vec3_rotate(state.camera_position, vertical_rotation, front);
        state.camera_direction = new_rot;
    }
}


static inline DrawCameraUnprojectResult unproject_point(Vec2 point) {
    return draw_camera_unproject(
        point,
        state.camera_position,
        state.camera_direction,
        (Vec3){0, 1, 0},
        state.camera_is_orthographic ? 0 : CAMERA_FOV,
        state.camera_zoom,
        100 );
}


static bool find_closest_point(uint8_t* object_result, uint16_t *point_result) {
    DrawCameraUnprojectResult pos_and_ray = unproject_point(ctx.mouse_position);

    /* step over every selectable object and find points closest to the view ray */
    /* by constructing triangles and finding their height, from perpendicular */
    uint16_t closest_point = INVALID_POINT;
    uint8_t closest_obj = INVALID_OBJECT;
    float closest_distance = INFINITY;
    for (uint8_t obj = 0; obj < state.objects_sz; ++obj) {
        Object *o = &state.objects[obj];

        if (o->is_invisible)
            continue;

        /* TODO: is it possible to skip repeated points? does it matter? */
        /*       as we limit the point could we could actually have bool array preallocated for this */
        for (uint16_t fi = 0; fi < o->faces_sz; ++fi) {
            Face *f = &o->faces[fi];
            for (uint16_t pi = 0; pi < 4; ++pi) {
                Vec3 p = point_to_vec3(obj, f->p[pi]);
                Vec3 d = vec3_sub(pos_and_ray.position, p);
                Vec3 b = vec3_cross(d, pos_and_ray.direction);
                float ray_dist = vec3_length(b);
                if (ray_dist > ((float)SELECTION_SPHERE_RADIUS / POINTS_PER_METER))
                    continue;
                float dist = vec3_length(vec3_add(p, b));
                if (dist < closest_distance) {
                    closest_distance = dist;
                    closest_obj = obj;
                    closest_point = f->p[pi];
                }
            }
        }
    }

    if (closest_point == INVALID_POINT)
        return false;

    if (object_result)
        *object_result = closest_obj;
    if (point_result)
        *point_result = closest_point;

    return true;
}


static void process_operation_move_point(Operation *op) {
    /* finish dragging around */
    /* TODO: dont keep empty ops on stack? */
    if (input_action_just_released("select")) {
        state.op_active = false;
        return;
    }

    float size = sinf(ctx.frame_number / 10) * 0.05f + 0.05f;
    draw_billboard("/data/grab.png",
                   point_to_vec3(op->data.move_point.object, op->data.move_point.point),
                   (Vec2){size, size},
                   (Rect){0,0,16,16},
                   (Color){255,255,255,255},
                   false);

    DrawCameraUnprojectResult pos_and_ray = unproject_point(ctx.mouse_position);

    Vec3 p = point_to_vec3(op->data.move_point.object, op->data.move_point.point);
    Vec3 d = vec3_sub(pos_and_ray.position, p);
    Vec3 b = vec3_cross(d, pos_and_ray.direction);

    /* TODO: show thresholds */

    log_vec3(b, "cross");

    /* change along axes, delta is accumulated when threshold is met*/
    int16_t xch = (int16_t)(floorf(vec3_dot(b, (Vec3){0,0,1}) * (float)POINTS_PER_METER)) * !state.axis_mask[0];
    xch -= xch % state.grid_snap_granularity;
    state.points[op->data.move_point.point].x += xch;
    op->data.move_point.delta_x += xch;

    int16_t ych = (int16_t)(floorf(vec3_dot(vec3_cross(b, pos_and_ray.direction), (Vec3){0,-1,0}) * (float)POINTS_PER_METER)) * !state.axis_mask[1];
    ych -= ych % state.grid_snap_granularity;
    state.points[op->data.move_point.point].y += ych;
    op->data.move_point.delta_y += ych;

    int16_t zch = (int16_t)(floorf(vec3_dot(b, (Vec3){-1,0,0}) * (float)POINTS_PER_METER)) * !state.axis_mask[2];
    zch -= zch % state.grid_snap_granularity;
    state.points[op->data.move_point.point].z += zch;
    op->data.move_point.delta_z += zch;

    if (xch != 0 || ych != 0 || zch != 0)
        audio_play("/data/bong.ogg", NULL, false, 0.12f, 0.0f);
}


static void reverse_operation_move_point(Operation *op) {
    SDL_assert(op->kind == OPERATION_MOVE_POINT);
    state.points[op->data.move_point.point].x -= op->data.move_point.delta_x;
    state.points[op->data.move_point.point].y -= op->data.move_point.delta_y;
    state.points[op->data.move_point.point].z -= op->data.move_point.delta_z;
    audio_play("/data/drop.ogg", NULL, false, 0.4f, 0.0f);
}


static void process_operations(void) {
    if (input_action_just_pressed("undo")) {
        /* TODO: checks and defined limit */
        Operation *op = &state.op_stack[state.op_stack_ptr % UNDO_STACK_SIZE];
        state.op_stack_ptr--;

        switch (op->kind) {
        case OPERATION_MOVE_POINT: {
            reverse_operation_move_point(op);

            break;
        }

        default:
            (void)0;
        }
    }

    if (!state.op_active) {
        uint16_t point_select; uint8_t obj_select;
        if (find_closest_point(&obj_select, &point_select)) {
            draw_billboard("/data/point.png",
                           point_to_vec3(obj_select, point_select),
                           (Vec2){0.05f, 0.05f},
                           (Rect){0,0,16,16},
                           (Color){255,255,255,255},
                           false);

            if (input_action_just_pressed("select"))
                push_operation((Operation){
                    .kind = OPERATION_MOVE_POINT,
                    .data = { .move_point = { .point = point_select } },
                }, true );
        }
    }

    if (state.op_active) {
        Operation *op = &state.op_stack[state.op_stack_ptr % UNDO_STACK_SIZE];
        switch (op->kind) {
        case OPERATION_MOVE_POINT: {
            process_operation_move_point(op);

            break;
        }

        default:
            (void)0;
        }
    }
}


void game_tick(void) {
    if (!init) {
        /* default state */
        new_cube((Point){0,0,0}, (Point){POINTS_PER_METER,POINTS_PER_METER,POINTS_PER_METER});
        state.camera_position = (Vec3){2,1,2};
        state.camera_direction = vec3_norm(((Vec3){-2,-1,-2}));
        state.camera_zoom = 0.5f;
        state.grid_snap_granularity = 16;
        state.axis_mask[1] = 1; state.axis_mask[2] = 1;
        init = true;
    }

    input_action("toggle_display_mode", "Q");
    input_action("toggle_projection", "E");

    input_action("toggle_x_axis", "Z");
    input_action("toggle_y_axis", "X");
    input_action("toggle_z_axis", "C");

    input_action("select", "LCLICK");
    input_action("undo", "F");

    if (input_action_just_pressed("toggle_display_mode")) {
        audio_play("/data/click.wav", NULL, false, 0.7f, 0.0f);
        state.solid_display_mode = !state.solid_display_mode;
    }

    if (input_action_just_pressed("toggle_projection")) {
        audio_play("/data/pop.wav", NULL, false, 0.8f, 0.0f);
        state.camera_is_orthographic = !state.camera_is_orthographic;
    }

    if (input_action_just_pressed("toggle_x_axis")) {
        state.axis_mask[0] = 0; state.axis_mask[1] = 1; state.axis_mask[2] = 1;
    }

    if (input_action_just_pressed("toggle_y_axis")) {
        state.axis_mask[0] = 1; state.axis_mask[1] = 0; state.axis_mask[2] = 1;
    }

    if (input_action_just_pressed("toggle_z_axis")) {
        state.axis_mask[0] = 1; state.axis_mask[1] = 1; state.axis_mask[2] = 0;
    }

    process_camera_movement();
    process_operations();

    /* axis helpers */
    /* idea: black out inactives when dragging points */
    /* idea: double selection of axes for diagonal edits */
    draw_line_3d((Vec3){0}, (Vec3){256,0,0}, 1, state.axis_mask[0] ? (Color){0,0,0,75} : (Color){255,0,0,125});
    draw_billboard("/data/x.png",
                   (Vec3){2,0,0},
                   (Vec2){0.1f, 0.1f},
                   (Rect){0},
                   state.axis_mask[0] ? (Color){0,0,0,255} : (Color){255,0,0,255},
                   false);

    draw_line_3d((Vec3){0}, (Vec3){0,256,0}, 1, state.axis_mask[1] ? (Color){0,0,0,75} : (Color){75,125,25,125});
    draw_billboard("/data/y.png",
                   (Vec3){0,1.5f,0},
                   (Vec2){0.1f, 0.1f},
                   (Rect){0},
                   state.axis_mask[1] ? (Color){0,0,0,255} : (Color){75,125,25,255},
                   false);

    draw_line_3d((Vec3){0}, (Vec3){0,0,256}, 1, state.axis_mask[2] ? (Color){0,0,0,75} : (Color){0,0,255,125});
    draw_billboard("/data/z.png",
                   (Vec3){0,0,2},
                   (Vec2){0.1f, 0.1f},
                   (Rect){0},
                   state.axis_mask[2] ? (Color){0,0,0,255} : (Color){0,0,255,255},
                   false);

    for (uint8_t obj = 0; obj < state.objects_sz; ++obj)
        render_object(obj);

    draw_text("twndel\x03", (Vec2){0, 2}, 32, (Color){255,255,255,200}, NULL);
    draw_camera(
        state.camera_position,
        state.camera_direction,
        (Vec3){0, 1, 0},
        state.camera_is_orthographic ? 0 : CAMERA_FOV,
        state.camera_zoom,
        100
    );
}


void game_end(void) {
    for (uint8_t obj = 0; obj < state.objects_sz; ++obj) {
        Object *o = &state.objects[obj];
        for (uint8_t t = 0; t < o->textures_sz; ++t)
            SDL_free(o->textures[t]);
        SDL_free(o->name);
    }
    if (state.objects_sz != 0)
        SDL_free(state.objects);
}