/*
    Copyright (C) 2008-2009 Paul Davis
    Author: Sakari Bergen

    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., 675 Mass Ave, Cambridge, MA 02139, USA.

*/

#include "ardour/export_handler.h"

#include <glibmm.h>

#include "pbd/convert.h"
#include "pbd/filesystem.h"

#include "ardour/ardour.h"
#include "ardour/configuration.h"
#include "ardour/export_graph_builder.h"
#include "ardour/export_timespan.h"
#include "ardour/export_channel_configuration.h"
#include "ardour/export_status.h"
#include "ardour/export_format_specification.h"
#include "ardour/export_filename.h"
#include "ardour/export_failed.h"

#include "i18n.h"

using namespace std;
using namespace PBD;

namespace ARDOUR
{

/*** ExportElementFactory ***/

ExportElementFactory::ExportElementFactory (Session & session) :
  session (session)
{

}

ExportElementFactory::~ExportElementFactory ()
{

}

ExportElementFactory::TimespanPtr
ExportElementFactory::add_timespan ()
{
	return TimespanPtr (new ExportTimespan (session.get_export_status(), session.frame_rate()));
}

ExportElementFactory::ChannelConfigPtr
ExportElementFactory::add_channel_config ()
{
	return ChannelConfigPtr (new ExportChannelConfiguration (session));
}

ExportElementFactory::FormatPtr
ExportElementFactory::add_format ()
{
	return FormatPtr (new ExportFormatSpecification (session));
}

ExportElementFactory::FormatPtr
ExportElementFactory::add_format (XMLNode const & state)
{
	return FormatPtr (new ExportFormatSpecification (session, state));
}

ExportElementFactory::FormatPtr
ExportElementFactory::add_format_copy (FormatPtr other)
{
	return FormatPtr (new ExportFormatSpecification (*other));
}

ExportElementFactory::FilenamePtr
ExportElementFactory::add_filename ()
{
	return FilenamePtr (new ExportFilename (session));
}

ExportElementFactory::FilenamePtr
ExportElementFactory::add_filename_copy (FilenamePtr other)
{
	return FilenamePtr (new ExportFilename (*other));
}

/*** ExportHandler ***/

ExportHandler::ExportHandler (Session & session)
  : ExportElementFactory (session)
  , session (session)
  , graph_builder (new ExportGraphBuilder (session))
  , export_status (session.get_export_status ())
  , realtime (false)
  , normalizing (false)
  , cue_tracknum (0)
  , cue_indexnum (0)
{
}

ExportHandler::~ExportHandler ()
{
	// TODO remove files that were written but not finsihed
}

bool
ExportHandler::add_export_config (TimespanPtr timespan, ChannelConfigPtr channel_config, FormatPtr format, FilenamePtr filename)
{
	FileSpec spec (channel_config, format, filename);
	ConfigPair pair (timespan, spec);
	config_map.insert (pair);
	
	return true;
}

void
ExportHandler::do_export (bool rt)
{
	/* Count timespans */

	export_status->init();
	std::set<TimespanPtr> timespan_set;
	for (ConfigMap::iterator it = config_map.begin(); it != config_map.end(); ++it) {
		timespan_set.insert (it->first);
	}
	export_status->total_timespans = timespan_set.size();

	/* Start export */

	realtime = rt;
	start_timespan ();
}

void
ExportHandler::start_timespan ()
{
	export_status->timespan++;

	if (config_map.empty()) {
		// freewheeling has to be stopped from outside the process cycle
		export_status->running = false;
		return;
	}

	current_timespan = config_map.begin()->first;

	/* Register file configurations to graph builder */

	timespan_bounds = config_map.equal_range (current_timespan);
	graph_builder->reset ();
	for (ConfigMap::iterator it = timespan_bounds.first; it != timespan_bounds.second; ++it) {
		// Filenames can be shared across timespans
		FileSpec & spec = it->second;
		spec.filename->set_timespan (it->first);
		graph_builder->add_config (spec);
	}

	/* start export */

	normalizing = false;
	session.ProcessExport.connect_same_thread (process_connection, boost::bind (&ExportHandler::process, this, _1));
	process_position = current_timespan->get_start();
	session.start_audio_export (process_position, realtime);
}

int
ExportHandler::process (nframes_t frames)
{
	if (!export_status->running) {
		return 0;
	} else if (normalizing) {
		return process_normalize ();
	} else {
		return process_timespan (frames);
	}
}

int
ExportHandler::process_timespan (nframes_t frames)
{
	/* update position */

	nframes_t frames_to_read = 0;
	sframes_t const start = current_timespan->get_start();
	sframes_t const end = current_timespan->get_end();
	
	bool const last_cycle = (process_position + frames >= end);

	if (last_cycle) {
		frames_to_read = end - process_position;
		export_status->stop = true;
		normalizing = true;
	} else {
		frames_to_read = frames;
	}

	process_position += frames_to_read;
	export_status->progress = (float) (process_position - start) / (end - start);

	/* Do actual processing */

	return graph_builder->process (frames_to_read, last_cycle);
}

int
ExportHandler::process_normalize ()
{
	if (graph_builder->process_normalize ()) {
		finish_timespan ();
	}
	
	return 0;
}

void
ExportHandler::finish_timespan ()
{
	while (config_map.begin() != timespan_bounds.second) {
		config_map.erase (config_map.begin());
	}

	start_timespan ();
}

/*** CD Marker sutff ***/

struct LocationSortByStart {
    bool operator() (Location *a, Location *b) {
	    return a->start() < b->start();
    }
};

void
ExportHandler::export_cd_marker_file (TimespanPtr timespan, FormatPtr file_format, std::string filename, CDMarkerFormat format)
{
	string filepath;
	string basename = Glib::path_get_basename(filename);

	size_t ext_pos = basename.rfind('.');
	if (ext_pos != string::npos) {
		basename = basename.substr(0, ext_pos); /* strip file extension, if there is one */
	}

	void (ExportHandler::*header_func) (CDMarkerStatus &);
	void (ExportHandler::*track_func) (CDMarkerStatus &);
	void (ExportHandler::*index_func) (CDMarkerStatus &);

	switch (format) {
	  case CDMarkerTOC:
		filepath = Glib::build_filename(Glib::path_get_dirname(filename), basename + ".toc");
		header_func = &ExportHandler::write_toc_header;
		track_func = &ExportHandler::write_track_info_toc;
		index_func = &ExportHandler::write_index_info_toc;
		break;
	  case CDMarkerCUE:
		filepath = Glib::build_filename(Glib::path_get_dirname(filename), basename + ".cue");
		header_func = &ExportHandler::write_cue_header;
		track_func = &ExportHandler::write_track_info_cue;
		index_func = &ExportHandler::write_index_info_cue;
		break;
	  default:
		return;
	}

	CDMarkerStatus status (filepath, timespan, file_format, filename);

	if (!status.out) {
		error << string_compose(_("Editor: cannot open \"%1\" as export file for CD marker file"), filepath) << endmsg;
		return;
	}

	(this->*header_func) (status);

	/* Get locations and sort */

	Locations::LocationList const & locations (session.locations()->list());
	Locations::LocationList::const_iterator i;
	Locations::LocationList temp;

	for (i = locations.begin(); i != locations.end(); ++i) {
		if ((*i)->start() >= timespan->get_start() && (*i)->end() <= timespan->get_end() && (*i)->is_cd_marker() && !(*i)->is_session_range()) {
			temp.push_back (*i);
		}
	}

	if (temp.empty()) {
		// TODO One index marker for whole thing
		return;
	}

	LocationSortByStart cmp;
	temp.sort (cmp);
	Locations::LocationList::const_iterator nexti;

	/* Start actual marker stuff */

	sframes_t last_end_time = timespan->get_start(), last_start_time = timespan->get_start();
	status.track_position = last_start_time - timespan->get_start();

	for (i = temp.begin(); i != temp.end(); ++i) {

		status.marker = *i;

		if ((*i)->start() < last_end_time) {
			if ((*i)->is_mark()) {
				/* Index within track */

				status.index_position = (*i)->start() - timespan->get_start();
				(this->*index_func) (status);
			}

			continue;
		}

		/* A track, defined by a cd range marker or a cd location marker outside of a cd range */

		status.track_position = last_end_time - timespan->get_start();
		status.track_start_frame = (*i)->start() - timespan->get_start();  // everything before this is the pregap
		status.track_duration = 0;

		if ((*i)->is_mark()) {
			// a mark track location needs to look ahead to the next marker's start to determine length
			nexti = i;
			++nexti;

			if (nexti != temp.end()) {
				status.track_duration = (*nexti)->start() - last_end_time;

				last_start_time = (*i)->start();
				last_end_time = (*nexti)->start();
			} else {
				// this was the last marker, use timespan end
				status.track_duration = timespan->get_end() - last_end_time;

				last_start_time = (*i)->start();
				last_end_time = timespan->get_end();
			}
		} else {
			// range
			status.track_duration = (*i)->end() - last_end_time;

			last_start_time = (*i)->start();
			last_end_time = (*i)->end();
		}

		(this->*track_func) (status);
	}
}

void
ExportHandler::write_cue_header (CDMarkerStatus & status)
{
	Glib::ustring title = status.timespan->name().compare ("Session") ? status.timespan->name() : (Glib::ustring) session.name();

	status.out << "REM Cue file generated by Ardour" << endl;
	status.out << "TITLE \"" << title << "\"" << endl;

	/*  The cue sheet syntax has originally five file types:
		WAVE     : 44.1 kHz, 16 Bit (little endian)
		AIFF     : 44.1 kHz, 16 Bit (big endian)
		BINARY   : 44.1 kHz, 16 Bit (little endian)
		MOTOROLA : 44.1 kHz, 16 Bit (big endian)
		MP3

		We want to use cue sheets not only as CD images but also as general playlyist
		format, thus for WAVE and AIFF we don't care if it's really 44.1 kHz/16 Bit, the
		soundfile's header shows it anyway.  But for the raw formats, i.e. BINARY
		and MOTOROLA we do care, because no header would tell us about a different format.

		For all other formats we just make up our own file type.  MP3 is not supported
		at the moment.
	*/

	status.out << "FILE \"" << Glib::path_get_basename(status.filename) << "\" ";
	if (!status.format->format_name().compare ("WAV")) {
		status.out  << "WAVE";
	} else if (status.format->format_name() == ExportFormatBase::F_RAW &&
	           status.format->sample_format() == ExportFormatBase::SF_16 &&
	           status.format->sample_rate() == ExportFormatBase::SR_44_1) {
		// Format is RAW 16bit 44.1kHz
		if (status.format->endianness() == ExportFormatBase::E_Little) {
			status.out << "BINARY";
		} else {
			status.out << "MOTOROLA";
		}
	} else {
		// AIFF should return "AIFF"
		status.out << status.format->format_name();
	}
	status.out << endl;
}

void
ExportHandler::write_toc_header (CDMarkerStatus & status)
{
	Glib::ustring title = status.timespan->name().compare ("Session") ? status.timespan->name() : (Glib::ustring) session.name();

	status.out << "CD_DA" << endl;
	status.out << "CD_TEXT {" << endl << "  LANGUAGE_MAP {" << endl << "    0 : EN" << endl << "  }" << endl;
	status.out << "  LANGUAGE 0 {" << endl << "    TITLE \"" << title << "\"" << endl << "  }" << endl << "}" << endl;
}

void
ExportHandler::write_track_info_cue (CDMarkerStatus & status)
{
	gchar buf[18];

	snprintf (buf, sizeof(buf), "  TRACK %02d AUDIO", status.track_number);
	status.out << buf << endl;

	status.out << "    FLAGS" ;
	if (status.marker->cd_info.find("scms") != status.marker->cd_info.end())  {
		status.out << " SCMS ";
	} else {
		status.out << " DCP ";
	}

	if (status.marker->cd_info.find("preemph") != status.marker->cd_info.end())  {
		status.out << " PRE";
	}
	status.out << endl;

	if (status.marker->cd_info.find("isrc") != status.marker->cd_info.end())  {
		status.out << "    ISRC " << status.marker->cd_info["isrc"] << endl;

	}
	if (status.marker->name() != "") {
		status.out << "    TITLE \"" << status.marker->name() << "\"" << endl;
	}

	if (status.marker->cd_info.find("performer") != status.marker->cd_info.end()) {
		status.out << "    PERFORMER \"" <<  status.marker->cd_info["performer"] << "\"" << endl;
	}

	if (status.marker->cd_info.find("string_composer") != status.marker->cd_info.end()) {
		status.out << "    SONGWRITER \"" << status.marker->cd_info["string_composer"]  << "\"" << endl;
	}

	if (status.track_position != status.track_start_frame) {
		frames_to_cd_frames_string (buf, status.track_position);
		status.out << "    INDEX 00" << buf << endl;
	}

	frames_to_cd_frames_string (buf, status.track_start_frame);
	status.out << "    INDEX 01" << buf << endl;

	status.index_number = 2;
	status.track_number++;
}

void
ExportHandler::write_track_info_toc (CDMarkerStatus & status)
{
	gchar buf[18];

	status.out << endl << "TRACK AUDIO" << endl;

	if (status.marker->cd_info.find("scms") != status.marker->cd_info.end())  {
		status.out << "NO ";
	}
	status.out << "COPY" << endl;

	if (status.marker->cd_info.find("preemph") != status.marker->cd_info.end())  {
		status.out << "PRE_EMPHASIS" << endl;
	} else {
		status.out << "NO PRE_EMPHASIS" << endl;
	}

	if (status.marker->cd_info.find("isrc") != status.marker->cd_info.end())  {
		status.out << "ISRC \"" << status.marker->cd_info["isrc"] << "\"" << endl;
	}

	status.out << "CD_TEXT {" << endl << "  LANGUAGE 0 {" << endl << "     TITLE \"" << status.marker->name() << "\"" << endl;
	if (status.marker->cd_info.find("performer") != status.marker->cd_info.end()) {
		status.out << "     PERFORMER \"" << status.marker->cd_info["performer"]  << "\"" << endl;
	}
	if (status.marker->cd_info.find("string_composer") != status.marker->cd_info.end()) {
		status.out  << "     COMPOSER \"" << status.marker->cd_info["string_composer"] << "\"" << endl;
	}

	if (status.marker->cd_info.find("isrc") != status.marker->cd_info.end()) {
		status.out  << "     ISRC \"";
		status.out << status.marker->cd_info["isrc"].substr(0,2) << "-";
		status.out << status.marker->cd_info["isrc"].substr(2,3) << "-";
		status.out << status.marker->cd_info["isrc"].substr(5,2) << "-";
		status.out << status.marker->cd_info["isrc"].substr(7,5) << "\"" << endl;
	}

	status.out << "  }" << endl << "}" << endl;

	frames_to_cd_frames_string (buf, status.track_position);
	status.out << "FILE \"" << status.filename << "\" " << buf;

	frames_to_cd_frames_string (buf, status.track_duration);
	status.out << buf << endl;

	frames_to_cd_frames_string (buf, status.track_start_frame - status.track_position);
	status.out << "START" << buf << endl;
}

void
ExportHandler::write_index_info_cue (CDMarkerStatus & status)
{
	gchar buf[18];

	snprintf (buf, sizeof(buf), "    INDEX %02d", cue_indexnum);
	status.out << buf;
	frames_to_cd_frames_string (buf, status.index_position);
	status.out << buf << endl;

	cue_indexnum++;
}

void
ExportHandler::write_index_info_toc (CDMarkerStatus & status)
{
	gchar buf[18];

	frames_to_cd_frames_string (buf, status.index_position - status.track_position);
	status.out << "INDEX" << buf << endl;
}

void
ExportHandler::frames_to_cd_frames_string (char* buf, sframes_t when)
{
	sframes_t remainder;
	nframes_t fr = session.nominal_frame_rate();
	int mins, secs, frames;

	mins = when / (60 * fr);
	remainder = when - (mins * 60 * fr);
	secs = remainder / fr;
	remainder -= secs * fr;
	frames = remainder / (fr / 75);
	sprintf (buf, " %02d:%02d:%02d", mins, secs, frames);
}

} // namespace ARDOUR
