#ifndef ENGINE_TEXTURE_HH
#define ENGINE_TEXTURE_HH

#include <shared/gl.hh>
#include <shared/tools.hh>

#include "shader.hh"

struct ImageData
{
    int w, h, bpp, levels, align, pitch;
    GLenum compressed;
    uchar *data;
    void *owner;
    void (*freefunc)(void *);

    ImageData()
        : data(nullptr), owner(nullptr), freefunc(nullptr)
    {}


    ImageData(int nw, int nh, int nbpp, int nlevels = 1, int nalign = 0, GLenum ncompressed = GL_FALSE)
    {
        setdata(nullptr, nw, nh, nbpp, nlevels, nalign, ncompressed);
    }

    ImageData(int nw, int nh, int nbpp, uchar *data)
        : owner(nullptr), freefunc(nullptr)
    {
        setdata(data, nw, nh, nbpp);
    }

    ImageData(SDL_Surface *s) { wrap(s); }
    ~ImageData() { cleanup(); }

    void setdata(uchar *ndata, int nw, int nh, int nbpp, int nlevels = 1, int nalign = 0, GLenum ncompressed = GL_FALSE)
    {
        w = nw;
        h = nh;
        bpp = nbpp;
        levels = nlevels;
        align = nalign;
        pitch = align ? 0 : w*bpp;
        compressed = ncompressed;
        data = ndata ? ndata : new uchar[calcsize()];
        if(!ndata) { owner = this; freefunc = nullptr; }
    }

    int calclevelsize(int level) const { return ((max(w>>level, 1)+align-1)/align)*((max(h>>level, 1)+align-1)/align)*bpp; }

    int calcsize() const
    {
        if(!align) return w*h*bpp;
        int lw = w, lh = h,
            size = 0;
        loopi(levels)
        {
            if(lw<=0) lw = 1;
            if(lh<=0) lh = 1;
            size += ((lw+align-1)/align)*((lh+align-1)/align)*bpp;
            if(lw*lh==1) break;
            lw >>= 1;
            lh >>= 1;
        }
        return size;
    }

    void disown()
    {
        data = nullptr;
        owner = nullptr;
        freefunc = nullptr;
    }

    void cleanup()
    {
        if(owner==this) delete[] data;
        else if(freefunc) (*freefunc)(owner);
        disown();
    }

    void replace(ImageData &d)
    {
        cleanup();
        *this = d;
        if(owner == &d) owner = this;
        d.disown();
    }

    void wrap(SDL_Surface *s)
    {
        setdata((uchar *)s->pixels, s->w, s->h, s->format->BytesPerPixel);
        pitch = s->pitch;
        owner = s;
        freefunc = (void (*)(void *))SDL_FreeSurface;
    }
};

// management of texture slots
// each texture slot can have multiple texture frames, of which currently only the first is used
// additional frames can be used for various shaders

struct Texture
{
    enum
    {
        IMAGE      = 0,
        CUBEMAP    = 1,
        TYPE       = 0xFF,

        STUB       = 1<<8,
        TRANSIENT  = 1<<9,
        COMPRESSED = 1<<10,
        ALPHA      = 1<<11,
        MIRROR     = 1<<12,
        FLAGS      = 0xFF00
    };

    char *name;
    int type, w, h, xs, ys, bpp, clamp;
    bool mipmap, canreduce;
    GLuint id;
    uchar *alphamask;

    Texture() : alphamask(nullptr) {}
};

enum
{
    TEX_DIFFUSE = 0,
    TEX_NORMAL,
    TEX_GLOW,
    TEX_ENVMAP,

    TEX_SPEC,
    TEX_DEPTH,
    TEX_ALPHA,
    TEX_UNKNOWN,

    TEX_DETAIL = TEX_SPEC
};

enum
{
    VSLOT_SHPARAM = 0,
    VSLOT_SCALE,
    VSLOT_ROTATION,
    VSLOT_OFFSET,
    VSLOT_SCROLL,
    VSLOT_LAYER,
    VSLOT_ALPHA,
    VSLOT_COLOR,
    VSLOT_RESERVED, // used by RE
    VSLOT_REFRACT,
    VSLOT_DETAIL,
    VSLOT_NUM
};

struct VSlot
{
    Slot *slot;
    VSlot *next;
    int index, changed;
    vector<SlotShaderParam> params;
    bool linked;
    float scale;
    int rotation;
    ivec2 offset;
    vec2 scroll;
    int layer, detail;
    float alphafront, alphaback;
    vec colorscale;
    vec glowcolor;
    float refractscale;
    vec refractcolor;

    VSlot(Slot *slot = nullptr, int index = -1) : slot(slot), next(nullptr), index(index), changed(0)
    {
        reset();
        if(slot) addvariant(slot);
    }

    void addvariant(Slot *slot);

    void reset()
    {
        params.setsize(0);
        linked = false;
        scale = 1;
        rotation = 0;
        offset = ivec2(0, 0);
        scroll = vec2(0, 0);
        layer = detail = 0;
        alphafront = 0.5f;
        alphaback = 0;
        colorscale = vec(1, 1, 1);
        glowcolor = vec(1, 1, 1);
        refractscale = 0;
        refractcolor = vec(1, 1, 1);
    }

    void cleanup()
    {
        linked = false;
    }

    bool isdynamic() const;
};

struct Slot
{
    enum { OCTA, MATERIAL, DECAL };

    struct Tex
    {
        int type;
        Texture *t;
        string name;
        int combined;

        Tex() : t(nullptr), combined(-1) {}
    };

    int index, smooth;
    vector<Tex> sts;
    Shader *shader;
    vector<SlotShaderParam> params;
    VSlot *variants;
    bool loaded;
    uint texmask;
    char *grass;
    Texture *grasstex, *thumbnail;

    Slot(int index = -1) : index(index), variants(nullptr), grass(nullptr) { reset(); }
    virtual ~Slot() {}

    virtual int type() const { return OCTA; }
    virtual const char *name() const;
    virtual const char *texturedir() const { return "media/texture"; }

    virtual VSlot &emptyvslot();

    virtual int cancombine(int type) const;
    virtual bool shouldpremul(int type) const { return false; }

    int findtextype(int type, int last = -1) const;

    void load(int index, Slot::Tex &t);
    void load();

    Texture *loadthumbnail();

    void reset()
    {
        smooth = -1;
        sts.setsize(0);
        shader = nullptr;
        params.setsize(0);
        loaded = false;
        texmask = 0;
        DELETEA(grass);
        grasstex = nullptr;
        thumbnail = nullptr;
    }

    void cleanup()
    {
        loaded = false;
        grasstex = nullptr;
        thumbnail = nullptr;
        loopv(sts)
        {
            Tex &t = sts[i];
            t.t = nullptr;
            t.combined = -1;
        }
    }
};

inline void VSlot::addvariant(Slot *slot)
{
    if(!slot->variants) slot->variants = this;
    else
    {
        VSlot *prev = slot->variants;
        while(prev->next) prev = prev->next;
        prev->next = this;
    }
}

inline bool VSlot::isdynamic() const
{
    return !scroll.iszero() || slot->shader->isdynamic();
}

struct MatSlot : Slot, VSlot
{
    MatSlot();

    int type() const { return MATERIAL; }
    const char *name() const;

    VSlot &emptyvslot() { return *this; }

    int cancombine(int type) const { return -1; }

    void reset()
    {
        Slot::reset();
        VSlot::reset();
    }

    void cleanup()
    {
        Slot::cleanup();
        VSlot::cleanup();
    }
};

struct DecalSlot : Slot, VSlot
{
    float depth, fade;

    DecalSlot(int index = -1) : Slot(index), VSlot(this), depth(1), fade(0.5f) {}

    int type() const { return DECAL; }
    const char *name() const;
    const char *texturedir() const { return "media/decal"; }

    VSlot &emptyvslot() { return *this; }

    int cancombine(int type) const;
    bool shouldpremul(int type) const;

    void reset()
    {
        Slot::reset();
        VSlot::reset();
        depth = 1;
        fade = 0.5f;
    }

    void cleanup()
    {
        Slot::cleanup();
        VSlot::cleanup();
    }
};

struct texrotation
{
    bool flipx, flipy, swapxy;
};

struct cubemapside
{
    GLenum target;
    const char *name;
    bool flipx, flipy, swapxy;
};

extern const texrotation texrotations[8];
extern const cubemapside cubemapsides[6];

extern int hwtexsize, hwcubetexsize, hwmaxaniso, maxtexsize, hwtexunits, hwvtexunits;

#define MAXBLURRADIUS 7
extern float blursigma;
void setupblurkernel(int radius, float *weights, float *offsets);
void setblurshader(int pass, int size, int radius, float *weights, float *offsets, GLenum target = GL_TEXTURE_2D);

void savepng(const char *filename, ImageData &image, bool flip = false);
void savetga(const char *filename, ImageData &image, bool flip = false);
bool loadimage(const char *filename, ImageData &image);

extern Slot dummyslot;
extern VSlot dummyvslot;
extern DecalSlot dummydecalslot;
extern vector<Slot *> slots;
extern vector<VSlot *> vslots;

MatSlot &lookupmaterialslot(int slot, bool load = true);
Slot &lookupslot(int slot, bool load = true);
VSlot &lookupvslot(int slot, bool load = true);
DecalSlot &lookupdecalslot(int slot, bool load = true);
VSlot *findvslot(Slot &slot, const VSlot &src, const VSlot &delta);
VSlot *editvslot(const VSlot &src, const VSlot &delta);
void mergevslot(VSlot &dst, const VSlot &src, const VSlot &delta);
void packvslot(vector<uchar> &buf, const VSlot &src);
void packvslot(vector<uchar> &buf, int index);
void packvslot(vector<uchar> &buf, const VSlot *vs);
bool unpackvslot(ucharbuf &buf, VSlot &dst, bool delta);

struct cube;

void compactvslots(cube *c, int n = 8);
void compactvslot(int &index);
void compactvslot(VSlot &vs);
int compactvslots(bool cull = false);

void clearslots();

void linkslotshaders();

extern Texture *notexture;

int texalign(const void *data, int w, int bpp);
bool floatformat(GLenum format);
uchar *loadalphamask(Texture *t);
void setuptexcompress();
void createtexture(int tnum, int w, int h, const void *pixels, int clamp, int filter, GLenum component = GL_RGB, GLenum target = GL_TEXTURE_2D, int pw = 0, int ph = 0, int pitch = 0, bool resize = true, GLenum format = GL_FALSE, bool swizzle = false);
void create3dtexture(int tnum, int w, int h, int d, const void *pixels, int clamp, int filter, GLenum component = GL_RGB, GLenum target = GL_TEXTURE_3D, bool swizzle = false);
Texture *textureload(const char *name, int clamp = 0, bool mipit = true, bool msg = true);
bool settexture(const char *name, int clamp = 0);
bool reloadtexture(Texture &tex);
bool reloadtexture(const char *name);
void reloadtextures();
void cleanuptextures();

Texture *cubemapload(const char *name, bool mipit = true, bool msg = true, bool transient = false);
void initenvmaps();
void genenvmaps();
ushort closestenvmap(const vec &o);
ushort closestenvmap(int orient, const ivec &o, int size);
GLuint lookupenvmap(ushort emid);
GLuint lookupenvmap(Slot &slot);

#endif