/* GMM (C) 2018 Stephane Charette <stephanecharette@gmail.com>
 * $Id: TabCanvas.cpp 2686 2018-12-05 16:40:06Z stephane $
 */

#include "GMM.hpp"


TabCanvas::TabCanvas(const std::string & name) :
	Thread("thread for device prefix " + name),
	prefix(name),
	opc_sequence_number(0),
	opc_output_filename(cfg().get_str("opc_output_filename")),
	opc_register_kiln(cfg().get_str(prefix + "_opc_kiln_register") + "="),
	opc_register_run(cfg().get_str(prefix + "_opc_run_register") + "="),
	opc_register_pkg(cfg().get_str(prefix + "_opc_pkg_register") + "="),
	tab_is_active(false),
	export_image(cfg().get_bool(prefix + "_export_image")),
	time_since_kiln_changed(std::time(nullptr)),
	time_since_run_changed(std::time(nullptr)),
	time_since_pkg_changed(std::time(nullptr)),
	time_since_last_board(std::time(nullptr)),
	timer_frequency_in_milliseconds(cfg().get_int(prefix + "_chart_redraw_ms")),
	comm_handle(INVALID_HANDLE_VALUE),
	current_kiln(0),
	current_run(0),
	current_pkg(0),
	new_kiln(cfg().get_int(prefix + "_last_kiln")),
	new_run(cfg().get_int(prefix + "_last_run")),
	new_pkg(cfg().get_int(prefix + "_last_pkg")),
	run_sum(0.0),
	pkg_sum(0.0),
	run_average(0.0),
	pkg_average(0.0),
	run_std_dev(0.0),
	pkg_std_dev(0.0),
	opc_via_csv_filename(cfg().get_str(prefix + "_opc_via_csv_filename"))
{
	const Colour pink_background(0xe6, 0xcc, 0xf2);	// total
	const Colour blue_background(0xc0, 0xc0, 0xff);	// local

	// ****************
	// top-right corner
	// ****************

	addAndMakeVisible(kiln_num_label				);
	addAndMakeVisible(kiln_num_editor				);
	addAndMakeVisible(run_label						);
	addAndMakeVisible(run_editor					);
	addAndMakeVisible(pkg_label						);
	addAndMakeVisible(pkg_editor					);
	addAndMakeVisible(run_number_of_boards_label	);
	addAndMakeVisible(run_number_of_boards_editor	);
	addAndMakeVisible(run_average_moisture_label	);
	addAndMakeVisible(run_average_moisture_editor	);
	addAndMakeVisible(run_std_dev_moisture_label	);
	addAndMakeVisible(run_std_dev_moisture_editor	);
	addAndMakeVisible(pkg_number_of_boards_label	);
	addAndMakeVisible(pkg_number_of_boards_editor	);
	addAndMakeVisible(pkg_average_moisture_label	);
	addAndMakeVisible(pkg_average_moisture_editor	);
	addAndMakeVisible(pkg_std_dev_moisture_label	);
	addAndMakeVisible(pkg_std_dev_moisture_editor	);

	kiln_num_label	.setText("Kiln:", NotificationType::sendNotification);
	run_label		.setText("Run:"	, NotificationType::sendNotification);
	pkg_label		.setText("Pkg:"	, NotificationType::sendNotification);
	kiln_num_label	.setJustificationType(Justification::centredRight);
	run_label		.setJustificationType(Justification::centredRight);
	pkg_label		.setJustificationType(Justification::centredRight);

	kiln_num_editor	.setReadOnly(true);		kiln_num_editor	.setJustification(Justification::centredRight);
	run_editor		.setReadOnly(true);		run_editor		.setJustification(Justification::centredRight);
	pkg_editor		.setReadOnly(true);		pkg_editor		.setJustification(Justification::centredRight);

	run_number_of_boards_label	.setText("Boards:"	, NotificationType::sendNotification);
	run_average_moisture_label	.setText("Avg:"		, NotificationType::sendNotification);
	run_std_dev_moisture_label	.setText("StDv:"	, NotificationType::sendNotification);
	run_number_of_boards_label	.setJustificationType(Justification::centredRight);
	run_average_moisture_label	.setJustificationType(Justification::centredRight);
	run_std_dev_moisture_label	.setJustificationType(Justification::centredRight);

	run_number_of_boards_editor	.setReadOnly(true);
	run_average_moisture_editor	.setReadOnly(true);
	run_std_dev_moisture_editor	.setReadOnly(true);
	run_number_of_boards_editor	.setJustification(Justification::centredRight);
	run_average_moisture_editor	.setJustification(Justification::centredRight);
	run_std_dev_moisture_editor	.setJustification(Justification::centredRight);
	run_number_of_boards_editor	.setColour(TextEditor::backgroundColourId, pink_background);
	run_average_moisture_editor	.setColour(TextEditor::backgroundColourId, pink_background);
	run_std_dev_moisture_editor	.setColour(TextEditor::backgroundColourId, pink_background);

	pkg_number_of_boards_label	.setText("Boards:"	, NotificationType::sendNotification);
	pkg_average_moisture_label	.setText("Avg:"		, NotificationType::sendNotification);
	pkg_std_dev_moisture_label	.setText("StDv:"	, NotificationType::sendNotification);
	pkg_number_of_boards_label	.setJustificationType(Justification::centredRight);
	pkg_average_moisture_label	.setJustificationType(Justification::centredRight);
	pkg_std_dev_moisture_label	.setJustificationType(Justification::centredRight);

	pkg_number_of_boards_editor	.setReadOnly(true);
	pkg_average_moisture_editor	.setReadOnly(true);
	pkg_std_dev_moisture_editor	.setReadOnly(true);
	pkg_number_of_boards_editor	.setJustification(Justification::centredRight);
	pkg_average_moisture_editor	.setJustification(Justification::centredRight);
	pkg_std_dev_moisture_editor	.setJustification(Justification::centredRight);
	pkg_number_of_boards_editor	.setColour(TextEditor::backgroundColourId, blue_background);
	pkg_average_moisture_editor	.setColour(TextEditor::backgroundColourId, blue_background);
	pkg_std_dev_moisture_editor	.setColour(TextEditor::backgroundColourId, blue_background);

	// ****************************
	// left of the moisture buckets
	// ****************************

	addAndMakeVisible(run_header);
	addAndMakeVisible(pkg_header);

	run_header.setText("Run:", NotificationType::sendNotification);
	pkg_header.setText("Pkg:", NotificationType::sendNotification);

	// **************************************
	// the buckets with the percentage labels
	// **************************************

	for (size_t idx=0; idx < number_of_buckets; idx++)
	{
		run_moisture_bucket[idx] = 0;
		pkg_moisture_bucket[idx] = 0;

		addAndMakeVisible(run_distribution	[idx]);
		addAndMakeVisible(percentages		[idx]);
		addAndMakeVisible(pkg_distribution	[idx]);

		run_distribution[idx].setColour(TextEditor::backgroundColourId, pink_background);
		pkg_distribution[idx].setColour(TextEditor::backgroundColourId, blue_background);

		run_distribution[idx].setReadOnly(true);
		pkg_distribution[idx].setReadOnly(true);

		run_distribution[idx].setJustification(Justification::centredRight);
		pkg_distribution[idx].setJustification(Justification::centredRight);

		percentages[idx].setText(String(percentage_of_first_bucket + idx) + "%", NotificationType::sendNotification);
		percentages[idx].setJustificationType(Justification::centredRight);
	}

	if (opc_via_csv_filename.empty() == false)
	{
		opc_via_csv_regex = std::regex(cfg().get_str(prefix + "_opc_via_csv_regex"));
	}

	// re-populate the kiln, run, and package numbers
	update_kiln_run_pkg();

	read_all_mc_files();

	// add the chart, and start the timer to redraw it every few seconds
	addAndMakeVisible(chart);
	timerCallback();

	// start the thread that will handle the serial communication
	startThread(10);

	return;
}


TabCanvas::~TabCanvas(void)
{
	signalThreadShouldExit();
	notify();

	stopTimer();

	waitForThreadToExit(10000); // milliseconds

	LOG_MSG("finished destructor for " << (tab_is_active ? "active" : "inactive") << " TabCanvas object with prefix=" << prefix);

	return;
}


void TabCanvas::timerCallback(void)
{
	/* This is called periodically using a timer.  Usually at 1-second intervals, though this can be tweaked via
	 * configuration.
	 *
	 * Note that since we stop the timer, do some redrawing and calculations, and then restart the timer, it means
	 * the timer will significantly drift over time.  This should not be a concern since all we're doing is updating
	 * portions of the screen, including the chart.
	 */

	stopTimer();

	if (threadShouldExit() == false)
	{
		periodic_update();

		// There are 2 different ways we can update the kiln/run/package numbers.  The first is with the OPC Console,
		// and the second is with the automatic .csv file that Curt wrote in VB.  The .csv solution was temporary and
		// the OPC Console method is the prefered method.  The code still attempts both, and returns if the .csv or
		// OPC Console output file does not exist on disk.

		parse_opc_console_output();
		read_opc_values_from_csv();

		if (timer_frequency_in_milliseconds > 0)
		{
			startTimer(timer_frequency_in_milliseconds); // in milliseconds
		}
	}

	return;
}


void TabCanvas::periodic_update(void)
{
	if (threadShouldExit() == false)
	{
		calculate_averages_and_std_deviations();
	}

	// re-populate the fields with the amounts stored in the buckets
	for (size_t idx=0; idx < number_of_buckets; idx++)
	{
		run_distribution[idx].setText(String(run_moisture_bucket[idx]));
		pkg_distribution[idx].setText(String(pkg_moisture_bucket[idx]));
	}

	if (threadShouldExit() == false)
	{
		run_number_of_boards_editor.setText(String(run_doubles.size()));
		pkg_number_of_boards_editor.setText(String(pkg_doubles.size()));
		run_average_moisture_editor.setText(Lox::Numbers::format(run_average, 2));
		pkg_average_moisture_editor.setText(Lox::Numbers::format(pkg_average, 2));
		run_std_dev_moisture_editor.setText(Lox::Numbers::format(run_std_dev, 2));
		pkg_std_dev_moisture_editor.setText(Lox::Numbers::format(pkg_std_dev, 2));
	}

//	LOG_MSG("prefix=" << prefix << ": periodic update for RUN: number_of_boards: " << run_doubles.size() << ", average: " << run_average << ", stddev: " << run_std_dev);
//	LOG_MSG("prefix=" << prefix << ": periodic update for PKG: number_of_boards: " << pkg_doubles.size() << ", average: " << pkg_average << ", stddev: " << pkg_std_dev);

	// don't bother redrawing the chart unless we have to
	if (threadShouldExit() == false && (tab_is_active || export_image))
	{
		auto rect = chart.getBounds();
		chart.setImage(draw_chart(rect.getWidth(), rect.getHeight()));

		std::string tooltip;

		tooltip = "the kiln was last updated " + Lox::approximateTime(time_since_kiln_changed);
		kiln_num_label	.setTooltip(tooltip);
		kiln_num_editor	.setTooltip(tooltip);

		tooltip = "the run was last updated " + Lox::approximateTime(time_since_run_changed);
		run_label.setTooltip(tooltip);
		run_editor.setTooltip(tooltip);

		tooltip = "the package was last updated " + Lox::approximateTime(time_since_pkg_changed);
		pkg_label.setTooltip(tooltip);
		pkg_editor.setTooltip(tooltip);

		tooltip = "the number of boards was last updated " + Lox::approximateTime(time_since_last_board);
		run_number_of_boards_label.setTooltip(tooltip);
		run_number_of_boards_editor.setTooltip(tooltip);
	}

	return;
}


inline double calc_std_dev(const LDouble & ld, const double average)
{
	double std_dev = 0.0;

	// STEP #1:  Find the average of the data set.  This is already done and provided as the variable "average".

	if (ld.size() > 1)
	{
		double sum_of_squares = 0.0;
		for (const double & d : ld)
		{
			// STEP #2:  Take each value in the data set and subtract the mean from it.
			const double val1 = d - average;

			// STEP #3:  Square each of the differences.
			const double val2 = std::pow(val1, 2.0);

			// STEP #4:  Add up all of the results from Step #3 to get the sum of squares.
			sum_of_squares += val2;
		}

		// STEP #5:  Divide the sum of squares (found in step #4) by the number of values in the data set minus one.
		const double val3 = sum_of_squares / static_cast<double>(ld.size() - 1);

		// STEP #6:  Take the square root to get the standard deviation.
		std_dev = std::sqrt(val3);
	}

	return std_dev;
}


void TabCanvas::calculate_averages_and_std_deviations()
{
	// the average is now kept in the class members run_average and pkg_average,
	// so the only thing left to do is calculate the standard deviation

	if (threadShouldExit() == false)
	{
		const ScopedLock scope_lock(numbers_and_calculations_mutex);

		run_std_dev = calc_std_dev(run_doubles, run_average);
		pkg_std_dev = calc_std_dev(pkg_doubles, pkg_average);
	}

	return;
}


Image TabCanvas::draw_chart(const size_t width, const size_t height)
{
	if (threadShouldExit() || (tab_is_active == false && export_image == false))
	{
		// don't bother creating images for inactive tabs
		return Image();
	}

	Image image(Image::ARGB, width, height, true);
	Graphics g(image);
	g.setColour(Colours::white);
	g.fillRect(0, 0, width, height);

	// draw a border around the entire image
	g.setColour(Colours::black);
	g.drawRect(0, 0, width, height);

	if (width < 75 || height < 75)
	{
		// too small of an image for us to do anything useful
		return image;
	}

	// put the text in the upper-left and upper-right corners
	const int edge_offset = 3;
	g.setColour(Colours::lightgrey);
	g.drawText(cfg().get_str(prefix + "_name"), edge_offset, edge_offset, width - edge_offset * 2, edge_offset * 2, Justification::topLeft);
	g.drawText(Time::getCurrentTime().formatted("%Y-%m-%d %H:%M:%S"), edge_offset, edge_offset, width - edge_offset * 2, edge_offset * 2, Justification::topRight);

	// some numbers are used in many places, so calculate them now so we don't have to do it multiple times
	const float width_float				= static_cast<float>(width);
	const float height_float			= static_cast<float>(height);
	const float h_step					= 1.0f / static_cast<float>(number_of_buckets);
	const float v_step					= 1.0f / 5.0f; // 20%, 40%, 60%, 80%, and 100%
	const float width_of_columns		= h_step * width_float;
	const float one_seventh_of_column	= 1.0f * width_of_columns / 7.0f;
	const float three_seventh_of_column	= 3.0f * width_of_columns / 7.0f;

	/* For drawing purposes, each column is divided into 7 equal parts.
	 * - 1st part is empty space.
	 * - 2nd through 4th is the pink.
	 * - 4th through 6th is the blue.
	 * - 7th is empty space.
	 * Note the 4th section is where the pink and blue parts overlap.
	 */

	// horizontal lines
	g.setColour(Colours::dimgrey);
	for (float y = v_step; y < 1.0; y += v_step)
	{
		g.drawRect(0.0f, y * height_float, width_float, 1.0f);
	}

	// vertical lines
	g.setColour(Colours::dimgrey);
	for (float x = h_step; x < 1.0; x += h_step)
	{
		g.drawRect(x * width_float, 0.0f, 1.0f, height_float);
	}

	const ScopedLock scope_lock(numbers_and_calculations_mutex);

	// determine the maximum value in all the buckets (initially set to "1" to prevent divide-by-zero later)
	size_t max_run_bucket_value = 1;
	size_t max_pkg_bucket_value = 1;
	for (size_t idx = 0; idx < number_of_buckets; idx++)
	{
		if (run_moisture_bucket[idx] > max_run_bucket_value)	max_run_bucket_value = run_moisture_bucket[idx];
		if (pkg_moisture_bucket[idx] > max_pkg_bucket_value)	max_pkg_bucket_value = pkg_moisture_bucket[idx];
	}

	// draw the columns of data
	for (size_t idx = 0; threadShouldExit() == false && idx < number_of_buckets; idx++)
	{
		const float col_x = idx * h_step * width_float;

		// the graph is based on % of the maximum, not on the raw numbers, so we need to convert this bucket to a percentage
		const float percent_total = (float)run_moisture_bucket[idx] / (float)max_run_bucket_value;
		const float percent_local = (float)pkg_moisture_bucket[idx] / (float)max_pkg_bucket_value;

		// 0% means the bottom of the chart, while 100% means the top of the chart
		// +2 offset means the bar will end 2 pixels from the top of the chart, leaving room for the black border and 1 pixel of white
		const float height_total = (1.0f - percent_total) * height_float + 2.0f;
		const float height_local = (1.0f - percent_local) * height_float + 2.0f;

		// -15 offset means we're going to start 15 pixels from the bottom of the chart, leaving some room for the % text to be printed
		const float bottom_total = height_float - height_total - 15.0f;
		const float bottom_local = height_float - height_local - 15.0f;

		// draw the rectangles in pure white first to wipe away the background grid
		g.setColour(Colours::white.withAlpha(0.85f));
		if (bottom_total > 0.0f)								g.fillRect(col_x + one_seventh_of_column	, height_total, three_seventh_of_column, bottom_total);
		if (bottom_local > 0.0f)								g.fillRect(col_x + three_seventh_of_column	, height_local, three_seventh_of_column, bottom_local);

		// now draw the real semi-transparent rectangles
		if (bottom_total > 0.0f)
		{
			g.setColour(Colours::darkorchid.withAlpha(0.25f));	g.fillRect(col_x + one_seventh_of_column	, height_total, three_seventh_of_column, bottom_total);
			g.setColour(Colours::darkorchid);					g.drawRect(col_x + one_seventh_of_column	, height_total, three_seventh_of_column, bottom_total);
		}

		if (bottom_local > 0.0f)
		{
			g.setColour(Colours::blue.withAlpha(0.25f));		g.fillRect(col_x + three_seventh_of_column	, height_local, three_seventh_of_column, bottom_local);
			g.setColour(Colours::blue);							g.drawRect(col_x + three_seventh_of_column	, height_local, three_seventh_of_column, bottom_local);
		}
	}

	// add % text labels at the bottom of the image
	g.setColour(Colours::black);
	const float label_h = 15;
	const float label_y = height - label_h;
	int percentage	= percentage_of_first_bucket;
	for (float x = 0.0; x < 1.0; x += h_step)
	{
		const float label_x = width_float * x		+ edge_offset;
		const float label_w = width_float * h_step	- edge_offset * 2;
		g.drawText(String(percentage)+"%", label_x, label_y, label_w, label_h, Justification::centredBottom);
		percentage ++;
	}

	// combine the two vectors of errors/warnings, and add them to the top-left of the image
	auto tmp = gmm().wnd->warnings_and_errors;
	tmp.insert(tmp.end(), warnings_and_errors.begin(), warnings_and_errors.end());
	if (tmp.empty() == false && threadShouldExit() == false)
	{
		g.setColour(Colours::red);
		float y = 0.0f;
		for (const auto & str : tmp)
		{
			y += 20.0f;
			g.drawText(str.c_str(), 10.0f, y, width_float - 20.0f, 20.0f, Justification::bottomLeft);
		}
	}

	if (export_image && threadShouldExit() == false)
	{
		// save to a .png file for debug purposes
		File file = File::getSpecialLocation(File::SpecialLocationType::tempDirectory).getChildFile(prefix + "_chart.png");
		file.deleteFile();
		FileOutputStream stream(file);
		PNGImageFormat png;
		png.writeImageToStream(image, stream);
	}

	return image;
}


void TabCanvas::resized(void)
{
	const float height			= 30.0;
	const float margin_size		= 5.0;
	const FlexItem::Margin margin(margin_size);

	FlexBox run_row_flexbox;
	run_row_flexbox.flexDirection		= FlexBox::Direction::row;
	run_row_flexbox.justifyContent	= FlexBox::JustifyContent::spaceAround;
	run_row_flexbox.items.add(FlexItem(kiln_num_label		).withMargin(margin).withWidth(40));
	run_row_flexbox.items.add(FlexItem(kiln_num_editor	).withMargin(margin).withFlex(1.0));
	run_row_flexbox.items.add(FlexItem(run_label			).withMargin(margin).withFlex(1.0));
	run_row_flexbox.items.add(FlexItem(run_editor			).withMargin(margin).withFlex(1.0));
	run_row_flexbox.items.add(FlexItem(pkg_label			).withMargin(margin).withFlex(1.0));
	run_row_flexbox.items.add(FlexItem(pkg_editor			).withMargin(margin).withFlex(1.0));
	// insert a few blank spacers so the row lines up with the moisture distribution row below
	for (size_t idx = 0; idx < number_of_buckets - 11; idx++)
	{
		run_row_flexbox.items.add(FlexItem().withMargin(margin).withFlex(1.0));
	}
	run_row_flexbox.items.add(FlexItem(run_number_of_boards_label	).withMargin(margin).withFlex(1.0));
	run_row_flexbox.items.add(FlexItem(run_number_of_boards_editor	).withMargin(margin).withFlex(1.0));
	run_row_flexbox.items.add(FlexItem(run_average_moisture_label	).withMargin(margin).withFlex(1.0));
	run_row_flexbox.items.add(FlexItem(run_average_moisture_editor	).withMargin(margin).withFlex(1.0));
	run_row_flexbox.items.add(FlexItem(run_std_dev_moisture_label	).withMargin(margin).withFlex(1.0));
	run_row_flexbox.items.add(FlexItem(run_std_dev_moisture_editor	).withMargin(margin).withFlex(1.0));

	FlexBox run_distribution_flexbox;
	run_distribution_flexbox.flexDirection	= FlexBox::Direction::row;
	run_distribution_flexbox.justifyContent	= FlexBox::JustifyContent::spaceAround;
	run_distribution_flexbox.items.add(FlexItem(run_header).withMargin(margin).withWidth(40));
	for (size_t idx = 0; idx < number_of_buckets; idx++)
	{
		run_distribution_flexbox.items.add(FlexItem(run_distribution[idx]).withMargin(margin).withFlex(1.0));
	}

	FlexBox percentage_flexbox;
	percentage_flexbox.flexDirection	= FlexBox::Direction::row;
	percentage_flexbox.justifyContent	= FlexBox::JustifyContent::spaceAround;
	percentage_flexbox.items.add(FlexItem(/* spacer */).withMargin(margin).withWidth(40));	// spacer
	for (size_t idx=0; idx < number_of_buckets; idx++)
	{
		percentage_flexbox.items.add(FlexItem(percentages[idx]).withMargin(margin).withFlex(1.0));
	}

	FlexBox pkg_distribution_flexbox;
	pkg_distribution_flexbox.flexDirection	= FlexBox::Direction::row;
	pkg_distribution_flexbox.justifyContent	= FlexBox::JustifyContent::spaceAround;
	pkg_distribution_flexbox.items.add(FlexItem(pkg_header).withMargin(margin).withWidth(40));
	for (size_t idx=0; idx < number_of_buckets; idx++)
	{
		pkg_distribution_flexbox.items.add(FlexItem(pkg_distribution[idx]).withMargin(margin).withFlex(1.0));
	}

	FlexBox pkg_row_flexbox;
	pkg_row_flexbox.flexDirection		= FlexBox::Direction::row;
	pkg_row_flexbox.justifyContent	= FlexBox::JustifyContent::spaceAround;
	pkg_row_flexbox.items.add(FlexItem(/* spacer */).withMargin(margin).withWidth(40));
	// insert a few blank spacers so the row lines up with the moisture distribution row below
	for (size_t idx = 0; idx < number_of_buckets - 6; idx++)
	{
		pkg_row_flexbox.items.add(FlexItem().withMargin(margin).withFlex(1.0));
	}
	pkg_row_flexbox.items.add(FlexItem(pkg_number_of_boards_label	).withMargin(margin).withFlex(1.0));
	pkg_row_flexbox.items.add(FlexItem(pkg_number_of_boards_editor	).withMargin(margin).withFlex(1.0));
	pkg_row_flexbox.items.add(FlexItem(pkg_average_moisture_label	).withMargin(margin).withFlex(1.0));
	pkg_row_flexbox.items.add(FlexItem(pkg_average_moisture_editor	).withMargin(margin).withFlex(1.0));
	pkg_row_flexbox.items.add(FlexItem(pkg_std_dev_moisture_label	).withMargin(margin).withFlex(1.0));
	pkg_row_flexbox.items.add(FlexItem(pkg_std_dev_moisture_editor	).withMargin(margin).withFlex(1.0));

	FlexBox chart_row_flexbox;
	chart_row_flexbox.flexDirection		= FlexBox::Direction::row;
	chart_row_flexbox.justifyContent	= FlexBox::JustifyContent::spaceAround;
	chart_row_flexbox.items.add(FlexItem(/* spacer */).withMargin(margin).withWidth(40));
	chart_row_flexbox.items.add(FlexItem(chart).withFlex(2.0));

	// this is the "main" flexbox for the component
	FlexBox flexbox;
	flexbox.flexDirection									= FlexBox::Direction::column;
	flexbox.items.add(FlexItem(run_row_flexbox				).withHeight(height));
	flexbox.items.add(FlexItem(run_distribution_flexbox		).withHeight(height));
	flexbox.items.add(FlexItem(percentage_flexbox			).withHeight(height));
	flexbox.items.add(FlexItem(pkg_distribution_flexbox		).withHeight(height));
	flexbox.items.add(FlexItem(pkg_row_flexbox				).withHeight(height));
	flexbox.items.add(FlexItem(chart_row_flexbox).withFlex(2.0).withMargin(margin));

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

	return;
}


void TabCanvas::run(void)
{
	// this new thread is started in the TabCanvas constructor

	try
	{
		device_thread();
	}
	CATCH_AND_LOG;

	close_serial_device();

	// thread is exiting once we reach here
	return;
}


void TabCanvas::device_thread(void)
{
	// This is started on a secondary thread.

	const std::string device_name	=	cfg().get_str	(prefix + "_device"				);
	const std::string tab_name		=	cfg().get_str	(prefix + "_name"				);
	const int interval_ms			=	cfg().get_int	(prefix + "_interval_ms"		);
	const int read_len				=	cfg().get_int	(prefix + "_read_len"			);
	const bool run_simulation		=	cfg().get_bool	(prefix + "_run_simulation"		);
	const std::regex rx				(	cfg().get_str	(prefix + "_regex_pattern"		));

	LOG_MSG(device_name << ": device thread for " << device_name << " has started (name=\"" << tab_name << "\", prefix=\"" << prefix << "\")");

	for (const auto k : cfg().getAllProperties().getAllKeys())
	{
		if (k.startsWith(prefix))
		{
			const std::string key = k.toStdString();
			LOG_MSG(device_name << ": " << key << "=\"" << cfg().get_str(key) << "\"");
		}
	}

	// The buffer into which bytes are read from the comm port.  The length of this buffer is defined by the *_read_len value in configuration.
	VBytes read_buffer(read_len, '\0');
	LOG_MSG(device_name << ": created a read buffer capable of reading " << read_len << " bytes every " << interval_ms << " milliseconds");

	// give this thread a priority boost
	setPriority(10);

	open_serial_device(device_name);

	auto interval	= std::chrono::milliseconds(interval_ms);
	auto time_now	= std::chrono::high_resolution_clock::now();
	auto next_read	= time_now + interval;

	while (threadShouldExit() == false)
	{
		std::string str;
		if (run_simulation)
		{
			str = get_simulated_string(read_buffer);
		}
		else
		{
			str = read_from_device(read_buffer);
		}

		if (str.empty() == false && threadShouldExit() == false)
		{
			std::smatch matches;
			const bool found = std::regex_search(str, matches, rx);

			if (found)
			{
				// There should be 2 values:  the peak, and the average.  We want to use the average when possible.
				// If the average is <= 1.0, then use the peak instead since sometimes the sensors seem to send the
				// value 0.0 for the average.  See email thread between Dominic and Stephane from 2018-10-18.
				const double peak_value		= (matches.size() > 1 ? std::stod(matches[1]) : 0.0);
				const double average_value	= (matches.size() > 2 ? std::stod(matches[2]) : 0.0);
				double value = average_value;
				if (value < 1.0)
				{
					value = peak_value;
					LOG_MSG(device_name << ": invalid average value from sensor: str=" << String(str).trim().toStdString());
				}
				else
				{
//					LOG_MSG(device_name << ": str=" << String(str).trim().toStdString() << ", reading=" << value);
				}

				if (value > 0.0)
				{
					// determine which bucket this should fit into
					int idx = static_cast<int>(value) - percentage_of_first_bucket;
					if (idx < 0)					idx = 0;
					if (idx >= number_of_buckets)	idx = number_of_buckets - 1;

					if (true)
					{
						const ScopedLock scope_lock(numbers_and_calculations_mutex);

						run_doubles.push_back(value);
						pkg_doubles.push_back(value);

						run_sum += value;
						pkg_sum += value;

						run_average = run_sum / static_cast<double>(run_doubles.size());
						pkg_average = pkg_sum / static_cast<double>(pkg_doubles.size());

						run_moisture_bucket[idx] ++;
						pkg_moisture_bucket[idx] ++;

						time_since_last_board = std::time(nullptr);
					}

//					LOG_MSG(device_name << ": value=" << value << ", incrementing bucket #" << idx << " to " << pkg_moisture_bucket[idx]);

					// every once in a while, write what we have back out to disk so we can restore things when we come back up
					if (0 == pkg_doubles.size() % 100)
					{
						write_mc_file();
					}
				}
			}
			else
			{
				std::string msg;
				for (const char c : str)
				{
					const int i = static_cast<int>(c);
					if (i > 32 && i < 127)	msg += c; // normal ASCII character
					else					msg += "<" + std::to_string(i) + ">"; // non-ASCII printable character
				}
				LOG_MSG(device_name << ": failed to parse string: len=" << str.size() << ": \"" << msg << "\"");
			}
		}

		// now that we've finished reading from the device, sleep until the next scheduled read

		time_now = std::chrono::high_resolution_clock::now();
		if (time_now >= next_read)
		{
			// this is a problem -- either the time interval is too small, or the
			// computer/VM/whatever where we are running is too slow to keep up
			const auto int_ms = std::chrono::duration_cast<std::chrono::milliseconds>(time_now - next_read);

			LOG_MSG(device_name << ": warning: time overflow by " << int_ms.count() << " milliseconds");

			// reset the next read time as if it was now, and hope this doesn't happen again
			next_read = time_now;
		}
		else if (threadShouldExit() == false)
		{
//			LOG_MSG(device_name << ": sleeping for " << std::chrono::duration_cast<std::chrono::milliseconds>(next_read - time_now).count() << " milliseconds");
			std::this_thread::sleep_until(next_read);
		}
		next_read += interval;
	}

	LOG_MSG("prefix=" << prefix << ": detected shutdown signal");

	write_mc_file();
	write_summary_file();

	LOG_MSG("prefix=" << prefix << ": device thread for \"" << tab_name << "\" on " << device_name << " is exiting");

	return;
}


void TabCanvas::write_mc_file(void)
{
	// make sure to see the Doxygen documentation to learn about all the fields

	String dirname = cfg().get_str("output_directory");
	dirname = dirname.replaceFirstOccurrenceOf("%Y", Time::getCurrentTime().formatted("%Y"));

	File dir(dirname);
	dir.createDirectory();

	const ScopedLock scope_lock(numbers_and_calculations_mutex);

	const std::string filename = dir.getChildFile(kiln_num_editor.getText() + "_" + run_editor.getText() + pkg_editor.getText() + ".mc").getFullPathName().toStdString();
	LOG_MSG("prefix=" << prefix << ": writing data out to: " << filename << " (pkg size=" << pkg_doubles.size() << ", pkg avg=" <<  pkg_average << ")");

	// LINE #1:

	std::ofstream fs(filename);
	fs << std::fixed << std::setw(4) << std::setfill('0') << pkg_doubles.size() << ",";

	for (size_t idx = 0; idx < number_of_buckets; idx++)
	{
		fs << std::setw(4) << std::setfill('0') << pkg_moisture_bucket[idx] << ",";
	}

	fs << "000,000,";	// unknown fields

	fs << pkg_std_dev << "," << pkg_average << ",000" << std::endl;

	// LINE #2:

	for (const double d : pkg_doubles)
	{
		fs << std::setw(4) << std::setfill('0') << std::setprecision(1) << d << ",";
	}
	fs << "." << std::endl;

	LOG_MSG("prefix=" << prefix << ": done writing data to " << filename);

	return;
}


void TabCanvas::write_summary_file(void)
{
	// make sure to see the Doxygen documentation to learn about all the fields

	String dirname = cfg().get_str("output_directory");
	dirname = dirname.replaceFirstOccurrenceOf("%Y", Time::getCurrentTime().formatted("%Y"));

	File dir(dirname);
	dir.createDirectory();

	const ScopedLock scope_lock(numbers_and_calculations_mutex);

	const std::string filename = dir.getChildFile(kiln_num_editor.getText() + "_" + run_editor.getText() + "000.mc").getFullPathName().toStdString();
	LOG_MSG("prefix=" << prefix << ": writing summary data to: " << filename << " (pkg size=" << pkg_doubles.size() << ", pkg avg=" << pkg_average << ")");

	std::ofstream fs(filename);
	fs << run_doubles.size() << ",";

	for (size_t idx = 0; idx < number_of_buckets; idx++)
	{
		fs << run_moisture_bucket[idx] << ",";
	}

	fs << "000,000,";	// unknown fields

	fs << std::fixed << run_std_dev << "," << run_average << ",0.0,0.0" << std::endl;

	LOG_MSG("prefix=" << prefix << ": done writing summary data to " << filename);

	return;
}


void TabCanvas::read_all_mc_files(void)
{
	String dirname = cfg().get_str("output_directory");
	dirname = dirname.replaceFirstOccurrenceOf("%Y", Time::getCurrentTime().formatted("%Y"));

	File dir(dirname);
	dir.createDirectory();

	/* The .mc output files are named as follows:  K_RRRPPP.mc where:
	 *
	 *		K is the 1-digit kiln number.
	 *		RRR is the 3-digit run number.
	 *		PPP is the 3-digit package number.
	 */

	// note how index starts at 1, not zero, since index zero is the summary file
	for (int package_index = 1; package_index <= 999; package_index ++)
	{
		std::stringstream filename_ss;
		filename_ss
			<< current_kiln << "_"
			<< std::setfill('0') << std::setw(3) << current_run
			<< std::setfill('0') << std::setw(3) << package_index
			<< ".mc";
		File file = dir.getChildFile(filename_ss.str());

		if (file.existsAsFile() == false)
		{
			// this package doesn't exist, so look for the next one
			continue;
		}

		// we're starting to read a new package, so clear out the package-specific values
		for (size_t idx = 0; idx < number_of_buckets; idx++)
		{
			pkg_moisture_bucket[idx] = 0;
		}
		pkg_doubles.clear();
		pkg_sum = 0.0;
		pkg_std_dev = 0.0;

		const std::string filename = file.getFullPathName().toStdString();
		LOG_MSG("prefix=" << prefix << ": importing all existing values from " << filename);

		std::ifstream fs(filename);
		std::string str;
		std::getline(fs, str); // ignore the first line
		std::getline(fs, str); // parse the 2nd line
		fs.close();

		// replace comma with spaces to make it easier to parse the individual fields
		while (true)
		{
			const size_t pos = str.find(',');
			if (pos == std::string::npos)
			{
				break;
			}
			str[pos] = ' ';
		}

		const ScopedLock scope_lock(numbers_and_calculations_mutex);

		size_t counter = 0;
		std::stringstream ss(str);
		while (ss)
		{
			ss >> str;
			if (str.empty() == false && str != ".")
			{
				const double d = std::stod(str);
				if (d > 0.0)
				{
					counter ++;
					int idx = static_cast<int>(d) - percentage_of_first_bucket;
					if (idx < 0)					idx = 0;
					if (idx >= number_of_buckets)	idx = number_of_buckets - 1;

					run_moisture_bucket[idx] ++;
					pkg_moisture_bucket[idx] ++;

					run_doubles.push_back(d);
					pkg_doubles.push_back(d);

					run_sum += d;
					pkg_sum += d;
				}
			}
		}

		if (run_doubles.empty() == false)
		{
			run_average = run_sum / static_cast<double>(run_doubles.size());
			LOG_MSG("prefix=" << prefix << ": RUN: filename=" << filename << ": imported " << counter << " existing entries, run average is " << run_average);
		}
		if (pkg_doubles.empty() == false)
		{
			pkg_average = pkg_sum / static_cast<double>(pkg_doubles.size());
			LOG_MSG("prefix=" << prefix << ": PKG: filename=" << filename << ": imported " << counter << " existing entries, pkg average is " << pkg_average);
		}
	}

	return;
}


void TabCanvas::parse_opc_console_output(void)
{
	//LOG_MSG("prefix=" << prefix << ": checking to see we need to parse OPC output file \"" << opc_output_filename << "\"");

	const Lox::VStr v = Lox::readTextFile(opc_output_filename, false, false);
	if (v.empty())
	{
		// not much we can do if we failed to read the file
		LOG_MSG("prefix=" << prefix << ": failed to load the OPC output file \"" << opc_output_filename << "\"");
		return;
	}

	/* The file should look something like this:
	 *
	 *	37
	 *	[INFEED]N75:3=1
	 *	[INFEED]N75:5=136
	 *	[INFEED]N75:4=455
	 *	[INFEED]N75:13=2
	 *	[INFEED]N75:15=216
	 *	[INFEED]N75:14=57
	 *
	 * The order of the lines is not guaranteed.
	 */

	// first line is the sequence number
	const size_t new_sequence_number = std::stoul(v[0]);
	if (new_sequence_number == opc_sequence_number)
	{
		// file hasn't changed, so nothing else we need to do
		//LOG_MSG("prefix=" << prefix << ": OPC Console output file " << opc_output_filename << " has not changed (sequence number is \"" << opc_sequence_number << "\")");
		return;
	}
	opc_sequence_number = new_sequence_number;
	LOG_MSG("prefix=" << prefix << ": detected OPC Console output file " << opc_output_filename << " has a sequence number of \"" << opc_sequence_number << "\"");

	// loop through the rest of the values to see if it has anything we need
	for (size_t idx=1; idx < v.size(); idx++)
	{
		const std::string & line = v.at(idx);
		size_t pos = line.find("=");
		if (pos == std::string::npos)
		{
			// skip invalid lines
			continue;
		}

		// Note we want (and need) the key to include the trailing "=" character,
		// otherwise we could get some false positives since "N75:1" would match "N75:10",
		// but "N75:1=" wont ever be a match for "N75:10=".
		const std::string key = line.substr(0, pos + 1);
		const int value = std::stoi(line.substr(pos + 1));

		if (value <= 0)
		{
			/* One of two things have happened:
			 * 1) The line is invalid and we haven't actually read a positive numeric value we can use.
			 * 2) The RSLinx server has sent out an update where every variable is zero, which we see every
			 * once in a while.  As Dom says, "Ya ignore them. We would never have any of those values as zero".
			 */
			 continue;
		}

//		LOG_MSG("prefix=" << prefix << ": -> idx=" << idx << ", key=" << key << " value=" << value);

		if (key.find(opc_register_kiln)	!= std::string::npos)	new_kiln	= value;
		if (key.find(opc_register_run)	!= std::string::npos)	new_run		= value;
		if (key.find(opc_register_pkg)	!= std::string::npos)	new_pkg		= value;
	}

	update_kiln_run_pkg();

	return;
}


void TabCanvas::read_opc_values_from_csv(void)
{
	if (opc_via_csv_filename.empty())
	{
		// nothing we can do if we don't know the .csv filename
		return;
	}

	LOG_MSG("prefix=" << prefix << ": checking to see if there are any changes in \"" << opc_via_csv_filename << "\"");

	const Lox::VStr v = Lox::readTextFile(opc_via_csv_filename, false, false);
	for (const auto & line : v)
	{
		std::smatch matches;
		const bool found = std::regex_search(line, matches, opc_via_csv_regex);
		if (found)
		{
			const std::string key = matches[1];
			const std::string val = matches[2];
			const int value = std::stoi(val);

			LOG_MSG("prefix=" << prefix << ": read from CSV: key=" << key << ", val=" << val);

			if (key == "Kiln")
			{
				new_kiln = value;
			}
			else if (key == "Run")
			{
				new_run = value;
			}
			else if (key == "Pkg")
			{
				new_pkg = value;
			}
		}
	}

	update_kiln_run_pkg();

	return;
}


void TabCanvas::update_kiln_run_pkg(void)
{
	if (current_kiln == new_kiln	&&
		current_run == new_run		&&
		current_pkg == new_pkg		)
	{
		// nothing has changed
		return;
	}

	bool need_to_output_mc_file = false;
	bool run_needs_to_be_reset = false;
	bool pkg_needs_to_be_reset = false;

	if (current_kiln	> 0 &&
		current_run		> 0 &&
		current_pkg		> 0)
	{
		need_to_output_mc_file = true;

		if (current_pkg != new_pkg)
		{
			pkg_needs_to_be_reset = true;
		}
		if (current_run != new_run || current_kiln != new_kiln)
		{
			// if the run changes, then both the run and the package both need to be reset
			pkg_needs_to_be_reset = true;
			run_needs_to_be_reset = true;
		}
	}

	LOG_MSG("prefix=" << prefix << ": need to update the kiln, run, or package number");
	LOG_MSG("prefix=" << prefix << ": -> current k=" << current_kiln	<< " r=" << current_run	<< " p=" << current_pkg	);
	LOG_MSG("prefix=" << prefix << ": -> updated k=" << new_kiln		<< " r=" << new_run		<< " p=" << new_pkg		);
	LOG_MSG("prefix=" << prefix << ": flag indicating we need to output .mc files " << (need_to_output_mc_file ? "has been set" : "is not set"));
	LOG_MSG("prefix=" << prefix << ": flag indicating pkg variables will be reset: " << (pkg_needs_to_be_reset ? "true" : "false"));
	LOG_MSG("prefix=" << prefix << ": flag indicating run variables will be reset: " << (run_needs_to_be_reset ? "true" : "false"));

	if (need_to_output_mc_file)
	{
		const ScopedLock scope_lock(numbers_and_calculations_mutex);
		write_mc_file();
		write_summary_file();

		for (size_t idx = 0; idx < number_of_buckets; idx++)
		{
			if (pkg_needs_to_be_reset)
			{
				pkg_moisture_bucket[idx] = 0;
			}
			if (run_needs_to_be_reset)
			{
				run_moisture_bucket[idx] = 0;
			}
		}

		if (pkg_needs_to_be_reset)
		{
			pkg_doubles.clear();
			pkg_sum		= 0.0;
			pkg_average	= 0.0;
			pkg_std_dev	= 0.0;
		}
		if (run_needs_to_be_reset)
		{
			run_doubles.clear();
			run_sum		= 0.0;
			run_average	= 0.0;
			run_std_dev	= 0.0;
		}
	}

	std::string kiln_str = std::to_string(new_kiln);
	std::string run_str = std::to_string(new_run);
	std::string pkg_str = std::to_string(new_pkg);

	while (run_str.length() < 3)
	{
		run_str = "0" + run_str;
	}
	while (pkg_str.length() < 3)
	{
		pkg_str = "0" + pkg_str;
	}

	kiln_num_editor.setText(kiln_str);
	run_editor.setText(run_str);
	pkg_editor.setText(pkg_str);

	if (current_kiln != new_kiln)
	{
		current_kiln = new_kiln;
		time_since_kiln_changed = std::time(nullptr);
	}

	if (current_run != new_run)
	{
		current_run = new_run;
		time_since_run_changed = std::time(nullptr);
	}

	if (current_pkg != new_pkg)
	{
		current_pkg = new_pkg;
		time_since_pkg_changed = std::time(nullptr);
	}

	cfg().setValue(prefix + "_last_kiln", new_kiln);
	cfg().setValue(prefix + "_last_run", new_run);
	cfg().setValue(prefix + "_last_pkg", new_pkg);

	// if the run was reset, re-read all the new files (if any) now that we've updated the run and package numbers
	if (run_needs_to_be_reset)
	{
		read_all_mc_files();
	}

	return;
}
