#include "BedShapeDialog.hpp"

#include <wx/wx.h> 
#include <wx/numformatter.h>
#include <wx/sizer.h>
#include <wx/statbox.h>

#include "libslic3r/BoundingBox.hpp"
#include "libslic3r/Model.hpp"
#include "libslic3r/Polygon.hpp"

#include "boost/nowide/iostream.hpp"

#include <algorithm>

namespace Slic3r {
namespace GUI {

void BedShapeDialog::build_dialog(ConfigOptionPoints* default_pt)
{
	m_panel = new BedShapePanel(this);
	m_panel->build_panel(default_pt);

	auto main_sizer = new wxBoxSizer(wxVERTICAL);
	main_sizer->Add(m_panel, 1, wxEXPAND);
	main_sizer->Add(CreateButtonSizer(wxOK | wxCANCEL), 0, wxALIGN_CENTER_HORIZONTAL | wxBOTTOM, 10);

	SetSizer(main_sizer);
	SetMinSize(GetSize());
	main_sizer->SetSizeHints(this);

	// needed to actually free memory
	this->Bind(wxEVT_CLOSE_WINDOW, ([this](wxCloseEvent e) {
		EndModal(wxID_OK);
		Destroy();
	}));
}

void BedShapePanel::build_panel(ConfigOptionPoints* default_pt)
{
//  on_change(nullptr);

	auto box = new wxStaticBox(this, wxID_ANY, _(L("Shape")));
	auto sbsizer = new wxStaticBoxSizer(box, wxVERTICAL);

	// shape options
	m_shape_options_book = new wxChoicebook(this, wxID_ANY, wxDefaultPosition, wxSize(300, -1), wxCHB_TOP);
	sbsizer->Add(m_shape_options_book);

	auto optgroup = init_shape_options_page(_(L("Rectangular")));
		ConfigOptionDef def;
		def.type = coPoints;
		def.default_value = new ConfigOptionPoints{ Vec2d(200, 200) };
		def.label = L("Size");
		def.tooltip = L("Size in X and Y of the rectangular plate.");
		Option option(def, "rect_size");
		optgroup->append_single_option_line(option);

		def.type = coPoints;
		def.default_value = new ConfigOptionPoints{ Vec2d(0, 0) };
		def.label = L("Origin");
		def.tooltip = L("Distance of the 0,0 G-code coordinate from the front left corner of the rectangle.");
		option = Option(def, "rect_origin");
		optgroup->append_single_option_line(option);

		optgroup = init_shape_options_page(_(L("Circular")));
		def.type = coFloat;
		def.default_value = new ConfigOptionFloat(200);
		def.sidetext = L("mm");
		def.label = L("Diameter");
		def.tooltip = L("Diameter of the print bed. It is assumed that origin (0,0) is located in the center.");
		option = Option(def, "diameter");
		optgroup->append_single_option_line(option);

		optgroup = init_shape_options_page(_(L("Custom")));
		Line line{ "", "" };
		line.full_width = 1;
		line.widget = [this](wxWindow* parent) {
			auto btn = new wxButton(parent, wxID_ANY, _(L("Load shape from STL...")), wxDefaultPosition, wxDefaultSize);
			
			auto sizer = new wxBoxSizer(wxHORIZONTAL);
			sizer->Add(btn);

			btn->Bind(wxEVT_BUTTON, ([this](wxCommandEvent e)
			{
				load_stl();
			}));

			return sizer;
		};
		optgroup->append_line(line);

	Bind(wxEVT_CHOICEBOOK_PAGE_CHANGED, ([this](wxCommandEvent e)
	{
		update_shape();
	}));

	// right pane with preview canvas
	m_canvas = new Bed_2D(this);
	m_canvas->m_bed_shape = default_pt->values;

	// main sizer
	auto top_sizer = new wxBoxSizer(wxHORIZONTAL);
	top_sizer->Add(sbsizer, 0, wxEXPAND | wxLeft | wxTOP | wxBOTTOM, 10);
	if (m_canvas)
		top_sizer->Add(m_canvas, 1, wxEXPAND | wxALL, 10) ;

	SetSizerAndFit(top_sizer);

	set_shape(default_pt);
	update_preview();
}

#define SHAPE_RECTANGULAR	0
#define SHAPE_CIRCULAR		1
#define SHAPE_CUSTOM		2

// Called from the constructor.
// Create a panel for a rectangular / circular / custom bed shape.
ConfigOptionsGroupShp BedShapePanel::init_shape_options_page(wxString title)
{

	auto panel = new wxPanel(m_shape_options_book);
	ConfigOptionsGroupShp optgroup;
	optgroup = std::make_shared<ConfigOptionsGroup>(panel, _(L("Settings")));

	optgroup->label_width = 100;
	optgroup->m_on_change = [this](t_config_option_key opt_key, boost::any value) {
		update_shape();
	};
		
	m_optgroups.push_back(optgroup);
	panel->SetSizerAndFit(optgroup->sizer);
	m_shape_options_book->AddPage(panel, title);

	return optgroup;
}

// Called from the constructor.
// Set the initial bed shape from a list of points.
// Deduce the bed shape type(rect, circle, custom)
// This routine shall be smart enough if the user messes up
// with the list of points in the ini file directly.
void BedShapePanel::set_shape(ConfigOptionPoints* points)
{
	auto polygon = Polygon::new_scale(points->values);

	// is this a rectangle ?
	if (points->size() == 4) {
		auto lines = polygon.lines();
		if (lines[0].parallel_to(lines[2]) && lines[1].parallel_to(lines[3])) {
			// okay, it's a rectangle
			// find origin
            coordf_t x_min, x_max, y_min, y_max;
            x_max = x_min = points->values[0](0);
			y_max = y_min = points->values[0](1);
			for (auto pt : points->values)
            {
                x_min = std::min(x_min, pt(0));
                x_max = std::max(x_max, pt(0));
                y_min = std::min(y_min, pt(1));
                y_max = std::max(y_max, pt(1));
            }

            auto origin = new ConfigOptionPoints{ Vec2d(-x_min, -y_min) };

			m_shape_options_book->SetSelection(SHAPE_RECTANGULAR);
			auto optgroup = m_optgroups[SHAPE_RECTANGULAR];
			optgroup->set_value("rect_size", new ConfigOptionPoints{ Vec2d(x_max - x_min, y_max - y_min) });//[x_max - x_min, y_max - y_min]);
			optgroup->set_value("rect_origin", origin);
			update_shape();
			return;
		}
	}

	// is this a circle ?
	{
		// Analyze the array of points.Do they reside on a circle ?
		auto center = polygon.bounding_box().center();
		std::vector<double> vertex_distances;
		double avg_dist = 0;
		for (auto pt: polygon.points)
		{
			double distance = (pt - center).cast<double>().norm();
			vertex_distances.push_back(distance);
			avg_dist += distance;
		}

		avg_dist /= vertex_distances.size();
		bool defined_value = true;
		for (auto el: vertex_distances)
		{
			if (abs(el - avg_dist) > 10 * SCALED_EPSILON)
				defined_value = false;
			break;
		}
		if (defined_value) {
			// all vertices are equidistant to center
			m_shape_options_book->SetSelection(SHAPE_CIRCULAR);
			auto optgroup = m_optgroups[SHAPE_CIRCULAR];
			boost::any ret = wxNumberFormatter::ToString(unscale<double>(avg_dist * 2), 0);
 			optgroup->set_value("diameter", ret);
			update_shape();
			return;
		}
	}

	if (points->size() < 3) {
		// Invalid polygon.Revert to default bed dimensions.
		m_shape_options_book->SetSelection(SHAPE_RECTANGULAR);
		auto optgroup = m_optgroups[SHAPE_RECTANGULAR];
		optgroup->set_value("rect_size", new ConfigOptionPoints{ Vec2d(200, 200) });
		optgroup->set_value("rect_origin", new ConfigOptionPoints{ Vec2d(0, 0) });
		update_shape();
		return;
	}

	// This is a custom bed shape, use the polygon provided.
	m_shape_options_book->SetSelection(SHAPE_CUSTOM);
	// Copy the polygon to the canvas, make a copy of the array.
	m_canvas->m_bed_shape = points->values;
	update_shape();
}

void BedShapePanel::update_preview()
{
	if (m_canvas) m_canvas->Refresh();
	Refresh();
}

// Update the bed shape from the dialog fields.
void BedShapePanel::update_shape()
{
	auto page_idx = m_shape_options_book->GetSelection();
	if (page_idx == SHAPE_RECTANGULAR) {
		Vec2d rect_size(Vec2d::Zero());
		Vec2d rect_origin(Vec2d::Zero());
		try{
			rect_size = boost::any_cast<Vec2d>(m_optgroups[SHAPE_RECTANGULAR]->get_value("rect_size")); }
		catch (const std::exception & /* e */) {
			return;
		}
		try {
			rect_origin = boost::any_cast<Vec2d>(m_optgroups[SHAPE_RECTANGULAR]->get_value("rect_origin"));
		}
		catch (const std::exception & /* e */) {
			return;
		}
		
		auto x = rect_size(0);
		auto y = rect_size(1);
		// empty strings or '-' or other things
		if (x == 0 || y == 0)	return;
		double x0 = 0.0;
		double y0 = 0.0;
		double x1 = x;
		double y1 = y;

		auto dx = rect_origin(0);
		auto dy = rect_origin(1);

		x0 -= dx;
		x1 -= dx;
		y0 -= dy;
		y1 -= dy;
		m_canvas->m_bed_shape = {	Vec2d(x0, y0),
									Vec2d(x1, y0),
									Vec2d(x1, y1),
									Vec2d(x0, y1)};
	} 
	else if(page_idx == SHAPE_CIRCULAR) {
		double diameter;
		try{
			diameter = boost::any_cast<double>(m_optgroups[SHAPE_CIRCULAR]->get_value("diameter"));
		}
		catch (const std::exception & /* e */) {
			return;
		} 
 		if (diameter == 0.0) return ;
		auto r = diameter / 2;
		auto twopi = 2 * PI;
		auto edges = 60;
		std::vector<Vec2d> points;
		for (size_t i = 1; i <= 60; ++i) {
			auto angle = i * twopi / edges;
			points.push_back(Vec2d(r*cos(angle), r*sin(angle)));
		}
		m_canvas->m_bed_shape = points;
	}

//	$self->{on_change}->();
	update_preview();
}

// Loads an stl file, projects it to the XY plane and calculates a polygon.
void BedShapePanel::load_stl()
{
	auto dialog = new wxFileDialog(this, _(L("Choose a file to import bed shape from (STL/OBJ/AMF/3MF/PRUSA):")), "", "",
		file_wildcards(FT_MODEL), wxFD_OPEN | wxFD_FILE_MUST_EXIST);
	if (dialog->ShowModal() != wxID_OK) {
		dialog->Destroy();
		return;
	}
	wxArrayString input_file;
	dialog->GetPaths(input_file);
	dialog->Destroy();

	std::string file_name = input_file[0].ToStdString();

	Model model;
	try {
		model = Model::read_from_file(file_name);
	}
	catch (std::exception &e) {
		auto msg = _(L("Error! ")) + file_name + " : " + e.what() + ".";
		show_error(this, msg);
		exit(1);
	}

	auto mesh = model.mesh();
	auto expolygons = mesh.horizontal_projection();

	if (expolygons.size() == 0) {
		show_error(this, _(L("The selected file contains no geometry.")));
		return;
	}
	if (expolygons.size() > 1) {
		show_error(this, _(L("The selected file contains several disjoint areas. This is not supported.")));
		return;
	}

	auto polygon = expolygons[0].contour;
	std::vector<Vec2d> points;
	for (auto pt : polygon.points)
		points.push_back(unscale(pt));
	m_canvas->m_bed_shape = points;
	update_preview();
}

} // GUI
} // Slic3r
