/* GPC (C) 2017-2018 Stephane Charette <stephanecharette@gmail.com>
 * $Id: SummaryComponent.cpp 2513 2018-04-07 07:21:06Z stephane $
 */

#include "GPC.hpp"


SummaryComponent::SummaryComponent(void) :
	Component		("canvas for summary window"						),
	import_button	("import .ijs"	, "Import .IJS file."				),
	clone_button	("clone"		, "Clone selected print job."		),
	refresh_button	("refresh"		, "Reload the set of print jobs."	),
	table			("summary table", this								)
{
	table.setMultipleSelectionEnabled(false);
	table.setColour(TableListBox::outlineColourId, Colours::black);
	table.setOutlineThickness(1);

	const auto hidden_column = TableHeaderComponent::ColumnPropertyFlags::defaultFlags & ~TableHeaderComponent::ColumnPropertyFlags::visible;
	TableHeaderComponent &header = table.getHeader();
	header.addColumn( "UUID"			, SessionRecord::EField::kUUID					, 100, 30, -1, hidden_column);
	header.addColumn( "Created [1]"		, SessionRecord::EField::kCreationTimestamp		, 100);
	header.addColumn( "Created [2]"		, SessionRecord::EField::kCreationText			, 100);
	header.addColumn( "Last Used [1]"	, SessionRecord::EField::kRecentlyUsedTimestamp	, 100);
	header.addColumn( "Last Used [2]"	, SessionRecord::EField::kRecentlyUsedText		, 100);
	header.addColumn( "Printer"			, SessionRecord::EField::kPrinterName			, 100);
	header.addColumn( "Print Job"		, SessionRecord::EField::kIjsFilename			, 100);
	header.addColumn( "Uses"			, SessionRecord::EField::kNumberOfUses			, 100);
	header.addColumn( "Images"			, SessionRecord::EField::kNumberOfImages		, 100);
	header.addColumn( "Username"		, SessionRecord::EField::kUsername				, 100);
	header.addColumn( "Description"		, SessionRecord::EField::kDescription			, 100);

	header.setPopupMenuActive( true );

	if (cfg().containsKey("SummaryTableHeaders"))
	{
		header.restoreFromString( cfg().getValue("SummaryTableHeaders") );
	}
	else
	{
		header.setStretchToFitActive( true );
	}

	selectedRowsChanged(-1);

#if 0
	/// @todo re-enable this once text filters are implemented
	if (cfg().containsKey("SummaryFilter"))
	{
		search_bar.setText( cfg().getValue("SummaryFilter") );
		search_bar.selectAll();
	}
#else
	search_bar.setText("text filter is disabled in this release");
	search_bar.setAlpha(0.4f);
	search_bar.setEnabled(false);
	search_bar.setVisible(false);
#endif

	addAndMakeVisible( summary_label	);
//	addAndMakeVisible( search_bar		);
	addAndMakeVisible( import_button	);
	addAndMakeVisible( clone_button		);
	addAndMakeVisible( refresh_button	);
	addAndMakeVisible( table			);

	import_button	.addListener(this);
	clone_button	.addListener(this);
	refresh_button	.addListener(this);

	rebuild_index();

	// start the timer
	startTimer(500);

	return;
}


SummaryComponent::~SummaryComponent(void)
{
	stopTimer();

	cfg().setValue("SummaryTableHeaders", table.getHeader().toString());

#if 0
	/// @todo re-enable this line once text filters are implemented
	const std::string filter_string = Lox::evenWhitespace(search_bar.getText().toStdString());
#else
	const std::string filter_string = "";
#endif
	cfg().setValue("SummaryFilter", filter_string.c_str());

	return;
}


void SummaryComponent::resized(void)
{
	const float margin_size		= 5.0;
	const float button_height	= 25.0;
	const FlexItem::Margin margin(margin_size);

	FlexBox searchflex;		// this is the flexbox for the search field and label
	searchflex.flexDirection	= FlexBox::Direction::row;
	searchflex.items.add(FlexItem(summary_label	).withFlex(0.2f).withMinWidth(200).withMaxWidth(300)	);
	searchflex.items.add(FlexItem(search_bar	).withFlex(0.8f)										);

	FlexBox buttonflex;			// this is the flexbox for the buttons
	buttonflex.flexDirection	= FlexBox::Direction::row;
	buttonflex.justifyContent	= FlexBox::JustifyContent::spaceBetween;
	buttonflex.items.add(FlexItem(import_button	).withWidth(100));
	buttonflex.items.add(FlexItem(clone_button	).withWidth(100));
	buttonflex.items.add(FlexItem(refresh_button).withWidth(100));

	FlexBox flexbox;	// this is the "main" flexbox for the component
	flexbox.flexDirection = FlexBox::Direction::column;
	flexbox.items.add(
		FlexItem(searchflex).
			withMargin		(margin			).
			withHeight		(button_height	).
			withMinHeight	(button_height	).
			withMaxHeight	(button_height	) );

	flexbox.items.add(
		FlexItem(buttonflex).
			withMargin		(margin			).
			withHeight		(button_height	).
			withMinHeight	(button_height	).
			withMaxHeight	(button_height	) );

	flexbox.items.add(
		FlexItem(table).
			withFlex		(1.0			).
			withMargin		(margin			) );

	Rectangle<float> r = getLocalBounds().toFloat();
	r.reduce(margin_size, margin_size);	// leave a margin around the edge of the window
	flexbox.performLayout( r );

	return;
}


void SummaryComponent::buttonClicked(Button *button)
{
	if (button == &import_button || button == &clone_button)
	{
		File file;

		if (button == &import_button)
		{
			FileChooser chooser("Select the .IJS print job file to import...", File::getSpecialLocation(File::userDesktopDirectory), "*.ijs");
			const bool result = chooser.browseForFileToOpen();
			if (result)
			{
				file = chooser.getResult();
			}
		}
		else if (button == &clone_button)
		{
			const int row = table.getSelectedRow();
			const std::string uuid = uuid_index[row];
			file = gpc().sessions[uuid].get_ijs_file();
		}

		if (file.existsAsFile())
		{
			const std::string filename = file.getFileName().toStdString();

			// message to show the user if something fails during the import
			std::string exception_message = "Error detected while importing .ijs file.";

			try
			{
				PrintJob print_job;
				print_job.load(file);

				const std::string name1 = cfg().get_str("print_controller_1_name");
				const std::string name2 = cfg().get_str("print_controller_2_name");
				const bool pinter_result = AlertWindow::showOkCancelBox(AlertWindow::AlertIconType::QuestionIcon, filename, "This print job is for which printer?", name1, name2);

				const std::string uuid = create_new_session();
				SessionRecord &rec = get_session_record(uuid);
				rec.field[SessionRecord::EField::kIjsFilename	] = filename;
				rec.field[SessionRecord::EField::kNumberOfImages] = std::to_string(print_job.ijbs.size());
				rec.printer_number = (pinter_result ? 1 : 2);
				rec.initialize();

				const std::time_t now = std::time(nullptr);
				std::tm * tm = std::localtime(&now);
				char timestamp[50];
				strftime(timestamp, sizeof(timestamp), "%Y-%m-%d %H:%M:%S", tm);

				if (button == &clone_button)
				{
					rec.field[SessionRecord::EField::kDescription] = "cloned on " + std::string(timestamp);
				}
				else
				{
					rec.field[SessionRecord::EField::kDescription] = "imported on " + std::string(timestamp);
				}

				rec.schedule_sessions_to_be_saved();

				print_job.copy_to_new_directory(rec.get_dir());

				uuid_index.push_back(uuid);
				table.updateContent();
				uuids_of_selected_rows.clear();
				uuids_of_selected_rows.insert(uuid);
				restore_selected_rows();
				summary_label.setText("Showing all " + std::to_string(uuid_index.size()) + " print jobs.", NotificationType::sendNotification);
				exception_message.clear();
			}
			catch (const Lox::Exception &e)
			{
				LOG_MSG(e.to_string());
				exception_message += "\r\n\r\n";
				exception_message += e.what();
			}
			catch (const std::exception &e)
			{
				exception_message += "\r\n\r\n";
				exception_message += e.what();
			}
			catch (...)
			{
				// use the default message defined above where exception_message is declared
			}

			if (exception_message.empty() == false)
			{
				LOG_MSG("exception caught: " << exception_message);
				AlertWindow::showMessageBox(AlertWindow::AlertIconType::WarningIcon, filename, exception_message);
			}
		}
	}

	if (button == &refresh_button)
	{
		load_all_sessions();
		rebuild_index();
	}

	return;
}


void SummaryComponent::rebuild_index(void)
{
	MouseCursor::showWaitCursor();
	remember_selected_rows();
	table.setModel(nullptr);

	// sort the "last use" times
	std::set<std::time_t> s;
	for (auto iter : gpc().sessions)
	{
		const auto & session = iter.second;
		s.insert(session.recentlyUsedEpoch);
	}

	// now that we have all the unique times, rebuild the index
	uuid_index.clear();
	for (auto iter = s.rbegin(); iter != s.rend(); iter++)
	{
		const std::time_t time_to_find = *iter;
		for (auto iter : gpc().sessions)
		{
			const std::string &uuid = iter.first;
			const auto & session = iter.second;
			if (time_to_find == session.recentlyUsedEpoch)
			{
				uuid_index.push_back(uuid);
			}
		}
	}

	table.setModel(this);
	table.updateContent();
	restore_selected_rows();
	table.repaint();
	MouseCursor::hideWaitCursor();

	return;
}


int SummaryComponent::getNumRows(void)
{
	return uuid_index.size();
}


void SummaryComponent::paintCell(Graphics &g, int rowNumber, int columnId, int width, int height, bool rowIsSelected)
{
	if (rowNumber < 0 || rowNumber >= getNumRows() || columnId <= 0 || columnId > SessionRecord::EField::kMax)
	{
		// do nothing -- invalid row or column
		return;
	}

	// draw the text and the right-hand-side dividing line between cells

	const std::string &uuid = uuid_index[rowNumber];
	std::string text = gpc().sessions[uuid].field[columnId];

	g.setColour( Colours::darkblue );
	g.drawFittedText( text, 2, 2, width - 4, height - 4, Justification::centredLeft, 1 );

	// draw the divider on the right side of the column
	g.setColour( Colours::lightgrey );
	g.drawVerticalLine(width - 1, 0.0f, static_cast<float>(height) );

	return;
}


void SummaryComponent::paintRowBackground(Graphics &g, int rowNumber, int width, int height, bool rowIsSelected)
{
	if (rowNumber < 0 || rowNumber >= getNumRows())
	{
		// invalid row
		return;
	}

	Colour colour = Colours::white;
	if (rowIsSelected)
	{
		colour = Colours::lightgreen;
	}

	g.fillAll( colour );

	// draw the cell bottom divider between rows
	g.setColour( Colours::lightgrey );
	g.drawHorizontalLine(height - 1, 0.0f, static_cast<float>(width));

	return;
}


void SummaryComponent::sortOrderChanged(int newSortColumnId, bool isForwards)
{
#if 0 // TODO

	// this is automatically called by JUCE when the user click on a column header

	const std::string column_name = "#" + std::to_string(newSortColumnId) + " (" + table.getHeader().getColumnName(newSortColumnId).toStdString() + ")";
	LOG_MSG("sort order changed: column " << column_name << ", order is " << (isForwards?"forwards":"backwards"));

	remember_selected_rows();

	auto & sessions = gpc().sessions;

#if 0 // log the sort order
	std::string debug;
	for (const auto &row : sessions)
	{
		if (debug.empty() == false)
		{
			debug += ", ";
		}
		debug += "#" + row.field[SessionRecord::EField::kUUID] + "=" + row.data[kDisplayNumber];
	}
	LOG_MSG("old sort order: " << debug);
#endif

	table.setModel(nullptr);
	if (newSortColumnId == SessionRecord::EField::kCreationTimestamp	||
		newSortColumnId == SessionRecord::EField::kCreationText			)
	{
		// perform a numerical sort on a special field not in the field[] array
		LOG_MSG("sorting " << sessions.size() << " log records based on numerical 'creationEpoch' field");
		std::stable_sort(sessions.begin(), sessions.end(),
				[isForwards](const SessionRecord &lhs, const SessionRecord &rhs)
				{
					return (isForwards ?
						(lhs.creationEpoch < rhs.creationEpoch) :
						(rhs.creationEpoch < lhs.creationEpoch) );
				} );
	}
	else if (	newSortColumnId == SessionRecord::EField::kRecentlyUsedTimestamp	||
				newSortColumnId == SessionRecord::EField::kRecentlyUsedText			)
	{
		// perform a numerical sort on a special field not in the field[] array
		LOG_MSG("sorting " << sessions.size() << " log records based on numerical 'recentlyUsedEpoch' field");
		std::stable_sort(sessions.begin(), sessions.end(),
				[isForwards](const SessionRecord &lhs, const SessionRecord &rhs)
				{
					return (isForwards ?
						(lhs.recentlyUsedEpoch < rhs.recentlyUsedEpoch) :
						(rhs.recentlyUsedEpoch < lhs.recentlyUsedEpoch) );
				} );
	}
	else
	{
		// ...otherwise, every other field can be sorted as a normal text string
		LOG_MSG("sorting " << sessions.size() << " log records based on text strings: column " << column_name);
		std::stable_sort(sessions.begin(), sessions.end(),
				[newSortColumnId, isForwards](const SessionRecord &lhs, const SessionRecord &rhs)
				{
					std::string lhs_text = lhs.field[newSortColumnId];
					std::string rhs_text = rhs.field[newSortColumnId];
					Lox::uppercase(lhs_text);
					Lox::uppercase(rhs_text);
					return (isForwards ?
						(lhs_text < rhs_text) :
						(rhs_text < lhs_text) );
				} );
	}

	LOG_MSG("done sorting on column " << column_name << ", now going to apply log filtering");

#if 0 // log the sort order
	debug.clear();
	for (const auto &row : sessions)
	{
		if (debug.empty() == false)
		{
			debug += ", ";
		}
		debug += "#" + row.data[kId] + "=" + row.data[kDisplayNumber];
	}
	LOG_MSG("new sort order: " << debug);
#endif

#endif

	apply_log_filtering(true);

	return;
}


void SummaryComponent::selectedRowsChanged(int lastRowSelected)
{
	const bool selectionIsValid = (lastRowSelected >= 0 && lastRowSelected < getNumRows());

	clone_button.setEnabled(selectionIsValid);

	return;
}


void SummaryComponent::cellDoubleClicked(int rowNumber, int columnId, const MouseEvent &mouseEvent)
{
	openSession(rowNumber);

	return;
}


void SummaryComponent::returnKeyPressed(int rowNumber)
{
	openSession(rowNumber);

	return;
}


void SummaryComponent::openSession(const int rowNumber)
{
	// rows are zero-based
	if (rowNumber < 0 || rowNumber >= getNumRows())
	{
		// ignore invalid rows
		return;
	}

	const std::string uuid = uuid_index[rowNumber];

	const ScopedLock lock(critial_section_for_all_session_windows);

	bool found = false;
	for (auto iter : all_session_windows)
	{
		if (iter.first == uuid)
		{
			LOG_MSG("found an existing editor already open for session #" << uuid);
			iter.second->setVisible(true);
			iter.second->toFront(true);
			found = true;
			break;
		}
	}

	if (! found)
	{
		LOG_MSG("opening session " << uuid);

		if (all_session_windows.size() >= 5)
		{
			AlertWindow::showMessageBoxAsync(
				AlertWindow::WarningIcon,
				"Too many open windows!",
				"There are too many open GPC session windows.  Please close one of the other windows before opening another session." );
		}
		else
		{
			all_session_windows[uuid] = new SessionWnd(uuid);

			table.repaintRow(rowNumber);
		}
	}

	return;
}


void SummaryComponent::timerCallback(void)
{
	stopTimer();
	apply_log_filtering();
	delete_hidden_session_windows();
	startTimer(750); // in milliseconds

	return;
}


void SummaryComponent::delete_hidden_session_windows(void)
{
	if (all_session_windows.empty())
	{
		return;
	}

	const ScopedLock lock(critial_section_for_all_session_windows);

	auto iter = all_session_windows.begin();
	while (iter != all_session_windows.end())
	{
		auto uuid = iter->first;
		auto ptr = iter->second;
		if (ptr->isShowing() == false)
		{
			// this editor window has been hidden and must be deleted
			LOG_MSG("deleting hidden session window for uuid " << uuid);
			delete ptr;
			iter = all_session_windows.erase(iter);
		}
		else
		{
			iter ++;
		}
	}

	return;
}


void SummaryComponent::apply_log_filtering(const bool table_was_sorted)
{
	// this is called by a timer that triggers every 750ms, or after the user changes the sort criteria; see bottom of SummaryComponent::sortOrderChanged()

#if 0
	/// @todo re-enable this line once text filters are implemented
	const std::string text = Lox::uppercase(Lox::evenWhitespace(search_bar.getText().toStdString()));
#else
	const std::string text = "";
#endif
	const VStr new_text_filter = Lox::splitOnWords(text);

	if (new_text_filter == filters && gpc().sessions.empty() == false && table_was_sorted == false && summary_label.getText().isNotEmpty())
	{
		// nothing has changed
//		LOG_MSG("filter is exactly the same, no need to keep filtering");
		return;
	}

	// if we get here, then it appears we need to apply a new filter,
	// or the table was re-sorted meaning we have to re-apply the filter

	remember_selected_rows();

	// easiest case first -- no filter to apply and show every single row
	if (new_text_filter.empty())
	{
		LOG_MSG("text filter is blank: showing all rows");
		search_bar.removeColour(TextEditor::backgroundColourId);
		table.setModel(nullptr);
		filters.clear();

		summary_label.setText("Showing all " + std::to_string(uuid_index.size()) + " print jobs.", NotificationType::sendNotification);

		table.setModel(this);
		table.updateContent();

		restore_selected_rows();

		return;
	}

	LOG_MSG("applying new text filter: " << text);

	search_bar.removeColour(TextEditor::backgroundColourId);

	/// @todo apply filter here

	// remember the filter that we used
	filters = new_text_filter;

	const size_t showing	= uuid_index.size();
	const size_t total		= uuid_index.size();
	std::string msg = "Showing " + std::to_string(showing) + " of " + std::to_string(total) + " print jobs";
	if (total)
	{
		msg += " [" + Lox::Numbers::format( double(showing) / double(total) * 100.0, 1 ) + "%]";
	}
	msg += ".";
	summary_label.setText(msg, NotificationType::sendNotification);
	LOG_MSG( "text filter applied: " << msg);

	if (showing > 0 && table_was_sorted == false)
	{
		// with non-zero matches, we want to ensure we sort by the number of matches
		auto column_to_use_when_sorting = SessionRecord::EField::kRecentlyUsedTimestamp;
		table.getHeader().setSortColumnId(column_to_use_when_sorting, false);
		sortOrderChanged(column_to_use_when_sorting, false);
	}

	table.setModel(this);
	table.updateContent();

	restore_selected_rows();

	return;
}


void SummaryComponent::remember_selected_rows(void)
{
	if (table.getModel() != nullptr)
	{
		// remember which rows are selected so we select them again after the database has been reloaded
		uuids_of_selected_rows.clear();
		for (int idx = 0; idx < table.getNumSelectedRows(); idx++)
		{
			const int row = table.getSelectedRow(idx);
			const std::string uuid = uuid_index[row];
			uuids_of_selected_rows.insert(uuid);
			LOG_MSG("-> remembering that this row is selected: " << uuid);
		}
		LOG_MSG("number of selected rows to remember: " << uuids_of_selected_rows.size());
	}

	return;
}


void SummaryComponent::restore_selected_rows(void)
{
	// re-apply previously-remembered row selection
	if (uuids_of_selected_rows.size() > 0 && table.getModel() != nullptr)
	{
		LOG_MSG("re-applying row selection; number of selected rows: " << uuids_of_selected_rows.size());
		for (int row = 0; row < getNumRows(); row++)
		{
			const std::string &uuid = uuid_index[row];
			if (uuids_of_selected_rows.count(uuid) > 0)
			{
				table.selectRow(row, true, false);
				LOG_MSG("-> re-selecting this row: " << uuid);
			}
		}
	}

	return;
}
