/*
===========================================================================
Copyright (C) 2023-2024 the OpenMoHAA team

This file is part of OpenMoHAA source code.

OpenMoHAA source code 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.

OpenMoHAA source code 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 OpenMoHAA source code; if not, write to the Free Software
Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301  USA
===========================================================================
*/

// tr_sun_flare.cpp: Sun flares

#include "tr_local.h"

#define MAX_FLARES 20

class flare_s
{
public:
    float     size;
    float     where;
    shader_t *shader;
    float     alphascale;
};

class lens_flare
{
public:
    static int max_flares;

    float dot_min;
    float fullscale;

    // List of flares
    int     num_flares;
    flare_s flares[MAX_FLARES];

    shader_t *fullscreen;
    int       fullfade;
    qboolean  blend_active;

    // Flare location
    vec3_t trace_v;
    vec3_t v;

    // Color and alpha
    vec3_t color;
    float  alpha;

    // Screen projection
    vec2_t screen;
    float  dot;

    int   lasttime;
    float lastbright;

    // Whether or not it's inside a portal sky
    bool inportalsky;

    qboolean initted;

public:
    lens_flare()
    {
        color[0] = 1.0;
        color[1] = 1.0;
        color[2] = 1.0;
        alpha    = 1.0;
        initted  = false;
    }

    bool         CheckRange();
    bool         CheckRay();
    bool         ScreenCalc();
    virtual void Try();
    virtual void Init(const char *which);
    void         ScreenBlend();
    void         InPortalSky();
    void         NotInPortalSky();
    void         SetVect(const float *vect);

    void SetColor(const float *c) { VectorCopy(c, color); }

    void SetAlpha(float a) { alpha = a; }
};

class dlight_lens_flare : public lens_flare
{
public:
    shader_t *corona_shader;

public:
    dlight_lens_flare() { corona_shader = NULL; }

    void Try() override;

    void Init(const char *which) override
    {
        lens_flare::Init(which);
        corona_shader = R_FindShader("textures/sprites/corona", -1, qfalse);
    }
};

class sun_flare_class : public lens_flare
{
public:
    bool SunCheckRay();
    void SunScreenCalc();
    void SunTry();
};

lens_flare        torches;
dlight_lens_flare dlights;
sun_flare_class   sunFlare;
int               lens_flare::max_flares;

void lens_flare::SetVect(const float *vect)
{
    if (inportalsky) {
        vec3_t offset;
        vec3_t rot_offset;

        VectorSubtract(vect, tr.refdef.sky_origin, offset);
        VectorRotate(offset, tr.refdef.sky_axis, rot_offset);
        VectorAdd(tr.refdef.vieworg, rot_offset, v);

        VectorNormalize(offset);
        VectorMA(tr.refdef.vieworg, 16384, offset, trace_v);
    } else {
        VectorCopy(vect, v);
        VectorCopy(vect, trace_v);
    }
}

void lens_flare::InPortalSky()
{
    inportalsky = true;
}

void lens_flare::NotInPortalSky()
{
    inportalsky = false;
}

bool lens_flare::CheckRange()
{
    vec3_t diff;

    VectorSubtract(v, backEnd.viewParms.or.origin, diff);
    VectorNormalizeFast(diff);
    dot = DotProduct(backEnd.viewParms.or.axis[0], diff);

    return dot > dot_min;
}

bool lens_flare::CheckRay()
{
    trace_t trace;

    ri.CM_BoxTrace(&trace, backEnd.viewParms.or.origin, trace_v, vec3_origin, vec3_origin, 0, CONTENTS_SOLID, qfalse);
    if (inportalsky) {
        return (trace.surfaceFlags & 4) != 0;
    }

    return trace.fraction == 1.0;
}

bool lens_flare::ScreenCalc()
{
    vec4_t eye;
    vec4_t clip;
    vec4_t point;
    bool   retval;
    int    i, j;

    retval = true;

    VectorCopy(v, point);
    point[3] = 1.0;

    VectorClear4(eye);
    for (i = 0; i < 4; i++) {
        for (j = 0; j < 4; j++) {
            eye[i] += point[j] * tr.or.modelMatrix[i + j * 4];
        }
    }

    VectorClear4(clip);
    for (i = 0; i < 4; i++) {
        for (j = 0; j < 4; j++) {
            clip[i] += eye[j] * tr.viewParms.projectionMatrix[i + j * 4];
        }
    }

    for (i = 0; i < 3; i++) {
        if (clip[i] >= clip[3] || clip[i] <= -clip[3]) {
            retval = false;
        }
    }

    for (i = 0; i < 2; i++) {
        screen[i] = clip[i] / clip[3];
    }

    return retval;
}

void lens_flare::Try()
{
    vec2_t diff;
    vec2_t point;
    float  alpha;
    int    i;

    if (!ScreenCalc()) {
        return;
    }
    if (!CheckRange()) {
        return;
    }
    if (!CheckRay()) {
        return;
    }

    VectorScale2D(screen, -2.0, diff);
    alpha = (dot - dot_min) / (1.0 - dot_min);

    for (i = 0; i < num_flares; i++) {
        flare_s *flare = &flares[i];

        VectorMA2D(screen, flare->where, diff, point);

        RB_Color4f(color[0], color[1], color[2], alpha * this->alpha * flare->alphascale);
        RB_StreamBegin(flare->shader);
        RB_Texcoord2f(0.0, 0.0);

        RB_Vertex2f(point[0] - flare->size, point[1] + flare->size);
        RB_Texcoord2f(1.0, 0.0);

        RB_Vertex2f(point[0] + flare->size, point[1] + flare->size);
        RB_Texcoord2f(0.0, 1.0);

        RB_Vertex2f(point[0] - flare->size, point[1] - flare->size);
        RB_Texcoord2f(1.0, 1.0);

        RB_Vertex2f(point[0] + flare->size, point[1] - flare->size);
        RB_StreamEnd();
    }

    lasttime     = backEnd.refdef.time;
    blend_active = true;
    lastbright   = Square(alpha) * Square(alpha) * this->fullscale;
}

void dlight_lens_flare::Try()
{
    float size;

    size = r_lightcoronasize->value;
    lens_flare::Try();
    lens_flare::CheckRange();

    if (dot <= 0) {
        return;
    }

    if (!CheckRay()) {
        return;
    }

    RB_Color4f(color[0], color[1], color[2], alpha);
    RB_StreamBegin(corona_shader);
    RB_Texcoord2f(0.0, 0.0);

    RB_Vertex2f(screen[0] - size, screen[1] + size);
    RB_Texcoord2f(1.0, 0.0);

    RB_Vertex2f(screen[0] + size, screen[1] + size);
    RB_Texcoord2f(0.0, 1.0);

    RB_Vertex2f(screen[0] - size, screen[1] - size);
    RB_Texcoord2f(1.0, 1.0);

    RB_Vertex2f(screen[0] + size, screen[1] - size);
    RB_StreamEnd();
}

void lens_flare::ScreenBlend()
{
    float alpha;
    int   timediff;

    if (!fullscreen || !blend_active) {
        return;
    }

    timediff = backEnd.refdef.time - lasttime;
    if (timediff > fullfade) {
        blend_active = false;
        return;
    }

    alpha = (float)(fullfade - timediff) / fullfade * lastbright;
    if (alpha <= 0.0001) {
        blend_active = false;
        return;
    }

    RB_Color4f(1.0, 1.0, 1.0, alpha);

    RB_StreamBegin(fullscreen);
    RB_Vertex2f(-1.0, -1.0);
    RB_Vertex2f(1.0, -1.0);
    RB_Vertex2f(-1.0, 1.0);
    RB_Vertex2f(1.0, 1.0);

    RB_StreamEnd();
}

class tmpRiFile
{
private:
    bool  file_exists;
    void *buf;

public:
    tmpRiFile()
    {
        file_exists = false;
        buf         = NULL;
    }

    ~tmpRiFile() { close(); }

    int open(const char *which, void **buffer)
    {
        close();

        if (ri.FS_ReadFile(which, buffer) != -1) {
            file_exists = true;
            buf         = *buffer;
        }

        return file_exists;
    }

    bool exists() const { return file_exists; }

    void close()
    {
        if (file_exists) {
            ri.FS_FreeFile(buf);
            file_exists = false;
        }
    }
};

void lens_flare::Init(const char *which)
{
    char       *file;
    const char *token;
    tmpRiFile   theFile;

    initted = false;
    theFile.open("global/lensflaredefs.txt", (void **)&file);

    if (!theFile.exists()) {
        ri.Printf(PRINT_WARNING, "WARNING: Could not open lens flare info file!\n");
        return;
    }

    while ((token = COM_ParseExt(&file, qtrue))) {
        if (!file) {
            ri.Printf(PRINT_WARNING, "WARNING: could not find begin for section '%s' in lensflares\n", which);
            return;
        }

        if (!Q_stricmp(token, "begin")) {
            token = COM_ParseExt(&file, qfalse);
            if (!Q_stricmp(token, which)) {
                break;
            }
        }
    }

    dot_min      = 0.8f;
    fullscale    = 0.7f;
    num_flares   = 0;
    fullscreen   = NULL;
    initted      = true;
    fullfade     = 0;
    blend_active = false;

    while ((token = COM_ParseExt(&file, qtrue))) {
        if (!file) {
            ri.Printf(PRINT_WARNING, "WARNING: could not find end for section '%s' in lensflares\n", which);
            break;
        }

        if (!Q_stricmp(token, "dot_min")) {
            token = COM_ParseExt(&file, qfalse);
            if (!token[0]) {
                ri.Printf(PRINT_WARNING, "WARNING: invalid dot_min in lensflares\n");
                continue;
            }
            dot_min = atof(token);
        } else if (!Q_stricmp(token, "flare")) {
            assert(num_flares < MAX_FLARES);

            flares[num_flares].alphascale = 1.0;

            token = COM_ParseExt(&file, qfalse);
            if (!token[0]) {
                ri.Printf(PRINT_WARNING, "WARNING: no flare arg 1 for flare '%d' in lensflares\n", num_flares);
                continue;
            }
            flares[num_flares].size = atof(token);

            token = COM_ParseExt(&file, qfalse);
            if (!token[0]) {
                ri.Printf(PRINT_WARNING, "WARNING: no flare arg 2 for flare '%d' in lensflares\n", num_flares);
                continue;
            }
            flares[num_flares].where = atof(token);

            token = COM_ParseExt(&file, qfalse);
            if (!token[0]) {
                ri.Printf(PRINT_WARNING, "WARNING: no flare arg 3 for flare '%d' in lensflares\n", num_flares);
                continue;
            }
            flares[num_flares].shader = R_FindShader(token, -1, qfalse);

            token = COM_ParseExt(&file, qfalse);
            if (token[0]) {
                flares[num_flares].alphascale = atof(token);
            }

            num_flares++;
        } else if (!Q_stricmp(token, "fullscale")) {
            token = COM_ParseExt(&file, qfalse);
            if (!token[0]) {
                ri.Printf(PRINT_WARNING, "WARNING: no arg for fullscale, assuming default (in lensflares)\n");
                continue;
            }
            fullscale = atof(token);
        } else if (!Q_stricmp(token, "fullscreen")) {
            token = COM_ParseExt(&file, qfalse);
            if (!token[0]) {
                ri.Printf(PRINT_WARNING, "WARNING: no arg for fullsreen in lensflares\n");
                continue;
            }
            fullscreen = R_FindShader(token, -1, qfalse);
        } else if (!Q_stricmp(token, "fullfade")) {
            token = COM_ParseExt(&file, qfalse);
            if (!token[0]) {
                ri.Printf(PRINT_WARNING, "WARNING: no arg for fullfade in lensflares\n");
                continue;
            }
            fullfade = atof(token);
        } else if (!Q_stricmp(token, "end")) {
            break;
        }
    }

    if (!num_flares) {
        ri.Printf(PRINT_WARNING, "WARNING: no lensflares defined!\n");
    }
    lasttime = backEnd.refdef.time - fullfade;
}

static void R_DrawSunFlare()
{
    if (!s_sun.exists) {
        return;
    }
    if (!sunFlare.initted) {
        if (!s_sun.szFlareName[0]) {
            return;
        }
        if (Q_stricmp(s_sun.szFlareName, "none")) {
            sunFlare.Init(s_sun.szFlareName);
        }
    }

    if (!sunFlare.initted) {
        s_sun.szFlareName[0] = 0;
        return;
    }

    if (!sunFlare.num_flares) {
        return;
    }

    VectorMA(backEnd.viewParms.or.origin, 16384, s_sun.flaredirection, sunFlare.trace_v);
    VectorMA(backEnd.viewParms.or.origin, 128, s_sun.flaredirection, sunFlare.v);

    sunFlare.SunTry();
}

static void R_DrawSunFlareBlend()
{
    if (!s_sun.exists) {
        return;
    }
    if (!sunFlare.initted) {
        return;
    }

    sunFlare.ScreenBlend();
}

void R_DrawLensFlares()
{
    // FIXME: unimplemented (GL2)
#if 0
    int i;

    R_RotateForViewer();

    qglPushMatrix();
    qglLoadIdentity();
    qglMatrixMode(GL_PROJECTION);
    qglPushMatrix();
    qglLoadIdentity();
    qglOrtho(-1.0, 1.0, -1.0, 1.0, -99999.0, 99999.0);

    tess.no_global_fog = true;
    R_DrawSunFlare();

    for (i = 0; i < backEnd.refdef.num_entities; i++) {
        if ((backEnd.viewParms.isPortalSky && !(backEnd.refdef.entities[i].e.renderfx & RF_SKYENTITY))
            || (!backEnd.viewParms.isPortalSky && (backEnd.refdef.entities[i].e.renderfx & RF_SKYENTITY))) {
            continue;
        }

        if (backEnd.refdef.entities[i].e.renderfx & RF_LENSFLARE) {
            vec3_t rgb;

            if (backEnd.refdef.entities[i].e.renderfx & RF_SKYENTITY) {
                dlights.InPortalSky();
            } else {
                dlights.NotInPortalSky();
            }

            dlights.SetVect(backEnd.refdef.entities[i].e.origin);

            //
            // Set the dlight color from entity
            //

            rgb[0] = backEnd.refdef.entities[i].e.shaderRGBA[0] / 255.0;
            rgb[1] = backEnd.refdef.entities[i].e.shaderRGBA[1] / 255.0;
            rgb[2] = backEnd.refdef.entities[i].e.shaderRGBA[2] / 255.0;
            dlights.SetColor(rgb);

            dlights.Try();
        }

        if (backEnd.refdef.entities[i].e.renderfx & RF_VIEWLENSFLARE) {
            vec3_t rgb;

            if (backEnd.refdef.entities[i].e.renderfx & RF_SKYENTITY) {
                torches.InPortalSky();
            } else {
                torches.NotInPortalSky();
            }

            torches.SetVect(backEnd.refdef.entities[i].e.origin);

            rgb[0] = backEnd.refdef.entities[i].e.shaderRGBA[0] / 255.0;
            rgb[1] = backEnd.refdef.entities[i].e.shaderRGBA[1] / 255.0;
            rgb[2] = backEnd.refdef.entities[i].e.shaderRGBA[2] / 255.0;
            torches.SetColor(rgb);

            torches.Try();
        }
    }

    for (i = 0; i < backEnd.refdef.num_dlights; i++) {
        if ((backEnd.refdef.dlights[i].type & dlighttype_t::lensflare)
            || (backEnd.refdef.dlights[i].type & dlighttype_t::additive)) {
            dlights.NotInPortalSky();
            dlights.SetVect(backEnd.refdef.dlights[i].origin);
            dlights.SetColor(backEnd.refdef.dlights[i].color);

            dlights.Try();
        }
    }

    R_DrawSunFlareBlend();
    tess.no_global_fog = qfalse;

    qglPopMatrix();
    qglMatrixMode(GL_MODELVIEW);
    qglPopMatrix();
#endif
}

void R_InitLensFlare()
{
    torches.Init("entity");
    dlights.Init("dlight");
    sunFlare.initted = false;
}

bool sun_flare_class::SunCheckRay()
{
    mnode_t *pViewLeaf;
    trace_t  trace;

    pViewLeaf = R_PointInLeaf(tr.refdef.vieworg);

    if (pViewLeaf->area == -1 || !tr.world->vis || tr.sSunLight.leaf != (mnode_s *)-1
        || pViewLeaf->numlights && pViewLeaf->lights[0] == &tr.sSunLight) {
        ri.CM_BoxTrace(
            &trace, backEnd.viewParms.or.origin, trace_v, vec3_origin, vec3_origin, 0, CONTENTS_SOLID, qfalse
        );

        if (trace.surfaceFlags & SURF_SKY) {
            return true;
        }
    }

    return false;
}

void sun_flare_class::SunScreenCalc()
{
    vec4_t eye;
    vec4_t clip;
    vec4_t point;
    int    i, j;

    VectorCopy(v, point);
    point[3] = 1.0;

    VectorClear4(eye);
    for (i = 0; i < 4; i++) {
        for (j = 0; j < 4; j++) {
            eye[i] += point[j] * tr.or.modelMatrix[i + j * 4];
        }
    }

    VectorClear4(clip);
    for (i = 0; i < 4; i++) {
        for (j = 0; j < 4; j++) {
            clip[i] += eye[j] * tr.viewParms.projectionMatrix[i + j * 4];
        }
    }

    for (i = 0; i < 2; i++) {
        screen[i] = clip[i] / clip[3];
    }
}

void sun_flare_class::SunTry()
{
    vec2_t   diff;
    vec2_t   point;
    float    alpha;
    int      i;
    qboolean bDrawingFade = qfalse;

    if (!CheckRange()) {
        return;
    }

    if (SunCheckRay()) {
        alpha = (dot - dot_min) / (1.0 - dot_min);
    } else {
        int timediff;

        if (!fullscreen) {
            return;
        }
        if (!blend_active) {
            return;
        }

        timediff = backEnd.refdef.time - lasttime;
        alpha    = (float)(fullfade - timediff) / fullfade * lastbright;

        if (alpha <= 0.0001) {
            return;
        }

        bDrawingFade = qtrue;
    }

    SunScreenCalc();
    VectorScale2D(screen, -2.0, diff);

    for (i = 0; i < num_flares; i++) {
        flare_s *flare = &flares[i];

        VectorMA2D(screen, flare->where, diff, point);

        RB_Color4f(color[0], color[1], color[2], alpha * this->alpha * flare->alphascale);
        RB_StreamBegin(flare->shader);
        RB_Texcoord2f(0.0, 0.0);

        RB_Vertex2f(point[0] - flare->size, point[1] + flare->size);
        RB_Texcoord2f(1.0, 0.0);

        RB_Vertex2f(point[0] + flare->size, point[1] + flare->size);
        RB_Texcoord2f(0.0, 1.0);

        RB_Vertex2f(point[0] - flare->size, point[1] - flare->size);
        RB_Texcoord2f(1.0, 1.0);

        RB_Vertex2f(point[0] + flare->size, point[1] - flare->size);
        RB_StreamEnd();
    }

    if (!bDrawingFade) {
        lasttime     = backEnd.refdef.time;
        blend_active = true;
        lastbright   = Square(alpha) * Square(alpha) * fullscale;
    }
}
