struct iqm;

struct iqmheader
{
    char magic[16];
    uint version;
    uint filesize;
    uint flags;
    uint num_text, ofs_text;
    uint num_meshes, ofs_meshes;
    uint num_vertexarrays, num_vertexes, ofs_vertexarrays;
    uint num_triangles, ofs_triangles, ofs_adjacency;
    uint num_joints, ofs_joints;
    uint num_poses, ofs_poses;
    uint num_anims, ofs_anims;
    uint num_frames, num_framechannels, ofs_frames, ofs_bounds;
    uint num_comment, ofs_comment;
    uint num_extensions, ofs_extensions;
};

struct iqmmesh
{
    uint name;
    uint material;
    uint first_vertex, num_vertexes;
    uint first_triangle, num_triangles;
};

enum
{
    IQM_POSITION     = 0,
    IQM_TEXCOORD     = 1,
    IQM_NORMAL       = 2,
    IQM_TANGENT      = 3,
    IQM_BLENDINDEXES = 4,
    IQM_BLENDWEIGHTS = 5,
    IQM_COLOR        = 6,
    IQM_CUSTOM       = 0x10
};

enum
{
    IQM_BYTE   = 0,
    IQM_UBYTE  = 1,
    IQM_SHORT  = 2,
    IQM_USHORT = 3,
    IQM_INT    = 4,
    IQM_UINT   = 5,
    IQM_HALF   = 6,
    IQM_FLOAT  = 7,
    IQM_DOUBLE = 8,
};

struct iqmtriangle
{
    uint vertex[3];
};

struct iqmjoint
{
    uint name;
    int parent;
    vec pos;
    quat orient;
    vec size;
};

struct iqmpose
{
    int parent;
    uint mask;
    vec offsetpos;
    vec4 offsetorient;
    vec offsetsize;
    vec scalepos;
    vec4 scaleorient;
    vec scalesize;
};

struct iqmanim
{
    uint name;
    uint first_frame, num_frames;
    float framerate;
    uint flags;
};

struct iqmvertexarray
{
    uint type;
    uint flags;
    uint format;
    uint size;
    uint offset;
};

struct iqm : skelloader<iqm>
{
    iqm(const char *name) : skelloader(name) {}

    static const char *formatname() { return "iqm"; }
    int type() const { return MDL_IQM; }

    struct iqmmeshgroup : skelmeshgroup
    {
        iqmmeshgroup()
        {
        }

        bool loadiqmmeshes(const char *filename, const iqmheader &hdr, uchar *buf)
        {
            lilswap((uint *)&buf[hdr.ofs_vertexarrays], hdr.num_vertexarrays*sizeof(iqmvertexarray)/sizeof(uint));
            lilswap((uint *)&buf[hdr.ofs_triangles], hdr.num_triangles*sizeof(iqmtriangle)/sizeof(uint));
            lilswap((uint *)&buf[hdr.ofs_meshes], hdr.num_meshes*sizeof(iqmmesh)/sizeof(uint));
            lilswap((uint *)&buf[hdr.ofs_joints], hdr.num_joints*sizeof(iqmjoint)/sizeof(uint));

            const char *str = hdr.ofs_text ? (char *)&buf[hdr.ofs_text] : "";
            float *vpos = NULL, *vnorm = NULL, *vtan = NULL, *vtc = NULL;
            uchar *vindex = NULL, *vweight = NULL;
            iqmvertexarray *vas = (iqmvertexarray *)&buf[hdr.ofs_vertexarrays];
            loopi(hdr.num_vertexarrays)
            {
                iqmvertexarray &va = vas[i];
                switch(va.type)
                {
                    case IQM_POSITION: if(va.format != IQM_FLOAT || va.size != 3) return false; vpos = (float *)&buf[va.offset]; lilswap(vpos, 3*hdr.num_vertexes); break;
                    case IQM_NORMAL: if(va.format != IQM_FLOAT || va.size != 3) return false; vnorm = (float *)&buf[va.offset]; lilswap(vnorm, 3*hdr.num_vertexes); break;
                    case IQM_TANGENT: if(va.format != IQM_FLOAT || va.size != 4) return false; vtan = (float *)&buf[va.offset]; lilswap(vtan, 4*hdr.num_vertexes); break;
                    case IQM_TEXCOORD: if(va.format != IQM_FLOAT || va.size != 2) return false; vtc = (float *)&buf[va.offset]; lilswap(vtc, 2*hdr.num_vertexes); break;
                    case IQM_BLENDINDEXES: if(va.format != IQM_UBYTE || va.size != 4) return false; vindex = (uchar *)&buf[va.offset]; break;
                    case IQM_BLENDWEIGHTS: if(va.format != IQM_UBYTE || va.size != 4) return false; vweight = (uchar *)&buf[va.offset]; break;
                }
            }
            if(!vpos) return false;

            iqmtriangle *tris = (iqmtriangle *)&buf[hdr.ofs_triangles];
            iqmmesh *imeshes = (iqmmesh *)&buf[hdr.ofs_meshes];
            iqmjoint *joints = (iqmjoint *)&buf[hdr.ofs_joints];

            if(hdr.num_joints)
            {
                if(skel->numbones <= 0)
                {
                    skel->numbones = hdr.num_joints;
                    skel->bones = new boneinfo[skel->numbones];
                    loopi(hdr.num_joints)
                    {
                        iqmjoint &j = joints[i];
                        boneinfo &b = skel->bones[i];
                        if(!b.name) b.name = newstring(&str[j.name]);
                        b.parent = j.parent;
                        if(skel->shared <= 1)
                        {
                            j.pos.y = -j.pos.y;
                            j.orient.x = -j.orient.x;
                            j.orient.z = -j.orient.z;
                            j.orient.normalize();
                            b.base = dualquat(j.orient, j.pos);
                            if(b.parent >= 0) b.base.mul(skel->bones[b.parent].base, dualquat(b.base));
                            (b.invbase = b.base).invert();
                        }
                    }
                }

                if(skel->shared <= 1)
                    skel->linkchildren();
            }

            loopi(hdr.num_meshes)
            {
                iqmmesh &im = imeshes[i];
                skelmesh *m = new skelmesh;
                m->group = this;
                meshes.add(m);
                m->name = newstring(&str[im.name]);
                m->numverts = im.num_vertexes;
                int noblend = -1;
                if(m->numverts)
                {
                    m->verts = new vert[m->numverts];
                    if(!vindex || !vweight)
                    {
                        blendcombo c;
                        c.finalize(0);
                        noblend = m->addblendcombo(c);
                    }
                }
                int fv = im.first_vertex;
                float *mpos = vpos + 3*fv,
                      *mnorm = vnorm ? vnorm + 3*fv : NULL,
                      *mtan = vtan ? vtan + 4*fv : NULL,
                      *mtc = vtc ? vtc + 2*fv : NULL;
                uchar *mindex = vindex ? vindex + 4*fv : NULL, *mweight = vweight ? vweight + 4*fv : NULL;
                loopj(im.num_vertexes)
                {
                    vert &v = m->verts[j];
                    v.pos = vec(mpos[0], -mpos[1], mpos[2]);
                    mpos += 3;
                    if(mtc)
                    {
                        v.tc = vec2(mtc[0], mtc[1]);
                        mtc += 2;
                    }
                    else v.tc = vec2(0, 0);
                    if(mnorm)
                    {
                        v.norm = vec(mnorm[0], -mnorm[1], mnorm[2]);
                        mnorm += 3;
                        if(mtan)
                        {
                            m->calctangent(v, v.norm, vec(mtan[0], -mtan[1], mtan[2]), mtan[3]);
                            mtan += 4;
                        }
                    }
                    else { v.norm = vec(0, 0, 0); v.tangent = quat(0, 0, 0, 1); }
                    if(noblend < 0)
                    {
                        blendcombo c;
                        int sorted = 0;
                        loopk(4) sorted = c.addweight(sorted, mweight[k], mindex[k]);
                        mweight += 4;
                        mindex += 4;
                        c.finalize(sorted);
                        v.blend = m->addblendcombo(c);
                    }
                    else v.blend = noblend;
                }
                m->numtris = im.num_triangles;
                if(m->numtris) m->tris = new tri[m->numtris];
                iqmtriangle *mtris = tris + im.first_triangle;
                loopj(im.num_triangles)
                {
                    tri &t = m->tris[j];
                    t.vert[0] = mtris->vertex[0] - fv;
                    t.vert[1] = mtris->vertex[1] - fv;
                    t.vert[2] = mtris->vertex[2] - fv;
                    ++mtris;
                }
                if(!m->numtris || !m->numverts)
                {
                    conoutf("empty mesh in %s", filename);
                    meshes.removeobj(m);
                    delete m;
                    continue;
                }
                if(vnorm && !vtan) m->calctangents();
            }

            sortblendcombos();

            return true;
        }

        bool loadiqmanims(const char *filename, const iqmheader &hdr, uchar *buf)
        {
            lilswap((uint *)&buf[hdr.ofs_poses], hdr.num_poses*sizeof(iqmpose)/sizeof(uint));
            lilswap((uint *)&buf[hdr.ofs_anims], hdr.num_anims*sizeof(iqmanim)/sizeof(uint));
            lilswap((ushort *)&buf[hdr.ofs_frames], hdr.num_frames*hdr.num_framechannels);

            const char *str = hdr.ofs_text ? (char *)&buf[hdr.ofs_text] : "";
            iqmpose *poses = (iqmpose *)&buf[hdr.ofs_poses];
            iqmanim *anims = (iqmanim *)&buf[hdr.ofs_anims];
            ushort *frames = (ushort *)&buf[hdr.ofs_frames];
            loopi(hdr.num_anims)
            {
                iqmanim &a = anims[i];
                string name;
                copystring(name, filename);
                concatstring(name, ":");
                concatstring(name, &str[a.name]);
                skelanimspec *sa = skel->findskelanim(name);
                if(sa) continue;
                sa = &skel->addskelanim(name);
                sa->frame = skel->numframes;
                sa->range = a.num_frames;
                dualquat *animbones = new dualquat[(skel->numframes+a.num_frames)*skel->numbones];
                if(skel->bones)
                {
                    memcpy(animbones, skel->framebones, skel->numframes*skel->numbones*sizeof(dualquat));
                    delete[] skel->framebones;
                }
                skel->framebones = animbones;
                animbones += skel->numframes*skel->numbones;
                skel->numframes += a.num_frames;
                ushort *animdata = &frames[a.first_frame*hdr.num_framechannels];
                loopj(a.num_frames)
                {
                    dualquat *frame = &animbones[j*skel->numbones];
                    loopk(skel->numbones)
                    {
                        iqmpose &p = poses[k];
                        vec pos;
                        quat orient;
                        pos.x = p.offsetpos.x; if(p.mask&0x01) pos.x += *animdata++ * p.scalepos.x;
                        pos.y = -p.offsetpos.y; if(p.mask&0x02) pos.y -= *animdata++ * p.scalepos.y;
                        pos.z = p.offsetpos.z; if(p.mask&0x04) pos.z += *animdata++ * p.scalepos.z;
                        orient.x = -p.offsetorient.x; if(p.mask&0x08) orient.x -= *animdata++ * p.scaleorient.x;
                        orient.y = p.offsetorient.y; if(p.mask&0x10) orient.y += *animdata++ * p.scaleorient.y;
                        orient.z = -p.offsetorient.z; if(p.mask&0x20) orient.z -= *animdata++ * p.scaleorient.z;
                        orient.w = p.offsetorient.w; if(p.mask&0x40) orient.w += *animdata++ * p.scaleorient.w;
                        orient.normalize();
                        if(p.mask&0x380)
                        {
                            if(p.mask&0x80) animdata++;
                            if(p.mask&0x100) animdata++;
                            if(p.mask&0x200) animdata++;
                        }
                        dualquat dq(orient, pos);
                        if(adjustments.inrange(k)) adjustments[k].adjust(dq);
                        boneinfo &b = skel->bones[k];
                        dq.mul(b.invbase);
                        dualquat &dst = frame[k];
                        if(p.parent < 0) dst = dq;
                        else dst.mul(skel->bones[p.parent].base, dq);
                        dst.fixantipodal(skel->framebones[k]);
                    }
                }
            }

            return true;
        }

        bool loadiqm(const char *filename, bool doloadmesh, bool doloadanim)
        {
            stream *f = openfile(filename, "rb");
            if(!f) return false;

            uchar *buf = NULL;
            iqmheader hdr;
            if(f->read(&hdr, sizeof(hdr)) != sizeof(hdr) || memcmp(hdr.magic, "INTERQUAKEMODEL", sizeof(hdr.magic))) goto error;
            lilswap(&hdr.version, (sizeof(hdr) - sizeof(hdr.magic))/sizeof(uint));
            if(hdr.version != 2) goto error;
            if(hdr.filesize > (16<<20)) goto error; // sanity check... don't load files bigger than 16 MB
            buf = new (false) uchar[hdr.filesize];
            if(!buf || f->read(buf + sizeof(hdr), hdr.filesize - sizeof(hdr)) != hdr.filesize - sizeof(hdr)) goto error;

            if(doloadmesh && !loadiqmmeshes(filename, hdr, buf)) goto error;
            if(doloadanim && !loadiqmanims(filename, hdr, buf)) goto error;

            delete[] buf;
            delete f;
            return true;

        error:
            if(buf) delete[] buf;
            delete f;
            return false;
        }

        bool load(const char *filename, float smooth)
        {
            name = newstring(filename);

            return loadiqm(filename, true, false);
        }

        skelanimspec *loadanim(const char *animname)
        {
            const char *sep = strchr(animname, ':');
            skelanimspec *sa = skel->findskelanim(animname, sep ? '\0' : ':');
            if(!sa)
            {
                string filename;
                copystring(filename, animname);
                if(sep) filename[sep - animname] = '\0';
                if(loadiqm(filename, false, true))
                    sa = skel->findskelanim(animname, sep ? '\0' : ':');
            }
            return sa;
        }
    };

    skelmeshgroup *newmeshes() { return new iqmmeshgroup; }

    bool loaddefaultparts()
    {
        skelpart &mdl = addpart();
        const char *fname = name + strlen(name);
        do --fname; while(fname >= name && *fname!='/' && *fname!='\\');
        fname++;
        defformatstring(meshname, "media/model/%s/%s.iqm", name, fname);
        mdl.meshes = sharemeshes(path(meshname));
        if(!mdl.meshes) return false;
        mdl.initanimparts();
        mdl.initskins();
        return true;
    }
};

skelcommands<iqm> iqmcommands;

