// K-3D
// Copyright © 1995-2003, 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 k3d::render_farm class, which encapsulates a virtual render farm
		\author Tim Shead (tshead@k-3d.com)
*/

#include "render_farm.h"

#include <k3dsdk/application.h>
#include <k3dsdk/ioptions.h>
#include <k3dsdk/result.h>
#include <k3dsdk/string_modifiers.h>

#include <sdpxml/sdpxml.h>

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

#include <fstream>
#include <list>
#include <iostream>

#include <errno.h>

namespace
{

/////////////////////////////////////////////////////////////////////////////
// render_frame_implementation

class render_frame_implementation :
	public k3d::irender_frame
{
public:
	render_frame_implementation(const boost::filesystem::path& JobPath, const std::string& Name) :
		m_Path(JobPath / Name)
	{
		try
			{
				create_directory(m_Path);
			}
		catch(const std::exception& ex)
			{
				std::cerr << __PRETTY_FUNCTION__ << ": Error creating frame directory [" << m_Path.native_file_string() << "]" << std::endl;
			}
	}

	const boost::filesystem::path add_input_file(const std::string& Name)
	{
		// Sanity checks ...
		assert_warning(Name.size());

		// Make sure the filepath is unique ...
		unsigned long index = 0;
		std::string name(Name);
		while(std::find(m_input_files.begin(), m_input_files.end(), name) != m_input_files.end())
			name = Name + '-' + k3d::to_string(++index);

		m_input_files.push_back(name);

		return m_Path / name;
	}

	const boost::filesystem::path add_output_file(const std::string& Name)
	{
		// Sanity checks ...
		assert_warning(Name.size());

		// Make sure the filepath is unique ...
		unsigned long index = 0;
		std::string name(Name);
		while(std::find(m_output_files.begin(), m_output_files.end(), name) != m_output_files.end())
			name = Name + '-' + k3d::to_string(++index);

		m_output_files.push_back(name);

		return m_Path / name;
	}

	void add_render_operation(const std::string& EngineType, const std::string& Engine, const boost::filesystem::path& SourceFilePath, const bool VisibleRender)
	{
		// Sanity checks ...
		assert_warning(EngineType.size());
		assert_warning(Engine.size());
		assert_warning(!SourceFilePath.empty());

		m_render_operations.push_back(render_operation(EngineType, Engine, SourceFilePath, VisibleRender));
	}

	void add_copy_operation(const boost::filesystem::path& SourceFilePath, const boost::filesystem::path& TargetFilePath)
	{
		// Sanity checks ...
		assert_warning(!SourceFilePath.empty());
		assert_warning(!TargetFilePath.empty());

		m_copy_operations.push_back(copy_operation(SourceFilePath, TargetFilePath));
	}

	void add_view_operation(const boost::filesystem::path& FilePath)
	{
		m_view_files.push_back(FilePath);
	}

	void write_control_file()
	{
		// Create the empty control file ...
		boost::filesystem::ofstream file(m_Path / "control.k3d");

		// Create the XML document ...
		sdpxml::Document document("k3dml");
		sdpxml::Element& frame = document.Append(sdpxml::Element("frame"));

		// Specify which files will be used as input for rendering the frame ...
		for(input_files_t::const_iterator inputfile = m_input_files.begin(); inputfile != m_input_files.end(); ++inputfile)
			frame.Append(sdpxml::Element("inputfile", "", sdpxml::Attribute("path", *inputfile)));

		// Specify which files will be generated during the render process ...
		for(output_files_t::const_iterator outputfile = m_output_files.begin(); outputfile != m_output_files.end(); ++outputfile)
			frame.Append(sdpxml::Element("outputfile", "", sdpxml::Attribute("path", *outputfile)));

		// Specify the steps necessary to render ...
		for(render_operations_t::const_iterator renderop = m_render_operations.begin(); renderop != m_render_operations.end(); ++renderop)
			frame.Append(sdpxml::Element("renderoperation", "",
				sdpxml::Attribute("type", renderop->type),
				sdpxml::Attribute("engine", renderop->engine),
				sdpxml::Attribute("sourcepath", renderop->source_path.native_file_string()),
				sdpxml::Attribute("shaderspath", k3d::application().shader_cache_path().native_file_string()),
				sdpxml::Attribute("sharepath", k3d::application().share_path().native_file_string()),
				sdpxml::Attribute("visiblerender", sdpToString(renderop->visible_render))));

		// Specify any copy operations to be performed after the render ...
		for(copy_operations_t::const_iterator copyop = m_copy_operations.begin(); copyop != m_copy_operations.end(); ++copyop)
			frame.Append(sdpxml::Element("copyoperation", "",
				sdpxml::Attribute("from", copyop->from.native_file_string()),
				sdpxml::Attribute("to", copyop->to.native_file_string())));

		// Specify any viewing operations to be performed after the render ...
		for(view_files_t::const_iterator viewop = m_view_files.begin(); viewop != m_view_files.end(); ++viewop)
			frame.Append(sdpxml::Element("viewoperation", "", sdpxml::Attribute("path", viewop->native_file_string())));

		// Write the document ...
		file << document << std::endl;
	}

	void mark_ready()
	{
		// Create the empty "ready" status file ...
		boost::filesystem::ofstream file(m_Path / "ready");

		// Boost::filesystem does not rename empty files
		file << "Status file." << std::ends;
	}

private:
	/// Stores the filesystem path for this job
	const boost::filesystem::path m_Path;
	/// Stores the set of input files used during rendering
	typedef std::vector<std::string> input_files_t;
	input_files_t m_input_files;
	/// Stores the set of output files generated by rendering
	typedef std::vector<std::string> output_files_t;
	output_files_t m_output_files;
	/// Defines a render operation to be performed
	struct render_operation
	{
		explicit render_operation(const std::string& Type, const std::string& Engine, const boost::filesystem::path& SourcePath, const bool VisibleRender) :
			type(Type),
			engine(Engine),
			source_path(SourcePath),
			visible_render(VisibleRender)
		{
		}

		const std::string type;
		const std::string engine;
		const boost::filesystem::path source_path;
		const bool visible_render;
	};
	typedef std::list<render_operation> render_operations_t;
	render_operations_t m_render_operations;
	/// Defines a post-render copy operation
	struct copy_operation
	{
		explicit copy_operation(const boost::filesystem::path& From, const boost::filesystem::path& To) :
			from(From),
			to(To)
		{
		}

		const boost::filesystem::path from;
		const boost::filesystem::path to;
	};
	typedef std::list<copy_operation> copy_operations_t;
	copy_operations_t m_copy_operations;
	/// Stores an array of files to view once rendering is complete
	typedef std::vector<boost::filesystem::path> view_files_t;
	view_files_t m_view_files;
};

/////////////////////////////////////////////////////////////////////////////
// render_job_implementation

class render_job_implementation :
	public k3d::irender_job
{
public:
	render_job_implementation(const boost::filesystem::path Path, const std::string JobName) :
		m_Path(Path / JobName)
	{
		try
			{
				create_directory(m_Path);
			}
		catch(const std::exception& ex)
			{
				std::cerr << __PRETTY_FUNCTION__ << ": Error creating job directory [" << m_Path.native_file_string() << "]" << std::endl;
			}
	}

	k3d::irender_frame& create_frame(const std::string FrameName)
	{
		m_frames.push_back(render_frame_implementation(m_Path, FrameName));
		return m_frames.back();
	}

	bool write_control_files()
	{
		// Create a control file for each frame ...
		std::for_each(m_frames.begin(), m_frames.end(), std::mem_fun_ref(&render_frame_implementation::write_control_file));

		// Create the control file ...
		boost::filesystem::ofstream file(m_Path / "control.k3d");

		// Create the XML document ...
		sdpxml::Document document("k3dml");

		// Write the document ...
		file << document << std::endl;

		return true;
	}

	bool mark_ready()
	{
		// Mark each frame as "ready" ...
		std::for_each(m_frames.begin(), m_frames.end(), std::mem_fun_ref(&render_frame_implementation::mark_ready));

		// Create the "ready" status file ...
		boost::filesystem::ofstream file(m_Path / "ready");

		// Boost::filesystem does not rename empty files
		file << "Status file." << std::ends;

		return true;
	}

	const std::string path()
	{
		return m_Path.native_file_string();
	}

private:
	const boost::filesystem::path m_Path;

	typedef std::list<render_frame_implementation> frames_t;
	frames_t m_frames;
};

} // namespace

/////////////////////////////////////////////////////////////////////////////
// k3d::render_farm::implementation

class k3d::render_farm::implementation
{
public:
	implementation(const boost::filesystem::path& OptionsPath) :
		m_options_path(OptionsPath)
	{
	}

	k3d::irender_job& create_job(const std::string JobName)
	{
		// Sanity checks ...
		assert_warning(JobName.size());

		// Render farm path ...
		const boost::filesystem::path job_path(k3d::application().options().render_farm_path());

		// Ensure that the job gets a unique name ...
		unsigned long index = 0;
		std::string job_name(JobName);
		while(boost::filesystem::exists(job_path / job_name))
			job_name = JobName + '-' + k3d::to_string(index++);

		m_jobs.push_back(render_job_implementation(job_path, job_name));
		return m_jobs.back();
	}

	void start_job(k3d::irender_job& Job)
	{
		// Make sure it's one of ours ...
		render_job_implementation* const job = dynamic_cast<render_job_implementation*>(&Job);
		assert_warning(job);

		// Update control files for the job and all its frames ...
		if(!job->write_control_files())
			{
				std::cerr << __PRETTY_FUNCTION__ << ": error writing control files" << std::endl;
				return;
			}

		// Mark the job ready-to-run ...
		if(!job->mark_ready())
			{
				std::cerr << __PRETTY_FUNCTION__ << ": error marking job ready" << std::endl;
				return;
			}

		// Start the local rendering process ...
		std::string commandline = "k3d-renderjob \"";
		commandline += m_options_path.native_file_string();
		commandline += "\" \"";
		commandline += job->path();
		commandline += "\" &";

		errno = 0;
		int result = system(commandline.c_str());
		if(-1 == result || (127 == result && errno))
			{
				std::cerr << "CRenderFarm::StartJob(): Error starting render job " << commandline << std::endl;
				return;
			}
	}

private:
	const boost::filesystem::path m_options_path;
	typedef std::list<render_job_implementation> jobs_t;
	jobs_t m_jobs;
};


namespace k3d
{

/////////////////////////////////////////////////////////////////////////////
// render_farm

render_farm::render_farm(const boost::filesystem::path& OptionsPath) :
	m_implementation(new implementation(OptionsPath))
{
}

render_farm::~render_farm()
{
	delete m_implementation;
}

irender_job& render_farm::create_job(const std::string JobName)
{
	return m_implementation->create_job(JobName);
}

void render_farm::start_job(irender_job& Job)
{
	m_implementation->start_job(Job);
}

} // namespace k3d


