// K-3D
// Copyright (c) 1995-2004, Timothy M. Shead
//
// Contact: tshead@k-3d.com
//
// 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.
//
// This program 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, write to the Free Software
// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA

/** \file
		\brief Implements the NetPBMReader K-3D object, which reads and writes a large number of image file formats, using the NetPBM package
		\author Tim Shead (tshead@k-3d.com)
*/

#include <k3dsdk/application.h>
#include <k3dsdk/file_helpers.h>
#include <k3dsdk/ibitmap_read_format.h>
#include <k3dsdk/ideletable.h>
#include <k3dsdk/ifile_format.h>
#include <k3dsdk/module.h>
#include <k3dsdk/string_modifiers.h>
#include <k3dsdk/system_functions.h>

#include <sdpxml/sdpxml.h>

#include <boost/filesystem/fstream.hpp>
#include <boost/filesystem/path.hpp>
#include <boost/regex.hpp>

#include <fstream>
#include <sstream>

namespace
{

/////////////////////////////////////////////////////////////////////////////
// netpbm_reader_implementation

class netpbm_reader_implementation :
	public k3d::ifile_format,
	public k3d::ibitmap_read_format,
	public k3d::ideletable
{
public:
	netpbm_reader_implementation() :
		m_Formats("netpbm")
	{
	}

	unsigned long priority()
	{
		return 0;
	}

	bool query_can_handle(const boost::filesystem::path& FilePath)
	{
		sdpxml::ElementPointer reader = GetReader(FilePath);
		if(!reader)
			return false;

		const std::string format = sdpxml::GetAttribute<std::string>(*reader, "format", "");
		return_val_if_fail(format.size(), false);

		return true;
	}

	bool read_file(const boost::filesystem::path& FilePath, k3d::bitmap& Bitmap)
	{
		std::cerr << info << "Read " << FilePath.native_file_string() << " using NetPBMReader" << std::endl;

		// Convert the source file to a ppm file ...
		boost::filesystem::path pnm_file_path;
		return_val_if_fail(ConvertToPNM(FilePath, pnm_file_path), false);

		// Read the PNM file header to get its dimensions ...
		boost::filesystem::ifstream pnm_file_header(pnm_file_path);
		return_val_if_fail(pnm_file_header.good(), false);

		unsigned long width = 0;
		unsigned long height = 0;
		ImageType type;

		return_val_if_fail(ReadPNMHeader(pnm_file_header, width, height, type), false);
		return_val_if_fail(width, false);
		return_val_if_fail(height, false);
		pnm_file_header.close();

		// Open the file ... (binary mode to avoid auto-conversion of std::endl)
		boost::filesystem::ifstream pnm_file(pnm_file_path, std::ios::in | std::ios::binary);
		return_val_if_fail(pnm_file.good(), false);

		// Load it up ...
		return_val_if_fail(ReadPNM(pnm_file, Bitmap), false);

		return true;
	}

	k3d::iplugin_factory& factory()
	{
		return get_factory();
	}

	static k3d::iplugin_factory& get_factory()
	{
		static k3d::plugin_factory<
			k3d::application_plugin<netpbm_reader_implementation>,
			k3d::interface_list<k3d::ibitmap_read_format> > factory(
				k3d::uuid(0x36fbaf80, 0xe8d548ee, 0xa0ba549d, 0x4ff35114),
				"NetPBMReader",
				"NetPBM Formats ( many )",
				"Bitmap File Formats");

		return factory;
	}

private:
	/// Loads the netpbm.conf XML file, which describes how to identify and convert filetypes supported by NetPBM
	bool LoadFormats();

	/// Enumerates the three most common NetPBM file formats
	typedef enum
	{
		PBM,
		PGM,
		PPM
	} ImageType;

	/// Returns a filter description from the netpbm.conf XML file, based on the filename
	sdpxml::ElementPointer GetReader(const boost::filesystem::path& FilePath);

	/// Converts a file to a temp file in the native NetPBM file format
	bool ConvertToPNM(const boost::filesystem::path& FilePath, boost::filesystem::path& PNMFilePath);

	bool ReadPNMHeader(std::istream& Stream, k3d::pixel_size_t& Width, k3d::pixel_size_t& Height, ImageType& Type);
	bool ReadPNM(std::istream& Stream, k3d::bitmap& Bitmap);
	bool ReadPBM(std::istream& Stream, k3d::pixel* Destination, k3d::pixel_size_t Width, k3d::pixel_size_t Height);
	bool ReadPGM(std::istream& Stream, k3d::pixel* Destination, k3d::pixel_size_t Width, k3d::pixel_size_t Height);
	bool ReadPPM(std::istream& Stream, k3d::pixel* Destination, k3d::pixel_size_t Width, k3d::pixel_size_t Height);

	sdpxml::Document m_Formats;
};

/////////////////////////////////////////////////////////////////////////////
// Helper functions

bool netpbm_reader_implementation::LoadFormats()
{
	if(m_Formats.Children().size())
		return true;

	const boost::filesystem::path format_file(k3d::application().share_path() / "netpbm.conf");
	boost::filesystem::ifstream stream(format_file);
	if(!m_Formats.Load(stream, format_file.native_file_string()))
		{
			std::cerr << __PRETTY_FUNCTION__ << ": Error loading " << format_file.native_file_string() << " configuration file" << std::endl;
			return false;
		}

	return true;
}

sdpxml::ElementPointer netpbm_reader_implementation::GetReader(const boost::filesystem::path& FilePath)
{
	// Load our external format description file ...
	return_val_if_fail(LoadFormats(), 0);

	// Make sure we've got something ...
	return_val_if_fail(m_Formats.Children().size(), 0);
	return_val_if_fail(m_Formats.Name() == "netpbm", 0);

	sdpxml::ElementPointer readers = sdpxml::FindElement(m_Formats, sdpxml::SameName("readers"));
	return_val_if_fail(readers, 0);

//#ifndef SDPWIN32
	// First, see if the filetype can be identified by /etc/magic through the "file" command ...
	const std::string commandline = "file -b \"" + FilePath.native_file_string() + "\"";

	std::ostringstream filetype;
	if(k3d::system::run_process(commandline, filetype))
		{
			// For each netpbm reader ...
			for(sdpxml::ElementCollection::iterator reader = readers->Children().begin(); reader != readers->Children().end(); reader++)
				{
					// See if it has a "magic" regular expression ...
					const std::string magic = sdpxml::GetAttribute<std::string>(*reader, "magic", "");
					if(magic.empty())
						continue;
				
					// See if the "magic" regular expression has any matches against the output of the "file" command ...
					const boost::regex expression(magic);
					boost::smatch match_results;
					if(boost::regex_search(filetype.str(), match_results, expression))
						return &(*reader);
				}
		}
//#endif // !SDPWIN32

	// Second try ... look at filename for standard file extensions ...
	for(sdpxml::ElementCollection::iterator reader = readers->Children().begin(); reader != readers->Children().end(); reader++)
		{
			// See if it has a "filename" regular expression ...
			const std::string filename = sdpxml::GetAttribute<std::string>(*reader, "filename", "");
			if(filename.empty())
				continue;

			// See if the filename regular expression has any matches against the input filename ...
			const boost::regex expression(filename);
			boost::smatch match_results;
			if(boost::regex_search(FilePath.native_file_string(), match_results, expression))
				return &(*reader);
		}

	return 0;
}

bool netpbm_reader_implementation::ConvertToPNM(const boost::filesystem::path& FilePath, boost::filesystem::path& PNMFilePath)
{
	PNMFilePath = boost::filesystem::path();

	sdpxml::ElementPointer reader = GetReader(FilePath);
	return_val_if_fail(reader, false);

	// Build command
	//std::string command = sdpxml::GetAttribute<std::string>(*reader, "command", "");
	std::string command = sdpxml::GetAttribute<std::string>(*reader, "command", "");
	return_val_if_fail(command.size(), false);

	k3d::formatted_replace(command, '%', "p", FilePath.native_file_string());

	// Get temporary file
	const boost::filesystem::path pnmpath = k3d::system::get_temp_directory() / "netpbmreader.pnm";

	// Redirect output to pnm file
	command += " > " + pnmpath.native_file_string();

	return_val_if_fail(k3d::system::run_process(command, std::cerr), false);

	PNMFilePath = pnmpath;

	return true;
}

bool netpbm_reader_implementation::ReadPNMHeader(std::istream& Stream, k3d::pixel_size_t& Width, k3d::pixel_size_t& Height, ImageType& Type)
{
	// Sanity checks ...
	return_val_if_fail(Stream.good(), false);

	// A buffer for reading lines ...
	std::string linebuffer;

	// Check for magic characters at the beginning of the file ...
	k3d::getline(Stream, linebuffer);
	if(Stream.eof())
		return false;

	if(linebuffer.substr(0, 2) == "P4")
		Type = PBM;
	else if(linebuffer.substr(0, 2) == "P5")
		Type = PGM;
	else if(linebuffer.substr(0, 2) == "P6")
		Type = PPM;
	else
		return false;

	// Detect (optional) comments here ...
	while(Stream.peek() == '#')
		k3d::getline(Stream, linebuffer);

	// Get the file dimensions ...
	Stream >> Width >> Height;
	if(Stream.eof())
		return_val_if_fail(0, false);

	// If it's a bitmap image, we're done ...
	if(PBM == Type)
		{
			Stream >> std::ws;
			return true;
		}

	// Detect (optional) comments here ...
	while(Stream.peek() == '#')
		k3d::getline(Stream, linebuffer);

	// Get the maximum per-channel value for the image ...
	unsigned long maxvalue = 0;
	Stream >> maxvalue >> std::ws;
	if(Stream.eof())
		return_val_if_fail(0, false);

	return true;
}

bool netpbm_reader_implementation::ReadPNM(std::istream& Stream, k3d::bitmap& Bitmap)
{
	// Sanity checks ...
	assert_warning(Stream.good());

	unsigned long sourcewidth = 0;
	unsigned long sourceheight = 0;
	ImageType sourcetype;

	return_val_if_fail(ReadPNMHeader(Stream, sourcewidth, sourceheight, sourcetype), false);
	return_val_if_fail(sourcewidth, false);
	return_val_if_fail(sourceheight, false);

	// Get the destination bitmap buffer ...
	k3d::pixel* destination = Bitmap.data();
	k3d::pixel_size_t destinationwidth = Bitmap.width();
	k3d::pixel_size_t destinationheight = Bitmap.height();

	// Sanity checks ...
	return_val_if_fail(destination, false);
	return_val_if_fail(sourcewidth == destinationwidth, false);
	return_val_if_fail(sourceheight == destinationheight, false);

	switch(sourcetype)
		{
			case PBM:
				return ReadPBM(Stream, destination, sourcewidth, sourceheight);
			case PGM:
				return ReadPGM(Stream, destination, sourcewidth, sourceheight);
			case PPM:
				return ReadPPM(Stream, destination, sourcewidth, sourceheight);
			default:
				return_val_if_fail(0, false);
		}

	return false;
}

bool netpbm_reader_implementation::ReadPBM(std::istream& Stream, k3d::pixel* Destination, k3d::pixel_size_t Width, k3d::pixel_size_t Height)
{
	// Sanity checks ...
	assert_warning(Stream.good());
	assert_warning(Destination);
	assert_warning(Width);
	assert_warning(Height);

	char packed, pixel;

	// For each source row ...
	for(unsigned long y = 0; y < Height; y++)
		{
			for(unsigned long x = 0; x < Width; x++)
				{
					if(0 == x%8)
						{
							Stream.get(packed);
							return_val_if_fail(!Stream.eof(), false);
						}

					pixel = (packed >> (7 - (x%8))) & 0x01;

					Destination->red = pixel * 255;
					Destination->green = pixel * 255;
					Destination->blue = pixel * 255;
					Destination->alpha = 255;

					++Destination;
				}
		}
	return true;
}

bool netpbm_reader_implementation::ReadPGM(std::istream& Stream, k3d::pixel* Destination, k3d::pixel_size_t Width, k3d::pixel_size_t Height)
{
	// Sanity checks ...
	assert_warning(Stream.good());
	assert_warning(Destination);
	assert_warning(Width);
	assert_warning(Height);

	char grey;

	// For each source row ...
	for(unsigned long y = 0; y < Height; y++)
		{
			for(unsigned long x = 0; x < Width; x++)
				{
					Stream.get(grey);
					return_val_if_fail(!Stream.eof(), false);

					Destination->red = grey;
					Destination->green = grey;
					Destination->blue = grey;
					Destination->alpha = 255;

					++Destination;
				}
		}
	return true;
}

bool netpbm_reader_implementation::ReadPPM(std::istream& Stream, k3d::pixel* Destination, k3d::pixel_size_t Width, k3d::pixel_size_t Height)
{
	// Sanity checks ...
	assert_warning(Stream.good());
	assert_warning(Destination);
	assert_warning(Width);
	assert_warning(Height);

	char red, green, blue;

	// For each source row ...
	for(unsigned long y = 0; y < Height; y++)
		{
			for(unsigned long x = 0; x < Width; x++)
				{
					Stream.get(red);
					Stream.get(green);
					Stream.get(blue);
					return_val_if_fail(!Stream.eof(), false);

					Destination->red = red;
					Destination->green = green;
					Destination->blue = blue;
					Destination->alpha = 255;

					++Destination;
				}
		}
	return true;
}

} // namespace

namespace libk3dbitmap
{

/////////////////////////////////////////////////////////////////////////////
// netpbm_reader_factory

k3d::iplugin_factory& netpbm_reader_factory()
{
	return netpbm_reader_implementation::get_factory();
}

} // namespace libk3dbitmap


