/* GMM (C) 2018 Stephane Charette <stephanecharette@gmail.com>
 * $Id: TabCanvas.cpp 2630 2018-10-13 06:37:31Z stephane $
 */

#include "GMM.hpp"


TabCanvas::TabCanvas(const std::string & name) :
	Thread("thread for device prefix " + name),
	prefix(name),
	read_len(0),
	tab_is_active(false),
	timer_frequency_in_milliseconds(cfg().get_int(prefix + "_chart_redraw_ms")),
	comm_handle(INVALID_HANDLE_VALUE)
{
	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(total_number_of_boards_label	);
	addAndMakeVisible(total_number_of_boards_editor	);
	addAndMakeVisible(total_average_moisture_label	);
	addAndMakeVisible(total_average_moisture_editor	);
	addAndMakeVisible(total_std_dev_moisture_label	);
	addAndMakeVisible(total_std_dev_moisture_editor	);
	addAndMakeVisible(local_number_of_boards_label	);
	addAndMakeVisible(local_number_of_boards_editor	);
	addAndMakeVisible(local_average_moisture_label	);
	addAndMakeVisible(local_average_moisture_editor	);
	addAndMakeVisible(local_std_dev_moisture_label	);
	addAndMakeVisible(local_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);

	total_number_of_boards_label	.setText("Boards:"	, NotificationType::sendNotification);
	total_average_moisture_label	.setText("Avg:"		, NotificationType::sendNotification);
	total_std_dev_moisture_label	.setText("StDv:"	, NotificationType::sendNotification);
	total_number_of_boards_label	.setJustificationType(Justification::centredRight);
	total_average_moisture_label	.setJustificationType(Justification::centredRight);
	total_std_dev_moisture_label	.setJustificationType(Justification::centredRight);

	total_number_of_boards_editor	.setReadOnly(true);
	total_average_moisture_editor	.setReadOnly(true);
	total_std_dev_moisture_editor	.setReadOnly(true);
	total_number_of_boards_editor	.setJustification(Justification::centredRight);
	total_average_moisture_editor	.setJustification(Justification::centredRight);
	total_std_dev_moisture_editor	.setJustification(Justification::centredRight);
	total_number_of_boards_editor	.setColour(TextEditor::backgroundColourId, pink_background);
	total_average_moisture_editor	.setColour(TextEditor::backgroundColourId, pink_background);
	total_std_dev_moisture_editor	.setColour(TextEditor::backgroundColourId, pink_background);

	local_number_of_boards_label	.setText("Boards:"	, NotificationType::sendNotification);
	local_average_moisture_label	.setText("Avg:"		, NotificationType::sendNotification);
	local_std_dev_moisture_label	.setText("StDv:"	, NotificationType::sendNotification);
	local_number_of_boards_label	.setJustificationType(Justification::centredRight);
	local_average_moisture_label	.setJustificationType(Justification::centredRight);
	local_std_dev_moisture_label	.setJustificationType(Justification::centredRight);

	local_number_of_boards_editor	.setReadOnly(true);
	local_average_moisture_editor	.setReadOnly(true);
	local_std_dev_moisture_editor	.setReadOnly(true);
	local_number_of_boards_editor	.setJustification(Justification::centredRight);
	local_average_moisture_editor	.setJustification(Justification::centredRight);
	local_std_dev_moisture_editor	.setJustification(Justification::centredRight);
	local_number_of_boards_editor	.setColour(TextEditor::backgroundColourId, blue_background);
	local_average_moisture_editor	.setColour(TextEditor::backgroundColourId, blue_background);
	local_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++)
	{
		total_moisture_content[idx] = 0;
		local_moisture_content[idx] = 0;

		addAndMakeVisible(total_distribution[idx]);
		addAndMakeVisible(percentages		[idx]);
		addAndMakeVisible(local_distribution[idx]);

		total_distribution[idx].setColour(TextEditor::backgroundColourId, pink_background);
		local_distribution[idx].setColour(TextEditor::backgroundColourId, blue_background);

		total_distribution[idx].setReadOnly(true);
		local_distribution[idx].setReadOnly(true);

		total_distribution[idx].setJustification(Justification::centredRight);
		local_distribution[idx].setJustification(Justification::centredRight);

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

	// insert a few fake numbers
	kiln_num_editor	.setText("7");
	run_editor		.setText("026");
	pkg_editor		.setText("118");
#if 0
	total_moisture_content[0] = 72;
	total_moisture_content[1] = 155;
	total_moisture_content[2] = 227;
	total_moisture_content[3] = 214;
	total_moisture_content[4] = 378;
	total_moisture_content[5] = 355;
	total_moisture_content[6] = 524;
	total_moisture_content[7] = 535;
	total_moisture_content[8] = 650;
	total_moisture_content[9] = 675;
	total_moisture_content[10] = 532;
	total_moisture_content[11] = 537;
	total_moisture_content[12] = 387;
	total_moisture_content[13] = 377;
	total_moisture_content[14] = 220;
	total_moisture_content[15] = 215;
	total_moisture_content[16] = 170;
#endif

	// 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(1000); // 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 drawing, it means
	 * the timer will significantly drift over time.
	 */

	stopTimer();

	periodic_update();

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

	return;
}


void TabCanvas::periodic_update(void)
{
	// randomly assign some new values as if we're reading from the serial port
#if 0
	Random r;
	for (size_t idx=0; idx < number_of_buckets; idx++)
	{
		// the closer to middle of the range, the greater chance we have of getting a board
		double chance = 0.0;
		switch (idx)
		{
			case 0:	case 17:							chance = 0.98;		break;
			case 1:	case 16:							chance = 0.95;		break;
			case 2:	case 3:		case 14:	case 15:	chance = 0.85;		break;
			case 4:	case 5:		case 12:	case 13:	chance = 0.65;		break;
			case 6:	case 11:	case 7:		case 10:	chance = 0.55;		break;
			case 8: case 9:								chance = 0.25;		break;
		}
		if (r.nextDouble() >= chance)
		{
			total_moisture_content[idx] ++;
			local_moisture_content[idx] ++;
		}
	}
#endif

#if 0
	// add the local bucket numbers to the totals
	for (size_t idx = 0; idx < number_of_buckets; idx++)
	{
		total_moisture_content[idx] += local_moisture_content[idx];
	}
#endif

	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++)
	{
		total_distribution[idx].setText(String(total_moisture_content[idx]));
		local_distribution[idx].setText(String(local_moisture_content[idx]));
	}

	// only re-create the chart image if the tab is active
	if (tab_is_active)
	{
		auto rect = chart.getBounds();
		chart.setImage(draw_chart(rect.getWidth(), rect.getHeight()));
	}

	return;
}


void TabCanvas::calculate_averages_and_std_deviations()
{
	double average_of_total_moisture = 0.0;
	double average_of_local_moisture = 0.0;
	double std_dev_of_total_moisture = 0.0;
	double std_dev_of_local_moisture = 0.0;

	double number_of_boards_total = 0.0;
	double number_of_boards_local = 0.0;
	double moisture_value_total = 0.0;
	double moisture_value_local = 0.0;

	for (size_t idx = 0; idx < number_of_buckets; idx++)
	{
		const double moisture_percentage	= static_cast<double>(idx + percentage_of_first_bucket);
		const size_t bucket_total			= total_moisture_content[idx];
		const size_t bucket_local			= local_moisture_content[idx];
		number_of_boards_total				+= bucket_total;
		number_of_boards_local				+= bucket_local;
		moisture_value_total				+= (bucket_total * moisture_percentage);
		moisture_value_local				+= (bucket_local * moisture_percentage);
	}

	if (number_of_boards_total > 0.0)
	{
		average_of_total_moisture = moisture_value_total / number_of_boards_total;
	}
	if (number_of_boards_local > 0.0)
	{
		average_of_local_moisture = moisture_value_local / number_of_boards_local;
	}

	// now that we have the mean (average) we can loop through again and calculate standard deviation

	double sum_for_standard_deviation_total = 0.0;
	double sum_for_standard_deviation_local = 0.0;

	for (size_t idx = 0; idx < number_of_buckets; idx++)
	{
		const double moisture_percentage	= static_cast<double>(idx + percentage_of_first_bucket);
		const size_t bucket_total			= total_moisture_content[idx];
		const size_t bucket_local			= local_moisture_content[idx];

		sum_for_standard_deviation_total += bucket_total * (std::pow(moisture_percentage - average_of_total_moisture, 2));
		sum_for_standard_deviation_local += bucket_local * (std::pow(moisture_percentage - average_of_local_moisture, 2));
	}

	if (number_of_boards_total > 0.0)
	{
		std_dev_of_total_moisture = std::sqrt(sum_for_standard_deviation_total / number_of_boards_total);
	}
	if (number_of_boards_local > 0.0)
	{
		std_dev_of_local_moisture = std::sqrt(sum_for_standard_deviation_local / number_of_boards_local);
	}

//	LOG_MSG("TOTAL:  number_of_boards: " << number_of_boards_total << ", moisture_value: " << moisture_value_total << ", average: " << average_of_total_moisture << ", stddev: " << std_dev_of_total_moisture);
//	LOG_MSG("LOCAL:  number_of_boards: " << number_of_boards_local << ", moisture_value: " << moisture_value_local << ", average: " << average_of_local_moisture << ", stddev: " << std_dev_of_local_moisture);

	total_number_of_boards_editor.setText(String(number_of_boards_total));
	total_average_moisture_editor.setText(Lox::Numbers::format(average_of_total_moisture, 2).c_str());
	total_std_dev_moisture_editor.setText(Lox::Numbers::format(std_dev_of_total_moisture, 2).c_str());

	local_number_of_boards_editor.setText(String(number_of_boards_local));
	local_average_moisture_editor.setText(Lox::Numbers::format(average_of_local_moisture, 2).c_str());
	local_std_dev_moisture_editor.setText(Lox::Numbers::format(std_dev_of_local_moisture, 2).c_str());

	return;
}


Image TabCanvas::draw_chart(const size_t width, const size_t height)
{
	if (tab_is_active == 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 < 100 || height < 100)
	{
		// 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 again
	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;
	const float width_of_columns		= h_step * width_float;
	const float one_seventh_of_column	= width_of_columns / 7.0f;
	const float three_seventh_of_column	= 3.0f * one_seventh_of_column;

	// 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);
	}

	// determine the maximum moisture content value
	size_t max_total_moisture_value = 0;
	size_t max_local_moisture_value = 0;
	for (size_t idx = 0; idx < number_of_buckets; idx++)
	{
		if (total_moisture_content[idx] > max_total_moisture_value)
		{
			max_total_moisture_value = total_moisture_content[idx];
		}
		if (local_moisture_content[idx] > max_local_moisture_value)
		{
			max_local_moisture_value = local_moisture_content[idx];
		}
	}

	// prevent divide-by-zero
	if (max_total_moisture_value == 0) max_total_moisture_value = 1;
	if (max_local_moisture_value == 0) max_local_moisture_value = 1;

	// draw the columns of data
	for (size_t idx = 0; 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)total_moisture_content[idx] / (float)max_total_moisture_value;
		const float percent_local = (float)local_moisture_content[idx] / (float)max_local_moisture_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 label 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 ++;
	}

	// add warnings and errors to the top-left of the image
	if (warnings_and_errors.empty() == false)
	{
		g.setColour(Colours::red);
		float y = 0.0f;
		for (const auto & str : warnings_and_errors)
		{
			y += 20.0f;
			g.drawText(str.c_str(), 10.0f, y, width_float - 20.0f, 20.0f, Justification::bottomLeft);
		}
	}

	// 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 total_row_flexbox;
	total_row_flexbox.flexDirection		= FlexBox::Direction::row;
	total_row_flexbox.justifyContent	= FlexBox::JustifyContent::spaceAround;
	total_row_flexbox.items.add(FlexItem(kiln_num_label		).withMargin(margin).withWidth(40));
	total_row_flexbox.items.add(FlexItem(kiln_num_editor	).withMargin(margin).withFlex(1.0));
	total_row_flexbox.items.add(FlexItem(run_label			).withMargin(margin).withFlex(1.0));
	total_row_flexbox.items.add(FlexItem(run_editor			).withMargin(margin).withFlex(1.0));
	total_row_flexbox.items.add(FlexItem(pkg_label			).withMargin(margin).withFlex(1.0));
	total_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++)
	{
		total_row_flexbox.items.add(FlexItem().withMargin(margin).withFlex(1.0));
	}
	total_row_flexbox.items.add(FlexItem(total_number_of_boards_label	).withMargin(margin).withFlex(1.0));
	total_row_flexbox.items.add(FlexItem(total_number_of_boards_editor	).withMargin(margin).withFlex(1.0));
	total_row_flexbox.items.add(FlexItem(total_average_moisture_label	).withMargin(margin).withFlex(1.0));
	total_row_flexbox.items.add(FlexItem(total_average_moisture_editor	).withMargin(margin).withFlex(1.0));
	total_row_flexbox.items.add(FlexItem(total_std_dev_moisture_label	).withMargin(margin).withFlex(1.0));
	total_row_flexbox.items.add(FlexItem(total_std_dev_moisture_editor	).withMargin(margin).withFlex(1.0));

	FlexBox total_distribution_flexbox;
	total_distribution_flexbox.flexDirection	= FlexBox::Direction::row;
	total_distribution_flexbox.justifyContent	= FlexBox::JustifyContent::spaceAround;
	total_distribution_flexbox.items.add(FlexItem(run_header).withMargin(margin).withWidth(40));
	for (size_t idx = 0; idx < number_of_buckets; idx++)
	{
		total_distribution_flexbox.items.add(FlexItem(total_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 local_distribution_flexbox;
	local_distribution_flexbox.flexDirection	= FlexBox::Direction::row;
	local_distribution_flexbox.justifyContent	= FlexBox::JustifyContent::spaceAround;
	local_distribution_flexbox.items.add(FlexItem(pkg_header).withMargin(margin).withWidth(40));
	for (size_t idx=0; idx < number_of_buckets; idx++)
	{
		local_distribution_flexbox.items.add(FlexItem(local_distribution[idx]).withMargin(margin).withFlex(1.0));
	}

	FlexBox local_row_flexbox;
	local_row_flexbox.flexDirection		= FlexBox::Direction::row;
	local_row_flexbox.justifyContent	= FlexBox::JustifyContent::spaceAround;
	local_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++)
	{
		local_row_flexbox.items.add(FlexItem().withMargin(margin).withFlex(1.0));
	}
	local_row_flexbox.items.add(FlexItem(local_number_of_boards_label	).withMargin(margin).withFlex(1.0));
	local_row_flexbox.items.add(FlexItem(local_number_of_boards_editor	).withMargin(margin).withFlex(1.0));
	local_row_flexbox.items.add(FlexItem(local_average_moisture_label	).withMargin(margin).withFlex(1.0));
	local_row_flexbox.items.add(FlexItem(local_average_moisture_editor	).withMargin(margin).withFlex(1.0));
	local_row_flexbox.items.add(FlexItem(local_std_dev_moisture_label	).withMargin(margin).withFlex(1.0));
	local_row_flexbox.items.add(FlexItem(local_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(total_row_flexbox			).withHeight(height));
	flexbox.items.add(FlexItem(total_distribution_flexbox	).withHeight(height));
	flexbox.items.add(FlexItem(percentage_flexbox			).withHeight(height));
	flexbox.items.add(FlexItem(local_distribution_flexbox	).withHeight(height));
	flexbox.items.add(FlexItem(local_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.

	std::string device_name = cfg().get_str(prefix + "_device");

	LOG_MSG("device thread for " << device_name << " has started (prefix=" << prefix << ")");

	const int interval_ms = cfg().get_int(prefix + + "_interval_ms");

	read_len = cfg().get_int(prefix + "_read_len");
	read_buffer = std::vector<uint8_t>(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);

	const std::regex rx(cfg().get_str(prefix + "_regex_pattern"));

	const bool run_simulation = cfg().get_bool(prefix + "_run_simulation");

	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 buffer;
		if (run_simulation)
		{
			buffer = get_simulated_string();
		}
		else
		{
			buffer = read_from_device();
		}

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

			if (found)
			{
				const double data = std::stod(matches[1]);
				LOG_MSG(device_name << ": str=" << String(buffer).trim().toStdString() << ", data=" << data);

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

				local_moisture_content[idx] ++;
				total_moisture_content[idx] ++;
				LOG_MSG(device_name << ": data=" << data << ", incrementing bucket #" << idx << " to " << local_moisture_content[idx]);
			}
			else
			{
				std::string msg;
				for (const char c : buffer)
				{
					if (c > 32 && c < 127)	msg += c;
					else					msg += "<" + std::to_string((int)c) + ">";
				}
				LOG_MSG(device_name << ": failed to parse string: len=" << buffer.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
			//
			// reset the next read time as if it was now, and hope this doesn't happen again
			next_read = time_now;
		}
		else
		{
//			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("device thread for " << device_name << " is exiting (prefix=" << prefix << ")");

	return;
}
