#include "stdafx.h"
#include "Image.h"
#include "Png.h"
#include "Jpeg.h"
#include "Bmp.h"
#include "Ppm.h"
#include "Ico.h"
#include "Exception.h"
#include "Core/Join.h"
#include "Shared/Engine.h"

namespace graphics {

	static void addDefaultFormats(Engine &e, Array<ImageFormat *> *to) {
		*to << pngFormat(e);
		*to << jpegFormat(e);
		*to << bmpFormat(e);
		*to << ppmFormat(e);
		*to << icoFormat(e);
	}

	ImageFormat::ImageFormat(Str *name, Array<Str *> *extensions,
							Fn<Bool, IStream *> *applicable,
							Fn<FormatOptions *> *options)
		: fmtName(name), fmtExtensions(extensions),
		  isApplicable(applicable), createOptions(options) {}

	ImageFormat::ImageFormat(const wchar *name, const wchar **extensions,
							CppIsApplicable applicable, CppOptions options) {
		fmtName = new (this) Str(name);
		fmtExtensions = new (this) Array<Str *>();
		for (const wchar **ext = extensions; *ext; ext++)
			fmtExtensions->push(new (this) Str(*ext));

		isApplicable = fnPtr(engine(), applicable);
		createOptions = fnBoundPtr(engine(), options, this);
	}

	Str *ImageFormat::name() {
		return fmtName;
	}

	Array<Str *> *ImageFormat::extensions() {
		return new (this) Array<Str *>(*fmtExtensions);
	}

	// ASCII to-upper.
	static wchar toUpper(wchar ch) {
		if (ch >= 'a' && ch <= 'z')
			return ch - 'a' + 'A';
		else
			return ch;
	}

	// Case-insensitive compare. Simplified to only work on ASCII
	static Bool compareNoCase(Str *a, Str *b) {
		const wchar *x = a->c_str();
		const wchar *y = b->c_str();
		while (*x && *y) {
			if (toUpper(*x) != toUpper(*y))
				return false;

			x++;
			y++;
		}
		return *x == 0 && *y == 0;
	}

	Bool ImageFormat::hasExtension(Str *ext) {
		for (Nat i = 0; i < fmtExtensions->count(); i++) {
			if (compareNoCase(fmtExtensions->at(i), ext))
				return true;
		}
		return false;
	}

	Bool ImageFormat::applicable(IStream *stream) {
		return isApplicable->call(stream);
	}

	FormatOptions *ImageFormat::options() {
		return createOptions->call();
	}

	Image *ImageFormat::load(IStream *from) {
		return options()->load(from);
	}

	ImageSet *ImageFormat::loadSet(IStream *from) {
		return options()->loadSet(from);
	}

	Image *ImageFormat::load(Url *from) {
		if (!from->exists()) {
			Str *msg = TO_S(from, S("The file ") << from << S(" does not exist."));
			throw new (from) ImageLoadError(msg);
		}
		IStream *s = from->read();
		try {
			Image *i = load(s);
			s->close();
			return i;
		} catch (...) {
			s->close();
			throw;
		}
	}

	ImageSet *ImageFormat::loadSet(Url *from) {
		if (!from->exists()) {
			Str *msg = TO_S(from, S("The file ") << from << S(" does not exist."));
			throw new (from) ImageLoadError(msg);
		}
		IStream *s = from->read();
		try {
			ImageSet *i = loadSet(s);
			s->close();
			return i;
		} catch (...) {
			s->close();
			throw;
		}
	}

	void ImageFormat::save(Image *image, OStream *to) {
		return options()->save(image, to);
	}

	void ImageFormat::save(Image *image, Url *to) {
		OStream *s = to->write();
		try {
			save(image, s);
			s->close();
		} catch (...) {
			s->close();
			throw;
		}
	}

	void ImageFormat::toS(StrBuf *to) const {
		*to << fmtName << S(" (") << storm::join(fmtExtensions, S(", ")) << S(")");
	}


	Array<ImageFormat *> *supportedImageFormats(EnginePtr e) {
		GcArray<void *> *data = (GcArray<void *> *)e.v.data();
		os::Lock *lock = (os::Lock *)data->v[0];

		os::Lock::L z(*lock);

		Array<ImageFormat *> *formats = (Array<ImageFormat *> *)data->v[1];

		if (!formats) {
			formats = new (e.v) Array<ImageFormat *>();
			addDefaultFormats(e.v, formats);
		}

		return new (e.v) Array<ImageFormat *>(*formats);
	}

	void registerImageFormat(EnginePtr e, ImageFormat *format) {
		GcArray<void *> *data = (GcArray<void *> *)e.v.data();
		os::Lock *lock = (os::Lock *)data->v[0];

		os::Lock::L z(*lock);

		Array<ImageFormat *> *formats = (Array<ImageFormat *> *)data->v[1];
		formats->push(format);
	}


	MAYBE(ImageFormat *) findFormat(IStream *from) {
		Array<ImageFormat *> *fmts = supportedImageFormats(from->engine());
		for (Nat i = 0; i < fmts->count(); i++) {
			ImageFormat *fmt = fmts->at(i);
			if (fmt->applicable(from))
				return fmt;
		}
		return null;
	}

	Image *loadImage(IStream *from) {
		ImageFormat *format = findFormat(from);
		if (!format)
			throw new (from) ImageLoadError(S("The file type is not supported."));

		return format->load(from);
	}

	Image *loadImage(Url *from) {
		if (!from->exists()) {
			Str *msg = TO_S(from, S("The file ") << from << S(" does not exist."));
			throw new (from) ImageLoadError(msg);
		}
		IStream *stream = from->read();
		try {
			Image *out = loadImage(stream);
			stream->close();
			return out;
		} catch (...) {
			stream->close();
			throw;
		}
	}

	ImageSet *loadImageSet(IStream *from) {
		ImageFormat *format = findFormat(from);
		if (!format)
			throw new (from) ImageLoadError(S("The file type is not supported."));

		return format->loadSet(from);
	}

	ImageSet *loadImageSet(Url *from) {
		if (!from->exists()) {
			Str *msg = TO_S(from, S("The file ") << from << S(" does not exist."));
			throw new (from) ImageLoadError(msg);
		}
		IStream *stream = from->read();
		try {
			ImageSet *out = loadImageSet(stream);
			stream->close();
			return out;
		} catch (...) {
			stream->close();
			throw;
		}
	}


	MAYBE(ImageFormat *) findFormat(Str *fileExt) {
		Array<ImageFormat *> *fmts = supportedImageFormats(fileExt->engine());
		for (Nat i = 0; i < fmts->count(); i++) {
			ImageFormat *fmt = fmts->at(i);
			if (fmt->hasExtension(fileExt))
				return fmt;
		}
		return null;
	}

	void saveImage(Image *image, Url *file) {
		ImageFormat *format = findFormat(file->ext());
		if (!format) {
			Str *msg = TO_S(image, S("The file extension ") << file->ext() << S(" is not supported."));
			throw new (image) ImageSaveError(msg);
		}
		format->save(image, file);
	}

}

void *createLibData(storm::Engine &e) {
	storm::GcArray<void *> *data = storm::runtime::allocArray<void *>(e, &storm::pointerArrayType, 2);
	data->v[0] = new os::Lock();
	return data;
}

void destroyLibData(void *data) {
	if (!data)
		return;

	storm::GcArray<void *> *d = (storm::GcArray<void *> *)data;
	delete (os::Lock *)d->v[0];
}
