/**
 * \file xformsImage.C
 * This file is part of LyX, the document processor.
 * Licence details can be found in the file COPYING.
 *
 * \author Angus Leeming
 *
 * Full author contact details are available in file CREDITS.
 */

#include <config.h>

#include "xformsImage.h"
#include "Color.h"

#include "debug.h"
#include "format.h"
#include "LColor.h"

#include "graphics/GraphicsParams.h"

#include "support/lstrings.h"
#include "support/lyxlib.h"

#include "lyx_forms.h"

#if defined (HAVE_FLIMAGE_H)
# include <flimage.h>
#elif defined (HAVE_X11_FLIMAGE_H)
# include <X11/flimage.h>
#endif

#include <boost/bind.hpp>
#include <boost/tuple/tuple.hpp>

using lyx::frontend::getRGBColor;

using lyx::support::float_equal;
using lyx::support::prefixIs;
using lyx::support::rtrim;

using boost::bind;

using std::equal_to;
using std::find_if;
using std::string;


namespace {

void init_graphics();

unsigned int packedcolor(LColor::color c);

} // namespace anon


namespace lyx {
namespace graphics {

/// Access to this class is through this static method.
Image::ImagePtr xformsImage::newImage()
{
	init_graphics();

	ImagePtr ptr;
	ptr.reset(new xformsImage);
	return ptr;
}


/// Return the list of loadable formats.
Image::FormatList xformsImage::loadableFormats()
{
	static FormatList fmts;
	if (!fmts.empty())
		return fmts;

	init_graphics();

	// The formats recognised by LyX
	Formats::const_iterator begin = formats.begin();
	Formats::const_iterator end   = formats.end();

	lyxerr[Debug::GRAPHICS]
		<< "\nThe image loader can load the following directly:\n";

	// Don't forget the Fortran numbering used by xforms!
	int const nformats = flimage_get_number_of_formats();
	for (int i = 1; i <= nformats; ++i) {

		FLIMAGE_FORMAT_INFO const * info = flimage_get_format_info(i);
		string const formal_name =
			info->formal_name ? info->formal_name : string();
		string ext =
			info->extension   ? info->extension   : string();

		if (ext.empty() || ext == "gz")
			continue;

		if (ext == "rgb") ext = "sgi";

		lyxerr[Debug::GRAPHICS]
			<< formal_name << ", extension \"" << ext << "\"\n";

		Formats::const_iterator it =
			find_if(begin, end,
				bind(equal_to<string>(),
				     bind(&Format::extension, _1),
				     ext));
		if (it != end)
			fmts.push_back(it->name());
	}

	lyxerr[Debug::GRAPHICS]
		<< "\nOf these, LyX recognises the following formats:\n";

	FormatList::const_iterator fbegin = fmts.begin();
	FormatList::const_iterator fend   = fmts.end();
	for (FormatList::const_iterator fit = fbegin; fit != fend; ++fit) {
		if (fit != fbegin)
			lyxerr[Debug::GRAPHICS] << ", ";
		lyxerr[Debug::GRAPHICS] << *fit;
	}
	lyxerr[Debug::GRAPHICS] << '\n' << std::endl;

	return fmts;
}


xformsImage::xformsImage()
	: image_(0),
	  pixmap_(0),
	  pixmap_status_(PIXMAP_UNINITIALISED)
{}


xformsImage::xformsImage(xformsImage const & other)
	: Image(other),
	  image_(0),
	  pixmap_(0),
	  pixmap_status_(PIXMAP_UNINITIALISED)
{
	if (other.image_) {
		image_ = flimage_dup(other.image_);
		image_->u_vdata = this;
	}
}


xformsImage::~xformsImage()
{
	if (image_)
		flimage_free(image_);
	if (pixmap_)
		XFreePixmap(fl_get_display(), pixmap_);
}


Pixmap xformsImage::getPixmap() const
{
	if (!pixmap_status_ == PIXMAP_SUCCESS)
		return 0;
	return pixmap_;
}


Image * xformsImage::clone_impl() const
{
	return new xformsImage(*this);
}


unsigned int xformsImage::getWidth_impl() const
{
	if (!image_)
		return 0;

	return image_->w;
}


unsigned int xformsImage::getHeight_impl() const
{
	if (!image_)
		return 0;
	return image_->h;
}


bool xformsImage::isDrawable_impl() const
{
	return pixmap_;
}


void xformsImage::load_impl(string const & filename)
{
	if (image_) {
		lyxerr[Debug::GRAPHICS]
			<< "Image is loaded already!" << std::endl;
		finishedLoading(false);
		return;
	}

	image_ = flimage_open(filename.c_str());
	if (!image_) {
		lyxerr[Debug::GRAPHICS]
			<< "Unable to open image" << std::endl;
		finishedLoading(false);
		return;
	}

	// Set this now and we won't need to bother again.
	image_->fill_color = packedcolor(LColor::graphicsbg);

	// Used by the callback routines to return to this
	image_->u_vdata = this;

	// Begin the reading process.
	flimage_read(image_);
}


bool xformsImage::setPixmap_impl(Params const & params)
{
	if (!image_ || params.display == NoDisplay)
		return false;

	Display * display = fl_get_display();

	if (pixmap_ && pixmap_status_ == PIXMAP_SUCCESS)
		XFreePixmap(display, pixmap_);

	int color_key;
	switch (params.display) {
	case MonochromeDisplay:
		color_key = FL_IMAGE_MONO;
		break;
	case GrayscaleDisplay:
		color_key = FL_IMAGE_GRAY;
		break;
	case ColorDisplay:
	default: // NoDisplay cannot happen!
		color_key = FL_IMAGE_RGB;
		break;
	}

	if (color_key != FL_IMAGE_RGB) {
		flimage_convert(image_, color_key, 0);
	}

	unsigned int fill = packedcolor(LColor::graphicsbg);
	if (fill != image_->fill_color) {
		// the background color has changed.
		// Note that in grayscale/monochrome images the background is
		// grayed also, so this call will have no visible effect. Sorry!
		flimage_replace_pixel(image_, image_->fill_color, fill);
		image_->fill_color = fill;
	}

	image_->xdisplay = display;
	Screen * screen  = ScreenOfDisplay(display, fl_screen);

	pixmap_ = flimage_to_pixmap(image_, XRootWindowOfScreen(screen));
	pixmap_status_ = pixmap_ ? PIXMAP_SUCCESS : PIXMAP_FAILED;

	return pixmap_status_ == PIXMAP_SUCCESS;
}


void xformsImage::clip_impl(Params const & params)
{
	if (!image_)
		return;

	if (params.bb.empty())
		// No clipping is necessary.
		return;

	int const new_width  = params.bb.xr - params.bb.xl;
	int const new_height = params.bb.yt - params.bb.yb;

	// No need to check if the width, height are > 0 because the
	// Bounding Box would be empty() in this case.
	if (new_width > image_->w || new_height > image_->h)
		// Bounds are invalid.
		return;

	if (new_width == image_->w && new_height == image_->h)
		// Bounds are unchanged.
		return;

	// flimage.h: image_ members w and h are of type int
	// (though always >= 0)
	// GraphicsParams.h: params.bb members xl, xr, yt and yb are of
	// type unsigned int.
	// We must, therefore, be careful...
	int const xoffset_l = params.bb.xl;
	int const yoffset_b = params.bb.yb;
	int const xoffset_r = image_->w > int(params.bb.xr) ?
		image_->w - params.bb.xr : 0;
	int const yoffset_t = image_->h > int(params.bb.yt) ?
		image_->h - params.bb.yt : 0;

	flimage_crop(image_, xoffset_l, yoffset_t, xoffset_r, yoffset_b);
}


void xformsImage::rotate_impl(Params const & params)
{
	if (!image_)
		return ;

	if (float_equal(params.angle, 0.0, 0.1))
		// No rotation is necessary.
		return;

	// The angle passed to flimage_rotate is the angle in one-tenth of a
	// degree units.

	// Work around xforms bug when params.angle == 270
	// the 'InternalError: bad special angle' error.
	// This bug fix is not needed in xforms 1.0 and greater.
	if (float_equal(params.angle, 270.0, 0.1)) {
		flimage_rotate(image_,  900, FLIMAGE_SUBPIXEL);
		flimage_rotate(image_, 1800, FLIMAGE_SUBPIXEL);
	} else {
		flimage_rotate(image_,
			       int(params.angle * 10),
			       FLIMAGE_SUBPIXEL);
	}
}


void xformsImage::scale_impl(Params const & params)
{
	if (!image_)
		return;

	unsigned int width;
	unsigned int height;
	boost::tie(width, height) = getScaledDimensions(params);

	if (width == getWidth() && height == getHeight())
		// No scaling needed
		return;

	flimage_scale(image_, width, height, FLIMAGE_SUBPIXEL);
}


void xformsImage::statusCB(string const & status_message)
{
	if (status_message.empty())
		return;

	if (prefixIs(status_message, "Done Reading")) {
		if (image_) {
			flimage_close(image_);
		}

		finishedLoading(true);
	}
}


void xformsImage::errorCB(string const & error_message)
{
	if (error_message.empty())
		return;

	if (image_) {
		flimage_close(image_);
	}

	finishedLoading(false);
}

} // namespace graphics
} // namespace lyx


namespace {

extern "C" {

int status_report(FL_IMAGE * ob, const char *s)
{
	BOOST_ASSERT(ob && ob->u_vdata);

	string const str = s ? rtrim(s) : string();
	if (str.empty())
		return 0;

	lyxerr[Debug::GRAPHICS]
		<< "xforms image loader. Status: " << str << std::endl;

	lyx::graphics::xformsImage * ptr =
		static_cast<lyx::graphics::xformsImage *>(ob->u_vdata);
	ptr->statusCB(str);

	return 0;
}


static void error_report(FL_IMAGE * ob, const char *s)
{
	BOOST_ASSERT(ob && ob->u_vdata);

	string const str = s ? rtrim(s) : string();
	if (str.empty())
		return;

	lyxerr[Debug::GRAPHICS]
		<< "xforms image loader. Error: " << str << std::endl;

	lyx::graphics::xformsImage * ptr =
		static_cast<lyx::graphics::xformsImage *>(ob->u_vdata);
	ptr->errorCB(str);
}

} // extern "C"


void init_graphics()
{
	// Paranoia check
	static bool initialised = false;
	if (initialised)
		return;
	initialised = true;

	flimage_enable_bmp();
	flimage_enable_fits();
	flimage_enable_gif();
#ifdef USE_JPEG_IMAGE_LOADER
	flimage_enable_jpeg();
#endif

	// xforms itself uses pngtopnm to convert to a loadable format.
	// We prefer to use our own conversion mechanism, therefore.
	// flimage_enable_png();

	flimage_enable_pnm();

	// xforms recognises PS but not EPS
	// It dies horribly with lots of older PostScript files.
	// Easiest, therefore, to disable PS support and insist that a PS-type
	// file is converted to a bitmap format.
	// flimage_enable_ps();

	flimage_enable_sgi();
	flimage_enable_tiff();
	flimage_enable_xbm();
	flimage_enable_xwd();
	// xforms can load most XPM files, but will occasionally crash
	// with some files created by ImakeMagick's convert program.
	// Turn off xpm support for the time being.
	// flimage_enable_xpm();

	// xforms stores this permanently (does not make a copy) so
	// this should never be destroyed.
	static FLIMAGE_SETUP setup;
	setup.visual_cue    = status_report;
	setup.error_message = error_report;
	flimage_setup(&setup);
}


unsigned int packedcolor(LColor::color col)
{
	unsigned int r, g, b;
	bool const success = getRGBColor(col, r, g, b);
	if (!success)
		// Set to black on failure
		return FL_PACK(255, 255, 255);

	return FL_PACK(r, g, b);
}

} // namespace anon
