#include "engine.h"

struct stainvert
{
    vec pos;
    vec4 color;
    float u, v;
};

struct staininfo
{
    int millis;
    vec color;
    uchar owner;
    ushort startvert, endvert;
};

enum
{
    SF_RND4       = 1<<0,
    SF_ROTATE     = 1<<1,
    SF_INVMOD     = 1<<2,
    SF_OVERBRIGHT = 1<<3,
    SF_GLOW       = 1<<4,
    SF_SATURATE   = 1<<5
};

VARFP(maxstaintris, 1, 2048, 16384, initstains());
VARMP(stainfade, 1, 15, 60, 1000);
VAR(dbgstain, 0, 0, 1);

struct stainbuffer
{
    stainvert *verts;
    int maxverts, startvert, endvert, lastvert, availverts;
    GLuint vbo;

    stainbuffer() : verts(NULL), maxverts(0), startvert(0), endvert(0), lastvert(0), availverts(0), vbo(0)
    {}

    ~stainbuffer()
    {
        DELETEA(verts);
    }

    void init(int tris)
    {
        if(verts)
        {
            DELETEA(verts);
            maxverts = startvert = endvert = lastvert = availverts = 0;
        }
        if(tris)
        {
            maxverts = tris*3 + 3;
            availverts = maxverts - 3;
            verts = new stainvert[maxverts];
        }
    }

    void cleanup()
    {
        if(vbo) { glDeleteBuffers_(1, &vbo); vbo = 0; }
    }

    void clear()
    {
        startvert = endvert = lastvert = 0;
        availverts = max(maxverts - 3, 0);
    }

    int freestain(const staininfo &d)
    {
        int removed = d.endvert < d.startvert ? maxverts - (d.startvert - d.endvert) : d.endvert - d.startvert;
        startvert = d.endvert;
        if(startvert==endvert) startvert = endvert = lastvert = 0;
        availverts += removed;
        return removed;
    }

    void clearstains(const staininfo &d)
    {
        startvert = d.endvert;
        availverts = endvert < startvert ? startvert - endvert - 3 : maxverts - 3 - (endvert - startvert);
    }

    bool faded(const staininfo &d) const { return verts[d.startvert].color.a < 1.0f; }

    void fadestain(const staininfo &d, const vec4 &color)
    {
        stainvert *vert = &verts[d.startvert],
                  *end = &verts[d.endvert < d.startvert ? maxverts : d.endvert];
        while(vert < end)
        {
            vert->color = color;
            vert++;
        }
        if(d.endvert < d.startvert)
        {
            vert = verts;
            end = &verts[d.endvert];
            while(vert < end)
            {
                vert->color = color;
                vert++;
            }
        }
    }

    void render()
    {
        if(startvert == endvert) return;

        if(!vbo) glGenBuffers_(1, &vbo);
        glBindBuffer_(GL_ARRAY_BUFFER, vbo);

        int count = endvert < startvert ? maxverts - startvert : endvert - startvert;
        glBufferData_(GL_ARRAY_BUFFER, maxverts*sizeof(stainvert), NULL, GL_STREAM_DRAW);
        glBufferSubData_(GL_ARRAY_BUFFER, 0, count*sizeof(stainvert), &verts[startvert]);
        if(endvert < startvert)
        {
            glBufferSubData_(GL_ARRAY_BUFFER, count*sizeof(stainvert), endvert*sizeof(stainvert), verts);
            count += endvert;
        }

        const stainvert *ptr = 0;
        gle::vertexpointer(sizeof(stainvert), &ptr->pos);
        gle::texcoord0pointer(sizeof(stainvert), &ptr->u);
        gle::colorpointer(sizeof(stainvert), &ptr->color, GL_FLOAT, 4);

        glDrawArrays(GL_TRIANGLES, 0, count);
        xtravertsva += count;
    }

    stainvert *addtri()
    {
        stainvert *tri = &verts[endvert];
        availverts -= 3;
        endvert += 3;
        if(endvert >= maxverts) endvert = 0;
        return tri;
    }

    bool hasverts() const { return startvert != endvert; }

    int nextverts() const
    {
        return endvert < lastvert ? endvert + maxverts - lastvert : endvert - lastvert;
    }

    int totalverts() const
    {
        return endvert < startvert ? maxverts - (startvert - endvert) : endvert - startvert;
    }

    int totaltris() const
    {
        return (maxverts - 3 - availverts)/3;
    }
};

struct stainrenderer
{
    const char *texname;
    int flags, fadeintime, fadeouttime, timetolive;
    Texture *tex;
    staininfo *stains;
    int maxstains, startstain, endstain;
    stainbuffer verts[NUMSTAINBUFS];

    stainrenderer(const char *texname, int flags = 0, int fadeintime = 0, int fadeouttime = 1000, int timetolive = -1)
        : texname(texname), flags(flags),
          fadeintime(fadeintime), fadeouttime(fadeouttime), timetolive(timetolive),
          tex(NULL),
          stains(NULL), maxstains(0), startstain(0), endstain(0),
          stainu(0), stainv(0)
    {
    }

    ~stainrenderer()
    {
        DELETEA(stains);
    }

    bool usegbuffer() const { return !(flags&(SF_INVMOD|SF_GLOW)); }

    void init(int tris)
    {
        if(stains)
        {
            DELETEA(stains);
            maxstains = startstain = endstain = 0;
        }
        stains = new staininfo[tris];
        maxstains = tris;
        loopi(NUMSTAINBUFS) verts[i].init(i == STAINBUF_TRANSPARENT ? tris/2 : tris);
    }

    void preload()
    {
        tex = textureload(texname, 3);
    }

    int totalstains()
    {
        return endstain < startstain ? maxstains - (startstain - endstain) : endstain - startstain;
    }

    bool hasstains(int sbuf)
    {
        return verts[sbuf].hasverts();
    }

    void clearstains()
    {
        startstain = endstain = 0;
        loopi(NUMSTAINBUFS) verts[i].clear();
    }

    int freestain()
    {
        if(startstain==endstain) return 0;

        staininfo &d = stains[startstain];
        startstain++;
        if(startstain >= maxstains) startstain = 0;

        return verts[d.owner].freestain(d);
    }

    bool faded(const staininfo &d) const { return verts[d.owner].faded(d); }

    void fadestain(const staininfo &d, float alpha)
    {
        vec color = d.color;
        if(flags&(SF_OVERBRIGHT|SF_GLOW|SF_INVMOD)) loopk(3) color[k] = color[k]*alpha/255.0f;
        verts[d.owner].fadestain(d, vec4(color, alpha));
    }

    void clearfadedstains()
    {
        int threshold = lastmillis - (timetolive>=0 ? timetolive : stainfade) - fadeouttime;
        staininfo *d = &stains[startstain],
                  *end = &stains[endstain < startstain ? maxstains : endstain],
                  *cleared[NUMSTAINBUFS] = { NULL };
        for(; d < end && d->millis <= threshold; d++)
            cleared[d->owner] = d;
        if(d >= end && endstain < startstain)
            for(d = stains, end = &stains[endstain]; d < end && d->millis <= threshold; d++)
                cleared[d->owner] = d;
        startstain = d - stains;
        if(startstain == endstain) loopi(NUMSTAINBUFS) verts[i].clear();
        else loopi(NUMSTAINBUFS) if(cleared[i]) verts[i].clearstains(*cleared[i]);
    }

    void fadeinstains()
    {
        if(!fadeintime) return;
        staininfo *d = &stains[endstain],
                  *end = &stains[endstain < startstain ? 0 : startstain];
        while(d > end)
        {
            d--;
            int fade = lastmillis - d->millis;
            if(fade < fadeintime) fadestain(*d, (fade<<8)/(fadeintime*255.0f));
            else if(faded(*d)) fadestain(*d, 1.0f);
            else return;
        }
        if(endstain < startstain)
        {
            d = &stains[maxstains];
            end = &stains[startstain];
            while(d > end)
            {
                d--;
                int fade = lastmillis - d->millis;
                if(fade < fadeintime) fadestain(*d, (fade<<8)/(fadeintime*255.0f));
                else if(faded(*d)) fadestain(*d, 1.0f);
                else return;
            }
        }
    }

    void fadeoutstains()
    {
        staininfo *d = &stains[startstain],
                  *end = &stains[endstain < startstain ? maxstains : endstain];
        int offset = (timetolive>=0 ? timetolive : stainfade) + fadeouttime - lastmillis;
        while(d < end)
        {
            int fade = d->millis + offset;
            if(fade >= fadeouttime) return;
            fadestain(*d, (fade<<8)/(fadeouttime*255.0f));
            d++;
        }
        if(endstain < startstain)
        {
            d = stains;
            end = &stains[endstain];
            while(d < end)
            {
                int fade = d->millis + offset;
                if(fade >= fadeouttime) return;
                fadestain(*d, (fade<<8)/(fadeouttime*255.0f));
                d++;
            }
        }
    }

    static void setuprenderstate(int sbuf, bool gbuf, int layer)
    {
        if(gbuf) maskgbuffer(sbuf == STAINBUF_TRANSPARENT ? "cg" : "c");
        else zerofogcolor();

        if(layer && ghasstencil)
        {
            glStencilFunc(GL_EQUAL, layer, 0x07);
            glStencilOp(GL_KEEP, GL_KEEP, GL_KEEP);
        }

        glColorMask(GL_TRUE, GL_TRUE, GL_TRUE, GL_FALSE);

        enablepolygonoffset(GL_POLYGON_OFFSET_FILL);

        glDepthMask(GL_FALSE);
        glEnable(GL_BLEND);

        gle::enablevertex();
        gle::enabletexcoord0();
        gle::enablecolor();
    }

    static void cleanuprenderstate(int sbuf, bool gbuf, int layer)
    {
        glBindBuffer_(GL_ARRAY_BUFFER, 0);

        gle::disablevertex();
        gle::disabletexcoord0();
        gle::disablecolor();

        glDepthMask(GL_TRUE);
        glDisable(GL_BLEND);

        disablepolygonoffset(GL_POLYGON_OFFSET_FILL);

        glColorMask(GL_TRUE, GL_TRUE, GL_TRUE, GL_TRUE);

        if(gbuf) maskgbuffer(sbuf == STAINBUF_TRANSPARENT ? "cndg" : "cnd");
        else resetfogcolor();
    }

    void cleanup()
    {
        loopi(NUMSTAINBUFS) verts[i].cleanup();
    }

    void render(int sbuf)
    {
        float colorscale = 1, alphascale = 1;
        if(flags&SF_OVERBRIGHT)
        {
            glBlendFunc(GL_DST_COLOR, GL_SRC_COLOR);
            SETVARIANTSWIZZLE(overbrightstain, tex, sbuf == STAINBUF_TRANSPARENT ? 1 : 0);
        }
        else if(flags&SF_GLOW)
        {
            glBlendFunc(GL_ONE, GL_ONE);
            colorscale = ldrscale;
            if(flags&SF_SATURATE) colorscale *= 2;
            alphascale = 0;
            SETSWIZZLE(foggedstain, tex);
        }
        else if(flags&SF_INVMOD)
        {
            glBlendFunc(GL_ZERO, GL_ONE_MINUS_SRC_COLOR);
            alphascale = 0;
            SETSWIZZLE(foggedstain, tex);
        }
        else
        {
            glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);
            colorscale = ldrscale;
            if(flags&SF_SATURATE) colorscale *= 2;
            SETVARIANTSWIZZLE(stain, tex, sbuf == STAINBUF_TRANSPARENT ? 1 : 0);
        }
        LOCALPARAMF(colorscale, colorscale, colorscale, colorscale, alphascale);

        glBindTexture(GL_TEXTURE_2D, tex->id);

        verts[sbuf].render();
    }

    staininfo &newstain()
    {
        staininfo &d = stains[endstain];
        int next = endstain + 1;
        if(next>=maxstains) next = 0;
        if(next==startstain) freestain();
        endstain = next;
        return d;
    }

    ivec bbmin, bbmax;
    vec staincenter, stainnormal, staintangent, stainbitangent;
    float stainradius, stainu, stainv;
    vec4 staincolor;

    void addstain(const vec &center, const vec &dir, float radius, const vec &color, int info)
    {
        bbmin = ivec(center).sub(radius);
        bbmax = ivec(center).add(radius).add(1);

        staincolor = vec4(color, 1.0f);
        staincenter = center;
        stainradius = radius;
        stainnormal = dir;
#if 0
        staintangent.orthogonal(dir);
#else
        staintangent = vec(dir.z, -dir.x, dir.y);
        staintangent.project(dir);
#endif
        if(flags&SF_ROTATE) staintangent.rotate(sincos360[rnd(360)], dir);
        staintangent.normalize();
        stainbitangent.cross(staintangent, dir);
        if(flags&SF_RND4)
        {
            stainu = 0.5f*(info&1);
            stainv = 0.5f*((info>>1)&1);
        }

        loopi(NUMSTAINBUFS) verts[i].lastvert = verts[i].endvert;
        gentris(worldroot, ivec(0, 0, 0), worldsize>>1);
        loopi(NUMSTAINBUFS)
        {
            stainbuffer &buf = verts[i];
            if(buf.endvert == buf.lastvert) continue;

            if(dbgstain)
            {
                int nverts = buf.nextverts();
                static const char * const sbufname[NUMSTAINBUFS] = { "opaque", "transparent", "mapmodel" };
                conoutf(CON_DEBUG, "tris = %d, verts = %d, total tris = %d, %s", nverts/3, nverts, buf.totaltris(), sbufname[i]);
            }

            staininfo &d = newstain();
            d.owner = i;
            d.color = color;
            d.millis = lastmillis;
            d.startvert = buf.lastvert;
            d.endvert = buf.endvert;
        }
    }

    void gentris(cube &cu, int orient, const ivec &o, int size, materialsurface *mat = NULL, int vismask = 0)
    {
        vec pos[MAXFACEVERTS+4];
        int numverts = 0, numplanes = 1;
        vec planes[2];
        if(mat)
        {
            planes[0] = vec(0, 0, 0);
            switch(orient)
            {
            #define GENFACEORIENT(orient, v0, v1, v2, v3) \
                case orient: \
                    planes[0][dimension(orient)] = dimcoord(orient) ? 1 : -1; \
                    v0 v1 v2 v3 \
                    break;
            #define GENFACEVERT(orient, vert, x,y,z, xv,yv,zv) \
                    pos[numverts++] = vec(x xv, y yv, z zv);
                GENFACEVERTS(o.x, o.x, o.y, o.y, o.z, o.z, , + mat->csize, , + mat->rsize, + 0.1f, - 0.1f);
            #undef GENFACEORIENT
            #undef GENFACEVERT
            }
        }
        else if(cu.texture[orient] == DEFAULT_SKY) return;
        else if(cu.ext && (numverts = cu.ext->surfaces[orient].numverts&MAXFACEVERTS))
        {
            vertinfo *verts = cu.ext->verts() + cu.ext->surfaces[orient].verts;
            ivec vo = ivec(o).mask(~0xFFF).shl(3);
            loopj(numverts) pos[j] = vec(verts[j].getxyz().add(vo)).mul(1/8.0f);
            planes[0].cross(pos[0], pos[1], pos[2]).normalize();
            if(numverts >= 4 && !(cu.merged&(1<<orient)) && !flataxisface(cu, orient) && faceconvexity(verts, numverts, size))
            {
                planes[1].cross(pos[0], pos[2], pos[3]).normalize();
                numplanes++;
            }
        }
        else if(cu.merged&(1<<orient)) return;
        else if(!vismask || (vismask&0x40 && visibleface(cu, orient, o, size, MAT_AIR, (cu.material&MAT_ALPHA)^MAT_ALPHA, MAT_ALPHA)))
        {
            ivec v[4];
            genfaceverts(cu, orient, v);
            int vis = 3, convex = faceconvexity(v, vis), order = convex < 0 ? 1 : 0;
            vec vo(o);
            pos[numverts++] = vec(v[order]).mul(size/8.0f).add(vo);
            if(vis&1) pos[numverts++] = vec(v[order+1]).mul(size/8.0f).add(vo);
            pos[numverts++] = vec(v[order+2]).mul(size/8.0f).add(vo);
            if(vis&2) pos[numverts++] = vec(v[(order+3)&3]).mul(size/8.0f).add(vo);
            planes[0].cross(pos[0], pos[1], pos[2]).normalize();
            if(convex) { planes[1].cross(pos[0], pos[2], pos[3]).normalize(); numplanes++; }
        }
        else return;

        stainbuffer &buf = verts[mat || cu.material&MAT_ALPHA ? STAINBUF_TRANSPARENT : STAINBUF_OPAQUE];
        loopl(numplanes)
        {
            const vec &n = planes[l];
            float facing = n.dot(stainnormal);
            if(facing <= 0) continue;
            vec p = vec(pos[0]).sub(staincenter);
#if 0
            // intersect ray along stain normal with plane
            float dist = n.dot(p) / facing;
            if(fabs(dist) > stainradius) continue;
            vec pcenter = vec(stainnormal).mul(dist).add(staincenter);
#else
            // travel back along plane normal from the stain center
            float dist = n.dot(p);
            if(fabs(dist) > stainradius) continue;
            vec pcenter = vec(n).mul(dist).add(staincenter);
#endif
            vec ft, fb;
            ft.orthogonal(n);
            ft.normalize();
            fb.cross(ft, n);
            vec pt = vec(ft).mul(ft.dot(staintangent)).add(vec(fb).mul(fb.dot(staintangent))).normalize(),
                pb = vec(ft).mul(ft.dot(stainbitangent)).add(vec(fb).mul(fb.dot(stainbitangent))).project(pt).normalize();
            vec v1[MAXFACEVERTS+4], v2[MAXFACEVERTS+4];
            float ptc = pt.dot(pcenter), pbc = pb.dot(pcenter);
            int numv;
            if(numplanes >= 2)
            {
                if(l) { pos[1] = pos[2]; pos[2] = pos[3]; }
                numv = polyclip(pos, 3, pt, ptc - stainradius, ptc + stainradius, v1);
                if(numv<3) continue;
            }
            else
            {
                numv = polyclip(pos, numverts, pt, ptc - stainradius, ptc + stainradius, v1);
                if(numv<3) continue;
            }
            numv = polyclip(v1, numv, pb, pbc - stainradius, pbc + stainradius, v2);
            if(numv<3) continue;
            float tsz = flags&SF_RND4 ? 0.5f : 1.0f, scale = tsz*0.5f/stainradius,
                  tu = stainu + tsz*0.5f - ptc*scale, tv = stainv + tsz*0.5f - pbc*scale;
            pt.mul(scale); pb.mul(scale);
            stainvert dv1 = { v2[0], staincolor, pt.dot(v2[0]) + tu, pb.dot(v2[0]) + tv },
                      dv2 = { v2[1], staincolor, pt.dot(v2[1]) + tu, pb.dot(v2[1]) + tv };
            int totalverts = 3*(numv-2);
            if(totalverts > buf.maxverts-3) return;
            while(buf.availverts < totalverts)
            {
                if(!freestain()) return;
            }
            loopk(numv-2)
            {
                stainvert *tri = buf.addtri();
                tri[0] = dv1;
                tri[1] = dv2;
                dv2.pos = v2[k+2];
                dv2.u = pt.dot(v2[k+2]) + tu;
                dv2.v = pb.dot(v2[k+2]) + tv;
                tri[2] = dv2;
            }
        }
    }

    void findmaterials(vtxarray *va)
    {
        materialsurface *matbuf = va->matbuf;
        int matsurfs = va->matsurfs;
        loopi(matsurfs)
        {
            materialsurface &m = matbuf[i];
            if(!isclipped(m.material&MATF_VOLUME)) { i += m.skip; continue; }
            int dim = dimension(m.orient), dc = dimcoord(m.orient);
            if(dc ? stainnormal[dim] <= 0 : stainnormal[dim] >= 0) { i += m.skip; continue; }
            int c = C[dim], r = R[dim];
            for(;;)
            {
                materialsurface &m = matbuf[i];
                if(m.o[dim] >= bbmin[dim] && m.o[dim] <= bbmax[dim] &&
                   m.o[c] + m.csize >= bbmin[c] && m.o[c] <= bbmax[c] &&
                   m.o[r] + m.rsize >= bbmin[r] && m.o[r] <= bbmax[r])
                {
                    static cube dummy;
                    gentris(dummy, m.orient, m.o, max(m.csize, m.rsize), &m);
                }
                if(i+1 >= matsurfs) break;
                materialsurface &n = matbuf[i+1];
                if(n.material != m.material || n.orient != m.orient) break;
                i++;
            }
        }
    }

    void findescaped(cube *c, const ivec &o, int size, int escaped)
    {
        loopi(8)
        {
            cube &cu = c[i];
            if(escaped&(1<<i))
            {
                ivec co(i, o, size);
                if(cu.children) findescaped(cu.children, co, size>>1, cu.escaped);
                else
                {
                    int vismask = cu.merged;
                    if(vismask) loopj(6) if(vismask&(1<<j)) gentris(cu, j, co, size);
                }
            }
        }
    }

    void genmmtri(const vec v[3])
    {
        vec n;
        n.cross(v[0], v[1], v[2]).normalize();
        float facing = n.dot(stainnormal);
        if(facing <= 0) return;

        vec p = vec(v[0]).sub(staincenter);
#if 0
        float dist = n.dot(p) / facing;
        if(fabs(dist) > stainradius) return;
        vec pcenter = vec(stainnormal).mul(dist).add(staincenter);
#else
        float dist = n.dot(p);
        if(fabs(dist) > stainradius) return;
        vec pcenter = vec(n).mul(dist).add(staincenter);
#endif

        vec ft, fb;
        ft.orthogonal(n);
        ft.normalize();
        fb.cross(ft, n);
        vec pt = vec(ft).mul(ft.dot(staintangent)).add(vec(fb).mul(fb.dot(staintangent))).normalize(),
            pb = vec(ft).mul(ft.dot(stainbitangent)).add(vec(fb).mul(fb.dot(stainbitangent))).project(pt).normalize();
        vec v1[3+4], v2[3+4];
        float ptc = pt.dot(pcenter), pbc = pb.dot(pcenter);
        int numv = polyclip(v, 3, pt, ptc - stainradius, ptc + stainradius, v1);
        if(numv<3) return;
        numv = polyclip(v1, numv, pb, pbc - stainradius, pbc + stainradius, v2);
        if(numv<3) return;
        float tsz = flags&SF_RND4 ? 0.5f : 1.0f, scale = tsz*0.5f/stainradius,
              tu = stainu + tsz*0.5f - ptc*scale, tv = stainv + tsz*0.5f - pbc*scale;
        pt.mul(scale); pb.mul(scale);
        stainvert dv1 = { v2[0], staincolor, pt.dot(v2[0]) + tu, pb.dot(v2[0]) + tv },
                  dv2 = { v2[1], staincolor, pt.dot(v2[1]) + tu, pb.dot(v2[1]) + tv };
        int totalverts = 3*(numv-2);
        stainbuffer &buf = verts[STAINBUF_MAPMODEL];
        if(totalverts > buf.maxverts-3) return;
        while(buf.availverts < totalverts)
        {
            if(!freestain()) return;
        }
        loopk(numv-2)
        {
            stainvert *tri = buf.addtri();
            tri[0] = dv1;
            tri[1] = dv2;
            dv2.pos = v2[k+2];
            dv2.u = pt.dot(v2[k+2]) + tu;
            dv2.v = pb.dot(v2[k+2]) + tv;
            tri[2] = dv2;
        }
    }

    void genmmtris(octaentities &oe)
    {
        const vector<extentity *> &ents = entities::getents();
        loopv(oe.mapmodels)
        {
            extentity &e = *ents[oe.mapmodels[i]];
            if (e.type != ET_MAPMODEL) continue;
            model *m = entities::getmodel(e);
            if (!m) continue;

            vec center, radius;
            float rejectradius = m->collisionbox(center, radius), scale = e.attr[3] > 0 ? e.attr[3]/100.0f : 1;
            center.mul(scale);
            if(staincenter.reject(vec(e.o).add(center), stainradius + rejectradius*scale)) continue;

            if(m->animated() || (!m->bih && !m->setBIH())) continue; 

            int yaw = e.attr[0], pitch = e.attr[1], roll = e.attr[2];

            m->bih->genstaintris(this, staincenter, stainradius, e.o, yaw, pitch, roll, scale);
        }
    }
    
    void gentris(cube *c, const ivec &o, int size, int escaped = 0)
    {
        int overlap = octaboxoverlap(o, size, bbmin, bbmax);
        loopi(8)
        {
            cube &cu = c[i];
            if(overlap&(1<<i))
            {
                ivec co(i, o, size);
                if(cu.ext)
                {
                    if(cu.ext->va && cu.ext->va->matsurfs) findmaterials(cu.ext->va);
                    if(cu.ext->ents && cu.ext->ents->mapmodels.length()) genmmtris(*cu.ext->ents);            
                }
                if(cu.children) gentris(cu.children, co, size>>1, cu.escaped);
                else
                {
                    int vismask = cu.visible;
                    if(vismask&0xC0)
                    {
                        if(vismask&0x80) loopj(6) gentris(cu, j, co, size, NULL, vismask);
                        else loopj(6) if(vismask&(1<<j)) gentris(cu, j, co, size);
                    }
                }
            }
            else if(escaped&(1<<i))
            {
                ivec co(i, o, size);
                if(cu.children) findescaped(cu.children, co, size>>1, cu.escaped);
                else
                {
                    int vismask = cu.merged;
                    if(vismask) loopj(6) if(vismask&(1<<j)) gentris(cu, j, co, size);
                }
            }
        }
    }
};

static vector<stainrenderer*> stains;
static hashtable<const char*, int> stainmap;

static bool get_renderer(lua_State *L, const char *name) {
    int *id = stainmap.access(name);
    if (id) {
        lua_pushinteger(L, *id);
        lua_pushboolean(L, false);
        return true;
    }
    return false;
}

static void register_renderer(lua_State *L, const char *s, stainrenderer *rd) {
    int n = stains.length();
    stainmap.access(s, n);
    stains.add(rd);
    rd->init(maxstaintris);
    lua_pushinteger(L, n);
    lua_pushboolean(L, true);
}

LUAICOMMAND(stain_register_renderer, {
    const char *name = luaL_checkstring(L, 1);
    if (get_renderer(L, name)) return 2;
    const char *path = luaL_checkstring(L, 2);
    int flags   = luaL_optinteger(L, 3, 0);
    int fadein  = luaL_optinteger(L, 4, 0);
    int fadeout = luaL_optinteger(L, 5, 1000);
    int timeout = luaL_optinteger(L, 6, -1);
    register_renderer(L, newstring(name), new stainrenderer(newstring(path),
        flags, fadein, fadeout, timeout));
    return 2;
});

LUAICOMMAND(stain_get_renderer, {
    const char *name = luaL_checkstring(L, 1);
    int *id = stainmap.access(name);
    if (!id) return 0;
    lua_pushinteger(L, *id);
    return 1;
})

void initstains()
{
    if (initing) return;
    loopv(stains) stains[i]->init(maxstaintris);
    loopv(stains) {
        loadprogress = float(i + 1) / stains.length();
        stains[i]->preload();
    }
    loadprogress = 0;
}

void clearstains()
{
    loopv(stains) stains[i]->clearstains();
}

void genstainmmtri(stainrenderer *s, const vec v[3])
{
    s->genmmtri(v);
}

VARNP(stains, showstains, 0, 1, 1);

bool renderstains(int sbuf, bool gbuf, int layer)
{
    bool rendered = false;
    loopv(stains)
    {
        stainrenderer &d = *stains[i];
        if(d.usegbuffer() != gbuf) continue;
        if(sbuf == STAINBUF_OPAQUE)
        {
            d.clearfadedstains();
            d.fadeinstains();
            d.fadeoutstains();
        }
        if(!showstains || !d.hasstains(sbuf)) continue;
        if(!rendered)
        {
            rendered = true;
            stainrenderer::setuprenderstate(sbuf, gbuf, layer);
        }
        d.render(sbuf);
    }
    if(!rendered) return false;
    stainrenderer::cleanuprenderstate(sbuf, gbuf, layer);
    return true;
}

void cleanupstains()
{
    loopv(stains) stains[i]->cleanup();
}

void deletestains() {
    clearstains();
    cleanupstains();
    while (stains.length()) {
        stainrenderer *rd = stains.pop();
        delete[] rd->texname;
        delete rd;
    }
    enumeratekt(stainmap, const char*, name, int, value, {
        delete[] (char*)name;
        (void)value; /* supress warnings */
    });
    stainmap.clear();
}

VARP(maxstaindistance, 1, 512, 10000);

void addstain(int type, const vec &center, const vec &surface, float radius, const vec &color, int info)
{
    if(!showstains || !stains.inrange(type) || center.dist(camera1->o) - radius > maxstaindistance) return;
    stains[type]->addstain(center, surface, radius, color, info);
}

CLUAICOMMAND(stain_add, bool, (int type, float cx, float cy, float cz,
float sx, float sy, float sz, float radius, float r, float g, float b,
int info), {
    if (!stains.inrange(type)) return false;
    addstain(type, vec(cx, cy, cz), vec(sx, sy, sz), radius,
        vec(r, g, b), info);
    return true;
})
