/*
    This file is part of Mitsuba, a physically based rendering system.

    Copyright (c) 2007-2012 by Wenzel Jakob and others.

    Mitsuba is free software; you can redistribute it and/or modify
    it under the terms of the GNU General Public License Version 3
    as published by the Free Software Foundation.

    Mitsuba 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, see <http://www.gnu.org/licenses/>.
*/

#include <mitsuba/core/bitmap.h>
#include <mitsuba/core/fresolver.h>
#include <mitsuba/core/fstream.h>
#include <mitsuba/core/mstream.h>
#include <mitsuba/core/plugin.h>
#include <mitsuba/core/sched.h>
#include <mitsuba/render/texture.h>
#include <mitsuba/render/mipmap.h>
#include <mitsuba/hw/renderer.h>
#include <mitsuba/hw/gputexture.h>
#include <mitsuba/hw/gpuprogram.h>
#include <boost/algorithm/string.hpp>

MTS_NAMESPACE_BEGIN
	
/*!\plugin{bitmap}{Bitmap texture}
 * \order{1}
 * \parameters{
 *     \parameter{filename}{\String}{
 *       Filename of the bitmap to be loaded
 *     }
 *     \parameter{wrapMode, wrapModeU, wrapModeV}{\String}{
 *       Behavior of texture lookups outside of the $[0,1]$ $uv$ range.\vspace{-1mm}
 *       \begin{enumerate}[(i)]
 *           \item \code{repeat}: Repeat the texture indefinitely\vspace{-1mm}
 *           \item \code{mirror}: Mirror the texture along its boundaries\vspace{-1mm}
 *           \item \code{clamp}: Clamp $uv$ coordinates to $[0,1]$ before a lookup\vspace{-1mm}
 *           \item \code{zero}: Switch to a zero-valued texture \vspace{-1mm}
 *           \item \code{one}: Switch to a one-valued texture \vspace{-1mm}
 *       \end{enumerate}
 *       Default: \code{repeat}. The parameter \code{wrapMode} is a shortcut for 
 *       setting both \code{wrapModeU} and \code{wrapModeV} at the same time.
 *     }
 *     \parameter{gamma}{\Float}{
 *       Optional parameter to override the gamma value of the source bitmap,
 *       where 1 indicates a linear color space and the special value -1 
 *       corresponds to sRGB. \default{automatically detect based on the 
 *       image type and metadata}
 *     }
 *     \parameter{filterType}{\String}{
 *       Specifies the texture filturing that should be used for lookups\vspace{-1mm}
 *       \begin{enumerate}[(i)]
 *           \item \code{ewa}: Elliptically weighted average (a.k.a.
 *           anisotropic filtering). This produces the best quality\vspace{-1mm}
 *           \item \code{trilinear}: Simple trilinear (isotropic) filtering.\vspace{-1mm}
 *           \item \code{nearest}: No filtering, do nearest neighbor lookups.\vspace{-1mm}
 *       \end{enumerate}
 *       Default: \code{ewa}.
 *     }
 *     \parameter{maxAnisotropy}{\Float}{
 *        Specific to \code{ewa} filtering, this parameter limits the 
 *        anisotropy (and thus the computational cost) of filtured texture lookups. The
 *        default of 20 is a good compromise.
 *     }
 *     \parameter{cache}{\Boolean}{
 *        Preserve generated MIP map data in a cache file? This will cause a file named
 *        \emph{filename}\code{.mip} to be created.
 *        \default{automatic---use caching for textures larger than 1M pixels.}
 *     }
 *     \parameter{uoffset, voffset}{\Float}{
 *       Numerical offset that should be applied to UV values before a lookup
 *     }
 *     \parameter{uscale, vscale}{\Float}{
 *       Multiplicative factors that should be applied to UV values before a lookup
 *     }
 * }
 * This plugin provides a bitmap-backed texture source that supports \emph{filtered}
 * texture lookups on\footnote{Some of these may not be available depending on how 
 * Mitsuba was compiled.} JPEG, PNG, OpenEXR, RGBE, TGA, and BMP files. Filtered 
 * lookups are useful to avoid aliasing when rendering textures that contain high 
 * frequencies (see the next page for an example).
 *
 * The plugin operates as follows: when loading a bitmap file, it is first converted 
 * into a linear color space. Following this, a MIP map is constructed that is necessary
 * to perform filtered lookups during rendering. A \emph{MIP map} is a hierarchy of
 * progressively lower resolution versions of the input image, where the resolution of
 * adjacent levels differs by a factor of two. Mitsuba creates this hierarchy using
 * Lanczos resampling to obtain very high quality results.
 * Note that textures may have an arbitrary resolution and are not limited to powers of two.
 * Three different filtering modes are supported: 
 *
 * \begin{enumerate}[(i)]
 * \item Nearest neighbor lookups effectively disable filtering and always query the 
 * highest-resolution version of the texture without any kind of interpolation. This is 
 * fast and requires little memory (no MIP map is created), but results in visible aliasing.
 * Only a single pixel value is accessed.
 *
 * \item The trilinear filter performs bilinear interpolation on two adjacent MIP levels
 * and blends the results. Because it cannot do anisotropic (i.e. slanted) lookups in texture space,
 * it must compromise either on the side of blurring or aliasing. The implementation in Mitsuba
 * chooses blurring over aliasing (though note that (\textbf{b}) is an extreme case).
 * Only 8 pixel values are accessed.
 *
 * \item The EWA filter performs anisotropicically filtered lookups on two adjacent MIP map levels 
 * and blends them. This produces the best quality, but at the expense of computation time.
 * Generally, 20-40 pixel values must be read for a single EWA texture lookup. To limit
 * the number of pixel accesses, the \code{maxAnisotropy} parameter can be used to bound
 * the amount of anisotropy that a texture lookup is allowed to have.
 * \end{enumerate}
 * \renderings{
 *     \rendering{Nearest-neighbor filter. Note the aliasing}{tex_bitmap_nearest}
 *     \rendering{Trilinear filter. Note the blurring}{tex_bitmap_trilinear}
 *     \vspace{-5mm}
 * }
 * \renderings{
 *     \setcounter{subfigure}{2}
 *     \rendering{EWA filter}{tex_bitmap_ewa}
 *     \rendering{Ground truth (512 samples per pixel)}{tex_bitmap_gt}
 *     \caption{A somewhat contrived comparison of the different filters when rendering a high-frequency
 *     checkerboard pattern using four samples per pixel. The EWA method (the default) 
 *     pre-filters the texture anisotropically to limit blurring and aliasing, but has a 
 *     higher computational cost than the other filters.}
 * }
 * \paragraph{Caching and memory requirements:}
 * When a texture is read, Mitsuba internally converts it into an uncompressed linear format 
 * using a half precision (\code{float16})-based representation. This is convenient for
 * rendering but means that textures require copious amounts of memory (in particular, the 
 * size of the occupied memory region might be orders of magnitude greater than that of the 
 * original input file).
 *
 * For instance, a basic 10 megapixel image requires as much as 76 MiB of memory! Loading,
 * color space transformation, and MIP map construction require up to several seconds in this case.
 * To reduce these overheads, Mitsuba 0.4.0 introduced MIP map caches. When a large
 * texture is loaded for the first time, a MIP map cache file with the name \emph{filename}\code{.mip}
 * is generated. This is essentially a verbatim copy of the in-memory representation created
 * during normal rendering. Storing this information as a separate file has two advantages:
 *
 * \begin{enumerate}[(i)]
 *    \item MIP maps do not have to be regenerated in subsequent Mitsuba runs,
 *     which substantially reduces scene loading times.
 *    \item Because the texture storage is entirely disk-backed and can be \emph{memory-mapped}, 
 *    Mitsuba is able to work with truly massive textures that would otherwise exhaust the main system memory.
 * \end{enumerate}
 *
 * The texture caches are automatically regenerated when the input texture is modified.
 * Of course, the cache files can be cumbersome when they are not needed anymore. On Linux
 * or Mac OS, they can safely be deleted by executing the following command within a scene directory.
 *
 * \begin{shell}
 * $\code{\$}$ find . -name "*.mip" -delete
 * \end{shell}
 */

class BitmapTexture : public Texture2D {
public:
	/* Store texture data using half precision, but perform computations in 
	   single/double precision based on compilation flags. The following
	   generates efficient implementations for both luminance and RGB data */
	typedef TSpectrum<Float, 1> Color1;
	typedef TSpectrum<Float, 3> Color3;
	typedef TSpectrum<half, 1>  Color1h;
	typedef TSpectrum<half, 3>  Color3h;
	typedef TMIPMap<Color1, Color1h> MIPMap1;
	typedef TMIPMap<Color3, Color3h> MIPMap3;

	BitmapTexture(const Properties &props) : Texture2D(props) {
		uint64_t timestamp = 0;
		bool tryReuseCache = false;
		fs::path cacheFile;
		ref<Bitmap> bitmap;

		if (props.hasProperty("bitmap")) {
			/* Support initialization via raw data passed from another plugin */
			bitmap = reinterpret_cast<Bitmap *>(props.getData("bitmap").ptr);
		} else {
			m_filename = Thread::getThread()->getFileResolver()->resolve(
				props.getString("filename"));

			Log(EInfo, "Loading texture \"%s\"", m_filename.filename().string().c_str());
			if (!fs::exists(m_filename))
				Log(EError, "Texture file \"%s\" could not be found!", m_filename.c_str());

			boost::system::error_code ec;
			timestamp = (uint64_t) fs::last_write_time(m_filename, ec);
			if (ec.value())
				Log(EError, "Could not determine modification time of \"%s\"!", m_filename.c_str());

			cacheFile = m_filename;
			cacheFile.replace_extension(".mip");
			tryReuseCache = fs::exists(cacheFile) && props.getBoolean("cache", true);
		}

		std::string filterType = boost::to_lower_copy(props.getString("filterType", "ewa"));
		std::string wrapMode = props.getString("wrapMode", "repeat");
		m_wrapModeU = parseWrapMode(props.getString("wrapModeU", wrapMode));
		m_wrapModeV = parseWrapMode(props.getString("wrapModeV", wrapMode));

		m_gamma = props.getFloat("gamma", 0);

		if (filterType == "ewa")
			m_filterType = EEWA;
		else if (filterType == "trilinear")
			m_filterType = ETrilinear;
		else if (filterType == "nearest")
			m_filterType = ENearest;
		else
			Log(EError, "Unknown filter type '%s' -- must be "
				"'ewa', 'trilinear', or 'nearest'!", filterType.c_str());

		m_maxAnisotropy = props.getFloat("maxAnisotropy", 20);

		if (m_filterType != EEWA)
			m_maxAnisotropy = 1.0f;

		if (tryReuseCache && MIPMap3::validateCacheFile(cacheFile, timestamp,
				Bitmap::ERGB, m_wrapModeU, m_wrapModeV, m_filterType, m_gamma)) {
			/* Reuse an existing MIP map cache file */
			m_mipmap3 = new MIPMap3(cacheFile, m_maxAnisotropy);
		} else if (tryReuseCache && MIPMap1::validateCacheFile(cacheFile, timestamp,
				Bitmap::ELuminance, m_wrapModeU, m_wrapModeV, m_filterType, m_gamma)) {
			/* Reuse an existing MIP map cache file */
			m_mipmap1 = new MIPMap1(cacheFile, m_maxAnisotropy);
		} else {
			if (bitmap == NULL) {
				/* Load the input image if necessary */
				ref<Timer> timer = new Timer();
				ref<FileStream> fs = new FileStream(m_filename, FileStream::EReadOnly);
				bitmap = new Bitmap(Bitmap::EAuto, fs);
				if (m_gamma != 0)
					bitmap->setGamma(m_gamma);
				Log(EDebug, "Loaded \"%s\" in %i ms", m_filename.filename().string().c_str(),
					timer->getMilliseconds());
			}

			Bitmap::EPixelFormat pixelFormat;
			switch (bitmap->getPixelFormat()) {
				case Bitmap::ELuminance:
				case Bitmap::ELuminanceAlpha:
					pixelFormat = Bitmap::ELuminance;
					break;
				case Bitmap::ERGB:
				case Bitmap::ERGBA:
					pixelFormat = Bitmap::ERGB;
					break;
				default:
					Log(EError, "The input image has an unsupported pixel format!");
					return;
			}

			/* (Re)generate the MIP map hierarchy; downsample using a 
			    2-lobed Lanczos reconstruction filter */
			Properties rfilterProps("lanczos");
			rfilterProps.setInteger("lobes", 2);
			ref<ReconstructionFilter> rfilter = static_cast<ReconstructionFilter *> (
				PluginManager::getInstance()->createObject(
				MTS_CLASS(ReconstructionFilter), rfilterProps));
			rfilter->configure();

			/* Potentially create a new MIP map cache file */
			bool createCache = !cacheFile.empty() && props.getBoolean("cache", 
				bitmap->getSize().x * bitmap->getSize().y > 1024*1024);

			if (pixelFormat == Bitmap::ELuminance)
				m_mipmap1 = new MIPMap1(bitmap, pixelFormat, Bitmap::EFloat,
					rfilter, m_wrapModeU, m_wrapModeV, m_filterType, m_maxAnisotropy, 
					createCache ? cacheFile : fs::path(), timestamp);
			else
				m_mipmap3 = new MIPMap3(bitmap, pixelFormat, Bitmap::EFloat,
					rfilter, m_wrapModeU, m_wrapModeV, m_filterType, m_maxAnisotropy, 
					createCache ? cacheFile : fs::path(), timestamp);
		}
	}

	inline ReconstructionFilter::EBoundaryCondition parseWrapMode(const std::string &wrapMode) {
		if (wrapMode == "repeat")
			return ReconstructionFilter::ERepeat;
		else if (wrapMode == "clamp")
			return ReconstructionFilter::EClamp;
		else if (wrapMode == "mirror")
			return ReconstructionFilter::EMirror;
		else if (wrapMode == "zero" || wrapMode == "black")
			return ReconstructionFilter::EZero;
		else if (wrapMode == "one" || wrapMode == "white")
			return ReconstructionFilter::EOne;
		else
			Log(EError, "Unknown wrap mode '%s' -- must be "
				"'repeat', 'clamp', 'black', or 'white'!", wrapMode.c_str());
		return ReconstructionFilter::EZero; // make gcc happy
	}

	BitmapTexture(Stream *stream, InstanceManager *manager) 
	 : Texture2D(stream, manager) {
		m_filename = stream->readString();
		Log(EDebug, "Unserializing texture \"%s\"", m_filename.filename().string().c_str());
		m_filterType = (EMIPFilterType) stream->readUInt();
		m_wrapModeU = (ReconstructionFilter::EBoundaryCondition) stream->readUInt();
		m_wrapModeV = (ReconstructionFilter::EBoundaryCondition) stream->readUInt();
		m_gamma = stream->readFloat();
		m_maxAnisotropy = stream->readFloat();

		size_t size = stream->readSize();
		ref<MemoryStream> mStream = new MemoryStream(size);
		stream->copyTo(mStream, size);
		mStream->seek(0);
		ref<Bitmap> bitmap = new Bitmap(Bitmap::EAuto, mStream);
		if (m_gamma != 0)
			bitmap->setGamma(m_gamma);

		/* Downsample using a 2-lobed Lanczos reconstruction filter */
		Properties rfilterProps("lanczos");
		rfilterProps.setInteger("lobes", 2);
		ref<ReconstructionFilter> rfilter = static_cast<ReconstructionFilter *> (
			PluginManager::getInstance()->createObject(
			MTS_CLASS(ReconstructionFilter), rfilterProps));
		rfilter->configure();

		Bitmap::EPixelFormat pixelFormat;
		switch (bitmap->getPixelFormat()) {
			case Bitmap::ELuminance:
			case Bitmap::ELuminanceAlpha:
				pixelFormat = Bitmap::ELuminance;
				break;
			case Bitmap::ERGB:
			case Bitmap::ERGBA:
				pixelFormat = Bitmap::ERGB;
				break;
			default:
				Log(EError, "The input image has an unsupported pixel format!");
				return;
		}

		if (pixelFormat == Bitmap::ELuminance)
			m_mipmap1 = new MIPMap1(bitmap, pixelFormat, Bitmap::EFloat,
				rfilter, m_wrapModeU, m_wrapModeV, m_filterType, m_maxAnisotropy, 
				fs::path(), 0);
		else
			m_mipmap3 = new MIPMap3(bitmap, pixelFormat, Bitmap::EFloat,
				rfilter, m_wrapModeU, m_wrapModeV, m_filterType, m_maxAnisotropy, 
				fs::path(), 0);
	}

	void serialize(Stream *stream, InstanceManager *manager) const {
		Texture2D::serialize(stream, manager);
		stream->writeString(m_filename.string());
		stream->writeUInt(m_filterType);
		stream->writeUInt(m_wrapModeU);
		stream->writeUInt(m_wrapModeV);
		stream->writeFloat(m_gamma);
		stream->writeFloat(m_maxAnisotropy);

		if (!m_filename.empty() && fs::exists(m_filename)) {
			/* We still have access to the original image -- use that, since 
			   it is probably much smaller than the in-memory representation */
			ref<Stream> is = new FileStream(m_filename, FileStream::EReadOnly);
			stream->writeSize(is->getSize());
			is->copyTo(stream);
		} else {
			/* No access to the original image anymore. Create an EXR image
			   from the top MIP map level and serialize that */
			ref<MemoryStream> mStream = new MemoryStream();
			ref<Bitmap> bitmap = m_mipmap1.get() ? 
				m_mipmap1->toBitmap() : m_mipmap3->toBitmap();
			bitmap->write(Bitmap::EOpenEXR, mStream);

			stream->writeSize(mStream->getSize());
			stream->write(mStream->getData(), mStream->getSize());
		}
	}

	Spectrum eval(const Point2 &uv) const {
		/* There are no ray differentials to do any kind of 
		   prefiltering. Evaluate the full-resolution texture */
		
		Spectrum result;
		if (m_mipmap3.get()) {
			Color3 value;
			if (m_mipmap3->getFilterType() != ENearest)
				value = m_mipmap3->evalBilinear(0, uv);
			else
				value = m_mipmap3->evalBox(0, uv);
			result.fromLinearRGB(value[0], value[1], value[2]);
		} else {
			Color1 value;
			if (m_mipmap1->getFilterType() != ENearest)
				value = m_mipmap1->evalBilinear(0, uv);
			else
				value = m_mipmap1->evalBox(0, uv);
			result = Spectrum(value[0]);
		}
		stats::filteredLookups.incrementBase();

		return result;
	}

	Spectrum eval(const Point2 &uv, const Vector2 &d0, const Vector2 &d1) const {
		stats::filteredLookups.incrementBase();
		++stats::filteredLookups;

		Spectrum result;
		if (m_mipmap3.get()) {
			Color3 value = m_mipmap3->eval(uv, d0, d1);
			result.fromLinearRGB(value[0], value[1], value[2]);
		} else {
			Color1 value = m_mipmap1->eval(uv, d0, d1);
			result = Spectrum(value[0]);
		}
		return result;
	}

	Spectrum getAverage() const {
		Spectrum result;
		if (m_mipmap3.get()) {
			Color3 value = m_mipmap3->getAverage();
			result.fromLinearRGB(value[0], value[1], value[2]);
		} else {
			Color1 value = m_mipmap1->getAverage();
			result = Spectrum(value[0]);
		}
		return result;
	}

	Spectrum getMaximum() const {
		Spectrum result;
		if (m_mipmap3.get()) {
			Color3 value = m_mipmap3->getMaximum();
			result.fromLinearRGB(value[0], value[1], value[2]);
		} else {
			Color1 value = m_mipmap1->getMaximum();
			result = Spectrum(value[0]);
		}
		return result;
	}

	Spectrum getMinimum() const {
		Spectrum result;
		if (m_mipmap3.get()) {
			Color3 value = m_mipmap3->getMinimum();
			result.fromLinearRGB(value[0], value[1], value[2]);
		} else {
			Color1 value = m_mipmap1->getMinimum();
			result = Spectrum(value[0]);
		}
		return result;
	}

	bool isConstant() const {
		return false;
	}

	bool usesRayDifferentials() const {
		return true;
	}

	Vector3i getResolution() const {
		if (m_mipmap3.get()) {
			return Vector3i(
				m_mipmap3->getWidth(),
				m_mipmap3->getHeight(),
				1
			);
		} else {
			return Vector3i(
				m_mipmap1->getWidth(),
				m_mipmap1->getHeight(),
				1
			);
		}
	}

	std::string toString() const {
		std::ostringstream oss;
		oss << "BitmapTexture[" << endl
			<< "  filename = \"" << m_filename.string() << "\"," << endl;

		if (m_mipmap3.get())
			oss << "  mipmap = " << indent(m_mipmap3.toString()) << endl;
		else
			oss << "  mipmap = " << indent(m_mipmap1.toString()) << endl;
		
		oss << "]";
		return oss.str();
	}

	Shader *createShader(Renderer *renderer) const;

	MTS_DECLARE_CLASS()
protected:
	ref<MIPMap1> m_mipmap1;
	ref<MIPMap3> m_mipmap3;
	EMIPFilterType m_filterType;
	ReconstructionFilter::EBoundaryCondition m_wrapModeU;
	ReconstructionFilter::EBoundaryCondition m_wrapModeV;
	Float m_gamma, m_maxAnisotropy;
	fs::path m_filename;
};

// ================ Hardware shader implementation ================ 
class BitmapTextureShader : public Shader {
public:
	BitmapTextureShader(Renderer *renderer, const std::string &filename, 
			const BitmapTexture::MIPMap1* mipmap1,
			const BitmapTexture::MIPMap3* mipmap3,
			const Point2 &uvOffset, const Vector2 &uvScale, 
			ReconstructionFilter::EBoundaryCondition wrapModeU, 
			ReconstructionFilter::EBoundaryCondition wrapModeV, 
			Float maxAnisotropy) 
		: Shader(renderer, ETextureShader), m_uvOffset(uvOffset), m_uvScale(uvScale) {

		ref<Bitmap> bitmap = mipmap1 ? mipmap1->toBitmap() : mipmap3->toBitmap();
		m_gpuTexture = renderer->createGPUTexture(filename, bitmap);

		switch (wrapModeU) {
			case ReconstructionFilter::EClamp: 
				m_gpuTexture->setWrapType(GPUTexture::EClampToEdge);
				break;
			case ReconstructionFilter::EMirror: 
				m_gpuTexture->setWrapType(GPUTexture::EMirror);
				break;
			case ReconstructionFilter::ERepeat: 
				m_gpuTexture->setWrapType(GPUTexture::ERepeat);
				break;
			case ReconstructionFilter::EZero: 
				m_gpuTexture->setWrapType(GPUTexture::EClampToBorder);
				m_gpuTexture->setBorderColor(Color3(0.0f));
				break;
			case ReconstructionFilter::EOne: 
				m_gpuTexture->setWrapType(GPUTexture::EClampToBorder);
				m_gpuTexture->setBorderColor(Color3(1.0f));
				break;
			default:
				Log(EError, "Unknown wrap mode!");
		}

		switch (mipmap1 ? mipmap1->getFilterType() : mipmap3->getFilterType()) {
			case ENearest:
				m_gpuTexture->setFilterType(GPUTexture::ENearest);
				break;
			default:
				m_gpuTexture->setFilterType(GPUTexture::EMipMapLinear);
				break;
		}

		m_gpuTexture->setMaxAnisotropy(maxAnisotropy);
		m_gpuTexture->setMaxAnisotropy(maxAnisotropy);
		m_gpuTexture->initAndRelease();
	}

	void cleanup(Renderer *renderer) {
		m_gpuTexture->cleanup();
	}

	void generateCode(std::ostringstream &oss,
			const std::string &evalName,
			const std::vector<std::string> &depNames) const {
		oss << "uniform sampler2D " << evalName << "_texture;" << endl
			<< "uniform vec2 " << evalName << "_uvOffset;" << endl
			<< "uniform vec2 " << evalName << "_uvScale;" << endl
			<< endl
			<< "vec3 " << evalName << "(vec2 uv) {" << endl
			<< "    return texture2D(" << evalName << "_texture, vec2(" << endl
			<< "          uv.x * " << evalName << "_uvScale.x + " << evalName << "_uvOffset.x," << endl 
			<< "          uv.y * " << evalName << "_uvScale.y + " << evalName << "_uvOffset.y)).rgb;" << endl 
			<< "}" << endl;
	}

	void resolve(const GPUProgram *program, const std::string &evalName, std::vector<int> &parameterIDs) const {
		parameterIDs.push_back(program->getParameterID(evalName + "_texture", false));
		parameterIDs.push_back(program->getParameterID(evalName + "_uvOffset", false));
		parameterIDs.push_back(program->getParameterID(evalName + "_uvScale", false));
	}

	void bind(GPUProgram *program, const std::vector<int> &parameterIDs, 
		int &textureUnitOffset) const {
		m_gpuTexture->bind(textureUnitOffset++);
		program->setParameter(parameterIDs[0], m_gpuTexture.get());
		program->setParameter(parameterIDs[1], m_uvOffset);
		program->setParameter(parameterIDs[2], m_uvScale);
	}

	void unbind() const {
		m_gpuTexture->unbind();
	}
	
	MTS_DECLARE_CLASS()
private:
	ref<GPUTexture> m_gpuTexture;
	Point2 m_uvOffset;
	Vector2 m_uvScale;
};

Shader *BitmapTexture::createShader(Renderer *renderer) const {
	return new BitmapTextureShader(renderer, m_filename.filename().string(),
			m_mipmap1.get(), m_mipmap3.get(), m_uvOffset, m_uvScale,
			m_wrapModeU, m_wrapModeV, m_maxAnisotropy);
}

MTS_IMPLEMENT_CLASS_S(BitmapTexture, false, Texture2D)
MTS_IMPLEMENT_CLASS(BitmapTextureShader, false, Shader)
MTS_EXPORT_PLUGIN(BitmapTexture, "Bitmap texture");
MTS_NAMESPACE_END