/*
Copyright (C) 1996-2001 Id Software, Inc.
Copyright (C) 2002-2009 John Fitzgibbons and others
Copyright (C) 2007-2008 Kristian Duske
Copyright (C) 2010-2014 QuakeSpasm developers

This program is free software; you can redistribute it and/or
modify it under the terms of the GNU General Public License
as published by the Free Software Foundation; either version 2
of the License, or (at your option) any later version.

This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.

See the GNU General Public License for more details.

You should have received a copy of the GNU General Public License
along with this program; if not, write to the Free Software
Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA  02111-1307, USA.

*/
// r_misc.c

#include <sstream>

#include "quakedef.hpp"

//johnfitz -- new cvars
extern convar r_stereo;
extern convar r_stereodepth;
extern convar r_clearcolor;
extern convar r_drawflat;
extern convar r_flatlightstyles;
extern convar gl_fullbrights;
extern convar gl_farclip;
extern convar gl_overbright;
extern convar gl_overbright_models;
extern convar r_waterquality;
extern convar r_oldwater;
extern convar r_waterwarp;
extern convar r_oldskyleaf;
extern convar r_drawworld;
extern convar r_showtris;
extern convar r_showbboxes;
extern convar r_lerpmodels;
extern convar r_lerpmove;
extern convar r_nolerp_list;
extern convar r_noshadow_list;
//johnfitz
extern convar gl_zfix; // QuakeSpasm z-fighting fix

extern gltexture_t *playertextures[MAX_SCOREBOARD]; //johnfitz

convar r_lightmapwide = {"r_lightmapwide", "0", {.rom = true}};


/*
====================
GL_Overbright_f -- johnfitz
====================
*/
static void GL_Overbright_f(convar *var) {
    R_RebuildAllLightmaps();
}

/*
====================
GL_Fullbrights_f -- johnfitz
====================
*/
static void GL_Fullbrights_f(convar *var) {
    TexMgr_ReloadNobrightImages();
}

/*
====================
R_SetClearColor_f -- johnfitz
====================
*/
static void R_SetClearColor_f(convar *var) {
    byte *rgb;
    int s;

    s = (int) r_clearcolor.value & 0xFF;
    rgb = (byte *) (d_8to24table + s);
    glClearColor(rgb[0] / 255.0, rgb[1] / 255.0, rgb[2] / 255.0, 0);
}

/*
===============
R_Model_ExtraFlags_List_f -- johnfitz -- called when r_nolerp_list or r_noshadow_list cvar changes
===============
*/
static void R_Model_ExtraFlags_List_f(convar *var) {
    int i;
    for (i = 0; i < MAX_MODELS; i++)
        Mod_SetExtraFlags(cl.model_precache[i]);
}

/*
====================
R_SetWateralpha_f -- ericw
====================
*/
static void R_SetWateralpha_f(convar *var) {
    map_wateralpha = var->value;
}

/*
====================
R_SetLavaalpha_f -- ericw
====================
*/
static void R_SetLavaalpha_f(convar *var) {
    map_lavaalpha = var->value;
}

/*
====================
R_SetTelealpha_f -- ericw
====================
*/
static void R_SetTelealpha_f(convar *var) {
    map_telealpha = var->value;
}

/*
====================
R_SetSlimealpha_f -- ericw
====================
*/
static void R_SetSlimealpha_f(convar *var) {
    map_slimealpha = var->value;
}

/*
====================
GL_WaterAlphaForSurfface -- ericw
====================
*/
float GL_WaterAlphaForSurface(msurface_t *fa) {
    if (fa->flags & SURF_DRAWLAVA)
        return map_lavaalpha > 0 ? map_lavaalpha : map_wateralpha;
    else if (fa->flags & SURF_DRAWTELE)
        return map_telealpha > 0 ? map_telealpha : map_wateralpha;
    else if (fa->flags & SURF_DRAWSLIME)
        return map_slimealpha > 0 ? map_slimealpha : map_wateralpha;
    else
        return map_wateralpha;
}


/*
===============
R_Init
===============
*/
void R_Init(void) {
    command::add("timerefresh", R_TimeRefresh_f);
    command::add("pointfile", R_ReadPointFile_f);

    r_norefresh.inscribe();
    r_lightmap.inscribe();
    r_fullbright.inscribe();
    r_drawentities.inscribe();
    r_drawviewmodel.inscribe();
    r_shadows.inscribe();
    r_wateralpha.inscribe();
    r_wateralpha.set_callback(R_SetWateralpha_f);
    r_litwater.inscribe();
    r_dynamic.inscribe();
    r_novis.inscribe();
    r_speeds.inscribe();
    r_pos.inscribe();

    gl_finish.inscribe();
    gl_clear.inscribe();
    gl_cull.inscribe();
    gl_smoothmodels.inscribe();
    gl_affinemodels.inscribe();
    gl_polyblend.inscribe();
    gl_flashblend.inscribe();
    gl_playermip.inscribe();
    gl_nocolors.inscribe();

    //johnfitz -- new cvars
    r_stereo.inscribe();
    r_stereodepth.inscribe();
    r_clearcolor.inscribe();
    r_clearcolor.set_callback(R_SetClearColor_f);
    r_waterquality.inscribe();
    r_oldwater.inscribe();
    r_waterwarp.inscribe();
    r_drawflat.inscribe();
    r_flatlightstyles.inscribe();
    r_oldskyleaf.inscribe();
    r_drawworld.inscribe();
    r_showtris.inscribe();
    r_showbboxes.inscribe();
    gl_farclip.inscribe();
    gl_fullbrights.inscribe();
    gl_overbright.inscribe();
    gl_fullbrights.set_callback(GL_Fullbrights_f);
    gl_overbright.set_callback(GL_Overbright_f);
    gl_overbright_models.inscribe();
    r_lerpmodels.inscribe();
    r_lerpmove.inscribe();
    r_nolerp_list.inscribe();
    r_nolerp_list.set_callback(R_Model_ExtraFlags_List_f);
    r_noshadow_list.inscribe();
    r_noshadow_list.set_callback(R_Model_ExtraFlags_List_f);
    //johnfitz
    r_lightmapwide.inscribe();
    convar::set_rom("r_lightmapwide", gl_packed_pixels ? "1" : "0");

    gl_zfix.inscribe(); // QuakeSpasm z-fighting fix
    r_lavaalpha.inscribe();
    r_telealpha.inscribe();
    r_slimealpha.inscribe();
    r_scale.inscribe();
    r_lavaalpha.set_callback(R_SetLavaalpha_f);
    r_telealpha.set_callback(R_SetTelealpha_f);
    r_slimealpha.set_callback(R_SetSlimealpha_f);

    R_InitParticles();
    R_SetClearColor_f(&r_clearcolor); //johnfitz

    Sky_Init(); //johnfitz
    Fog_Init(); //johnfitz
}

/*
===============
R_TranslatePlayerSkin -- johnfitz -- rewritten.  also, only handles new colors, not new skins
===============
*/
void R_TranslatePlayerSkin(int playernum) {
    int top, bottom;

    top = (cl.scores[playernum].colors & 0xf0) >> 4;
    bottom = cl.scores[playernum].colors & 15;

    //FIXME: if gl_nocolors is on, then turned off, the textures may be out of sync with the scoreboard colors.
    if (!gl_nocolors.value) {
        if (playertextures[playernum])
            TexMgr_ReloadImage(playertextures[playernum], top, bottom);
    }
}

/*
===============
R_TranslateNewPlayerSkin -- johnfitz -- split off of TranslatePlayerSkin -- this is called when
the skin or model actually changes, instead of just new colors
added bug fix from bengt jardup
===============
*/
void R_TranslateNewPlayerSkin(int playernum) {
    char name[64];
    byte *pixels;
    aliashdr_t *paliashdr;
    int skinnum;

    //get correct texture pixels
    currententity = &cl_entities[1 + playernum];

    if (!currententity->model || currententity->model->type != mod_alias)
        return;

    paliashdr = (aliashdr_t *) Mod_Extradata(currententity->model);

    skinnum = currententity->skinnum;

    //TODO: move these tests to the place where skinnum gets received from the server
    if (skinnum < 0 || skinnum >= paliashdr->numskins) {
        console::debug("(%d): Invalid player skin #%d\n", playernum, skinnum);
        skinnum = 0;
    }

    pixels = (byte *) paliashdr + paliashdr->texels[skinnum]; // This is not a persistent place!

    //upload new image
    q_snprintf(name, sizeof(name), "player_%i", playernum);
    playertextures[playernum] = TexMgr_LoadImage(currententity->model, name, paliashdr->skinwidth,
                                                 paliashdr->skinheight,
                                                 SRC_INDEXED, pixels, paliashdr->gltextures[skinnum][0]->source_file,
                                                 paliashdr->gltextures[skinnum][0]->source_offset,
                                                 TEXPREF_PAD | TEXPREF_OVERWRITE);

    //now recolor it
    R_TranslatePlayerSkin(playernum);
}

/*
===============
R_NewGame -- johnfitz -- handle a game switch
===============
*/
void R_NewGame(void) {
    int i;

    //clear playertexture pointers (the textures themselves were freed by texmgr_newgame)
    for (i = 0; i < MAX_SCOREBOARD; i++)
        playertextures[i] = NULL;
}

/*
=============
R_ParseWorldspawn

called at map load
=============
*/
static void R_ParseWorldspawn() {
    map_wateralpha = r_wateralpha.value;
    map_lavaalpha = r_lavaalpha.value;
    map_telealpha = r_telealpha.value;
    map_slimealpha = r_slimealpha.value;

    std::string key{};
    std::string value{};
    std::istringstream ss{cl.worldmodel->entities};

    auto token = common::parse_token(ss);
    if (!token.has_value())
        return; // error
    if ((*token)[0] != '{')
        return; // error

    while (true) {
        token = common::parse_token(ss);
        if (!token.has_value()) {
            return; // error
        }
        if ((*token)[0] == '}') {
            break; // end of worldspawn
        }
        if ((*token)[0] == '_') {
            key = token->substr(1);
        } else {
            key = *token;
        }
        while (!key.empty() && key.back() == ' ') {
            // remove trailing spaces
            key.pop_back();
        }
        token = common::parse_token(ss);
        if (!token.has_value()) {
            return; // error
        }
        value = *token;

        if (key == "wateralpha") {
            map_wateralpha = std::strtof(value.c_str(), nullptr);
        }

        if (key == "lavaalpha") {
            map_lavaalpha = std::strtof(value.c_str(), nullptr);
        }

        if (key == "telealpha") {
            map_telealpha = std::strtof(value.c_str(), nullptr);
        }

        if (key == "slimealpha") {
            map_slimealpha = std::strtof(value.c_str(), nullptr);
        }
    }
}


/*
===============
R_NewMap
===============
*/
void R_NewMap(void) {
    int i;

    for (i = 0; i < 256; i++)
        d_lightstylevalue[i] = 264; // normal light value

    // clear out efrags in case the level hasn't been reloaded
    // FIXME: is this one short?
    for (i = 0; i < cl.worldmodel->numleafs; i++)
        cl.worldmodel->leafs[i].efrags = NULL;

    r_viewleaf = NULL;
    R_ClearParticles();

    GL_BuildLightmaps();
    GL_BuildBModelVertexBuffer();
    //ericw -- no longer load alias models into a VBO here, it's done in Mod_LoadAliasModel

    r_framecount = 0; //johnfitz -- paranoid?
    r_visframecount = 0; //johnfitz -- paranoid?

    Sky_NewMap(); //johnfitz -- skybox in worldspawn
    Fog_NewMap(); //johnfitz -- global fog in worldspawn
    R_ParseWorldspawn(); //ericw -- wateralpha, lavaalpha, telealpha, slimealpha in worldspawn

    load_subdivide_size = gl_subdivide_size.value; //johnfitz -- is this the right place to set this?
}

/*
====================
R_TimeRefresh_f

For program optimization
====================
*/
void R_TimeRefresh_f(void) {
    int i;
    float start, stop, time;

    if (cls.state != ca_connected) {
        console::info("Not connected to a server\n");
        return;
    }

    start = Sys_DoubleTime();
    for (i = 0; i < 128; i++) {
        GL_BeginRendering(&glx, &gly, &glwidth, &glheight);
        r_refdef.viewangles[1] = i / 128.0 * 360.0;
        R_RenderView();
        GL_EndRendering();
    }

    glFinish();
    stop = Sys_DoubleTime();
    time = stop - start;
    console::info("%f seconds (%f fps)\n", time, 128 / time);
}

void D_FlushCaches(void) {
}

static GLuint gl_programs[16];
static int gl_num_programs;

static bool GL_CheckShader(GLuint shader) {
    GLint status;
    GL_GetShaderivFunc(shader, GL_COMPILE_STATUS, &status);

    if (status != GL_TRUE) {
        char infolog[1024];

        memset(infolog, 0, sizeof(infolog));
        GL_GetShaderInfoLogFunc(shader, sizeof(infolog), NULL, infolog);

        console::warn("GLSL program failed to compile: %s", infolog);

        return false;
    }
    return true;
}

static bool GL_CheckProgram(GLuint program) {
    GLint status;
    GL_GetProgramivFunc(program, GL_LINK_STATUS, &status);

    if (status != GL_TRUE) {
        char infolog[1024];

        memset(infolog, 0, sizeof(infolog));
        GL_GetProgramInfoLogFunc(program, sizeof(infolog), NULL, infolog);

        console::warn("GLSL program failed to link: %s", infolog);

        return false;
    }
    return true;
}

/*
=============
GL_GetUniformLocation
=============
*/
GLint GL_GetUniformLocation(GLuint *programPtr, const char *name) {
    GLint location;

    if (!programPtr)
        return -1;

    location = GL_GetUniformLocationFunc(*programPtr, name);
    if (location == -1) {
        console::warn("GL_GetUniformLocationFunc %s failed\n", name);
        *programPtr = 0;
    }
    return location;
}

/*
====================
GL_CreateProgram

Compiles and returns GLSL program.
====================
*/
GLuint GL_CreateProgram(const GLchar *vertSource, const GLchar *fragSource, int numbindings,
                        const glsl_attrib_binding_t *bindings) {
    int i;
    GLuint program, vertShader, fragShader;

    if (!gl_glsl_able)
        return 0;

    vertShader = GL_CreateShaderFunc(GL_VERTEX_SHADER);
    GL_ShaderSourceFunc(vertShader, 1, &vertSource, NULL);
    GL_CompileShaderFunc(vertShader);
    if (!GL_CheckShader(vertShader)) {
        GL_DeleteShaderFunc(vertShader);
        return 0;
    }

    fragShader = GL_CreateShaderFunc(GL_FRAGMENT_SHADER);
    GL_ShaderSourceFunc(fragShader, 1, &fragSource, NULL);
    GL_CompileShaderFunc(fragShader);
    if (!GL_CheckShader(fragShader)) {
        GL_DeleteShaderFunc(vertShader);
        GL_DeleteShaderFunc(fragShader);
        return 0;
    }

    program = GL_CreateProgramFunc();
    GL_AttachShaderFunc(program, vertShader);
    GL_DeleteShaderFunc(vertShader);
    GL_AttachShaderFunc(program, fragShader);
    GL_DeleteShaderFunc(fragShader);

    for (i = 0; i < numbindings; i++) {
        GL_BindAttribLocationFunc(program, bindings[i].attrib, bindings[i].name);
    }

    GL_LinkProgramFunc(program);

    if (!GL_CheckProgram(program)) {
        GL_DeleteProgramFunc(program);
        return 0;
    } else {
        if (gl_num_programs == std::size(gl_programs))
            Host_Error("gl_programs overflow");

        gl_programs[gl_num_programs] = program;
        gl_num_programs++;

        return program;
    }
}

/*
====================
R_DeleteShaders

Deletes any GLSL programs that have been created.
====================
*/
void R_DeleteShaders(void) {
    int i;

    if (!gl_glsl_able)
        return;

    for (i = 0; i < gl_num_programs; i++) {
        GL_DeleteProgramFunc(gl_programs[i]);
        gl_programs[i] = 0;
    }
    gl_num_programs = 0;
}

static GLuint current_array_buffer, current_element_array_buffer;

/*
====================
GL_BindBuffer

glBindBuffer wrapper
====================
*/
void GL_BindBuffer(GLenum target, GLuint buffer) {
    GLuint *cache;

    if (!gl_vbo_able)
        return;

    switch (target) {
        case GL_ARRAY_BUFFER:
            cache = &current_array_buffer;
            break;
        case GL_ELEMENT_ARRAY_BUFFER:
            cache = &current_element_array_buffer;
            break;
        default:
            Host_Error("GL_BindBuffer: unsupported target %d", (int) target);
            return;
    }

    if (*cache != buffer) {
        *cache = buffer;
        GL_BindBufferFunc(target, *cache);
    }
}

/*
====================
GL_ClearBufferBindings

This must be called if you do anything that could make the cached bindings
invalid (e.g. manually binding, destroying the context).
====================
*/
void GL_ClearBufferBindings(void) {
    if (!gl_vbo_able)
        return;

    current_array_buffer = 0;
    current_element_array_buffer = 0;
    GL_BindBufferFunc(GL_ARRAY_BUFFER, 0);
    GL_BindBufferFunc(GL_ELEMENT_ARRAY_BUFFER, 0);
}
