/*  Copyright (C) 2016 Eric Wasylishen

    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

    See file, 'COPYING', for details.
*/

#include <light/light.hh>
#include <light/bounce.hh>
#include <light/trace_embree.hh>
#include <light/ltface.hh>
#include <common/bsputils.hh>
#include <common/polylib.hh>
#include <embree3/rtcore.h>
#include <embree3/rtcore_ray.h>
#include <vector>
#include <cassert>
#include <climits>
#include <cstdlib>
#include <limits>

using namespace std;
using namespace polylib;

class sceneinfo
{
public:
    unsigned geomID;

    std::vector<const mface_t *> triToFace;
    std::vector<const modelinfo_t *> triToModelinfo;
};

class raystream_embree_common_t;

struct ray_source_info : public RTCIntersectContext
{
    raystream_embree_common_t *raystream; // may be null if this ray is not from a ray stream
    const modelinfo_t *self;
    /// only used if raystream == null
    int singleRayShadowStyle;

    ray_source_info(raystream_embree_common_t *raystream_, const modelinfo_t *self_)
        : raystream(raystream_), self(self_), singleRayShadowStyle(0)
    {
        rtcInitIntersectContext(this);

        flags = RTC_INTERSECT_CONTEXT_FLAG_COHERENT;
    }
};

/**
 * Returns 1.0 unless a custom alpha value is set.
 * The priority is: "_light_alpha" (read from extended_texinfo_flags), then "alpha"
 */
static float Face_Alpha(const modelinfo_t *modelinfo, const mface_t *face)
{
    const surfflags_t &extended_flags = extended_texinfo_flags[face->texinfo];

    // for _light_alpha, 0 is considered unset
    const float alpha_float = (float)extended_flags.light_alpha / (float)UCHAR_MAX;
    if (alpha_float != 0.0f) {
        return alpha_float;
    }

    // next check modelinfo alpha (defaults to 1.0)
    return modelinfo->alpha.floatValue();
}

sceneinfo CreateGeometry(
    const mbsp_t *bsp, RTCDevice g_device, RTCScene scene, const std::vector<const mface_t *> &faces)
{
    // count triangles
    int numtris = 0;
    for (const mface_t *face : faces) {
        if (face->numedges < 3)
            continue;
        numtris += (face->numedges - 2);
    }

    unsigned int geomID;
    RTCGeometry geom_0 = rtcNewGeometry(g_device, RTC_GEOMETRY_TYPE_TRIANGLE);
    // we're not using masks, but they need to be set to something or else all rays miss
    // if embree is compiled with them
    rtcSetGeometryMask(geom_0, 1);
    rtcSetGeometryBuildQuality(geom_0, RTC_BUILD_QUALITY_MEDIUM);
    rtcSetGeometryTimeStepCount(geom_0, 1);
    geomID = rtcAttachGeometry(scene, geom_0);
    rtcReleaseGeometry(geom_0);

    struct Vertex
    {
        float point[4];
    }; // 4th element is padding
    struct Triangle
    {
        int v0, v1, v2;
    };

    // fill in vertices
    Vertex *vertices = (Vertex *)rtcSetNewGeometryBuffer(
        geom_0, RTC_BUFFER_TYPE_VERTEX, 0, RTC_FORMAT_FLOAT3, 4 * sizeof(float), bsp->dvertexes.size());

    size_t i = 0;
    for (auto &dvertex : bsp->dvertexes) {
        Vertex *vert = &vertices[i++];
        for (int j = 0; j < 3; j++) {
            vert->point[j] = dvertex[j];
        }
    }

    sceneinfo s;
    s.geomID = geomID;

    // fill in triangles
    Triangle *triangles = (Triangle *)rtcSetNewGeometryBuffer(
        geom_0, RTC_BUFFER_TYPE_INDEX, 0, RTC_FORMAT_UINT3, 3 * sizeof(int), numtris);
    int tri_index = 0;
    for (const mface_t *face : faces) {
        if (face->numedges < 3)
            continue;

        // NOTE: can be null for "skip" faces
        const modelinfo_t *modelinfo = ModelInfoForFace(bsp, Face_GetNum(bsp, face));

        for (int j = 2; j < face->numedges; j++) {
            Triangle *tri = &triangles[tri_index];
            tri->v0 = Face_VertexAtIndex(bsp, face, j - 1);
            tri->v1 = Face_VertexAtIndex(bsp, face, j);
            tri->v2 = Face_VertexAtIndex(bsp, face, 0);
            tri_index++;

            s.triToFace.push_back(face);
            s.triToModelinfo.push_back(modelinfo);
        }
    }

    rtcCommitGeometry(geom_0);
    return s;
}

static void CreateGeometryFromWindings(RTCDevice g_device, RTCScene scene, const std::vector<winding_t> &windings)
{
    if (windings.empty())
        return;

    // count triangles
    int numtris = 0;
    int numverts = 0;
    for (const auto &winding : windings) {
        Q_assert(winding.size() >= 3);
        numtris += (winding.size() - 2);
        numverts += winding.size();
    }

    unsigned int geomID;
    RTCGeometry geom_1 = rtcNewGeometry(g_device, RTC_GEOMETRY_TYPE_TRIANGLE);
    rtcSetGeometryBuildQuality(geom_1, RTC_BUILD_QUALITY_MEDIUM);
    rtcSetGeometryMask(geom_1, 1);
    rtcSetGeometryTimeStepCount(geom_1, 1);
    geomID = rtcAttachGeometry(scene, geom_1);
    rtcReleaseGeometry(geom_1);

    struct Vertex
    {
        float point[4];
    }; // 4th element is padding
    struct Triangle
    {
        int v0, v1, v2;
    };

    // fill in vertices
    Vertex *vertices = (Vertex *)rtcSetNewGeometryBuffer(
        geom_1, RTC_BUFFER_TYPE_VERTEX, 0, RTC_FORMAT_FLOAT3, 4 * sizeof(float), numverts);
    {
        int vert_index = 0;
        for (const auto &winding : windings) {
            for (int j = 0; j < winding.size(); j++) {
                for (int k = 0; k < 3; k++) {
                    vertices[vert_index + j].point[k] = winding.at(j)[k];
                }
            }
            vert_index += winding.size();
        }
    }

    // fill in triangles
    Triangle *triangles = (Triangle *)rtcSetNewGeometryBuffer(
        geom_1, RTC_BUFFER_TYPE_INDEX, 0, RTC_FORMAT_UINT3, 3 * sizeof(int), numtris);
    int tri_index = 0;
    int vert_index = 0;
    for (const auto &winding : windings) {
        for (int j = 2; j < winding.size(); j++) {
            Triangle *tri = &triangles[tri_index];
            tri->v0 = vert_index + (j - 1);
            tri->v1 = vert_index + j;
            tri->v2 = vert_index + 0;
            tri_index++;
        }
        vert_index += winding.size();
    }
    Q_assert(vert_index == numverts);
    Q_assert(tri_index == numtris);

    rtcCommitGeometry(geom_1);
}

RTCDevice device;
RTCScene scene;

sceneinfo skygeom; // sky. always occludes.
sceneinfo solidgeom; // solids. always occludes.
sceneinfo filtergeom; // conditional occluders.. needs to run ray intersection filter

static const mbsp_t *bsp_static;

void ErrorCallback(void *userptr, const RTCError code, const char *str)
{
    fmt::print("RTC Error {}: {}\n", code, str);
}

static const sceneinfo &Embree_SceneinfoForGeomID(unsigned int geomID)
{
    if (geomID == skygeom.geomID) {
        return skygeom;
    } else if (geomID == solidgeom.geomID) {
        return solidgeom;
    } else if (geomID == filtergeom.geomID) {
        return filtergeom;
    } else {
        FError("unexpected geomID");
    }
}

const mface_t *Embree_LookupFace(unsigned int geomID, unsigned int primID)
{
    const sceneinfo &info = Embree_SceneinfoForGeomID(geomID);
    return info.triToFace.at(primID);
}

const modelinfo_t *Embree_LookupModelinfo(unsigned int geomID, unsigned int primID)
{
    const sceneinfo &info = Embree_SceneinfoForGeomID(geomID);
    return info.triToModelinfo.at(primID);
}

static void Embree_RayEndpoint(RTCRayN *ray, size_t N, size_t i, vec3_t endpoint)
{
    vec3_t dir;
    dir[0] = RTCRayN_dir_x(ray, N, i);
    dir[1] = RTCRayN_dir_y(ray, N, i);
    dir[2] = RTCRayN_dir_z(ray, N, i);

    VectorNormalize(dir);

    vec3_t org;
    org[0] = RTCRayN_org_x(ray, N, i);
    org[1] = RTCRayN_org_y(ray, N, i);
    org[2] = RTCRayN_org_z(ray, N, i);

    float tfar = RTCRayN_tfar(ray, N, i);

    VectorMA(org, tfar, dir, endpoint);
}

enum class filtertype_t
{
    INTERSECTION,
    OCCLUSION
};

void AddGlassToRay(RTCIntersectContext *context, unsigned rayIndex, float opacity, const vec3_t glasscolor);
void AddDynamicOccluderToRay(RTCIntersectContext *context, unsigned rayIndex, int style);

// called to evaluate transparency
template<filtertype_t filtertype>
static void Embree_FilterFuncN(const struct RTCFilterFunctionNArguments *args)
{
    int *const valid = args->valid;
    void *const userDataPtr = args->geometryUserPtr;
    RTCIntersectContext *const context = args->context;
    struct RTCRayN *const ray = args->ray;
    struct RTCHitN *const potentialHit = args->hit;
    const unsigned int N = args->N;

    const int VALID = -1;
    const int INVALID = 0;

    const ray_source_info *rsi = static_cast<const ray_source_info *>(context);

    for (size_t i = 0; i < N; i++) {
        if (valid[i] != VALID) {
            // we only need to handle valid rays
            continue;
        }

        const unsigned &rayID = RTCRayN_id(ray, N, i);
        const unsigned &geomID = RTCHitN_geomID(potentialHit, N, i);
        const unsigned &primID = RTCHitN_primID(potentialHit, N, i);

        // unpack ray index
        const unsigned rayIndex = rayID;

        const modelinfo_t *source_modelinfo = rsi->self;
        const modelinfo_t *hit_modelinfo = Embree_LookupModelinfo(geomID, primID);
        if (!hit_modelinfo) {
            // we hit a "skip" face with no associated model
            // reject hit (???)
            valid[i] = INVALID;
            continue;
        }

        if (hit_modelinfo->shadowworldonly.boolValue()) {
            // we hit "_shadowworldonly" "1" geometry. Ignore the hit unless we are from world.
            if (!source_modelinfo || !source_modelinfo->isWorld()) {
                // reject hit
                valid[i] = INVALID;
                continue;
            }
        }

        if (hit_modelinfo->shadowself.boolValue()) {
            // only casts shadows on itself
            if (source_modelinfo != hit_modelinfo) {
                // reject hit
                valid[i] = INVALID;
                continue;
            }
        }

        if (hit_modelinfo->switchableshadow.boolValue()) {
            // we hit a dynamic shadow caster. reject the hit, but store the
            // info about what we hit.

            const int style = hit_modelinfo->switchshadstyle.intValue();

            AddDynamicOccluderToRay(context, rayIndex, style);

            // reject hit
            valid[i] = INVALID;
            continue;
        }

        // test fence textures and glass
        const mface_t *face = Embree_LookupFace(geomID, primID);
        float alpha = Face_Alpha(hit_modelinfo, face);

        // mxd
        bool isFence, isGlass;
        if (bsp_static->loadversion->game->id == GAME_QUAKE_II) {
            const int surf_flags = Face_ContentsOrSurfaceFlags(bsp_static, face);
            isFence = ((surf_flags & Q2_SURF_TRANSLUCENT) ==
                       Q2_SURF_TRANSLUCENT); // KMQuake 2-specific. Use texture alpha chanel when both flags are set.
            isGlass = !isFence && (surf_flags & Q2_SURF_TRANSLUCENT);
            if (isGlass)
                alpha = (surf_flags & Q2_SURF_TRANS33 ? 0.66f : 0.33f);
        } else {
            const std::string &name = Face_TextureName(bsp_static, face);
            isFence = (name[0] == '{');
            isGlass = (alpha < 1.0f);
        }

        if (isFence || isGlass) {
            vec3_t hitpoint;
            Embree_RayEndpoint(ray, N, i, hitpoint);
            const color_rgba sample = SampleTexture(face, bsp_static, hitpoint); // mxd. Palette index -> color_rgba

            if (isGlass) {
                // hit glass...

                // mxd. Adjust alpha by texture alpha?
                if (sample.a < 255)
                    alpha = sample.a / 255.0f;

                vec3_t rayDir = {RTCRayN_dir_x(ray, N, i), RTCRayN_dir_y(ray, N, i), RTCRayN_dir_z(ray, N, i)};
                vec3_t potentialHitGeometryNormal = {RTCHitN_Ng_x(potentialHit, N, i), RTCHitN_Ng_y(potentialHit, N, i),
                    RTCHitN_Ng_z(potentialHit, N, i)};

                VectorNormalize(rayDir);
                VectorNormalize(potentialHitGeometryNormal);

                const vec_t raySurfaceCosAngle = DotProduct(rayDir, potentialHitGeometryNormal);

                // only pick up the color of the glass on the _exiting_ side of the glass.
                // (we currently trace "backwards", from surface point --> light source)
                if (raySurfaceCosAngle < 0) {
                    vec3_t samplecolor{(float)sample.r, (float)sample.g, (float)sample.b};
                    VectorScale(samplecolor, 1 / 255.0f, samplecolor);

                    AddGlassToRay(context, rayIndex, alpha, samplecolor);
                }

                // reject hit
                valid[i] = INVALID;
                continue;
            }

            if (isFence) {
                if (sample.a < 255) {
                    // reject hit
                    valid[i] = INVALID;
                    continue;
                }
            }
        }

        // accept hit
        // (just need to leave the `valid` value set to VALID)
    }
}

// building faces for skip-textured bmodels

plane_t Node_Plane(const mbsp_t *bsp, const bsp2_dnode_t *node, bool side)
{
    const dplane_t *dplane = &bsp->dplanes[node->planenum];
    plane_t plane;

    VectorCopy(dplane->normal, plane.normal);
    plane.dist = dplane->dist;

    if (side) {
        VectorScale(plane.normal, -1, plane.normal);
        plane.dist *= -1.0f;
    }

    return plane;
}

/**
 * `planes` all of the node planes that bound this leaf, facing inward.
 */
static void Leaf_MakeFaces(
    const mbsp_t *bsp, const mleaf_t *leaf, const std::vector<plane_t> &planes, std::vector<winding_t> &result)
{
    for (const plane_t &plane : planes) {
        // flip the inward-facing split plane to get the outward-facing plane of the face we're constructing
        plane_t faceplane;
        VectorScale(plane.normal, -1, faceplane.normal);
        faceplane.dist = -plane.dist;

        std::optional<winding_t> winding = winding_t::from_plane(faceplane.normal, faceplane.dist, 10e6);

        // clip `winding` by all of the other planes
        for (const plane_t &plane2 : planes) {
            if (&plane2 == &plane)
                continue;

            auto clipped = winding->clip(plane2.normal, plane2.dist);

            // discard the back, continue clipping the front part
            winding = clipped[0];

            // check if everything was clipped away
            if (!winding)
                break;
        }

        if (winding) {
            // LogPrint("WARNING: winding clipped away\n");
        } else {
            result.push_back(std::move(*winding));
        }
    }
}

void MakeFaces_r(const mbsp_t *bsp, const int nodenum, std::vector<plane_t> *planes, std::vector<winding_t> &result)
{
    if (nodenum < 0) {
        const int leafnum = -nodenum - 1;
        const mleaf_t *leaf = &bsp->dleafs[leafnum];

        if ((bsp->loadversion->game->id == GAME_QUAKE_II) ? (leaf->contents & Q2_CONTENTS_SOLID)
                                                          : leaf->contents == CONTENTS_SOLID) {
            Leaf_MakeFaces(bsp, leaf, *planes, result);
        }
        return;
    }

    const bsp2_dnode_t *node = &bsp->dnodes[nodenum];

    // go down the front side
    const plane_t front = Node_Plane(bsp, node, false);
    planes->push_back(front);
    MakeFaces_r(bsp, node->children[0], planes, result);
    planes->pop_back();

    // go down the back side
    const plane_t back = Node_Plane(bsp, node, true);
    planes->push_back(back);
    MakeFaces_r(bsp, node->children[1], planes, result);
    planes->pop_back();
}

static void MakeFaces(const mbsp_t *bsp, const dmodelh2_t *model, std::vector<winding_t> &result)
{
    std::vector<plane_t> planes;
    MakeFaces_r(bsp, model->headnode[0], &planes, result);
    Q_assert(planes.empty());
}

void Embree_TraceInit(const mbsp_t *bsp)
{
    bsp_static = bsp;
    Q_assert(device == nullptr);

    std::vector<const mface_t *> skyfaces, solidfaces, filterfaces;

    // check all modelinfos
    for (size_t mi = 0; mi < bsp->dmodels.size(); mi++) {
        const modelinfo_t *model = ModelInfoForModel(bsp, mi);

        const bool isWorld = model->isWorld();
        const bool shadow = model->shadow.boolValue();
        const bool shadowself = model->shadowself.boolValue();
        const bool shadowworldonly = model->shadowworldonly.boolValue();
        const bool switchableshadow = model->switchableshadow.boolValue();

        if (!(isWorld || shadow || shadowself || shadowworldonly || switchableshadow))
            continue;

        for (int i = 0; i < model->model->numfaces; i++) {
            const mface_t *face = BSP_GetFace(bsp, model->model->firstface + i);

            // check for TEX_NOSHADOW
            const surfflags_t &extended_flags = extended_texinfo_flags[face->texinfo];
            if (extended_flags.extended & TEX_EXFLAG_NOSHADOW)
                continue;

            // handle switchableshadow
            if (switchableshadow) {
                filterfaces.push_back(face);
                continue;
            }

            const int contents_or_surf_flags = Face_ContentsOrSurfaceFlags(bsp, face); // mxd
            const gtexinfo_t *texinfo = Face_Texinfo(bsp, face);
            const bool is_q2 = bsp->loadversion->game->id == GAME_QUAKE_II;

            // mxd. Skip NODRAW faces, but not SKY ones (Q2's sky01.wal has both flags set)
            if (is_q2 && (contents_or_surf_flags & Q2_SURF_NODRAW) && !(contents_or_surf_flags & Q2_SURF_SKY))
                continue;

            // handle glass / water
            const float alpha = Face_Alpha(model, face);
            if (alpha < 1.0f ||
                (is_q2 && (contents_or_surf_flags & Q2_SURF_TRANSLUCENT))) { // mxd. Both fence and transparent textures
                                                                             // are done using SURF_TRANS flags in Q2
                filterfaces.push_back(face);
                continue;
            }

            // fence
            const std::string &texname = Face_TextureName(bsp, face);
            if (texname[0] == '{') {
                filterfaces.push_back(face);
                continue;
            }

            // handle sky
            if (is_q2) {
                // Q2: arghrad compat: sky faces only emit sunlight if:
                // sky flag set, light flag set, value nonzero
                if ((contents_or_surf_flags & Q2_SURF_SKY) != 0 &&
                    (!arghradcompat || ((contents_or_surf_flags & Q2_SURF_LIGHT) != 0 && texinfo->value != 0))) {
                    skyfaces.push_back(face);
                    continue;
                }
            } else {
                // Q1
                if (!Q_strncasecmp("sky", texname.data(), 3)) {
                    skyfaces.push_back(face);
                    continue;
                }
            }

            // liquids
            if (/* texname[0] == '*' */ ContentsOrSurfaceFlags_IsTranslucent(bsp, contents_or_surf_flags)) { // mxd
                if (!isWorld) {
                    // world liquids never cast shadows; shadow casting bmodel liquids do
                    solidfaces.push_back(face);
                }
                continue;
            }

            // solid faces

            if (isWorld || shadow) {
                solidfaces.push_back(face);
            } else {
                // shadowself or shadowworldonly
                Q_assert(shadowself || shadowworldonly);
                filterfaces.push_back(face);
            }
        }
    }

    /* Special handling of skip-textured bmodels */
    std::vector<winding_t> skipwindings;
    for (const modelinfo_t *model : tracelist) {
        if (model->model->numfaces == 0) {
            MakeFaces(bsp, model->model, skipwindings);
        }
    }

    device = rtcNewDevice(NULL);
    rtcSetDeviceErrorFunction(
        device, ErrorCallback, nullptr); // mxd. Changed from rtcDeviceSetErrorFunction to silence compiler warning...

    // log version
    const size_t ver_maj = rtcGetDeviceProperty(device, RTC_DEVICE_PROPERTY_VERSION_MAJOR);
    const size_t ver_min = rtcGetDeviceProperty(device, RTC_DEVICE_PROPERTY_VERSION_MINOR);
    const size_t ver_pat = rtcGetDeviceProperty(device, RTC_DEVICE_PROPERTY_VERSION_PATCH);
    FLogPrint("Embree version: {}.{}.{}\n", ver_maj, ver_min, ver_pat);

    scene = rtcNewScene(device);
    rtcSetSceneFlags(scene, RTC_SCENE_FLAG_NONE);
    rtcSetSceneBuildQuality(scene, RTC_BUILD_QUALITY_HIGH);
    skygeom = CreateGeometry(bsp, device, scene, skyfaces);
    solidgeom = CreateGeometry(bsp, device, scene, solidfaces);
    filtergeom = CreateGeometry(bsp, device, scene, filterfaces);
    CreateGeometryFromWindings(device, scene, skipwindings);

    rtcSetGeometryIntersectFilterFunction(
        rtcGetGeometry(scene, filtergeom.geomID), Embree_FilterFuncN<filtertype_t::INTERSECTION>);
    rtcSetGeometryOccludedFilterFunction(
        rtcGetGeometry(scene, filtergeom.geomID), Embree_FilterFuncN<filtertype_t::OCCLUSION>);

    rtcCommitScene(scene);

    FLogPrint("\n");
    LogPrint("\t{} sky faces\n", skyfaces.size());
    LogPrint("\t{} solid faces\n", solidfaces.size());
    LogPrint("\t{} filtered faces\n", filterfaces.size());
    LogPrint("\t{} shadow-casting skip faces\n", skipwindings.size());
}

static RTCRayHit SetupRay(unsigned rayindex, const qvec3d &start, const qvec3d &dir, vec_t dist)
{
    RTCRayHit ray;
    ray.ray.org_x = start[0];
    ray.ray.org_y = start[1];
    ray.ray.org_z = start[2];
    ray.ray.tnear = 0.f;

    ray.ray.dir_x = dir[0]; // can be un-normalized
    ray.ray.dir_y = dir[1];
    ray.ray.dir_z = dir[2];
    ray.ray.time = 0.f; // not using

    ray.ray.tfar = dist;
    ray.ray.mask = 1; // we're not using, but needs to be set if embree is compiled with masks
    ray.ray.id = rayindex;
    ray.ray.flags = 0; // reserved

    ray.hit.geomID = RTC_INVALID_GEOMETRY_ID;
    ray.hit.primID = RTC_INVALID_GEOMETRY_ID;
    ray.hit.instID[0] = RTC_INVALID_GEOMETRY_ID;
    return ray;
}

static RTCRayHit SetupRay_StartStop(const qvec3d &start, const qvec3d &stop)
{
    qvec3d dir = stop - start;
    vec_t dist = VectorNormalize(dir);

    return SetupRay(0, start, dir, dist);
}

// public
hitresult_t Embree_TestLight(const qvec3d &start, const qvec3d &stop, const modelinfo_t *self)
{
    RTCRay ray = SetupRay_StartStop(start, stop).ray;

    ray_source_info ctx2(nullptr, self);
    rtcOccluded1(scene, &ctx2, &ray);

    if (ray.tfar < 0.0f)
        return {false, 0}; // fully occluded

    // no obstruction (or a switchable shadow obstruction only)
    return {true, ctx2.singleRayShadowStyle};
}

// public
hitresult_t Embree_TestSky(const qvec3d &start, const qvec3d &dirn, const modelinfo_t *self, const mface_t **face_out)
{
    // trace from the sample point towards the sun, and
    // return true if we hit a sky poly.

    vec3_t dir_normalized;
    VectorCopy(dirn, dir_normalized);
    VectorNormalize(dir_normalized);

    RTCRayHit ray = SetupRay(0, start, dir_normalized, MAX_SKY_DIST);

    ray_source_info ctx2(nullptr, self);
    rtcIntersect1(scene, &ctx2, &ray);

    bool hit_sky = (ray.hit.geomID == skygeom.geomID);

    if (face_out) {
        if (hit_sky) {
            const sceneinfo &si = Embree_SceneinfoForGeomID(ray.hit.geomID);
            *face_out = si.triToFace.at(ray.hit.primID);
        } else {
            *face_out = nullptr;
        }
    }

    return {hit_sky, ctx2.singleRayShadowStyle};
}

// public
hittype_t Embree_DirtTrace(const qvec3d &start, const qvec3d &dirn, vec_t dist, const modelinfo_t *self,
    vec_t *hitdist_out, plane_t *hitplane_out, const mface_t **face_out)
{
    RTCRayHit ray = SetupRay(0, start, dirn, dist);
    ray_source_info ctx2(nullptr, self);
    rtcIntersect1(scene, &ctx2, &ray);
    ray.hit.Ng_x = -ray.hit.Ng_x;
    ray.hit.Ng_y = -ray.hit.Ng_y;
    ray.hit.Ng_z = -ray.hit.Ng_z;

    if (ray.hit.geomID == RTC_INVALID_GEOMETRY_ID)
        return hittype_t::NONE;

    if (hitdist_out) {
        *hitdist_out = ray.ray.tfar;
    }
    if (hitplane_out) {
        hitplane_out->normal[0] = ray.hit.Ng_x;
        hitplane_out->normal[1] = ray.hit.Ng_y;
        hitplane_out->normal[2] = ray.hit.Ng_z;
        VectorNormalize(hitplane_out->normal);

        vec3_t hitpoint;
        VectorMA(start, ray.ray.tfar, dirn, hitpoint);

        hitplane_out->dist = DotProduct(hitplane_out->normal, hitpoint);
    }
    if (face_out) {
        const sceneinfo &si = Embree_SceneinfoForGeomID(ray.hit.geomID);
        *face_out = si.triToFace.at(ray.hit.primID);
    }

    if (ray.hit.geomID == skygeom.geomID) {
        return hittype_t::SKY;
    } else {
        return hittype_t::SOLID;
    }
}

// enum class streamstate_t {
//    READY, DID_OCCLUDE, DID_INTERSECT
//};

static void *q_aligned_malloc(size_t align, size_t size)
{
#ifdef _MSC_VER
    return _aligned_malloc(size, align);
#else
    void *ptr;
    if (0 != posix_memalign(&ptr, align, size)) {
        return nullptr;
    }
    return ptr;
#endif
}

static void q_aligned_free(void *ptr)
{
#ifdef _MSC_VER
    _aligned_free(ptr);
#else
    free(ptr);
#endif
}

class raystream_embree_common_t : public virtual raystream_common_t
{
public:
    float *_rays_maxdist;
    int *_point_indices;
    vec3_t *_ray_colors;
    vec3_t *_ray_normalcontribs;

    // This is set to the modelinfo's switchshadstyle if the ray hit
    // a dynamic shadow caster. (note that for rays that hit dynamic
    // shadow casters, all of the other hit data is assuming the ray went
    // straight through).
    int *_ray_dynamic_styles;

    int _numrays;
    int _maxrays;
    //    streamstate_t _state;

public:
    raystream_embree_common_t(int maxRays)
        : _rays_maxdist{new float[maxRays]}, _point_indices{new int[maxRays]}, _ray_colors{new vec3_t[maxRays]{}},
          _ray_normalcontribs{new vec3_t[maxRays]{}}, _ray_dynamic_styles{new int[maxRays]}, _numrays{0}, _maxrays{
                                                                                                              maxRays}
    {
    }
    //,
    //_state { streamstate_t::READY } {}

    ~raystream_embree_common_t()
    {
        delete[] _rays_maxdist;
        delete[] _point_indices;
        delete[] _ray_colors;
        delete[] _ray_normalcontribs;
        delete[] _ray_dynamic_styles;
    }

    size_t numPushedRays() override { return _numrays; }

    int getPushedRayPointIndex(size_t j) override
    {
        // Q_assert(_state != streamstate_t::READY);
        Q_assert(j < _maxrays);
        return _point_indices[j];
    }

    void getPushedRayColor(size_t j, vec3_t out) override
    {
        Q_assert(j < _maxrays);
        VectorCopy(_ray_colors[j], out);
    }

    void getPushedRayNormalContrib(size_t j, vec3_t out) override
    {
        Q_assert(j < _maxrays);
        VectorCopy(_ray_normalcontribs[j], out);
    }

    int getPushedRayDynamicStyle(size_t j) override
    {
        Q_assert(j < _maxrays);
        return _ray_dynamic_styles[j];
    }

    void clearPushedRays() override
    {
        _numrays = 0;
        //_state = streamstate_t::READY;
    }
};

class raystream_embree_intersection_t : public raystream_embree_common_t, public raystream_intersection_t
{
public:
    RTCRayHit *_rays;

public:
    raystream_embree_intersection_t(int maxRays)
        : raystream_embree_common_t(maxRays), _rays{static_cast<RTCRayHit *>(
                                                  q_aligned_malloc(16, sizeof(RTCRayHit) * maxRays))}
    {
    }

    ~raystream_embree_intersection_t() { q_aligned_free(_rays); }

    void pushRay(int i, const qvec3d &origin, const qvec3d &dir, float dist, const qvec3d *color = nullptr,
        const qvec3d *normalcontrib = nullptr) override
    {
        Q_assert(_numrays < _maxrays);
        const RTCRayHit rayHit = SetupRay(_numrays, origin, dir, dist);
        _rays[_numrays] = rayHit;
        _rays_maxdist[_numrays] = dist;
        _point_indices[_numrays] = i;
        if (color) {
            VectorCopy(*color, _ray_colors[_numrays]);
        }
        if (normalcontrib) {
            VectorCopy(*normalcontrib, _ray_normalcontribs[_numrays]);
        }
        _ray_dynamic_styles[_numrays] = 0;
        _numrays++;
    }

    void tracePushedRaysIntersection(const modelinfo_t *self) override
    {
        if (!_numrays)
            return;

        ray_source_info ctx2(this, self);
        rtcIntersect1M(scene, &ctx2, _rays, _numrays, sizeof(_rays[0]));
    }

    void getPushedRayDir(size_t j, vec3_t out) override
    {
        Q_assert(j < _maxrays);
        out[0] = _rays[j].ray.dir_x;
        out[1] = _rays[j].ray.dir_y;
        out[2] = _rays[j].ray.dir_z;
    }

    float getPushedRayHitDist(size_t j) override
    {
        Q_assert(j < _maxrays);
        return _rays[j].ray.tfar;
    }

    hittype_t getPushedRayHitType(size_t j) override
    {
        Q_assert(j < _maxrays);

        const unsigned id = _rays[j].hit.geomID;
        if (id == RTC_INVALID_GEOMETRY_ID) {
            return hittype_t::NONE;
        } else if (id == skygeom.geomID) {
            return hittype_t::SKY;
        } else {
            return hittype_t::SOLID;
        }
    }

    const mface_t *getPushedRayHitFace(size_t j) override
    {
        Q_assert(j < _maxrays);

        const RTCRayHit &ray = _rays[j];

        if (ray.hit.geomID == RTC_INVALID_GEOMETRY_ID)
            return nullptr;

        const sceneinfo &si = Embree_SceneinfoForGeomID(ray.hit.geomID);
        const mface_t *face = si.triToFace.at(ray.hit.primID);
        Q_assert(face != nullptr);

        return face;
    }
};

class raystream_embree_occlusion_t : public raystream_embree_common_t, public raystream_occlusion_t
{
public:
    RTCRay *_rays;

public:
    raystream_embree_occlusion_t(int maxRays)
        : raystream_embree_common_t(maxRays), _rays{
                                                  static_cast<RTCRay *>(q_aligned_malloc(16, sizeof(RTCRay) * maxRays))}
    {
    }

    ~raystream_embree_occlusion_t() { q_aligned_free(_rays); }

    void pushRay(int i, const qvec3d &origin, const qvec3d &dir, float dist, const qvec3d *color = nullptr,
        const qvec3d *normalcontrib = nullptr) override
    {
        Q_assert(_numrays < _maxrays);
        _rays[_numrays] = SetupRay(_numrays, origin, dir, dist).ray;
        _rays_maxdist[_numrays] = dist;
        _point_indices[_numrays] = i;
        if (color) {
            VectorCopy(*color, _ray_colors[_numrays]);
        }
        if (normalcontrib) {
            VectorCopy(*normalcontrib, _ray_normalcontribs[_numrays]);
        }
        _ray_dynamic_styles[_numrays] = 0;
        _numrays++;
    }

    void tracePushedRaysOcclusion(const modelinfo_t *self) override
    {
        // Q_assert(_state == streamstate_t::READY);

        if (!_numrays)
            return;

        ray_source_info ctx2(this, self);
        rtcOccluded1M(scene, &ctx2, _rays, _numrays, sizeof(_rays[0]));
    }

    bool getPushedRayOccluded(size_t j) override
    {
        Q_assert(j < _maxrays);
        return (_rays[j].tfar < 0.0f);
    }

    void getPushedRayDir(size_t j, vec3_t out) override
    {
        Q_assert(j < _maxrays);

        out[0] = _rays[j].dir_x;
        out[1] = _rays[j].dir_y;
        out[2] = _rays[j].dir_z;
    }
};

raystream_occlusion_t *Embree_MakeOcclusionRayStream(int maxrays)
{
    return new raystream_embree_occlusion_t{maxrays};
}

raystream_intersection_t *Embree_MakeIntersectionRayStream(int maxrays)
{
    return new raystream_embree_intersection_t{maxrays};
}

void AddGlassToRay(RTCIntersectContext *context, unsigned rayIndex, float opacity, const vec3_t glasscolor)
{
    ray_source_info *ctx = static_cast<ray_source_info *>(context);
    raystream_embree_common_t *rs = ctx->raystream;

    if (rs == nullptr) {
        // FIXME: remove this.. once all ray casts use raystreams
        // happens for bounce lights, e.g. Embree_TestSky
        return;
    }

    // clamp opacity
    opacity = min(max(0.0f, opacity), 1.0f);

    Q_assert(rayIndex < rs->_numrays);

    Q_assert(glasscolor[0] >= 0.0 && glasscolor[0] <= 1.0);
    Q_assert(glasscolor[1] >= 0.0 && glasscolor[1] <= 1.0);
    Q_assert(glasscolor[2] >= 0.0 && glasscolor[2] <= 1.0);

    // multiply ray color by glass color
    vec3_t tinted;
    for (int i = 0; i < 3; i++) {
        tinted[i] = rs->_ray_colors[rayIndex][i] * glasscolor[i];
    }

    // lerp between original ray color and fully tinted, based on opacity
    vec3_t lerped = {0.0, 0.0, 0.0};
    VectorMA(lerped, opacity, tinted, lerped);
    VectorMA(lerped, 1.0 - opacity, rs->_ray_colors[rayIndex], lerped);

    // use the lerped color, scaled by (1-opacity) as the new ray color
    //  VectorScale(lerped, (1.0f - opacity), rs->_ray_colors[rayIndex]);

    // use the lerped color
    VectorCopy(lerped, rs->_ray_colors[rayIndex]);
}

void AddDynamicOccluderToRay(RTCIntersectContext *context, unsigned rayIndex, int style)
{
    ray_source_info *ctx = static_cast<ray_source_info *>(context);
    raystream_embree_common_t *rs = ctx->raystream;

    if (rs != nullptr) {
        rs->_ray_dynamic_styles[rayIndex] = style;
    } else {
        // TestLight case
        ctx->singleRayShadowStyle = style;
    }
}
