// texture.cpp
//
// Copyright (C) 2001-2003, Chris Laurel
//
// 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.

#ifndef JPEG_SUPPORT
#define JPEG_SUPPORT
#endif

#ifndef PNG_SUPPORT
#define PNG_SUPPORT
#endif

#ifdef TARGET_OS_MAC
#include <unistd.h>
#endif

#include <cmath>
#include <libintl.h>
#include <algorithm>
#include <iostream>
#include <fstream>
#include <cstdlib>
#include <cstdio>
#include <cstring> /* for memcpy */
#include <cassert>

#ifndef _WIN32
#ifndef TARGET_OS_MAC
#include <config.h>
#endif /* ! TARGET_OS_MAC */
#endif /* ! _WIN32 */

#include <celmath/vecmath.h>
#include <celutil/filetype.h>
#include <celutil/debug.h>
#include <celutil/util.h>

#include "gl.h"
#include "glext.h"
#include "celestia.h"

// OpenGL 1.2 stuff missing from Windows headers . . . probably should be
// moved into glext.h
#ifndef GL_TEXTURE_MAX_LEVEL
#define GL_TEXTURE_MAX_LEVEL 0x813D
#endif

#ifdef JPEG_SUPPORT

#ifndef PNG_SUPPORT
#include "setjmp.h"
#endif // PNG_SUPPORT

extern "C" {
#ifdef _WIN32
#include "jpeglib.h"
#else
#include <jpeglib.h>
#endif
}

#endif // JPEG_SUPPORT

#ifdef PNG_SUPPORT
#include "png.h"

// Define png_jmpbuf() in case we are using a pre-1.0.6 version of libpng
#ifndef png_jmpbuf
#define png_jmpbuf(png_ptr) png_ptr->jmpbuf
#endif // PNG_SUPPORT

// Define various expansion transformations for old versions of libpng
#if PNG_LIBPNG_VER < 10004
#define png_set_palette_to_rgb(p)  png_set_expand(p)
#define png_set_gray_1_2_4_to_8(p) png_set_expand(p)
#define png_set_tRNS_to_alpha(p)   png_set_expand(p)
#endif // PNG_LIBPNG_VER < 10004

#endif // PNG_SUPPORT

#include "texture.h"
#include "virtualtex.h"

using namespace std;

static bool texCapsInitialized = false;

struct TextureCaps
{
    bool compressionSupported;
    bool clampToEdgeSupported;
    bool clampToBorderSupported;
    bool autoMipMapSupported;
    bool maxLevelSupported;
    GLint maxTextureSize;
    bool nonPow2Supported;
};

static TextureCaps texCaps;


static bool testMaxLevel()
{
    unsigned char texels[64];

    glEnable(GL_TEXTURE_2D);
    // Test whether GL_TEXTURE_MAX_LEVEL is supported . . .
    glTexImage2D(GL_TEXTURE_2D,
                 0,
                 GL_LUMINANCE,
                 8, 8,
                 0,
                 GL_LUMINANCE,
                 GL_UNSIGNED_BYTE,
                 texels);
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAX_LEVEL, 2);
    float maxLev = -1.0f;
    glGetTexParameterfv(GL_TEXTURE_2D, GL_TEXTURE_MAX_LEVEL, &maxLev);
    glDisable(GL_TEXTURE_2D);

    return maxLev == 2;
}


static const TextureCaps& GetTextureCaps()
{
    if (!texCapsInitialized)
    {
        texCapsInitialized = true;
        texCaps.compressionSupported = ExtensionSupported("GL_ARB_texture_compression");
        if (texCaps.compressionSupported)
            InitExtension("GL_ARB_texture_compression");

#ifdef GL_VERSION_1_2
        texCaps.clampToEdgeSupported = true;
#else
        texCaps.clampToEdgeSupported = ExtensionSupported("GL_EXT_texture_edge_clamp");
#endif // GL_VERSION_1_2
        texCaps.clampToBorderSupported = ExtensionSupported("GL_ARB_texture_border_clamp");
        texCaps.autoMipMapSupported = ExtensionSupported("GL_SGIS_generate_mipmap");
        texCaps.maxLevelSupported = testMaxLevel();
        glGetIntegerv(GL_MAX_TEXTURE_SIZE, &texCaps.maxTextureSize);
        texCaps.nonPow2Supported = ExtensionSupported("GL_ARB_texture_non_power_of_two");
    }

    return texCaps;
}



static int getInternalFormat(int format)
{
    switch (format)
    {
    case GL_RGBA:
    case GL_BGRA_EXT:
        return 4;
    case GL_RGB:
    case GL_BGR_EXT:
        return 3;
    case GL_LUMINANCE_ALPHA:
        return 2;
    case GL_ALPHA:
    case GL_INTENSITY:
    case GL_LUMINANCE:
        return 1;
    case GL_DSDT_NV:
        return format;
    case GL_COMPRESSED_RGBA_S3TC_DXT1_EXT:
    case GL_COMPRESSED_RGBA_S3TC_DXT3_EXT:
    case GL_COMPRESSED_RGBA_S3TC_DXT5_EXT:
        return format;
    default:
        return 0;
    }
}


#if 0
// Required in order to support on-the-fly compression; currently, this
// feature is disabled.
static int getCompressedInternalFormat(int format)
{
    switch (format)
    {
    case GL_RGB:
    case GL_BGR_EXT:
        return GL_COMPRESSED_RGB_ARB;
    case GL_RGBA:
    case GL_BGRA_EXT:
        return GL_COMPRESSED_RGBA_ARB;
    case GL_ALPHA:
        return GL_COMPRESSED_ALPHA_ARB;
    case GL_LUMINANCE:
        return GL_COMPRESSED_LUMINANCE_ARB;
    case GL_LUMINANCE_ALPHA:
        return GL_COMPRESSED_LUMINANCE_ALPHA_ARB;
    case GL_INTENSITY:
        return GL_COMPRESSED_INTENSITY_ARB;
    case GL_COMPRESSED_RGBA_S3TC_DXT1_EXT:
    case GL_COMPRESSED_RGBA_S3TC_DXT3_EXT:
    case GL_COMPRESSED_RGBA_S3TC_DXT5_EXT:
        return format;
    default:
        return 0;
    }
}
#endif


static int getCompressedBlockSize(int format)
{
    if (format == GL_COMPRESSED_RGBA_S3TC_DXT1_EXT)
        return 8;
    else
        return 16;
}


static GLenum GetGLTexAddressMode(Texture::AddressMode addressMode)
{
    const TextureCaps& caps = GetTextureCaps();

    switch (addressMode)
    {
    case Texture::Wrap:
        return GL_REPEAT;

    case Texture::EdgeClamp:
        return caps.clampToEdgeSupported ? GL_CLAMP_TO_EDGE : GL_CLAMP;

    case Texture::BorderClamp:
        if (caps.clampToBorderSupported)
            return GL_CLAMP_TO_BORDER_ARB;
        else
            return caps.clampToEdgeSupported ? GL_CLAMP_TO_EDGE : GL_CLAMP;
    }

    return 0;
}


static void SetBorderColor(Color borderColor, GLenum target)
{
    float bc[4] = { borderColor.red(), borderColor.green(),
                    borderColor.blue(), borderColor.alpha() };
    glTexParameterfv(target, GL_TEXTURE_BORDER_COLOR, bc);
}


// Load a prebuilt set of mipmaps; assumes that the image contains
// a complete set of mipmap levels.
static void LoadMipmapSet(Image& img, GLenum target)
{
    int internalFormat = getInternalFormat(img.getFormat());

    for (int mip = 0; mip < img.getMipLevelCount(); mip++)
    {
        uint mipWidth  = max((uint) img.getWidth() >> mip, 1u);
        uint mipHeight = max((uint) img.getHeight() >> mip, 1u);

        if (img.isCompressed())
        {
            glx::glCompressedTexImage2DARB(target,
                                           mip,
                                           internalFormat,
                                           mipWidth, mipHeight,
                                           0,
                                           img.getMipLevelSize(mip),
                                           img.getMipLevel(mip));
        }
        else
        {
            glTexImage2D(target,
                         mip,
                         internalFormat,
                         mipWidth, mipHeight,
                         0,
                         (GLenum) img.getFormat(),
                         GL_UNSIGNED_BYTE,
                         img.getMipLevel(mip));
        }
    }
}


// Load a texture without any mipmaps
static void LoadMiplessTexture(Image& img, GLenum target)
{
    int internalFormat = getInternalFormat(img.getFormat());

    if (img.isCompressed())
    {
        glx::glCompressedTexImage2DARB(target,
                                       0,
                                       internalFormat,
                                       img.getWidth(), img.getHeight(),
                                       0,
                                       img.getMipLevelSize(0),
                                       img.getMipLevel(0));
    }
    else
    {
        glTexImage2D(target,
                     0,
                     internalFormat,
                     img.getWidth(), img.getHeight(),
                     0,
                     (GLenum) img.getFormat(),
                     GL_UNSIGNED_BYTE,
                     img.getMipLevel(0));
    }
}


static int ilog2(unsigned int x)
{
    int n = -1;

    while (x != 0)
    {
        x >>= 1;
        n++;
    }

    return n;
}


static int CalcMipLevelCount(int w, int h)
{
    return max(ilog2(w), ilog2(h)) + 1;
}


Texture::Texture(int w, int h, int d) :
    alpha(false),
    compressed(false),
    width(w),
    height(h),
    depth(d),
    formatOptions(0)
{
}


Texture::~Texture()
{
}


int Texture::getLODCount() const
{
    return 1;
}


int Texture::getUTileCount(int) const
{
    return 1;
}


int Texture::getVTileCount(int) const
{
    return 1;
}


int Texture::getWTileCount(int) const
{
    return 1;
}


void Texture::setBorderColor(Color)
{
}


int Texture::getWidth() const
{
    return width;
}


int Texture::getHeight() const
{
    return height;
}


int Texture::getDepth() const
{
    return depth;
}


unsigned int Texture::getFormatOptions() const
{
    return formatOptions;
}


void Texture::setFormatOptions(unsigned int opts)
{
    formatOptions = opts;
}


ImageTexture::ImageTexture(Image& img,
                           AddressMode addressMode,
                           MipMapMode mipMapMode) :
    Texture(img.getWidth(), img.getHeight()),
    glName(0)
{
    glGenTextures(1, (GLuint*) &glName);
    glBindTexture(GL_TEXTURE_2D, glName);


    bool mipmap = mipMapMode != NoMipMaps;
    bool precomputedMipMaps = false;

    // Use precomputed mipmaps only if a complete set is supplied
    int mipLevelCount = img.getMipLevelCount();
    if (mipmap && mipLevelCount == CalcMipLevelCount(img.getWidth(), img.getHeight()))
    {
        precomputedMipMaps = true;
    }

    // We can't automatically generate mipmaps for compressed textures.
    // If a precomputed mipmap set isn't provided, turn off mipmapping entirely.
    if (!precomputedMipMaps && img.isCompressed())
    {
        mipmap = false;
    }

    GLenum texAddress = GetGLTexAddressMode(addressMode);
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, texAddress);
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, texAddress);
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER,
                    mipmap ? GL_LINEAR_MIPMAP_LINEAR : GL_LINEAR);

    if (mipMapMode == AutoMipMaps)
        glTexParameteri(GL_TEXTURE_2D, GL_GENERATE_MIPMAP_SGIS, GL_TRUE);

    int internalFormat = getInternalFormat(img.getFormat());

    if (mipmap)
    {
        if (precomputedMipMaps)
        {
            LoadMipmapSet(img, GL_TEXTURE_2D);
        }
        else if (mipMapMode == DefaultMipMaps)
        {
            gluBuild2DMipmaps(GL_TEXTURE_2D,
                              internalFormat,
                              getWidth(), getHeight(),
                              (GLenum) img.getFormat(),
                              GL_UNSIGNED_BYTE,
                              img.getPixels());
        }
        else
        {
            assert(mipMapMode == AutoMipMaps);
            LoadMiplessTexture(img, GL_TEXTURE_2D);
        }
    }
    else
    {
        LoadMiplessTexture(img, GL_TEXTURE_2D);
    }

    alpha = img.hasAlpha();
    compressed = img.isCompressed();
}


ImageTexture::~ImageTexture()
{
    if (glName != 0)
        glDeleteTextures(1, (const GLuint*) &glName);
}


void ImageTexture::bind()
{
    glBindTexture(GL_TEXTURE_2D, glName);
}


const TextureTile ImageTexture::getTile(int lod, int u, int v)
{
    if (lod != 0 || u != 0 || v != 0)
        return TextureTile(0);
    else
        return TextureTile(glName);
}


unsigned int ImageTexture::getName() const
{
    return glName;
}


void ImageTexture::setBorderColor(Color borderColor)
{
    bind();
    SetBorderColor(borderColor, GL_TEXTURE_2D);
}


TiledTexture::TiledTexture(Image& img,
                           int _uSplit, int _vSplit,
                           MipMapMode mipMapMode) :
    Texture(img.getWidth(), img.getHeight()),
    uSplit(_uSplit),
    vSplit(_vSplit),
    glNames(NULL)
{
    glNames = new uint[uSplit * vSplit];
    {
        for (int i = 0; i < uSplit * vSplit; i++)
            glNames[i] = 0;
    }

    alpha = img.hasAlpha();
    compressed = img.isCompressed();

    bool mipmap = mipMapMode != NoMipMaps;
    bool precomputedMipMaps = false;

    // Require a complete set of mipmaps
    int mipLevelCount = img.getMipLevelCount();
    int completeMipCount = CalcMipLevelCount(img.getWidth(), img.getHeight());
    // Allow a bit of slack here--it turns out that some tools don't want to
    // calculate the 1x1 mip level.  Rather than turn off mipmaps, we'll just
    // point the 1x1 mip to the 2x1.
    if (mipmap && mipLevelCount >= completeMipCount - 1)
        precomputedMipMaps = true;

    // We can't automatically generate mipmaps for compressed textures.
    // If a precomputed mipmap set isn't provided, turn of mipmapping entirely.
    if (!precomputedMipMaps && img.isCompressed())
        mipmap = false;

    GLenum texAddress = GetGLTexAddressMode(EdgeClamp);
    int internalFormat = getInternalFormat(img.getFormat());
    int components = img.getComponents();

    // Create a temporary image which we'll use for the tile texels
    int tileWidth = img.getWidth() / uSplit;
    int tileHeight = img.getHeight() / vSplit;
    int tileMipLevelCount = CalcMipLevelCount(tileWidth, tileHeight);
    Image* tile = new Image(img.getFormat(),
                            tileWidth, tileHeight,
                            tileMipLevelCount);
    if (tile == NULL)
        return;

    for (int v = 0; v < vSplit; v++)
    {
        for (int u = 0; u < uSplit; u++)
        {
            // Create the texture and set up sampling and addressing
            glGenTextures(1, (GLuint*)&glNames[v * uSplit + u]);
            glBindTexture(GL_TEXTURE_2D, glNames[v * uSplit + u]);

            glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, texAddress);
            glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, texAddress);
            glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
            glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER,
                            mipmap ? GL_LINEAR_MIPMAP_LINEAR : GL_LINEAR);

            // Copy texels from the subtexture area to the pixel buffer.  This
            // is straightforward for normal textures, but an immense headache
            // for compressed textures with prebuilt mipmaps.
            if (precomputedMipMaps)
            {
                if (img.isCompressed())
                {
                    for (int mip = 0; mip < tileMipLevelCount; mip++)
                    {
                        int blockSize = getCompressedBlockSize(img.getFormat());
                        unsigned char* imgMip =
                            img.getMipLevel(min(mip, mipLevelCount));
                        uint mipWidth  = max((uint) img.getWidth() >> mip, 1u);
                        unsigned char* tileMip = tile->getMipLevel(mip);
                        uint tileMipWidth  = max((uint) tile->getWidth() >> mip, 1u);
                        uint tileMipHeight = max((uint) tile->getHeight() >> mip, 1u);
                        int uBlocks = max(tileMipWidth / 4, 1u);
                        int vBlocks = max(tileMipHeight / 4, 1u);
                        int destBytesPerRow = uBlocks * blockSize;
                        int srcBytesPerRow = max(mipWidth / 4, 1u) * blockSize;
                        int srcU = u * tileMipWidth / 4;
                        int srcV = v * tileMipHeight / 4;
                        int tileOffset = srcV * srcBytesPerRow +
                            srcU * blockSize;

                        for (int y = 0; y < vBlocks; y++)
                        {
                            memcpy(tileMip + y * destBytesPerRow,
                                   imgMip + tileOffset + y * srcBytesPerRow,
                                   destBytesPerRow);
                        }
                    }
                }
                else
                {
                    // TODO: Handle uncompressed textures with prebuilt mipmaps
                }

                LoadMipmapSet(*tile, GL_TEXTURE_2D);
            }
            else
            {
                if (img.isCompressed())
                {
                    int blockSize = getCompressedBlockSize(img.getFormat());
                    int uBlocks = max(tileWidth / 4, 1);
                    int vBlocks = max(tileHeight / 4, 1);
                    int destBytesPerRow = uBlocks * blockSize;
                    int srcBytesPerRow = max(img.getWidth() / 4, 1) * blockSize;
                    int srcU = u * tileWidth / 4;
                    int srcV = v * tileHeight / 4;
                    int tileOffset = srcV * srcBytesPerRow +
                            srcU * blockSize;

                    for (int y = 0; y < vBlocks; y++)
                    {
                        memcpy(tile->getPixels() + y * destBytesPerRow,
                               img.getPixels() + tileOffset + y * srcBytesPerRow,
                               destBytesPerRow);
                    }
                }
                else
                {
                    unsigned char* tilePixels = img.getPixels() +
                        (v * tileHeight * img.getWidth() + u * tileWidth) * components;
                    for (int y = 0; y < tileHeight; y++)
                    {
                        memcpy(tile->getPixels() + y * tileWidth * components,
                               tilePixels + y * img.getWidth() * components,
                               tileWidth * components);
                    }
                }

                if (mipmap)
                {
                    gluBuild2DMipmaps(GL_TEXTURE_2D,
                                      internalFormat,
                                      tileWidth, tileHeight,
                                      (GLenum) tile->getFormat(),
                                      GL_UNSIGNED_BYTE,
                                      tile->getPixels());
                }
                else
                {
                    LoadMiplessTexture(*tile, GL_TEXTURE_2D);
                }
            }
        }
    }

    delete tile;
}


TiledTexture::~TiledTexture()
{
    if (glNames != NULL)
    {
        for (int i = 0; i < uSplit * vSplit; i++)
        {
            if (glNames[i] != 0)
                glDeleteTextures(1, (const GLuint*) &glNames[i]);
        }
        delete[] glNames;
    }
}


void TiledTexture::bind()
{
}


void TiledTexture::setBorderColor(Color borderColor)
{
    for (int i = 0; i < vSplit; i++)
    {
        for (int j = 0; j < uSplit; j++)
        {
            glBindTexture(GL_TEXTURE_2D, glNames[i * uSplit + j]);
            SetBorderColor(borderColor, GL_TEXTURE_2D);
        }
    }
}


int TiledTexture::getUTileCount(int) const
{
    return uSplit;
}


int TiledTexture::getVTileCount(int) const
{
    return vSplit;
}


const TextureTile TiledTexture::getTile(int lod, int u, int v)
{
    if (lod != 0 || u >= uSplit || u < 0 || v >= vSplit || v < 0)
        return TextureTile(0);
    else
    {
        return TextureTile(glNames[v * uSplit + u]);
    }
}



CubeMap::CubeMap(Image* faces[]) :
    Texture(faces[0]->getWidth(), faces[0]->getHeight()),
    glName(0)
{
    // Verify that all the faces are square and have the same size
    int width = faces[0]->getWidth();
    int format = faces[0]->getFormat();
    int i = 0;
    for (i = 0; i < 6; i++)
    {
        if (faces[i]->getWidth() != width ||
            faces[i]->getHeight() != width ||
            faces[i]->getFormat() != format)
            return;
    }

    // For now, always enable mipmaps; in the future, it should be possible to
    // override this.
    bool mipmap = true;
    bool precomputedMipMaps = false;

    // Require a complete set of mipmaps
    int mipLevelCount = faces[0]->getMipLevelCount();
    if (mipmap && mipLevelCount == CalcMipLevelCount(width, width))
        precomputedMipMaps = true;

    // We can't automatically generate mipmaps for compressed textures.
    // If a precomputed mipmap set isn't provided, turn of mipmapping entirely.
    if (!precomputedMipMaps && faces[0]->isCompressed())
        mipmap = false;

    glGenTextures(1, (GLuint*) &glName);
    glBindTexture(GL_TEXTURE_CUBE_MAP_ARB, glName);

    glTexParameteri(GL_TEXTURE_CUBE_MAP_ARB, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
    glTexParameteri(GL_TEXTURE_CUBE_MAP_ARB, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
    glTexParameteri(GL_TEXTURE_CUBE_MAP_ARB, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
    glTexParameteri(GL_TEXTURE_CUBE_MAP_ARB, GL_TEXTURE_MIN_FILTER,
                    mipmap ? GL_LINEAR_MIPMAP_LINEAR : GL_LINEAR);

    int internalFormat = getInternalFormat(format);

    for (i = 0; i < 6; i++)
    {
        GLenum targetFace = (GLenum) ((int) GL_TEXTURE_CUBE_MAP_POSITIVE_X_ARB + i);
        Image* face = faces[i];

        if (mipmap)
        {
            if (precomputedMipMaps)
            {
                LoadMipmapSet(*face, targetFace);
            }
            else
            {
                gluBuild2DMipmaps(targetFace,
                                  internalFormat,
                                  getWidth(), getHeight(),
                                  (GLenum) face->getFormat(),
                                  GL_UNSIGNED_BYTE,
                                  face->getPixels());
            }
        }
        else
        {
            LoadMiplessTexture(*face, targetFace);
        }
    }
}


CubeMap::~CubeMap()
{
    if (glName != 0)
        glDeleteTextures(1, (const GLuint*) &glName);
}


void CubeMap::bind()
{
    glBindTexture(GL_TEXTURE_CUBE_MAP_ARB, glName);
}


const TextureTile CubeMap::getTile(int lod, int u, int v)
{
    if (lod != 0 || u != 0 || v != 0)
        return TextureTile(0);
    else
        return TextureTile(glName);
}


void CubeMap::setBorderColor(Color borderColor)
{
    bind();
    SetBorderColor(borderColor, GL_TEXTURE_CUBE_MAP_ARB);
}



Texture* CreateProceduralTexture(int width, int height,
                                 int format,
                                 ProceduralTexEval func,
                                 Texture::AddressMode addressMode,
                                 Texture::MipMapMode mipMode)
{
    Image* img = new Image(format, width, height);
    if (img == NULL)
        return NULL;

    for (int y = 0; y < height; y++)
    {
        for (int x = 0; x < width; x++)
        {
            float u = ((float) x + 0.5f) / (float) width * 2 - 1;
            float v = ((float) y + 0.5f) / (float) height * 2 - 1;
            func(u, v, 0, img->getPixelRow(y) + x * img->getComponents());
        }
    }

    Texture* tex = new ImageTexture(*img, addressMode, mipMode);
    delete img;

    return tex;
}


Texture* CreateProceduralTexture(int width, int height,
                                 int format,
                                 TexelFunctionObject& func,
                                 Texture::AddressMode addressMode,
                                 Texture::MipMapMode mipMode)
{
    Image* img = new Image(format, width, height);
    if (img == NULL)
        return NULL;

    for (int y = 0; y < height; y++)
    {
        for (int x = 0; x < width; x++)
        {
            float u = ((float) x + 0.5f) / (float) width * 2 - 1;
            float v = ((float) y + 0.5f) / (float) height * 2 - 1;
            func(u, v, 0, img->getPixelRow(y) + x * img->getComponents());
        }
    }

    Texture* tex = new ImageTexture(*img, addressMode, mipMode);
    delete img;

    return tex;
}


// Helper function for CreateProceduralCubeMap; return the normalized
// vector pointing to (s, t) on the specified face.
static Vec3f cubeVector(int face, float s, float t)
{
    Vec3f v;
    switch (face)
    {
    case 0:
        v = Vec3f(1.0f, -t, -s);
        break;
    case 1:
        v = Vec3f(-1.0f, -t, s);
        break;
    case 2:
        v = Vec3f(s, 1.0f, t);
        break;
    case 3:
        v = Vec3f(s, -1.0f, -t);
        break;
    case 4:
        v = Vec3f(s, -t, 1.0f);
        break;
    case 5:
        v = Vec3f(-s, -t, -1.0f);
        break;
    default:
        // assert(false);
        break;
    }

    v.normalize();

    return v;
}


extern Texture* CreateProceduralCubeMap(int size, int format,
                                        ProceduralTexEval func)
{
    Image* faces[6];
    bool failed = false;

    int i = 0;
    for (i = 0; i < 6; i++)
    {
        faces[i] = NULL;
        faces[i] = new Image(format, size, size);
        if (faces[i] == NULL)
            failed = true;
    }

    if (!failed)
    {
        for (int i = 0; i < 6; i++)
        {
            Image* face = faces[i];
            for (int y = 0; y < size; y++)
            {
                for (int x = 0; x < size; x++)
                {
                    float s = ((float) x + 0.5f) / (float) size * 2 - 1;
                    float t = ((float) y + 0.5f) / (float) size * 2 - 1;
                    Vec3f v = cubeVector(i, s, t);
                    func(v.x, v.y, v.z,
                         face->getPixelRow(y) + x * face->getComponents());
                }
            }
        }
    }

    Texture* tex = new CubeMap(faces);

    // Clean up the images
    for (i = 0; i < 6; i++)
    {
        if (faces[i] != NULL)
            delete faces[i];
    }

    return tex;
}

#if 0
static bool isPow2(int x)
{
    return ((x & (x - 1)) == 0);
}
#endif

static Texture* CreateTextureFromImage(Image& img,
                                       Texture::AddressMode addressMode,
                                       Texture::MipMapMode mipMode)
{
#if 0
    // Require texture dimensions to be powers of two.  Even though the
    // OpenGL driver will automatically rescale textures with non-power of
    // two sizes, some quality loss may result.  The power of two requirement
    // forces texture creators to resize their textures in an image editing
    // program, hopefully resulting in  textures that look as good as possible
    // when rendered on current hardware.
    if (!isPow2(img.getWidth()) || !isPow2(img.getHeight()))
    {
        clog << "Texture has non-power of two dimensions.\n";
        return NULL;
    }
#endif

    // If non power of two textures are supported switch mipmap generation to
    // automatic. gluBuildMipMaps may rescale the texture to a power of two
    // on some drivers even when the hardware supports non power of two textures,
    // whereas auto mipmap generation will properly deal with the dimensions.
    if (GetTextureCaps().nonPow2Supported)
    {
        if (mipMode == Texture::DefaultMipMaps)
            mipMode = Texture::AutoMipMaps;
    }

    bool splittingAllowed = true;
    Texture* tex = NULL;

    int maxDim = GetTextureCaps().maxTextureSize;
    if ((img.getWidth() > maxDim || img.getHeight() > maxDim) &&
        splittingAllowed)
    {
        // The texture is too large; we need to split it.
        int uSplit = max(1, img.getWidth() / maxDim);
        int vSplit = max(1, img.getHeight() / maxDim);
        clog << _("Creating tiled texture. Width=") << img.getWidth() << _(", max=") << maxDim << "\n";
        tex = new TiledTexture(img, uSplit, vSplit, mipMode);
    }
    else
    {
        clog << _("Creating ordinary texture: ") << img.getWidth() << "x" << img.getHeight() << "\n";
        // The image is small enough to fit in a single texture; or, splitting
        // was disallowed so we'll scale the large image down to fit in
        // an ordinary texture.
        tex = new ImageTexture(img, addressMode, mipMode);
    }

    return tex;
}


Texture* LoadTextureFromFile(const string& filename,
                             Texture::AddressMode addressMode,
                             Texture::MipMapMode mipMode)
{
    // Check for a Celestia texture--these need to be handled specially.
    ContentType contentType = DetermineFileType(filename);

    if (contentType == Content_CelestiaTexture)
        return LoadVirtualTexture(filename);

    // All other texture types are handled by first loading an image, then
    // creating a texture from that image.
    Image* img = LoadImageFromFile(filename);
    if (img == NULL)
        return NULL;

    Texture* tex = CreateTextureFromImage(*img, addressMode, mipMode);

    if (contentType == Content_DXT5NormalMap)
    {
        // If the texture came from a .dxt5nm file then mark it as a dxt5
        // compressed normal map. There's no separate OpenGL format for dxt5
        // normal maps, so the file extension is the only thing that
        // distinguishes it from a plain old dxt5 texture.
        if (img->getFormat() == GL_COMPRESSED_RGBA_S3TC_DXT5_EXT)
        {
            tex->setFormatOptions(Texture::DXT5NormalMap);
        }
    }

    delete img;

    return tex;
}


// Load a height map texture from a file and convert it to a normal map.
Texture* LoadHeightMapFromFile(const string& filename,
                               float height,
                               Texture::AddressMode addressMode)
{
    Image* img = LoadImageFromFile(filename);
    if (img == NULL)
        return NULL;
    Image* normalMap = img->computeNormalMap(height,
                                             addressMode == Texture::Wrap);
    delete img;
    if (normalMap == NULL)
        return NULL;

    Texture* tex = CreateTextureFromImage(*normalMap, addressMode,
                                          Texture::DefaultMipMaps);
    delete normalMap;

    return tex;
}
