/*


not implemented yet:

 * syntax
    - include name.scad
    - use name.scad
 * constants
    - undef
 * operators
    - modulo %
    - exponentiation ^
    - less than <
    - less or equal <=
    - equal ==
    - not equal !=
    - greater or equal >=
    - greater than >
    - logical and &&
    - logical or ||
    - negation !
 * special variables
    - $fa        minimum angle
    - $fs        minimum size
    - $fn        number of segments
    - $t         animation step
    - $vpr       viewport rotation angles in degrees
    - $vpt       viewport translation
    - $vpd       viewport camera distance
    - $vpf       viewport camera field of view
    - $children  number of module children
    - $preview   true in F5 preview, false for F6
 * modifier characters
 * lists
 * list comprehensions
 * flow control
 * type test functions
 * other
    - echo()
    - render()
    - children()
    - assert()
    - assign()
 * functions
    - concat
    - lookup
    - str
    - chr
    - ord
    - search
    - version
    - version_num
    - parent_module
	- let



partial implementations:



known bugs:

 - spheres disappear in non-uniform scaling
 - i had some crashes with just slightly overlapping objects


*/

#include <algorithm>
#include <array>
#include <cctype>
#include <cmath>
#include <cstddef>
#include <cstdlib>
#include <filesystem>
#include <fstream>
#include <functional>
#include <iostream>
#include <limits>
#include <memory>
#include <random>
#include <stdexcept>
#include <string>
#include <unordered_map>
#include <unordered_set>
#include <utility>
#include <variant>
#include <vector>


#include <Bnd_Box.hxx>
#include <BOPAlgo_Tools.hxx>
#include <BRep_Builder.hxx>
#include <BRep_Tool.hxx>
#include <BRepAlgoAPI_Common.hxx>
#include <BRepAlgoAPI_Cut.hxx>
#include <BRepAlgoAPI_Fuse.hxx>
#include <BRepAlgoAPI_Section.hxx>
#include <BRepBndLib.hxx>
#include <BRepBuilderAPI_GTransform.hxx>
#include <BRepBuilderAPI_Transform.hxx>
#include <BRepBuilderAPI_MakeEdge.hxx>
#include <BRepBuilderAPI_MakeFace.hxx>
#include <BRepBuilderAPI_MakePolygon.hxx>
#include <BRepBuilderAPI_MakeSolid.hxx>
#include <BRepBuilderAPI_MakeVertex.hxx>
#include <BRepBuilderAPI_MakeWire.hxx>
#include <BRepBuilderAPI_Sewing.hxx>
#include <BRepCheck_Analyzer.hxx>
#include <BRepClass_FaceClassifier.hxx>
#include <BRepGProp.hxx>
#include <BRepMesh_IncrementalMesh.hxx>
#include <BRepOffsetAPI_ThruSections.hxx>
#include <BRepPrimAPI_MakeBox.hxx>
#include <BRepPrimAPI_MakeCone.hxx>
#include <BRepPrimAPI_MakeCylinder.hxx>
#include <BRepPrimAPI_MakePrism.hxx>
#include <BRepPrimAPI_MakeRevol.hxx>
#include <BRepPrimAPI_MakeSphere.hxx>
#include <BRepTools.hxx>
#include <Font_FontAspect.hxx>
#include <Font_FontMgr.hxx>
#include <Font_SystemFont.hxx>
#include <gp_Ax2.hxx>
#include <gp_Circ.hxx>
#include <gp_Dir.hxx>
#include <gp_GTrsf.hxx>
#include <gp_Pln.hxx>
#include <gp_Pnt.hxx>
#include <gp_Trsf.hxx>
#include <gp_Vec.hxx>
#include <GProp_GProps.hxx>
#include <Graphic3d_HorizontalTextAlignment.hxx>
#include <Graphic3d_VerticalTextAlignment.hxx>
#include <HLRBRep_Algo.hxx>
#include <HLRBRep_HLRToShape.hxx>
#include <IFSelect_ReturnStatus.hxx>
#include <NCollection_String.hxx>
#include <Precision.hxx>
//#include <Prs3d_Projector.hxx>
#include <Quantity_Color.hxx>
// #include <Quantity_NameOfColor.hxx>
#include <Standard_Failure.hxx>
#include <StdPrs_BRepFont.hxx>
#include <StdPrs_BRepTextBuilder.hxx>
#include <STEPControl_Writer.hxx>
#include <STEPCAFControl_Controller.hxx>
#include <STEPCAFControl_Writer.hxx>
#include <TDocStd_Application.hxx>
#include <TDocStd_Document.hxx>
#include <TopAbs_ShapeEnum.hxx>
#include <TopAbs_State.hxx>
#include <TopExp.hxx>
#include <TopExp_Explorer.hxx>
#include <TopoDS.hxx>
#include <TopoDS_Compound.hxx>
#include <TopoDS_Edge.hxx>
#include <TopoDS_Face.hxx>
#include <TopoDS_Shape.hxx>
#include <TopoDS_Wire.hxx>
#include <XCAFApp_Application.hxx>
#include <XCAFDoc_DocumentTool.hxx>
#include <XCAFDoc_ShapeTool.hxx>
#include <XCAFDoc_ColorTool.hxx>
#include <XCAFDoc_ColorType.hxx>


#define ENUM_STR_CASE( etype, eval )	case etype::eval: return #eval

static int global_line_nr = 0;

[[noreturn]] void throw_error( int line_nr, const std::string& message )
{
	throw std::runtime_error( "Line " + std::to_string(line_nr) + ": " + message );
}

double d2r( double degrees )
{
	return degrees * 3.14159265358979323846 / 180.0;
}

using Rgb8 = std::array<std::uint8_t, 3>;
inline const std::unordered_map< std::string_view, Rgb8 > kCssColors
{
    // Basic color keywords
	{ "aqua",					{   0, 255, 255 } },
	{ "black",					{   0,   0,   0 } },
	{ "blue",					{   0,   0, 255 } },
	{ "fuchsia",				{ 255,   0, 255 } },
	{ "gray",					{ 128, 128, 128 } },
	{ "green",					{   0, 128,   0 } },
	{ "lime",					{   0, 255,   0 } },
	{ "maroon",					{ 128,   0,   0 } },
	{ "navy",					{   0,   0, 128 } },
	{ "olive",					{ 128, 128,   0 } },
	{ "purple",					{ 128,   0, 128 } },
	{ "red",					{ 255,   0,   0 } },
	{ "silver",					{ 192, 192, 192 } },
	{ "teal",					{   0, 128, 128 } },
	{ "white",					{ 255, 255, 255 } },
	{ "yellow",					{ 255, 255,   0 } },
	// Extended color keywords
	{ "aliceblue",				{ 240, 248, 255 } },
	{ "antiquewhite",			{ 250, 235, 215 } },
	{ "aqua",					{   0, 255, 255 } },
	{ "aquamarine",				{ 127, 255, 212 } },
	{ "azure",					{ 240, 255, 255 } },
	{ "beige",					{ 245, 245, 220 } },
	{ "bisque",					{ 255, 228, 196 } },
	{ "black",					{   0,   0,   0 } },
	{ "blanchedalmond",			{ 255, 235, 205 } },
	{ "blue",					{   0,   0, 255 } },
	{ "blueviolet",				{ 138,  43, 226 } },
	{ "brown",					{ 165,  42,  42 } },
	{ "burlywood",				{ 222, 184, 135 } },
	{ "cadetblue",				{  95, 158, 160 } },
	{ "chartreuse",				{  95, 158, 160 } },
	{ "chocolate",				{ 210, 105,  30 } },
	{ "coral",					{ 255, 127,  80 } },
	{ "cornflowerblue",			{ 100, 149, 237 } },
	{ "cornsilk",				{ 255, 248, 220 } },
	{ "crimson",				{ 220,  20,  60 } },
	{ "cyan",					{   0, 255, 255 } },
	{ "darkblue",				{   0,   0, 139 } },
	{ "darkcyan",				{   0, 139, 139 } },
	{ "darkgoldenrod",			{ 184, 134,  11 } },
	{ "darkgray",				{ 169, 169, 169 } },
	{ "darkgreen",				{   0, 100,   0 } },
	{ "darkkhaki",				{ 189, 183, 107 } },
	{ "darkmagenta",			{ 139,   0, 139 } },
	{ "darkolivegreen",			{  85, 107,  47 } },
	{ "darkorange",				{ 255, 140,   0 } },
	{ "darkorchid",				{ 153,  50, 204 } },
	{ "darkred",				{ 139,   0,   0 } },
	{ "darksalmon",				{ 233, 150, 122 } },
	{ "darkseagreen",			{ 143, 188, 143 } },
	{ "darkslateblue",			{  72,  61, 139 } },
	{ "darkslategray",			{  47,  79,  79 } },
	{ "darkturquoise",			{   0, 206, 209 } },
	{ "darkviolet",				{ 148,   0, 211 } },
	{ "deeppink",				{ 255,  20, 147 } },
	{ "deepskyblue",			{   0, 191, 255 } },
	{ "dimgray",				{   0, 191, 255 } },
	{ "dodgerblue",				{  30, 144, 255 } },
	{ "firebrick",				{ 178,  34,  34 } },
	{ "floralwhite",			{ 255, 250, 240 } },
	{ "forestgreen",			{  34, 139,  34 } },
	{ "fuchsia",				{ 255,   0, 255 } },
	{ "gainsboro",				{ 220, 220, 220 } },
	{ "ghostwhite",				{ 248, 248, 255 } },
	{ "gold",					{ 255, 215,   0 } },
	{ "goldenrod",				{ 218, 165,  32 } },
	{ "gray",					{ 127, 127, 127 } },
	{ "green",					{   0, 128,   0 } },
	{ "greenyellow",			{ 173, 255,  47 } },
	{ "honeydew",				{ 240, 255, 240 } },
	{ "hotpink",				{ 255, 105, 180 } },
	{ "indianred",				{ 205,  92,  92 } },
	{ "indigo",					{  75,   0, 130 } },
	{ "ivory",					{ 255, 255, 240 } },
	{ "khaki",					{ 240, 230, 140 } },
	{ "lavender",				{ 230, 230, 250 } },
	{ "lavenderblush",			{ 255, 240, 245 } },
	{ "lawngreen",				{ 124, 252,   0 } },
	{ "lemonchiffon",			{ 255, 250, 205 } },
	{ "lightblue",				{ 173, 216, 230 } },
	{ "lightcoral",				{ 240, 128, 128 } },
	{ "lightcyan",				{ 224, 255, 255 } },
	{ "lightgoldenrodyellow",	{ 250, 250, 210 } },
	{ "lightgreen",				{ 144, 238, 144 } },
	{ "lightgrey",				{ 211, 211, 211 } },
	{ "lightpink",				{ 255, 182, 193 } },
	{ "lightsalmon",			{ 255, 160, 122 } },
	{ "lightseagreen",			{  32, 178, 170 } },
	{ "lightskyblue",			{ 135, 206, 250 } },
	{ "lightslategray",			{ 119, 136, 153 } },
	{ "lightsteelblue",			{ 176, 196, 222 } },
	{ "lightyellow",			{ 255, 255, 224 } },
	{ "lime",					{   0, 255,   0 } },
	{ "limegreen",				{  50, 205,  50 } },
	{ "linen",					{ 250, 240, 230 } },
	{ "magenta",				{ 255,   0, 255 } },
	{ "maroon",					{ 128,   0,   0 } },
	{ "mediumaquamarine",		{ 102, 205, 170 } },
	{ "mediumblue",				{   0,   0, 205 } },
	{ "mediumorchid",			{ 186,  85, 211 } },
	{ "mediumpurple",			{ 147, 112, 219 } },
	{ "mediumseagreen",			{  60, 179, 113 } },
	{ "mediumslateblue",		{ 123, 104, 238 } },
	{ "mediumspringgreen",		{   0, 250, 154 } },
	{ "mediumturquoise",		{  72, 209, 204 } },
	{ "mediumvioletred",		{ 199,  21, 133 } },
	{ "midnightblue",			{  25,  25, 112 } },
	{ "mintcream",				{ 245, 255, 250 } },
	{ "mistyrose",				{ 255, 228, 225 } },
	{ "moccasin",				{ 255, 228, 181 } },
	{ "navajowhite",			{ 255, 222, 173 } },
	{ "navy",					{   0,   0, 128 } },
	{ "navyblue",				{ 159, 175, 223 } },
	{ "oldlace",				{ 253, 245, 230 } },
	{ "olive",					{ 128, 128,   0 } },
	{ "olivedrab",				{ 107, 142,  35 } },
	{ "orange",					{ 255, 165,   0 } },
	{ "orangered",				{ 255,  69,   0 } },
	{ "orchid",					{ 218, 112, 214 } },
	{ "palegoldenrod",			{ 238, 232, 170 } },
	{ "palegreen",				{ 152, 251, 152 } },
	{ "paleturquoise",			{ 175, 238, 238 } },
	{ "palevioletred",			{ 219, 112, 147 } },
	{ "papayawhip",				{ 255, 239, 213 } },
	{ "peachpuff",				{ 255, 218, 185 } },
	{ "peru",					{ 205, 133,  63 } },
	{ "pink",					{ 255, 192, 203 } },
	{ "plum",					{ 221, 160, 221 } },
	{ "powderblue",				{ 176, 224, 230 } },
	{ "purple",					{ 128,   0, 128 } },
	{ "red",					{ 255,   0,   0 } },
	{ "rosybrown",				{ 188, 143, 143 } },
	{ "royalblue",				{  65, 105, 225 } },
	{ "saddlebrown",			{ 139,  69,  19 } },
	{ "salmon",					{ 250, 128, 114 } },
	{ "sandybrown",				{ 244, 164,  96 } },
	{ "seagreen",				{  46, 139,  87 } },
	{ "seashell",				{ 255, 245, 238 } },
	{ "sienna",					{ 160,  82,  45 } },
	{ "silver",					{ 192, 192, 192 } },
	{ "skyblue",				{ 135, 206, 235 } },
	{ "slateblue",				{ 106,  90, 205 } },
	{ "slategray",				{ 112, 128, 144 } },
	{ "snow",					{ 255, 250, 250 } },
	{ "springgreen",			{   0, 255, 127 } },
	{ "steelblue",				{  70, 130, 180 } },
	{ "tan",					{ 210, 180, 140 } },
	{ "teal",					{   0, 128, 128 } },
	{ "thistle",				{ 216, 191, 216 } },
	{ "tomato",					{ 255,  99,  71 } },
	{ "turquoise",				{  64, 224, 208 } },
	{ "violet",					{ 238, 130, 238 } },
	{ "wheat",					{ 245, 222, 179 } },
	{ "white",					{ 255, 255, 255 } },
	{ "whitesmoke",				{ 245, 245, 245 } },
	{ "yellow",					{ 255, 255,   0 } },
	{ "yellowgreen",			{ 139, 205,  50 } }
};

const char* shape_type( const TopoDS_Shape& shape )
{
	switch( shape.ShapeType() )
	{
		case TopAbs_COMPOUND: return "COMPOUND";
		// case TopAbs_COMPOUND_ID: return "COMPOUND_ID";
		case TopAbs_SOLID: return "SOLID";
		case TopAbs_SHELL: return "SHELL";
		case TopAbs_FACE: return "FACE";
		case TopAbs_WIRE: return "WIRE";
		case TopAbs_EDGE: return "EDGE";
		case TopAbs_VERTEX: return "VERTEX";
		case TopAbs_SHAPE: return "SHAPE";
		default: return "<unknown>";
	}
}

struct Value
{
    using Vector = std::vector<Value>;

    using Storage = std::variant<
        bool,
        int,
        double,
        std::string,
        Vector
    >;

    Storage data;

    Value()
        : data( 0 )
    {
    }

    Value( bool value )
        : data( value )
    {
    }

    Value( int value )
        : data( value )
    {
    }

    Value( double value )
        : data( value )
    {
    }

    Value( const char* value )
        : data( std::string( value ) )
    {
    }

    Value( std::string value )
        : data( std::move( value ) )
    {
    }

    Value( Vector value )
        : data( std::move( value ) )
    {
    }

	std::string valueTypeStr() const
	{
		if( std::holds_alternative<bool>( data ) )
		{
			return "bool";
		}
		if( std::holds_alternative<int>( data ) )
		{
			return "int";
		}
		if( std::holds_alternative<double>( data ) )
		{
			return "double";
		}
		if( std::holds_alternative<std::string>( data ) )
		{
			return "string";
		}
		if( std::holds_alternative<Vector>( data ) )
		{
			return "vector";
		}

		return "unknown";
	}

	bool isNumeric() const
	{
		return std::holds_alternative<bool>( data ) ||
				std::holds_alternative<int>( data ) ||
				std::holds_alternative<double>( data );
	}

	bool isVector() const
	{
		return std::holds_alternative<Vector>( data );
	}

	/* Check if it is a vector containing numbers only.
	 * If size is positive check that it contains exactly that many numbers.
	 * If size is negative check that it contains at least that many numbers.
	 */
	bool isNumVector( int size0 ) const
	{
		if( !std::holds_alternative<Vector>( data ) )
		{
			return false;
		}
		const Vector& v = std::get<Vector>( data );
		if( size0 > 0 && v.size() != size0 )
		{
			return false;
		}
		else if( size0 < 0 && v.size() < -size0 )
		{
			return false;
		}
		for( int i=0; i<v.size(); i++ )
		{
			if( !v[i].isNumeric() )
			{
				return false;
			}
		}
		return true;
	}

	/* Check if it is a vector containing vectors containing numbers only.
	 */
	bool isNumVector( int size0, int size1 ) const
	{
		if( !std::holds_alternative<Vector>( data ) )
		{
			return false;
		}
		const Vector& v = std::get<Vector>( data );
		if( size0 > 0 && v.size() != size0 )
		{
			return false;
		}
		else if( size0 < 0 && v.size() < -size0 )
		{
			return false;
		}
		for( int i=0; i<v.size(); i++ )
		{
			if( !v[i].isNumVector( size1 ) )
			{
				return false;
			}
		}
		return true;
	}

	int length() const
	{
		if( std::holds_alternative<Vector>( data ) )
		{
			return std::get<Vector>( data ).size();
		}
		else if( std::holds_alternative<std::string>( data ) )
		{
			return std::get<std::string>( data ).size();
		}
		else
		{
			return 1;
		}
	}

	bool asBool( int line_nr=global_line_nr ) const
	{
		if( std::holds_alternative<bool>( data ) )
		{
			return std::get<bool>( data );
		}
		if( std::holds_alternative<int>( data ) )
		{
			return std::get<int>( data ) != 0;
		}
		if( std::holds_alternative<double>( data ) )
		{
			return std::get<double>( data ) != 0.0;
		}

		throw_error( line_nr, std::format(
				"Expected bool value but got {}", valueTypeStr() ) );
	}

	int asInt( int line_nr=global_line_nr ) const
	{
		if( std::holds_alternative<double>( data ) )
		{
			return std::get<double>( data );
		}
		if( std::holds_alternative<bool>( data ) )
		{
			return std::get<bool>( data );
		}
		if( std::holds_alternative<int>( data ) )
		{
			return std::get<int>( data );
		}

		throw_error( line_nr, std::format(
				"Expected int value but got {}", valueTypeStr() ) );
	}

	double asDouble( int line_nr=global_line_nr ) const
	{
		if( std::holds_alternative<double>( data ) )
		{
			return std::get<double>( data );
		}
		if( std::holds_alternative<bool>( data ) )
		{
			return std::get<bool>( data );
		}
		if( std::holds_alternative<int>( data ) )
		{
			return std::get<int>( data );
		}

		throw_error( line_nr, std::format(
				"Expected float value but got {}", valueTypeStr() ) );
	}

	std::string asString( int line_nr=global_line_nr ) const
	{
		if( std::holds_alternative<std::string>( data ) )
		{
			return std::get<std::string>( data );
		}
		if( std::holds_alternative<bool>( data ) )
		{
			return std::get<bool>( data ) ? "true" : "false";
		}
		if( std::holds_alternative<int>( data ) )
		{
			return std::to_string( std::get<int>( data ) );
		}

		throw_error( line_nr, std::format(
				"Expected string value but got {}", valueTypeStr() ) );
	}

	const Vector& asVector( int line_nr=global_line_nr ) const
	{
		if( std::holds_alternative<Vector>( data ) )
		{
			return std::get<Vector>( data );
		}

		throw_error( line_nr, std::format(
				"Expected vector value but got {}", valueTypeStr() ) );
	}

	bool vAsBool( int index0 ) const
	{
		if( !std::holds_alternative<Vector>( data ) )
		{
			throw_error( global_line_nr, "Expected vector value" );
		}
		const Vector& v = std::get<Vector>( data );
		if( index0 < 0 || index0 >= v.size() )
		{
			throw_error( global_line_nr, std::format(
					"Invalid vector index {} value", index0 ) );
		}
		return v[index0].asBool();
	}

	int vAsInt( int index0 ) const
	{
		if( !std::holds_alternative<Vector>( data ) )
		{
			throw_error( global_line_nr, "Expected vector value" );
		}
		const Vector& v = std::get<Vector>( data );
		if( index0 < 0 || index0 >= v.size() )
		{
			throw_error( global_line_nr, std::format(
					"Invalid vector index {} value", index0 ) );
		}
		return v[index0].asInt();
	}

	double vAsDouble( int index0 ) const
	{
		if( !std::holds_alternative<Vector>( data ) )
		{
			throw_error( global_line_nr, "Expected vector value" );
		}
		const Vector& v = std::get<Vector>( data );
		if( index0 < 0 || index0 >= v.size() )
		{
			throw_error( global_line_nr, std::format(
					"Invalid vector index {} value", index0 ) );
		}
		return v[index0].asDouble();
	}

	Value operator[]( int index0 )
	{
		if( !std::holds_alternative<Vector>( data ) )
		{
			throw_error( global_line_nr, "Expected vector value" );
		}
		const Vector& v = std::get<Vector>( data );
		if( index0 < 0 || index0 >= v.size() )
		{
			throw_error( global_line_nr, std::format(
					"Invalid vector index {} value", index0 ) );
		}
		return v[index0];
	}

	Value operator+( const Value& other )
	{
		if( std::holds_alternative<double>( data ) ||
			std::holds_alternative<double>( other.data ) )
		{
			return asDouble() + other.asDouble();
		}
		if( std::holds_alternative<int>( data ) ||
			std::holds_alternative<int>( other.data ) )
		{
			return asInt() + other.asInt();
		}

		throw_error( global_line_nr, std::format(
				"operator {} + {} not implemented",
				valueTypeStr(), other.valueTypeStr() ) );
	}

	Value operator-( const Value& other )
	{
		if( std::holds_alternative<double>( data ) ||
			std::holds_alternative<double>( other.data ) )
		{
			return asDouble() - other.asDouble();
		}
		if( std::holds_alternative<int>( data ) ||
			std::holds_alternative<int>( other.data ) )
		{
			return asInt() - other.asInt();
		}

		throw_error( global_line_nr, std::format(
				"operator {} - {} not implemented",
				valueTypeStr(), other.valueTypeStr() ) );
	}

	Value operator*( const Value& other )
	{
		if( std::holds_alternative<double>( data ) ||
			std::holds_alternative<double>( other.data ) )
		{
			return asDouble() * other.asDouble();
		}
		if( std::holds_alternative<int>( data ) ||
			std::holds_alternative<int>( other.data ) )
		{
			return asInt() * other.asInt();
		}

		throw_error( global_line_nr, std::format(
				"operator {} * {} not implemented",
				valueTypeStr(), other.valueTypeStr() ) );
	}

	Value operator/( const Value& other )
	{
		if( std::holds_alternative<double>( data ) ||
			std::holds_alternative<double>( other.data ) )
		{
			int other_f = other.asDouble();
			if( other_f == 0 )
			{
				throw_error( global_line_nr, "division by zero" );
			}
			return asDouble() / other_f;
		}
		if( std::holds_alternative<int>( data ) ||
			std::holds_alternative<int>( other.data ) )
		{
			int other_i = other.asInt();
			if( other_i == 0 )
			{
				throw_error( global_line_nr, "division by zero" );
			}
			return asInt() / other_i;
		}

		throw_error( global_line_nr, std::format(
				"operator {} / {} not implemented",
				valueTypeStr(), other.valueTypeStr() ) );
	}

	friend Value operator-( const Value& value )
	{
		if( std::holds_alternative<double>( value.data ) )
		{
			return -value.asDouble();
		}
		if( std::holds_alternative<int>( value.data ) ||
			std::holds_alternative<bool>( value.data ) )
		{
			return -value.asInt();
		}
		throw_error( global_line_nr, std::format(
				"operator - {} not implemented",
				value.valueTypeStr() ) );
	}

	friend Value operator!( const Value& value )
	{
		if( std::holds_alternative<double>( value.data ) )
		{
			return value.asDouble() != 0.0;
		}
		if( std::holds_alternative<int>( value.data ) )
		{
			return value.asInt() != 0;
		}
		if( std::holds_alternative<bool>( value.data ) )
		{
			return !value.asBool();
		}
		throw_error( global_line_nr, std::format(
				"operator ! {} not implemented",
				value.valueTypeStr() ) );
	}
};

// ============================================================
// Lexer
// ============================================================

struct Token
{
	enum class Kind
	{
		End,
		Identifier,
		Number,
		String,

		LeftParen,
		RightParen,
		LeftBracket,
		RightBracket,
		LeftBrace,
		RightBrace,

		Comma,
		Semicolon,
		Equal,
		Question,
		Colon,

		Plus,
		Minus,
		Star,
		Slash,
		Percent,
		Circumflex,

		LessThan,
		GreaterThan,
		LessOrEqual,
		GreaterOrEqual,
		NotEqual,
		Equals,
		LogicalAnd,
		LogicalOr,
		Negation       
	};
	// static constexpr std::string_view strKind( Kind kind )
	static constexpr const char* strKind( Kind kind )
	{
		switch( kind )
		{
			ENUM_STR_CASE( Kind, End );
			ENUM_STR_CASE( Kind, Identifier );
			ENUM_STR_CASE( Kind, Number );
			ENUM_STR_CASE( Kind, String );
			ENUM_STR_CASE( Kind, LeftParen );
			ENUM_STR_CASE( Kind, RightParen );
			ENUM_STR_CASE( Kind, LeftBracket );
			ENUM_STR_CASE( Kind, RightBracket );
			ENUM_STR_CASE( Kind, LeftBrace );
			ENUM_STR_CASE( Kind, RightBrace );
			ENUM_STR_CASE( Kind, Comma );
			ENUM_STR_CASE( Kind, Semicolon );
			ENUM_STR_CASE( Kind, Equal );
			ENUM_STR_CASE( Kind, Question );
			ENUM_STR_CASE( Kind, Colon );
			ENUM_STR_CASE( Kind, Plus );
			ENUM_STR_CASE( Kind, Minus );
			ENUM_STR_CASE( Kind, Star );
			ENUM_STR_CASE( Kind, Slash );
			ENUM_STR_CASE( Kind, Percent );
			ENUM_STR_CASE( Kind, Circumflex );
			ENUM_STR_CASE( Kind, LessThan );
			ENUM_STR_CASE( Kind, GreaterThan );
			ENUM_STR_CASE( Kind, LessOrEqual );
			ENUM_STR_CASE( Kind, GreaterOrEqual );
			ENUM_STR_CASE( Kind, NotEqual );
			ENUM_STR_CASE( Kind, Equals );
			ENUM_STR_CASE( Kind, LogicalAnd );
			ENUM_STR_CASE( Kind, LogicalOr );
			ENUM_STR_CASE( Kind, Negation );
			default: return "*unknown*";
		}
	}

	void print() const
	{
		std::cout << "Token " << strKind( kind ) << " '" << text << "'\n";
	}

	int line_nr;
	Kind kind;
	std::string text;
	Value number;
};

class Lexer
{
private:
	std::string source_;
	std::size_t position_ = 0;
	int line_nr = 1;

public:
	explicit Lexer( std::string source )
				: source_( std::move( source ) )
	{
	}

	Token next()
	{
		skipWhitespaceAndComments();

		if( position_ >= source_.size() )
		{
			// std::cout << line_nr << ": EOF" << std::endl;
			return { line_nr, Token::Kind::End, "" };
		}

		char c = source_[position_];

		if( std::isalpha( static_cast<unsigned char>( c ) ) ||
			c == '_' || c == '$' )
		{
			return readIdentifier();
		}

		if( std::isdigit( static_cast<unsigned char>( c ) ) || c == '.' )
		{
			return readNumber();
		}

		if( c == '"' )
		{
			return readString();
		}

		// std::cout << line_nr << ": " << c << std::endl;
		++position_;
		char c2 = position_ < source_.size() ? source_[position_] : 0;

		switch( c )
		{
			case '(': return { line_nr, Token::Kind::LeftParen, "(" };
			case ')': return { line_nr, Token::Kind::RightParen, ")" };
			case '[': return { line_nr, Token::Kind::LeftBracket, "[" };
			case ']': return { line_nr, Token::Kind::RightBracket, "]" };
			case '{': return { line_nr, Token::Kind::LeftBrace, "{" };
			case '}': return { line_nr, Token::Kind::RightBrace, "}" };
			case ',': return { line_nr, Token::Kind::Comma, "," };
			case ';': return { line_nr, Token::Kind::Semicolon, ";" };
			case '?': return { line_nr, Token::Kind::Question, "?" };
			case ':': return { line_nr, Token::Kind::Colon, ":" };
			case '+': return { line_nr, Token::Kind::Plus, "+" };
			case '-': return { line_nr, Token::Kind::Minus, "-" };
			case '*': return { line_nr, Token::Kind::Star, "*" };
			case '/': return { line_nr, Token::Kind::Slash, "/" };
			case '%': return { line_nr, Token::Kind::Percent, "%" };
			case '^': return { line_nr, Token::Kind::Circumflex, "^" };

			case '<':
				if( c2 == '=' )
				{
					return { line_nr, Token::Kind::LessOrEqual, "<=" };
				}
				else
				{
					return { line_nr, Token::Kind::LessThan, "<" };
				}

			case '>':
				if( c2 == '=' )
				{
					return { line_nr, Token::Kind::GreaterOrEqual, ">=" };
				}
				else
				{
					return { line_nr, Token::Kind::GreaterThan, ">" };
				}

			case '!':
				if( c2 == '=' )
				{
					return { line_nr, Token::Kind::NotEqual, "!=" };
				}
				else
				{
					return { line_nr, Token::Kind::Negation, "!" };
				}

			case '=':
				if( c2 == '=' )
				{
					return { line_nr, Token::Kind::Equals, "==" };
				}
				else
				{
					return { line_nr, Token::Kind::Equal, "=" };
				}

			case '&':
				if( c2 == '&' )
				{
					return { line_nr, Token::Kind::LogicalAnd, "&&" };
				}
				else
				{
					break;
				}

			case '|':
				if( c2 == '|' )
				{
					return { line_nr, Token::Kind::LogicalOr, "||" };
				}
				else
				{
					break;
				}

			default:
				break;
		}
		throw_error( line_nr, std::format(
					"Unexpected character '{}'", c ) );
	}

private:

	/*
	std::runtime_error error( const std::string& message ) const
	{
		return std::runtime_error(
			message + " at character " + std::to_string( position_ ) );
	}
	*/

	void skipWhitespaceAndComments()
	{
		while( position_ < source_.size() )
		{
			while( position_ < source_.size() &&
				std::isspace( static_cast<unsigned char>( source_[position_] ) ) )
			{
				if( source_[position_] == '\n' )
				{
					line_nr++;
				}
				++position_;
			}

			if( position_ + 1 < source_.size() &&
				source_[position_] == '/' &&
				source_[position_ + 1] == '/' )
				{
				position_ += 2;

				while( position_ < source_.size() &&
						source_[position_] != '\n' )
				{
					++position_;
				}
				++position_;
				line_nr++;

				continue;
			}

			if( position_ + 1 < source_.size() &&
				source_[position_] == '/' &&
				source_[position_ + 1] == '*' )
			{
				position_ += 2;

				while( position_ + 1 < source_.size() &&
						!( source_[position_] == '*' &&
							source_[position_ + 1] == '/' ) )
				{
					if( source_[position_] == '\n' )
					{
						line_nr++;
					}
					++position_;
				}

				if( position_ + 1 >= source_.size() )
				{
					throw_error( line_nr, "Unterminated block comment" );
				}

				position_ += 2;
				continue;
			}

			break;
		}
	}

	Token readIdentifier()
	{
		std::size_t start = position_;

		++position_;

		while( position_ < source_.size() )
		{
			char c = source_[position_];

			if( !std::isalnum( static_cast<unsigned char>( c ) ) &&
				c != '_' && c != '$' )
			{
				break;
			}

			++position_;
		}

		// std::cout << line_nr << ": " << source_.substr( start, position_ - start ) << std::endl;
		return
		{
			line_nr, 
			Token::Kind::Identifier,
			source_.substr( start, position_ - start )
		};
	}

	Token readNumber()
	{
		std::size_t start = position_;

		const char* begin = source_.c_str() + position_;
		char* end_i = nullptr;
		char* end_f = nullptr;

		int value_i = std::strtol( begin, &end_i, 10 );
		double value_f = std::strtod( begin, &end_f );

		if( end_i == begin && end_f == begin )
		{
			throw_error( line_nr, "Invalid number" );
		}
		if( end_i == end_f )
		{
			// same length of int and double => it is an int
			position_ = static_cast<std::size_t>( end_i - source_.c_str() );
			return
			{
				line_nr, 
				Token::Kind::Number,
				source_.substr( start, position_ - start ),
				value_i
			};
		}
		else
		{
			position_ = static_cast<std::size_t>( end_f - source_.c_str() );
			return
			{
				line_nr, 
				Token::Kind::Number,
				source_.substr( start, position_ - start ),
				value_f
			};
		}
	}

	Token readString()
	{
		position_++;
		std::size_t start = position_;

		while( position_ < source_.size() )
		{
			char c = source_[position_];

			if( c == '"' )
			{
				break;
			}
			if( c == '\n' )
			{
				throw_error( line_nr, "Unterminated string (EOL)" );
			}

			++position_;
		}
		while( position_ >= source_.size() )
		{
			throw_error( line_nr, "Unterminated string (EOF)" );
		}
		++position_;
		// std::cout << line_nr << ": str '" << source_.substr( start-1, position_ - start + 1 ) << "'" << std::endl;
		return
		{
			line_nr, 
			Token::Kind::String,
			source_.substr( start, position_ - start - 1 ),
			0.0
		};
	}
};

// ============================================================
// Abstract syntax tree
// ============================================================

struct Expression
{
	enum class Kind
	{
		Number,
		String,
		Identifier,
		Unary,
		Binary,
		Vector,
		ColonVector,
		Call,
		Assignment
	};
	// static constexpr std::string_view strKind( Kind kind )
	static constexpr const char* strKind( Kind kind )
	{
		switch( kind )
		{
			ENUM_STR_CASE( Kind, Number );
			ENUM_STR_CASE( Kind, String );
			ENUM_STR_CASE( Kind, Identifier );
			ENUM_STR_CASE( Kind, Unary );
			ENUM_STR_CASE( Kind, Binary );
			ENUM_STR_CASE( Kind, Vector );
			ENUM_STR_CASE( Kind, ColonVector );
			ENUM_STR_CASE( Kind, Call );
			ENUM_STR_CASE( Kind, Assignment );
			default: return "*unknown*";
		}
	}

	Kind kind;
	int line_nr;

	Value number;
	std::string text;

	char operation = '\0';

	std::unique_ptr<Expression> left;
	std::unique_ptr<Expression> right;

	std::vector<std::unique_ptr<Expression>> elements;

	std::string callName;
	std::vector<std::unique_ptr<Expression>> callArguments;

	static std::unique_ptr<Expression> numberValue( int line_nr, const Value& value )
	{
		auto result = std::make_unique<Expression>();
		result->kind = Kind::Number;
		result->line_nr = line_nr;
		result->number = value;
		return result;
	}

	static std::unique_ptr<Expression> stringValue( int line_nr, std::string& value )
	{
		auto result = std::make_unique<Expression>();
		result->kind = Kind::String;
		result->line_nr = line_nr;
		result->text = value;
		return result;
	}

	static std::unique_ptr<Expression> identifier( int line_nr, std::string name )
	{
		auto result = std::make_unique<Expression>();
		result->kind = Kind::Identifier;
		result->line_nr = line_nr;
		result->text = std::move( name );
		return result;
	}
};

struct Argument
{
	std::string name; // Empty means positional
	std::unique_ptr<Expression> value;
};

struct Node
{
	enum class Kind
	{
		UndefinedCall,
		BuiltinFunction,
		Primitive,
		Operation,
		Transform,
		Module,
		Function,
		For,
		Assignment
	};
	// static constexpr std::string_view strKind( Kind kind )
	static constexpr const char* strKind( Kind kind )
	{
		switch( kind )
		{
			ENUM_STR_CASE( Kind, UndefinedCall );
			ENUM_STR_CASE( Kind, BuiltinFunction );
			ENUM_STR_CASE( Kind, Primitive );
			ENUM_STR_CASE( Kind, Operation );
			ENUM_STR_CASE( Kind, Transform );
			ENUM_STR_CASE( Kind, Module );
			ENUM_STR_CASE( Kind, Function );
			ENUM_STR_CASE( Kind, For );
			ENUM_STR_CASE( Kind, Assignment );
			default: return "*unknown*";
		}
	}
	int line_nr;
	Kind kind = Kind::UndefinedCall;
	std::string name;
	std::vector<Argument> arguments;
	std::vector<std::unique_ptr<Node>> children;
	std::unique_ptr<Expression> assignmentValue;
	void print() const
	{
		std::cout << std::format( "Node {} kind {} argcnt {} childcnt {}",
			name, strKind(kind), arguments.size(), children.size()
		) << std::endl;
		for( int i=0; i<arguments.size(); i++ )
		{
			std::cout << std::format( "  arg {} name {} expr kind {}",
				i, arguments[i].name,
				Expression::strKind(arguments[i].value->kind)
			) << std::endl;
		}
	}
};

struct FunctionDefinition {
	std::string name;
	std::vector<std::string> parameters;
	std::unique_ptr<Expression> body;
};

struct ModuleDefinition {
	std::string name;
	std::vector<std::string> parameters;

	// The module body is represented as a list of geometry nodes.
	std::vector<std::unique_ptr<Node>> body;
};

struct ArgumentSpec
{
	enum class Kind
	{
		Bool,
		Int,
		Float,
		String,
		Vector2D,
		Vector3D,
		Vector4D,
		ListVector,
		ListVector2D,
		ListVector3D
	};
	// static constexpr std::string_view strKind( Kind kind )
	static constexpr const char* strKind( Kind kind )
	{
		switch( kind )
		{
			ENUM_STR_CASE( Kind, Bool );
			ENUM_STR_CASE( Kind, Int );
			ENUM_STR_CASE( Kind, Float );
			ENUM_STR_CASE( Kind, String );
			ENUM_STR_CASE( Kind, Vector2D );
			ENUM_STR_CASE( Kind, Vector3D );
			ENUM_STR_CASE( Kind, Vector4D );
			ENUM_STR_CASE( Kind, ListVector );
			ENUM_STR_CASE( Kind, ListVector2D );
			ENUM_STR_CASE( Kind, ListVector3D );
			default: return "*unknown*";
		}
	}
    std::string name;
    bool optional;
    Kind kind;
    Value default_value;
    //Value& get_default_value() const { return default_value; }
};

struct MatchResult
{
	bool success;
	int line_nr;
	std::string error;
	std::vector<Value> values;
	std::vector<bool> supplied;

	MatchResult( std::size_t size )
		: success( false ), line_nr(0),
		  values( size, false ), supplied( size, false )
	{
	}
};

/*

Operator precedence according to AI:

Precedence  Operators             Meaning
1           ( ... )               Parenthesized expression
2           ^                     Exponentiation
3           !, unary +, unary -   Logical negation, positive, negative
4           *, /, %               Multiplication, division, modulo
5           +, -                  Addition, subtraction
6           <, <=, >, >=          Relational comparisons
7           ==, !=                Equality comparisons
8           &&                    Logical AND
9           ||                    Logical OR
10          ? :                   Conditional expression

*/
class Parser
{
public:
	explicit Parser( std::string source )
		: lexer_( std::move( source ) )
	{
		current_ = lexer_.next();
		lookahead_ = lexer_.next();
	}

	std::vector<std::unique_ptr<Node>> parse()
	{
		std::cout << "********** parse **********" << std::endl;
		std::vector<std::unique_ptr<Node>> result;

		while( current_.kind != Token::Kind::End )
		{
			if( current_.kind == Token::Kind::Identifier &&
				current_.text == "function" )
			{
				parseFunctionDefinition();
			}
			else if( current_.kind == Token::Kind::Identifier &&
					   current_.text == "module" )
			{
				parseModuleDefinition();
			}
			else
			{
				result.push_back( parseStatement() );
			}
		}

		std::cout << "********** validateAllFunctions **********" << std::endl;
		validateAllFunctions( result );
		std::cout << "********** validateAllModules **********" << std::endl;
		validateAllModules( result );
		std::cout << "********** parse done **********" << std::endl;

		return result;
	}

	const std::unordered_map<std::string, ModuleDefinition>& get_modules()
	{
		return modules_;
	}

private:
	Lexer lexer_;
	Token current_;
	Token lookahead_;

	std::unordered_map<std::string, FunctionDefinition> functions_;
	std::unordered_map<std::string, ModuleDefinition> modules_;

	std::runtime_error error( const std::string& message ) const
	{
		return std::runtime_error( message );
	}

	void advance()
	{
		current_ = std::move( lookahead_ );
		lookahead_ = lexer_.next();
	}

	bool accept( Token::Kind kind )
	{
		if( current_.kind == kind )
		{
			advance();
			return true;
		}

		return false;
	}

	void expect( Token::Kind kind, const std::string& message )
	{
		if( !accept( kind ) )
		{
			throw_error( current_.line_nr, message );
		}
	}

	std::unique_ptr<Node> parseStatement()
	{
		if( current_.kind != Token::Kind::Identifier )
		{
			throw_error( current_.line_nr, "Expected an OpenSCAD call" );
		}

		if( current_.text == "if" )
		{
			return parseIfStatement();
		}

		if( current_.text == "for" )
		{
			return parseForLoop( false );
		}

		if( current_.text == "intersection_for" )
		{
			return parseForLoop( true );
		}

		if( lookahead_.kind == Token::Kind::Equal )
		{
        	return parseAssignment();
    	}

		return parseCallStatement();
	}

	std::unique_ptr<Expression> parseExpression()
	{
		return parseAddition();
	}

	std::unique_ptr<Expression> parseAddition()
	{
		auto result = parseMultiplication();

		while( current_.kind == Token::Kind::Plus ||
				current_.kind == Token::Kind::Minus )
		{
			char operation = current_.text[0];

			advance();

			auto right = parseMultiplication();

			auto expression = std::make_unique<Expression>();
			expression->kind = Expression::Kind::Binary;
			expression->line_nr = current_.line_nr;
			expression->operation = operation;
			expression->left = std::move( result );
			expression->right = std::move( right );

			result = std::move( expression );
		}

		return result;
	}

	std::unique_ptr<Expression> parseMultiplication()
	{
		auto result = parseUnary();

		while( current_.kind == Token::Kind::Star ||
				current_.kind == Token::Kind::Slash ||
				current_.kind == Token::Kind::Percent )
		{
			char operation = current_.text[0];

			advance();

			auto right = parseUnary();

			auto expression = std::make_unique<Expression>();
			expression->kind = Expression::Kind::Binary;
			expression->line_nr = current_.line_nr;
			expression->operation = operation;
			expression->left = std::move( result );
			expression->right = std::move( right );

			result = std::move( expression );
		}

		return result;
	}

	std::unique_ptr<Expression> parseUnary()
	{
		if( current_.kind == Token::Kind::Plus ||
			current_.kind == Token::Kind::Minus )
		{
			char operation = current_.text[0];

			advance();

			auto expression = std::make_unique<Expression>();
			expression->kind = Expression::Kind::Unary;
			expression->line_nr = current_.line_nr;
			expression->operation = operation;
			expression->left = parseUnary();

			return expression;
		}

		return parseExponentiation();
	}

	std::unique_ptr<Expression> parseExponentiation()
	{
		auto result = parseVectorAccess();

		while( current_.kind == Token::Kind::Circumflex )
		{
			char operation = current_.text[0];

			advance();

			auto right = parseVectorAccess();

			auto expression = std::make_unique<Expression>();
			expression->kind = Expression::Kind::Binary;
			expression->line_nr = current_.line_nr;
			expression->operation = operation;
			expression->left = std::move( result );
			expression->right = std::move( right );

			result = std::move( expression );
		}

		return result;
	}

	std::unique_ptr<Expression> parseVectorAccess()
	{
		auto result = parsePrimary();

		if( accept( Token::Kind::LeftBracket ) )
		{
			const char operation = '[';

			auto right = parseExpression();

			expect( Token::Kind::RightBracket, "Expected ']'" );

			auto expression = std::make_unique<Expression>();
			expression->kind = Expression::Kind::Binary;
			expression->line_nr = current_.line_nr;
			expression->operation = operation;
			expression->left = std::move( result );
			expression->right = std::move( right );

			result = std::move( expression );
		}

		return result;
	}

	std::unique_ptr<Expression> parsePrimary()
	{
		if( current_.kind == Token::Kind::Number )
		{
			Value value = current_.number;
			advance();
			return Expression::numberValue( current_.line_nr, value );
		}

		if( current_.kind == Token::Kind::String )
		{
			std::string value = current_.text;
			advance();
			return Expression::stringValue( current_.line_nr, value );
		}

		if( current_.kind == Token::Kind::Identifier )
		{
			std::string name = current_.text;
			advance();

			// Identifier followed by '(' means a function call.
			if( accept( Token::Kind::LeftParen ) )
			{
				auto result = std::make_unique<Expression>();
				result->kind = Expression::Kind::Call;
				result->line_nr = current_.line_nr;
				result->callName = std::move( name );

				if( current_.kind != Token::Kind::RightParen )
				{
					while( true )
					{
						result->callArguments.push_back( parseExpression() );

						if( !accept( Token::Kind::Comma ) )
						{
							break;
						}
					}
				}

				expect( Token::Kind::RightParen,
					   "Expected ')' after function arguments" );

				return result;
			}

			// Otherwise it is an identifier or variable.
			return Expression::identifier( current_.line_nr, std::move( name ) );
		}

		if( accept( Token::Kind::LeftParen ) )
		{
			auto result = parseExpression();
			expect( Token::Kind::RightParen, "Expected ')'" );
			return result;
		}

		if( accept( Token::Kind::LeftBracket ) )
		{
			auto result = std::make_unique<Expression>();
			result->kind = Expression::Kind::Vector;
			result->line_nr = current_.line_nr;

			if( current_.kind != Token::Kind::RightBracket )
			{
				while( true )
				{
					result->elements.push_back( parseExpression() );

					if( !accept( Token::Kind::Comma ) )
					{
						break;
					}
				}
			}

			expect( Token::Kind::RightBracket, "Expected ']'" );
			return result;
		}

		throw_error( current_.line_nr, "Expected expression" );
	}

	std::unique_ptr<Expression> parseForParam()
	{

		if( !accept( Token::Kind::LeftBracket ) )
		{
			throw_error( current_.line_nr, "Expected param for 'for' loop" );
		}

		auto result = std::make_unique<Expression>();
		result->kind = Expression::Kind::Vector;
		result->line_nr = current_.line_nr;
		Token::Kind separator_kind = Token::Kind::End;

		if( current_.kind != Token::Kind::RightBracket )
		{
			while( true )
			{
				result->elements.push_back( parseExpression() );

				if( separator_kind == Token::Kind::End )
				{
					if( current_.kind == Token::Kind::Comma )
					{
						separator_kind = Token::Kind::Comma;
					}
					else if( current_.kind == Token::Kind::Colon )
					{
						separator_kind = Token::Kind::Colon;
						result->kind = Expression::Kind::ColonVector;
					}
				}
				if( !accept( separator_kind ) )
				{
					break;
				}
			}
		}

		expect( Token::Kind::RightBracket, "Expected ']'" );
		return result;
	}

	std::unique_ptr<Node> parseCallStatement()
	{
		auto node = std::make_unique<Node>();
		node->line_nr = current_.line_nr;
		node->name = current_.text;
		if( isBuiltInFunction( node->name ) )
		{
			node->kind = Node::Kind::BuiltinFunction;
		}
		else if( isPrimitive( node->name ) )
		{
			node->kind = Node::Kind::Primitive;
		}
		else if( isOperation( node->name ) )
		{
			node->kind = Node::Kind::Operation;
		}
		else if( isTransform( node->name ) )
		{
			node->kind = Node::Kind::Transform;
		}
		else
		{
			node->kind = Node::Kind::UndefinedCall;
		}

		advance();

		expect( Token::Kind::LeftParen,
			   "Expected '(' after call name" );

		if( current_.kind != Token::Kind::RightParen )
		{
			while( true )
			{
				Argument argument;

				if( current_.kind == Token::Kind::Identifier &&
					lookahead_.kind == Token::Kind::Equal )
				{
					argument.name = current_.text;
					advance();

					expect( Token::Kind::Equal,
						   "Expected '=' in named argument" );
				}

				argument.value = parseExpression();
				node->arguments.push_back( std::move( argument ) );

				if( !accept( Token::Kind::Comma ) )
				{
					break;
				}
			}
		}

		expect( Token::Kind::RightParen,
			   "Expected ')' after call arguments" );

		if( accept( Token::Kind::LeftBrace ) )
		{
			while( current_.kind != Token::Kind::RightBrace )
			{
				if( current_.kind == Token::Kind::End )
				{
					throw_error( current_.line_nr, "Unterminated block" );
				}

				node->children.push_back( parseStatement() );
			}

			expect( Token::Kind::RightBrace,
				   "Expected '}'" );
		}
		else if( accept( Token::Kind::Semicolon ) )
		{
			// call without children
		}
		else
		{
			// OpenSCAD allows a single unbraced child statement.
			node->children.push_back( parseStatement() );
		}

		return node;
	}

	std::unique_ptr<Node> parseAssignment()
	{
		auto node = std::make_unique<Node>();
		node->line_nr = current_.line_nr;
		node->kind = Node::Kind::Assignment;

		node->name = current_.text;
		advance();

		expect( Token::Kind::Equal,
			   "Expected '=' after variable name" );

		node->assignmentValue = parseExpression();

		expect( Token::Kind::Semicolon,
			   "Expected ';' after variable assignment" );

		return node;
	}

	void parseFunctionDefinition()
	{
		// Consume "function".
		advance();

		if( current_.kind != Token::Kind::Identifier )
		{
			throw_error( current_.line_nr, "Expected function name" );
		}

		FunctionDefinition definition;
		definition.name = current_.text;
		advance();

		expect( Token::Kind::LeftParen, "Expected '(' after function name" );

		if( current_.kind != Token::Kind::RightParen )
		{
			while( true )
			{
				if( current_.kind != Token::Kind::Identifier )
				{
					throw_error( current_.line_nr, "Expected function parameter name" );
				}

				definition.parameters.push_back( current_.text );
				advance();

				if( !accept( Token::Kind::Comma ) )
				{
					break;
				}
			}
		}

		expect( Token::Kind::RightParen,
			   "Expected ')' after function parameters" );

		expect( Token::Kind::Equal, "Expected '=' after function parameters" );

		definition.body = parseExpression();

		expect( Token::Kind::Semicolon,
			   "Expected ';' after function definition" );

		if( functions_.contains( definition.name ) )
		{
			throw_error( current_.line_nr, "Duplicate function definition: " + definition.name );
		}

		functions_.emplace( definition.name, std::move( definition ) );
	}

	void parseModuleDefinition()
	{
		// Consume "module".
		advance();

		if( current_.kind != Token::Kind::Identifier )
		{
			throw_error( current_.line_nr, "Expected module name" );
		}

		ModuleDefinition definition;
		definition.name = current_.text;
		int definition_line = current_.line_nr;
		advance();

		expect( Token::Kind::LeftParen, "Expected '(' after module name" );

		if( current_.kind != Token::Kind::RightParen )
		{
			while( true )
			{
				if( current_.kind != Token::Kind::Identifier )
				{
					throw_error( current_.line_nr, "Expected module parameter name" );
				}

				definition.parameters.push_back( current_.text );
				advance();

				if( !accept( Token::Kind::Comma ) )
				{
					break;
				}
			}
		}

		expect( Token::Kind::RightParen,
			   "Expected ')' after module parameters" );

		expect( Token::Kind::LeftBrace,
			   "Expected '{' after module declaration" );

		while( current_.kind != Token::Kind::RightBrace )
		{
			if( current_.kind == Token::Kind::End )
			{
				throw_error( current_.line_nr, "Unterminated module body" );
			}

			definition.body.push_back( parseStatement() );
		}

		expect( Token::Kind::RightBrace, "Expected '}' after module body" );

		if( modules_.find( definition.name ) != modules_.end() )
		{
			throw_error( definition_line, "Duplicate module definition: " + definition.name );
		}

		modules_.emplace( definition.name, std::move(definition) );
	}

	std::unique_ptr<Node> parseIfStatement()
	{
		// Consume "if".
		advance();

		expect( Token::Kind::LeftParen, "Expected '(' after module name" );

		// ++++++++++ implement!!!!!
		throw_error( current_.line_nr, "not implemented" );
		std::unique_ptr<Node> node = std::make_unique<Node>();
		return node;
	}

	std::unique_ptr<Node> parseForLoop( bool intersection_for )
	{
		std::unique_ptr<Node> first_node = std::make_unique<Node>();
		Node* last_node = first_node.get();
		std::string node_name = current_.text;

		// Consume "for" or "intersection_for".
		advance();

		expect( Token::Kind::LeftParen, "Expected '(' after 'for'" );

		bool first = true;
		while( true )
		{
			if( first )
			{
				first = false;
			}
			else if( !accept( Token::Kind::Comma ) )
			{
				break;
			}
			else
			{
				last_node->children.push_back( std::make_unique<Node>() );
				last_node = last_node->children[0].get();
			}
			last_node->line_nr = current_.line_nr;
			last_node->name = node_name;
			last_node->kind = Node::Kind::For;
			Argument argument;
			argument.name = current_.text;
			expect( Token::Kind::Identifier,
				   "Expected variable name for the 'for' parameter" );
			expect( Token::Kind::Equal,
				   "Expected = after variable name for the 'for' parameter" );
			if( current_.kind != Token::Kind::LeftBracket )
			{
				expect( Token::Kind::LeftBracket,
					   "Expected a vector for the 'for' parameter" );
			}
			argument.value = parseForParam();
			last_node->arguments.push_back( std::move( argument ) );
		}

		expect( Token::Kind::RightParen,
			   "Expected ')' after 'for' parameters" );

		expect( Token::Kind::LeftBrace,
			   "Expected '{' after 'for' statement" );

		while( current_.kind != Token::Kind::RightBrace )
		{
			if( current_.kind == Token::Kind::End )
			{
				throw_error( current_.line_nr, "Unterminated 'for' body" );
			}

			last_node->children.push_back( parseStatement() );
		}

		expect( Token::Kind::RightBrace, "Expected '}' after 'for' body" );

		return first_node;
	}

	bool isModule( const std::string& name ) const
	{
		return modules_.find( name ) != modules_.end();
	}

	bool isFunction( const std::string& name ) const
	{
		return functions_.find( name ) != functions_.end();
	}

	void validateModuleCall( const Node& node )
	{
		auto iterator = modules_.find(node.name);

		if( iterator == modules_.end() )
		{
			return;
		}

		const ModuleDefinition& definition = iterator->second;

		if( node.arguments.size() != definition.parameters.size() )
		{
			throw_error( node.line_nr,
				"Module '" + node.name +
				"' expects " +
				std::to_string( definition.parameters.size() ) +
				" arguments, but received " +
				std::to_string( node.arguments.size() ) );
		}
	}

	void validateNode( Node& node )
	{
		// std::cout << "        ** validate node -> expr **" << std::endl;
		for( const auto& argument : node.arguments )
		{
			validateExpression( *argument.value );
		}

		if( node.kind != Node::Kind::Assignment )
		{
			// std::cout << "        ** validate node -> known **" << std::endl;
			switch( node.kind )
			{
				case Node::Kind::UndefinedCall:
					if( isModule( node.name ) )
					{
						node.kind = Node::Kind::Module;
					}
					else if( isFunction( node.name ) )
					{
						node.kind = Node::Kind::Function;
					}
					else
					{
						throw_error( node.line_nr, "Unknown call: " + node.name );
					}
					break;
				case Node::Kind::BuiltinFunction:
					break;
				case Node::Kind::Primitive:
					break;
				case Node::Kind::Operation:
					break;
				case Node::Kind::Transform:
					break;
				case Node::Kind::Module:
					break;
			}

			// std::cout << "        ** validate node -> call **" << std::endl;
			if( modules_.find( node.name ) != modules_.end() )
			{
				validateModuleCall( node );
			}
		}

		// std::cout << "        ** validate node -> child **" << std::endl;
		for( auto& child : node.children )
		{
			validateNode( *child );
		}
	}

	void validateCall( const Expression& call )
	{
		auto iterator = functions_.find( call.callName );

		if( iterator == functions_.end() )
		{
			return; // It may be a built-in function.
		}

		const FunctionDefinition& definition = iterator->second;

		if( call.callArguments.size() != definition.parameters.size() )
		{
			throw_error( call.line_nr,
				"Function '" + call.callName +
				"' expects " +
				std::to_string( definition.parameters.size() ) +
				" arguments, but received " +
				std::to_string( call.callArguments.size() ) );
		}
	}

	void validateAllModules(
		std::vector<std::unique_ptr<Node>>& roots )
	{
		std::cout << "     ***** validate body *****" << std::endl;
		// Validate every module body.
		for( auto& [ name, module ] : modules_ )
		{
			for( auto& bodyNode : module.body )
			{
				validateNode( *bodyNode );
			}
		}

		std::cout << "     ***** validate geometry *****" << std::endl;
		// Validate top-level geometry.
		for( auto& root : roots )
		{
			validateNode( *root );
		}
	}

	void validateExpression( const Expression& expression )
	{
		switch( expression.kind )
		{
			case Expression::Kind::Number:
				return;

			case Expression::Kind::Identifier:
				return;

			case Expression::Kind::Unary:
				validateExpression( *expression.left );
				return;

			case Expression::Kind::Binary:
				validateExpression( *expression.left );
				validateExpression( *expression.right );
				return;

			case Expression::Kind::Vector:
			case Expression::Kind::ColonVector:
				for( const auto& element : expression.elements )
				{
					validateExpression( *element );
				}
				return;

			case Expression::Kind::Call: {
				bool known =
					isBuiltInFunction( expression.callName ) ||
					functions_.find( expression.callName ) != functions_.end();

				if( !known )
				{
					throw_error( expression.line_nr,
						"Unknown function: " + expression.callName );
				}

				validateCall( expression );

				for( const auto& argument : expression.callArguments )
				{
					validateExpression( *argument );
				}

				return;
			}
		}
	}

	void validateAllFunctions( const std::vector<std::unique_ptr<Node>>& roots )
	{

		for( auto& entry : functions_ )
		{
			FunctionDefinition& definition = entry.second;
			validateExpression( *definition.body );
		}

		for( auto& root : roots )
		{
			validateNode( *root );
		}
	}

public:
	static bool isBuiltInFunction( const std::string& name )
	{
		static const std::unordered_set<std::string> builtIns = {
			"abs",
			"acos",
			"asin",
			"atan",
			"atan2",
			"ceil",
			"concat",
			"cos",
			"cross",
			"exp",
			"floor",
			"len",
			"let",
			"ln",
			"log",
			"lookup",
			"max",
			"min",
			"norm",
			"pow",
			"rands",
			"round",
			"sign",
			"sin",
			"sqrt",
			"tan"
		};

		return builtIns.find( name ) != builtIns.end();
	}

	static bool isPrimitive( const std::string& name )
	{
		static const std::unordered_set<std::string> primitives = {
			"cube",
			"sphere",
			"cylinder",
			"polyhedron",
			"polygon",
			"circle",
			"square",
			"text",
			"surface",
			"import"
		};

		return primitives.find( name ) != primitives.end();
	}

	static bool isOperation( const std::string& name )
	{
		static const std::unordered_set<std::string> operations = {
			"union",
			"difference",
			"intersection",
			"hull",
			"minkowski",
			"linear_extrude",
			"rotate_extrude",
			"resize",
			"projection",
			"color"
		};

		return operations.find( name ) != operations.end();
	}

	static bool isTransform( const std::string& name )
	{
		static const std::unordered_set<std::string> transforms = {
			"translate",
			"rotate",
			"scale",
			"mirror"
		};

		return transforms.find( name ) != transforms.end();
	}

};

// ============================================================
// Runtime values and expression evaluation
// ============================================================

using Environment = std::unordered_map<std::string, Value>;

class Evaluator
{
    std::mt19937_64 generator;
public:
	Evaluator()
	{
		std::random_device randomDevice;
		generator = std::mt19937_64( randomDevice() );
	}

	Value evaluate( const Expression& expression,
				const Environment& environment )
	{
		global_line_nr = expression.line_nr;
		Value value = do_evaluate( expression, environment );
		global_line_nr = 0;
		return value;
	}

	Value do_evaluate( const Expression& expression,
				const Environment& environment )
	{
		switch( expression.kind )
		{
			case Expression::Kind::Number:
				return expression.number;

			case Expression::Kind::String:
				return expression.text;

			case Expression::Kind::Identifier:
			{
				if( expression.text == "PI" )
				{
					return 3.14159265358979323846;
				}
				if( expression.text == "false" )
				{
					return false;
				}
				if( expression.text == "true" )
				{
					return true;
				}

				auto iterator = environment.find( expression.text );
				if( iterator != environment.end() )
				{
					return iterator->second;
				}

				throw_error( expression.line_nr, 
						"Unknown identifier: " + expression.text );
			}

			case Expression::Kind::Vector:
			case Expression::Kind::ColonVector:
			{
				Value::Vector result;

				for( const auto& element : expression.elements )
				{
					result.push_back( evaluate( *element, environment ) );
				}

				return result;
			}

			case Expression::Kind::Unary:
			{
				// ++++ us Value directly instead of asDouble
				Value value = evaluate( *expression.left, environment );

				if( expression.operation == '-' )
				{
					return -value;
				}

				// +++ expression.operation is just ignored here ???
				return value;
			}

			case Expression::Kind::Binary:
			{
				Value left = evaluate( *expression.left, environment );
				Value right = evaluate( *expression.right, environment );
				switch( expression.operation )
				{
					case '+':
						return left + right;
					case '-':
						return left - right;
					case '*':
						return left * right;
					case '/':
						return left / right;
					case '^':
						return std::pow( left.asDouble(), right.asDouble() );
					case '[':
						if( !left.isVector() )
						{
							throw_error( expression.line_nr, std::format(
									"Expected a vector for element access",
									expression.operation ) );
						}
						return left[right.asInt()];
					default:
						throw_error( expression.line_nr, std::format(
								"Unknown binary operator '{}'",
								expression.operation ) );
				}
			}

			case Expression::Kind::Call:
				if( Parser::isBuiltInFunction( expression.callName ) )
				{
					return evaluateBuiltin( expression, environment );
				}
				throw_error( expression.line_nr,
					"Call " + expression.callName + " !!!" );
				break;
			//case Expression::Kind::Assignment:
				//break;
			default:
				throw_error( expression.line_nr,
					std::string("Invalid expression ") + Expression::strKind(expression.kind) );
		}
	}

	Value evaluateBuiltin( const Expression& expression,
				const Environment& environment )
	{
		if( expression.callName == "abs" )
		{
			checkArgCount( expression, 1, 1 );
			double value = getFloatArg( expression, environment, 0 );
			return std::fabs( value );
		}
		else if( expression.callName == "acos" )
		{
			checkArgCount( expression, 1, 1 );
			double value = getFloatArg( expression, environment, 0 );
			if( !std::isfinite(value) || value < -1.0 || value > 1.0 )
			{
				throw_error( expression.line_nr,
					"The input of acos() must be in the range -1 to +1." );
			}
			return std::acos( value );
		}
		else if( expression.callName == "asin" )
		{
			checkArgCount( expression, 1, 1 );
			double value = getFloatArg( expression, environment, 0 );
			if( !std::isfinite(value) || value < -1.0 || value > 1.0 )
			{
				throw_error( expression.line_nr,
					"The input of asin() must be in the range -1 to +1." );
			}
			return std::asin( value );
		}
		else if( expression.callName == "atan" )
		{
			checkArgCount( expression, 1, 1 );
			double value = getFloatArg( expression, environment, 0 );
			return std::atan( value );
		}
		else if( expression.callName == "atan2" )
		{
			checkArgCount( expression, 2, 2 );
			double y = getFloatArg( expression, environment, 0 );
			double x = getFloatArg( expression, environment, 1 );
			if( !std::isfinite(y) || !std::isfinite(x) )
			{
				throw_error( expression.line_nr,
					"The inputs of atan2() must be finite numbers." );
			}
			if( y == 0.0 && x == 0.0 )
			{
				throw_error( expression.line_nr,
					"The inputs of atan2() must not both be zero." );
			}
			return std::atan2( y, x );
		}
		else if( expression.callName == "ceil" )
		{
			checkArgCount( expression, 1, 1 );
			double value = getFloatArg( expression, environment, 0 );
			return std::ceil( value );
		}
		/*
		else if( expression.callName == "concat" )
		{
		}
		*/
		else if( expression.callName == "cos" )
		{
			checkArgCount( expression, 1, 1 );
			double value = getFloatArg( expression, environment, 0 );
			return std::cos( value );
		}
		else if( expression.callName == "cross" )
		{
			checkArgCount( expression, 2, 2 );
			Value v0 = getValueArg( expression, environment, 0 );
			Value v1 = getValueArg( expression, environment, 1 );
			if( !v0.isNumVector( -2 ) || !v1.isNumVector( -2 ) ||
				v0.length() != v1.length() || v0.length() > 3 )
			{
				throw_error( expression.line_nr, std::format(
					"Function {} arguments must be vectors of same length 2 or 3.",
					expression.callName ) );
			}
			double x = 0;
			double y = 0;
			double z;
			z = v0.vAsDouble( 0 ) * v1.vAsDouble( 1 ) -
				v0.vAsDouble( 1 ) * v1.vAsDouble( 0 );
			if( v0.length() > 2 )
			{
				x = v0.vAsDouble( 1 ) * v1.vAsDouble( 2 ) -
					v0.vAsDouble( 2 ) * v1.vAsDouble( 1 );
				y = v0.vAsDouble( 2 ) * v1.vAsDouble( 0 ) -
					v0.vAsDouble( 0 ) * v1.vAsDouble( 2 );
			}
			return Value::Vector{ x, y, z };
		}
		else if( expression.callName == "exp" )
		{
			checkArgCount( expression, 1, 1 );
			double value = getFloatArg( expression, environment, 0 );
			return std::exp( value );
		}
		else if( expression.callName == "floor" )
		{
			checkArgCount( expression, 1, 1 );
			double value = getFloatArg( expression, environment, 0 );
			return std::floor( value );
		}
		else if( expression.callName == "len" )
		{
			checkArgCount( expression, 1, 1 );
			return getValueArg( expression, environment, 0 ).length();
		}
		else if( expression.callName == "let" )
		{
			// let: what an annoying stupid idea...
			throw_error( expression.line_nr,
				"let() is not implemented yet." );
		}
		else if( expression.callName == "ln" )
		{
			checkArgCount( expression, 1, 1 );
			double value = getFloatArg( expression, environment, 0 );
			if( value <= 0 )
			{
				throw_error( expression.line_nr,
					"The input of ln() must be positive." );
			}
			return std::log( value );
		}
		else if( expression.callName == "log" )
		{
			checkArgCount( expression, 1, 1 );
			double value = getFloatArg( expression, environment, 0 );
			if( value <= 0 )
			{
				throw_error( expression.line_nr,
					"The input of log() must be positive." );
			}
			return std::log10( value );
		}
		else if( expression.callName == "lookup" )
		{
			checkArgCount( expression, 2, 2 );
			double value = getFloatArg( expression, environment, 0 );
			Value tablevalue = getValueArg( expression, environment, 1 );
			if( !tablevalue.isNumVector( -2, 2 ) )
			{
				throw_error( expression.line_nr,
					"table must be a vector of at least two number pairs" );
			}
			Value::Vector table = tablevalue.asVector();
			int last = table.size() - 1;
			double low = table[0].vAsDouble( 0 );
			double high = table[last].vAsDouble( 0 );
			if( value <= low )
			{
				return table[0].vAsDouble( 1 );
			}
			if( value >= high )
			{
				return table[last].vAsDouble( 1 );
			}
			int i;
			for( i=0; i<last; i++ )
			{
				low = high;
				high = table[i+1].vAsDouble( 0 );
				if( value > low && value <= high )
				{
					break;
				}
			}
			if( i >= last )
			{
				return table[last].vAsDouble( 1 );
			}
			low = table[i].vAsDouble( 0 );
			double ratio = (value - low) / (high - low);
			low = table[i].vAsDouble( 1 );
			high = table[i+1].vAsDouble( 1 );
			return low * (1.0 - ratio) + high * ratio;
		}
		else if( expression.callName == "max" )
		{
			// argument can also be a vector
			int cnt = checkArgCount( expression, 1, -1 );
			double max = getFloatArg( expression, environment, 0 );
			for( int i=cnt-1; i>0; i-- )
			{
				double v = getFloatArg( expression, environment, i );
				if( v > max )
				{
					max = v;
				}
			}
			return max;
		}
		else if( expression.callName == "min" )
		{
			// argument can also be a vector
			int cnt = checkArgCount( expression, 1, -1 );
			double min = getFloatArg( expression, environment, 0 );
			for( int i=cnt-1; i>0; i-- )
			{
				double v = getFloatArg( expression, environment, i );
				if( v < min )
				{
					min = v;
				}
			}
			return min;
		}
		else if( expression.callName == "norm" )
		{
			checkArgCount( expression, 1, 1 );
			if( expression.callArguments.size() != 1 )
			{
				throw_error( expression.line_nr,
					"norm() expects exactly one argument." );
			}
			auto arg = getValueArg( expression, environment, 0 );
			if( !arg.isNumVector( 0 ) )
			{
				throw_error( expression.line_nr,
					"norm() expects a vector of numbers." );
			}
			double sum = 0.0;
			for( const Value& v : arg.asVector() )
			{
				double vv = v.asDouble();
				sum += vv * vv;
			}
			return std::sqrt( sum );
		}
		else if( expression.callName == "pow" )
		{
			checkArgCount( expression, 2, 2 );
			double base = getFloatArg( expression, environment, 0 );
			double expo = getFloatArg( expression, environment, 1 );
			return std::pow( base, expo );
		}
		else if( expression.callName == "rands" )
		{
			int argcnt = checkArgCount( expression, 3, 4 );
			double min = getFloatArg( expression, environment, 0 );
			double max = getFloatArg( expression, environment, 1 );
			double cnt = getFloatArg( expression, environment, 2 );
			if( min >= max )
			{
				throw_error( expression.line_nr,
					"minimum must be less than maximum"
				);
			}
			if( argcnt == 4 )
			{
				union {
					std::uint64_t u;
					double f;
				} seed;
				seed.u = 0;
				seed.f = getFloatArg( expression, environment, 3 );
				generator.seed( seed.u );
			}
			std::uniform_real_distribution<double> distribution( min, max );
			std::vector<Value> values( cnt );
			for( Value& value : values) {
				value = distribution( generator );
			}
			return values;
		}
		else if( expression.callName == "round" )
		{
			checkArgCount( expression, 1, 1 );
			double value = getFloatArg( expression, environment, 0 );
			return std::round( value );
		}
		else if( expression.callName == "sign" )
		{
			checkArgCount( expression, 1, 1 );
			double value = getFloatArg( expression, environment, 0 );
			if( value == 0.0 )
			{
				return 0.0;
			}
			else if( value > 0.0 )
			{
				return 1.0;
			}
			else if( value < 0.0 )
			{
				return -1.0;
			}
			else
			{
				return value;
			}
		}
		else if( expression.callName == "sin" )
		{
			checkArgCount( expression, 1, 1 );
			double value = getFloatArg( expression, environment, 0 );
			return std::sin( value );
		}
		else if( expression.callName == "sqrt" )
		{
			checkArgCount( expression, 1, 1 );
			double value = getFloatArg( expression, environment, 0 );
			return std::sqrt( value );
		}
		else if( expression.callName == "tan" )
		{
			checkArgCount( expression, 1, 1 );
			double value = getFloatArg( expression, environment, 0 );
			return std::tan( value );
		}
		else
		{
			throw_error( expression.line_nr, std::format(
				"Unknown function {}.",
				expression.callName ) );
		}
	}

	int checkArgCount( const Expression& expression, int imin, int imax )
	{
		int cnt = expression.callArguments.size();
		if( imin == imax && cnt != imax )
		{
			throw_error( expression.line_nr, std::format(
				"Function {} needs exactly {} arguments, {} given.",
				expression.callName, imax, expression.callArguments.size() ) );
		}
		else if( cnt < imin )
		{
			throw_error( expression.line_nr, std::format(
				"Function {} needs at least {} arguments, {} given.",
				expression.callName, imin, expression.callArguments.size() ) );
		}
		else if( imax >= 0 && cnt > imax )
		{
			throw_error( expression.line_nr, std::format(
				"Function {} needs at most {} arguments, {} given.",
				expression.callName, imax, expression.callArguments.size() ) );
		}
		return cnt;
	}

	Value getValueArg( const Expression& expression,
				const Environment& environment, int i )
	{
		return evaluate( *expression.callArguments[i], environment );
	}

	double getFloatArg( const Expression& expression,
				const Environment& environment, int i )
	{
		return getValueArg( expression, environment, i ).asDouble();
	}

};

// ============================================================
// Transformation matrix
// ============================================================

// maybe this whole thing could be replaced bygp_Trsf?
struct Transform
{
	std::array<double, 16> values{};
	bool simple;

	static Transform identity()
	{
		Transform result;

		result.values =
		{
			1, 0, 0, 0,
			0, 1, 0, 0,
			0, 0, 1, 0,
			0, 0, 0, 1
		};
		result.simple = true;

		return result;
	}

	static Transform translation( double x, double y, double z )
	{
		Transform result = identity();

		result.values[3] = x;
		result.values[7] = y;
		result.values[11] = z;

		return result;
	}

	static Transform scaling( double x, double y, double z )
	{
		Transform result = identity();

		result.values[0] = x;
		result.values[5] = y;
		result.values[10] = z;

		if( x != y || x != z )
		{
			result.simple = false;
		}

		return result;
	}

	/*
		 0  1  2  3 
		 4  5  6  7 
		 8  9 10 11 
		12 13 14 15 
	*/
	static Transform rotationX( double degrees )
	{
		double radians = d2r( degrees );
		double c = std::cos( radians );
		double s = std::sin( radians );

		Transform result = identity();

		result.values[5] = c;
		result.values[6] = -s;
		result.values[9] = s;
		result.values[10] = c;

		return result;
	}

	static Transform rotationY( double degrees )
	{
		double radians = d2r( degrees );
		double c = std::cos( radians );
		double s = std::sin( radians );

		Transform result = identity();

		result.values[0] = c;
		result.values[2] = s;
		result.values[8] = -s;
		result.values[10] = c;

		return result;
	}

	static Transform rotationZ( double degrees )
	{
		double radians = d2r( degrees );
		double c = std::cos( radians );
		double s = std::sin( radians );

		Transform result = identity();

		result.values[0] = c;
		result.values[1] = -s;
		result.values[4] = s;
		result.values[5] = c;

		return result;
	}

	static Transform rotationV( double degrees, double x, double y, double z )
	{
		const double axisLength = std::sqrt( x * x + y * y + z * z );

		if( axisLength <= 0.0 )
		{
			throw std::invalid_argument(
				"Rotation axis must not be zero" );
		}

		// Normalize the rotation axis.
		x /= axisLength;
		y /= axisLength;
		z /= axisLength;

		double radians = d2r( degrees );

		const double c = std::cos( radians );
		const double s = std::sin( radians );
		const double t = 1.0 - c;

		Transform result;

		result.values = {
			// Row 0
			t * x * x + c,
			t * x * y - s * z,
			t * x * z + s * y,
			0.0,

			// Row 1
			t * x * y + s * z,
			t * y * y + c,
			t * y * z - s * x,
			0.0,

			// Row 2
			t * x * z - s * y,
			t * y * z + s * x,
			t * z * z + c,
			0.0,

			// Row 3
			0.0,
			0.0,
			0.0,
			1.0
		};

		return result;
	}

	bool is_simple() { return simple; }

	// simple transform, can't change objects (too much)
	gp_Trsf toTrsf() const
	{
		gp_Trsf result;

		result.SetValues(
			values[0],  values[1],  values[2],  values[3],   // row 1
			values[4],  values[5],  values[6],  values[7],   // row 2
			values[8],  values[9],  values[10], values[11]   // row 3
		);

		return result;
	}

	// general transform that can change a sphere into an egg
	gp_GTrsf toGTrsf() const
	{
		gp_GTrsf result;
		gp_Mat vectorialPart(
			values[0], values[1], values[2],
			values[4], values[5], values[6],
			values[8], values[9], values[10] );
		result.SetVectorialPart( vectorialPart );
		result.SetTranslationPart( gp_XYZ( values[3], values[7], values[11] ) );
		return result;
	}

	void print() const
	{
		std::cout << std::showpos << std::fixed << std::setprecision(2);
		std::cout << "  " << values[0] << "  " << values[1]
				<< "  " << values[2] << "  " << values[3] << std::endl;
		std::cout << "  " << values[4] << "  " << values[5]
				<< "  " << values[6] << "  " << values[7] << std::endl;
		std::cout << "  " << values[8] << "  " << values[9]
				<< "  " << values[10] << "  " << values[11] << std::endl;
		std::cout << "  " << values[12] << "  " << values[13]
				<< "  " << values[14] << "  " << values[15] << std::endl;
		std::cout << std::defaultfloat;
	}
};

static Transform operator*( const Transform& a, const Transform& b )
{
	Transform result;

	for( int row = 0; row < 4; ++row )
	{
		for( int column = 0; column < 4; ++column )
		{
			double value = 0.0;

			for( int k = 0; k < 4; ++k )
			{
				value +=
					a.values[row * 4 + k] *
					b.values[k * 4 + column];
			}

			result.values[row * 4 + column] = value;
		}
	}
	result.simple = a.simple && b.simple;

	return result;
};

// ============================================================
// Tree traversal and operation execution
// ============================================================

struct Color
{
	uint8_t a;
	uint8_t r;
	uint8_t g;
	uint8_t b;
	Color() : a(0xFF), r(0), g(0), b(0) {}
	Color( uint8_t _r, uint8_t _g, uint8_t _b )
	: a(255), r(_r), g(_g), b(_b) {}
	Quantity_Color toQColor() const
	{
		double dr = r * (1.0/255);
		double dg = g * (1.0/255);
		double db = b * (1.0/255);
		return Quantity_Color( dr, dg, db, Quantity_TOC_RGB );
	}
};

struct ColoredFace
{
    TopoDS_Face face;
	Color color;
	operator const TopoDS_Face&() const { return face; }
	operator const Quantity_Color() const { return color.toQColor(); }
	bool IsSame( const TopoDS_Face& other ) const
	{
		return face.IsSame( other );
	}
};

struct ColoredShape
{
	TopoDS_Shape shape;
	std::vector<ColoredFace> faces;
	operator const TopoDS_Shape&() const { return shape; }
	ColoredShape& operator=( const TopoDS_Shape& newshape )
	{
		shape = newshape;
		return *this;
	}
	bool IsNull() const { return shape.IsNull(); }
	// bool IsSame( const TopoDS_Shape& other ) { return shape.IsSame( other ); }
	auto ShapeType() const { return shape.ShapeType(); }

	bool has_face( const TopoDS_Face& search_face ) const
	{
		for( const ColoredFace& resultFace : faces )
		{
			if( resultFace.face.IsSame( search_face ) )
			{
				return true;
			}
		}

		return false;
	}

	bool find_face_color( const TopoDS_Face& search_face, Color& color ) const
	{
		for( const ColoredFace& face : faces )
		{
			if( face.IsSame( search_face ) )
			{
				color = face.color;
				return true;
			}
		}
		return false;
	}

	void add_history_if_new( const TopTools_ListOfShape& new_faces,
						const Color& color )
	{
		for( TopTools_ListIteratorOfListOfShape it( new_faces );
					it.More(); it.Next() )
		{
			const TopoDS_Shape& shape = it.Value();

			if( shape.IsNull() || shape.ShapeType() != TopAbs_FACE )
			{
				continue;
			}

			const TopoDS_Face& face = TopoDS::Face( shape );

			if( has_face( face ) )
			{
				continue;
			}

			ColoredFace colored_face;
			colored_face.face = face;
			colored_face.color = color;

			faces.push_back( colored_face );
		}
	}

	void add_history( const ColoredShape& old_shape,
						BRepBuilderAPI_MakeShape& operation )
	{
		for( TopExp_Explorer it( old_shape, TopAbs_FACE ); it.More(); it.Next() )
		{
			const TopoDS_Face& face = TopoDS::Face( it.Current() );

			Color color;

			if( !old_shape.find_face_color( face, color ) )
			{
				continue;
			}

			const TopTools_ListOfShape& modified = operation.Modified( face );
			add_history_if_new( modified, color );

			const TopTools_ListOfShape& generated = operation.Generated( face );
			add_history_if_new( generated, color );

			if( modified.IsEmpty() && generated.IsEmpty() &&
				!operation.IsDeleted( face ) )
			{
				TopTools_ListOfShape unchanged;
				for( TopExp_Explorer it( shape, TopAbs_FACE ); it.More(); it.Next() )
				{
					const TopoDS_Face& new_face = TopoDS::Face( it.Current() );
					if( new_face.IsSame( face ) )
					{
						unchanged.Append( new_face );
					}
					else if( new_face.IsPartner( face ) )
					{
						unchanged.Append( new_face );
					}
				}
				add_history_if_new( unchanged, color );
			}
		}
	}

};

namespace GfxOps
{
	/*====================================================================
	 *
	 *  projection( cut=true )
	 *
	 */

	ColoredShape cross_section( const ColoredShape& shape3d )
	{
		/* The angular tolerance in radians used to determine whether
		 * input wires belong to the same plane.
		 */
		const double angular_tolerance = 1.0e-8;

		if( shape3d.IsNull() )
		{
			throw_error( -1, "specified shape is null." );
		}

		// The global XY plane: z = 0
		const gp_Pln sectionPlane(
			gp_Pnt( 0.0, 0.0, 0.0 ),
			gp_Dir( 0.0, 0.0, 1.0 )
		);

		// Compute the intersection between the 3D shape and z = 0.
		//
		// The result of BRepAlgoAPI_Section is normally a compound
		// containing intersection edges and, possibly, vertices.
		BRepAlgoAPI_Section section( shape3d, sectionPlane, false );

		section.Build();

		if( !section.IsDone() )
		{
			throw_error( -1, "BRepAlgoAPI_Section failed" );
		}

		TopoDS_Compound compound;
		BRep_Builder builder;
		builder.MakeCompound( compound );
		for( TopExp_Explorer it( section.Shape(), TopAbs_EDGE );
						 it.More(); it.Next() )
		{
			builder.Add( compound, it.Current() );
		}
		const TopoDS_Shape sectionEdges = TopoDS_Shape( compound );

		// No intersection edges means that the section is empty.
		if( sectionEdges.IsNull() )
		{
			throw_error( -1, "there is no intersection at Z=0" );
		}

		// Connect coincident section edges into planar wires.
		TopoDS_Shape wires;

		const int wireError = BOPAlgo_Tools::EdgesToWires(
			sectionEdges,
			wires,
			false,                 // perform edge intersection/sharing
			1.0e-8                 // angular tolerance
		);

		if( wireError != 0 )
		{
			throw_error( -1, "Unable to construct section wires" );
		}

		// Convert nested wires into planar faces.
		//
		// This handles:
		//   outer boundary -> face
		//   contained wire  -> hole
		//   island in hole  -> separate face
		//   arbitrary nesting depth
		TopoDS_Shape faces;

		if( !BOPAlgo_Tools::WiresToFaces( wires, faces, angular_tolerance ) )
		{
			throw_error( -1, "Unable to construct section faces" );
		}

		// this loses all color. should i care?
		return { faces };
	}

	/*====================================================================
	 *
	 *  projection( cut=false )
	 *
	 */

	ColoredShape ortho_projection( const ColoredShape& shape3d )
	{
		/* The angular tolerance in radians used to determine whether
		 * input wires belong to the same plane.
		 */
		const double angular_tolerance = 1.0e-8;

		if( shape3d.IsNull() )
		{
			throw_error( -1, "specified shape is null." );
		}

		/*
		 * Orthographic projection along the positive Z axis.
		 *
		 * The Z axis of this coordinate system is the projection direction.
		 * The Y axis defines the vertical direction in the projection plane.
		 */
		const gp_Ax2 projectionSystem(
			gp_Pnt( 0.0, 0.0, 0.0 ),
			gp_Dir( 0.0, 0.0, 1.0 ),
			gp_Dir( 0.0, 1.0, 0.0 )
		);

		const HLRAlgo_Projector projector( projectionSystem );

		Handle( HLRBRep_Algo ) hlr = new HLRBRep_Algo();

		hlr->Add( shape3d );

		/*
		 * HLRBRep_Algo::Projector() takes an HLRAlgo_Projector object,
		 * not a Prs3d_Projector handle.
		 */
		hlr->Projector( projector );
		hlr->Update();
		hlr->Hide();

		HLRBRep_HLRToShape hlrToShape( hlr );

		const TopoDS_Shape projectedOutlines = hlrToShape.OutLineVCompound();

		if( projectedOutlines.IsNull() )
		{
			throw_error( -1, "projectedOutlines is null." );
		}

#if 0
		// Continue here with the edge extraction, EdgesToWires(),
		// and WiresToFaces() code from the previous example.

		return projectedOutlines;
		if( shape3d.IsNull() )
		{
			throw_error( -1, "specified shape is null." );
		}

		/* The angular tolerance in radians used to determine whether
		 * input wires belong to the same plane.
		 */
		const double angular_tolerance = 1.0e-8;

		/*
		 * Orthographic projection along the Z axis.
		 *
		 * The projection direction is +Z in this example. Since the sun
		 * is assumed to be above the object, reversing the direction to
		 * -Z gives the same projected shadow outline for an orthographic
		 * projection.
		 *
		 * The last vector is the "up" direction in the projection plane.
		 */
		const bool isPerspective = false;

		const double focus = 1.0;

		const double projectionX = 0.0;
		const double projectionY = 0.0;
		const double projectionZ = 1.0;

		const double eyeX = 0.0;
		const double eyeY = 0.0;
		const double eyeZ = 0.0;

		const double upX = 0.0;
		const double upY = 1.0;
		const double upZ = 0.0;

		Handle( Prs3d_Projector ) projector =
			new Prs3d_Projector(
				isPerspective,
				focus,
				projectionX,
				projectionY,
				projectionZ,
				eyeX,
				eyeY,
				eyeZ,
				upX,
				upY,
				upZ
			);

		Handle( HLRBRep_Algo ) hlr = new HLRBRep_Algo();

		hlr->Add( shape3d );
		hlr->Projector( projector->Projector() );
		hlr->Update();
		hlr->Hide();

		HLRBRep_HLRToShape hlrToShape( hlr );

		/*
		 * OutLineVCompound() contains the projected outline edges.
		 *
		 * This is preferable to VCompound() for a shadow because VCompound()
		 * can also contain internal visible edges that are not part of the
		 * external shadow boundary.
		 */
		const TopoDS_Shape projectedOutlines = hlrToShape.OutLineVCompound();

		if( projectedOutlines.IsNull() )
		{
			throw_error( -1, "projectedOutlines is null." );
		}
#endif

		/*
		 * Put all projected edges into one compound. HLR may return a
		 * compound containing edges and other auxiliary topology.
		 */
		TopoDS_Compound projectedEdges;
		BRep_Builder builder;

		builder.MakeCompound( projectedEdges );

		for( TopExp_Explorer explorer( projectedOutlines, TopAbs_EDGE );
					 explorer.More(); explorer.Next() )
		{
			builder.Add( projectedEdges, explorer.Current() );
		}

		if( projectedEdges.IsNull() )
		{
			throw_error( -1, "projectedEdges is null." );
		}

		/*
		 * Connect coincident projected edges into closed wires.
		 */
		TopoDS_Shape projectedWires;

		const Standard_Integer wireError =
			BOPAlgo_Tools::EdgesToWires(
				projectedEdges,
				projectedWires,
				Standard_False,
				angular_tolerance
			);

		if( wireError != 0 )
		{
			throw_error( -1, "Unable to construct projected shadow wires" );
		}

		if( projectedWires.IsNull() )
		{
			throw_error( -1, "projectedWires is null." );
		}

		/*
		 * Convert nested wires into faces.
		 *
		 * This preserves:
		 *
		 *   - separate shadow components;
		 *   - holes;
		 *   - islands inside holes;
		 *   - further levels of nesting.
		 */
		TopoDS_Shape shadowFaces;

		if( !BOPAlgo_Tools::WiresToFaces( projectedWires, shadowFaces,
							angular_tolerance ) )
		{
			throw_error( -1, "Unable to construct projected shadow faces" );
		}

		// this loses all color. should i care?
		return { shadowFaces };
	}

	/*====================================================================
	 *
	 *  projection()
	 *
	 */

	ColoredShape projection( const ColoredShape& shape3d, bool cut )
	{
		
		return cut ? cross_section( shape3d ) : ortho_projection( shape3d );
	}

}; // GfxOps

class TreeExecutor
{
public:

	TreeExecutor(
			const std::unordered_map<std::string, ModuleDefinition>& modules
	)
		: modules_( modules ), fontManager( Font_FontMgr::GetInstance() )
	{
	}

	ColoredShape execute( const std::vector<std::unique_ptr<Node>>& roots,
					const Node& moduleCall )
	{
		Environment environment;

		if( moduleCall.name.empty() )
		{
			return union_visit_vector( roots, environment );
		}
		else
		{
			return visit( moduleCall, environment );
		}
	}

private:
	static constexpr const char* indent_chars = "                                        ";
	const char* indent = indent_chars + 40;
	int depth = 0;
	int call_depth = 0;

	Evaluator evaluator_;

	const std::unordered_map<std::string, ModuleDefinition>& modules_;
	Handle(Font_FontMgr) fontManager;


	void depth_inc()
	{
		depth++;
		if( depth <= 40 )
		{
			indent = indent_chars + (40 - depth);
		}
	}
	void depth_dec()
	{
		if( depth > 0 )
		{
			depth--;
		}
		if( depth <= 40 )
		{
			indent = indent_chars + (40 - depth);
		}
	}

	std::vector<Value> evaluateArguments( const Node& node, const Environment& environment )
	{
		std::vector<Value> result;

		for( const auto& argument : node.arguments )
		{
			result.push_back( evaluator_.evaluate( *argument.value, environment ) );
		}

		return result;
	}

	Transform getTransform( const Node& node, const Environment& environment )
	{

		// auto values = evaluateArguments( node, environment );
		// depth_inc();
		// std::cout << indent << node.name << std::endl;

		if( node.name == "translate" )
		{
			static std::vector<ArgumentSpec> argspec{
				{ "$$", false, ArgumentSpec::Kind::Vector3D, 0 }
			};
			auto result = matchArguments( node.arguments, argspec, environment );
			if( !result.success )
			{
				throw_error( node.line_nr,
						"Incompatible arguments for translate." );
			}
			double x = result.values[0].vAsDouble( 0 );
			double y = result.values[0].vAsDouble( 1 );
			double z = result.values[0].vAsDouble( 2 );
			// depth_dec();
			return Transform::translation( x, y, z );
		}
		else if( node.name == "scale" )
		{
			static std::vector<ArgumentSpec> argspec{
				{ "v", false, ArgumentSpec::Kind::Vector3D, 0 }
			};
			auto result = matchArguments( node.arguments, argspec, environment );
			if( !result.success )
			{
				throw_error( node.line_nr,
						"Incompatible arguments for scale." );
			}
			double x = result.values[0].vAsDouble( 0 );
			double y = result.values[0].vAsDouble( 1 );
			double z = result.values[0].vAsDouble( 2 );
			// depth_dec();
			return Transform::scaling( x, y, z );
		}

		else if( node.name == "resize" )
		{
			static std::vector<ArgumentSpec> argspec{
				{ "newsize", false, ArgumentSpec::Kind::Vector3D, 0 }
			};
			throw_error( node.line_nr,
					"resize() is not supported." );
		}

		else if( node.name == "rotate" )
		{
			static std::vector<ArgumentSpec> argspec1{
				{ "a", false, ArgumentSpec::Kind::Float, 0 },
				{ "v", true,  ArgumentSpec::Kind::Vector3D, 0 }
			};
			static std::vector<ArgumentSpec> argspec2{
				{ "a", false, ArgumentSpec::Kind::Vector3D, 0 },
				{ "v", true,  ArgumentSpec::Kind::Vector3D, 0 }
			};
			auto result = matchArguments( node.arguments, argspec1, environment );
			bool isfirst = true;
			if( !result.success )
			{
				// failed argspec1
				isfirst = false;
				result = matchArguments( node.arguments, argspec2, environment );
			}
			else if( result.supplied[1] )
			{
				// rotate( a, v )
				double a = result.values[0].asDouble();
				double x = result.values[1].vAsDouble( 0 );
				double y = result.values[1].vAsDouble( 1 );
				double z = result.values[1].vAsDouble( 2 );

				return Transform::rotationV( a, x, y, z );
			}
			if( !result.success )
			{
				throw_error( node.line_nr,
						"Incompatible arguments for rotate." );
			}
			double x = 0.0;
			double y = 0.0;
			double z = 0.0;
			if( isfirst )
			{
				// rotate( a )
				// z = std::get<double>( result.values[0] );
				z = result.values[0].asDouble();
			}
			else
			{
				// rotate( v )
				x = result.values[0].vAsDouble( 0 );
				y = result.values[0].vAsDouble( 1 );
				z = result.values[0].vAsDouble( 2 );
			}

			// depth_dec();
			Transform tx = Transform::rotationX( x );
			// std::cout << "transform tx:" << std::endl;
			// tx.print();
			Transform ty = Transform::rotationY( y );
			// std::cout << "transform ty:" << std::endl;
			// ty.print();
			Transform tz = Transform::rotationZ( z );
			// std::cout << "transform tz:" << std::endl;
			// tz.print();
			// Transform t = tx * ty * tz;
			Transform t = tz * ty * tx;
			// std::cout << "transform t:" << std::endl;
			// t.print();
			return t;
		}
		else
		{
			throw_error( node.line_nr,
					"unimplemented transform." );
		}
	}

	ColoredShape visit( const Node& node, Environment& environment )
	{
		if( node.kind == Node::Kind::Module )
		{
			auto moduleIterator = modules_.find( node.name );
			if( moduleIterator == modules_.end() )
			{
				throw_error( node.line_nr, std::format(
						"module '{}' not found", node.name ) );
			}
			return executeModule(
				moduleIterator->second,
				node,
				environment );
		}

		if( node.kind == Node::Kind::Transform )
		{
			depth_inc();
			std::cout << indent << node.name << std::endl;
			// get the transform
			Transform transform = getTransform( node, environment );
			// get the shape
			ColoredShape shape = union_visit( node, environment );
			// apply transform
			ColoredShape object = apply_transform( shape, transform );

			depth_dec();
			// return { operation.Shape() };
			return object;
		}

		if( node.kind == Node::Kind::Operation )
		{
			if( node.name == "union" )
			{
				return union_visit( node, environment );
			}
			else if( node.name == "difference" )
			{
				return difference_visit( node, environment );
			}
			else if( node.name == "intersection" )
			{
				return intersection_visit( node, environment );
			}
			else if( node.name == "hull" )
			{
				return hull_visit( node, environment );
			}
			else if( node.name == "minkowski" )
			{
				return minkowski_visit( node, environment );
			}
			else if( node.name == "linear_extrude" )
			{
				return linear_extrude_visit( node, environment );
			}
			else if( node.name == "rotate_extrude" )
			{
				return rotate_extrude_visit( node, environment );
			}
			else if( node.name == "resize" )
			{
				return resize_visit( node, environment );
			}
			else if( node.name == "projection" )
			{
				return projection_visit( node, environment );
			}
			else if( node.name == "color" )
			{
				return color_visit( node, environment );
			}
			else
			{
				throw std::runtime_error(
					"Unhandled operation: " + node.name );
			}
		}

		if( node.kind == Node::Kind::Assignment )
		{
			environment[node.name] =
				evaluator_.evaluate( *node.assignmentValue, environment );
			ColoredShape nil;
			return nil;
		}

		if( node.kind == Node::Kind::Primitive )
		{
			return primitive( node, environment );
		}

		if( node.kind == Node::Kind::For )
		{
			return for_loop( node, environment );
		}

		// A node without children is treated as a primitive.
		if( node.children.empty() )
		{
			throw_error( node.line_nr, std::format(
				"unhandled node {} kind {} with no children",
				node.name, Node::strKind(node.kind) ) );
		}

		return union_visit( node, environment );
	}

	ColoredShape executeModule( const ModuleDefinition& definition,
					const Node& call, Environment& parentEnvironment )
	{

		depth_inc();
		std::cout << indent << call.name << std::endl;
		call_depth++;
		// ++++ make this number configurable
		if( call_depth >= 100 )
		{
			throw std::runtime_error( std::format(
				"Call depth too high ({}).", call_depth ) );
		}
		if( call.arguments.size() != definition.parameters.size() )
		{
			throw std::runtime_error(
				"Incorrect argument count for module: " +
				definition.name );
		}

		Environment environment;

		for( std::size_t i = 0; i < definition.parameters.size(); ++i )
		{
			environment[definition.parameters[i]] =
				evaluator_.evaluate( *call.arguments[i].value,
							parentEnvironment );
		}

		ColoredShape shape = union_visit_vector( definition.body, environment );
		call_depth--;
		depth_dec();
		return shape;
	}

	MatchResult matchArguments(
					const std::vector<Argument>& arguments,
					const std::vector<ArgumentSpec>& specification,
					const Environment& environment )
	{
		MatchResult result( specification.size() );

		std::unordered_map<std::string, std::size_t> namedArguments;

		for( std::size_t i=0; i<specification.size(); ++i )
		{
			const auto& spec = specification[i];

			if( spec.name.empty() )
			{
				result.success = false;
				result.error = "Argument specification contains an empty name";
				return result;
			}

			if( !namedArguments.emplace( spec.name, i ).second )
			{
				result.success = false;
				result.error = "Duplicate argument specification: " + spec.name;
				return result;
			}
		}

		bool encounteredNamedArgument = false;
		std::size_t nextPositional = 0;

		for( const Argument& argument : arguments )
		{
			std::size_t specIndex = 0;

			if( argument.name.empty() )
			{
				// This implementation requires positional arguments to come
				// before named arguments.
				if( encounteredNamedArgument )
				{
					result.success = false;
					result.error =
						"Positional arguments must come before named arguments";
					return result;
				}

				while( nextPositional < specification.size() &&
					   result.supplied[nextPositional] )
				{
					++nextPositional;
				}

				if( nextPositional == specification.size() )
				{
					result.success = false;
					result.error = "Too many positional arguments";
					return result;
				}

				specIndex = nextPositional++;
			}
			else
			{
				encounteredNamedArgument = true;

				auto it = namedArguments.find( argument.name );

				if( it == namedArguments.end() )
				{
					result.success = false;
					result.error =
						"Unknown named argument: " + argument.name;
					return result;
				}

				specIndex = it->second;

				if( result.supplied[specIndex] )
				{
					result.success = false;
					result.error =
						"Argument supplied more than once: " + argument.name;
					return result;
				}
			}

			const ArgumentSpec& spec = specification[specIndex];

			if( !argument.value )
			{
				result.success = false;
				result.error =
					"Argument '" + spec.name + "' has no expression";
				return result;
			}

			Value arg_val = evaluator_.evaluate( *argument.value, environment );
			global_line_nr = argument.value->line_nr;
			std::string spec_kind;
			try
			{
				switch( spec.kind )
				{
					case ArgumentSpec::Kind::Bool:
						spec_kind = "bool";
						arg_val = arg_val.asBool();
						break;
					case ArgumentSpec::Kind::Int:
						spec_kind = "int";
						arg_val = arg_val.asInt();
						break;
					case ArgumentSpec::Kind::Float:
						spec_kind = "float";
						arg_val = arg_val.asDouble();
						break;
					case ArgumentSpec::Kind::String:
						spec_kind = "string";
						arg_val = arg_val.asString();
						break;
					case ArgumentSpec::Kind::Vector2D:
						spec_kind = "vector2d";
						if( !arg_val.isNumVector( 2 ) )
						{
							throw_error( 0, "vector2d must contain 2 numbers" );
						}
						break;
					case ArgumentSpec::Kind::Vector3D:
						spec_kind = "vector3d";
						if( !arg_val.isNumVector( 3 ) )
						{
							throw_error( 0, "vector3d must contain 3 numbers" );
						}
						break;
					case ArgumentSpec::Kind::Vector4D:
						spec_kind = "vector4d";
						if( !arg_val.isNumVector( 4 ) )
						{
							throw_error( 0, "vector4d must contain 4 numbers" );
						}
						break;
					case ArgumentSpec::Kind::ListVector:
						spec_kind = "listvector";
						if( !arg_val.isNumVector( 0, 0 ) )
						{
							throw_error( 0, "list of vector must contain numbers only" );
						}
						break;
					case ArgumentSpec::Kind::ListVector2D:
						spec_kind = "listvector2d";
						if( !arg_val.isNumVector( 0, 2 ) )
						{
							throw_error( 0, "list of vector must contain numbers only" );
						}
						break;
					case ArgumentSpec::Kind::ListVector3D:
						spec_kind = "listvector3d";
						if( !arg_val.isNumVector( 0, 3 ) )
						{
							throw_error( 0, "list of vector must contain numbers only" );
						}
						break;
					default:
						result.success = false;
						break;
				}
			}
			catch( std::runtime_error )
			{
				result.error = std::format(
					"Argument '{}' has the wrong kind, expected {}",
					spec.name, spec_kind );
				result.line_nr = argument.value->line_nr;
				global_line_nr = 0;
				return result;
			}
			global_line_nr = 0;

			result.supplied[specIndex] = true;
			result.values[specIndex] = arg_val;
		}

		for( std::size_t i=0; i<specification.size(); ++i )
		{
			if( result.supplied[i] )
			{
				// have it
			}
			else if( specification[i].optional )
			{
				// set default value
				result.values[i] = specification[i].default_value;
			}
			else
			{
				result.success = false;
				result.error =
					"Missing required argument: " + specification[i].name;
				return result;
			}
		}

		result.success = true;

		return result;
	}

	ColoredShape primitive( const Node& node, Environment& environment )
	{
		ColoredShape object;

		depth_inc();
		std::cout << indent << node.name << std::endl;
		if( node.name == "cube" )
		{
			static std::vector<ArgumentSpec> cube_vect{
				{ "size",   false, ArgumentSpec::Kind::Vector3D, 0 },
				{ "center", true,  ArgumentSpec::Kind::Bool, false }
			};
			static std::vector<ArgumentSpec> cube_num{
				{ "size",   false, ArgumentSpec::Kind::Float, 0 },
				{ "center", true,  ArgumentSpec::Kind::Bool, false }
			};

			auto result = matchArguments( node.arguments, cube_vect, environment );
			double x, y, z;
			bool center;
			if( result.success )
			{
				x = result.values[0].vAsDouble( 0 );
				y = result.values[0].vAsDouble( 1 );
				z = result.values[0].vAsDouble( 2 );
				center = result.values[1].asBool();
			}
			else
			{
				result = matchArguments( node.arguments, cube_num, environment );
				if( !result.success )
				{
					throw_error( node.line_nr,
							"Incompatible arguments for cube." );
				}
				x = y = z = result.values[0].asDouble();
				center = result.values[1].asBool();
			}

			if( x <= 0.0 || y <= 0.0 || z <= 0.0 )
			{
				throw_error( node.line_nr,
					"cube dimensions must be greater than zero" );
			}

			gp_Pnt corner;

			if( center )
			{
				corner = gp_Pnt(
					-x / 2.0,
					-y / 2.0,
					-z / 2.0
				);
			}
			else
			{
				corner = gp_Pnt( 0.0, 0.0, 0.0 );
			}

			object = BRepPrimAPI_MakeBox( corner, x, y, z ).Shape();
		}
		else if( node.name == "sphere" )
		{
			static std::vector<ArgumentSpec> sphere_r{
				{ "r",   false, ArgumentSpec::Kind::Float, 0 },
				{ "$fa", true,  ArgumentSpec::Kind::Float, 12.0 },
				{ "$fs", true,  ArgumentSpec::Kind::Float, 2.0 },
				{ "$fn", true,  ArgumentSpec::Kind::Float, 0.0 }
			};
			static std::vector<ArgumentSpec> sphere_d{
				{ "d",   false, ArgumentSpec::Kind::Float, 0 },
				{ "$fa", true,  ArgumentSpec::Kind::Float, 12.0 },
				{ "$fs", true,  ArgumentSpec::Kind::Float, 2.0 },
				{ "$fn", true,  ArgumentSpec::Kind::Float, 0.0 }
			};

			auto result = matchArguments( node.arguments, sphere_r, environment );
			double r;
			if( result.success )
			{
				r = result.values[0].asDouble();
			}
			else
			{
				result = matchArguments( node.arguments, sphere_d, environment );
				if( !result.success )
				{
					throw_error( node.line_nr,
							"Incompatible arguments for sphere." );
				}
				r = result.values[0].asDouble() * 0.5;
			}
			if( r <= 0.0 )
			{
				throw_error( node.line_nr,
					"sphere radius must be greater than zero" );
			}
			const gp_Pnt center( 0, 0, 0 );
			object = BRepPrimAPI_MakeSphere( center, r ).Shape();
		}
		else if( node.name == "cylinder" )
		{
			static std::vector<ArgumentSpec> cylinder{
				{ "h",      false, ArgumentSpec::Kind::Float, 0.0 },
				{ "r1",     true,  ArgumentSpec::Kind::Float, 0.0 },
				{ "r2",     true,  ArgumentSpec::Kind::Float, 0.0 },
				{ "center", true,  ArgumentSpec::Kind::Bool, false },
				{ "r",      true,  ArgumentSpec::Kind::Float, 0.0 },
				{ "d1",     true,  ArgumentSpec::Kind::Float, 0.0 },
				{ "d2",     true,  ArgumentSpec::Kind::Float, 0.0 },
				{ "d",      true,  ArgumentSpec::Kind::Float, 0.0 },
				{ "$fa",    true,  ArgumentSpec::Kind::Float, 0.0 },
				{ "$fs",    true,  ArgumentSpec::Kind::Float, 0.0 },
				{ "$fn",    true,  ArgumentSpec::Kind::Float, 0.0 }
			};
			auto result = matchArguments( node.arguments, cylinder, environment );
			if( !result.success )
			{
				throw_error( node.line_nr,
						"Incompatible arguments for cylinder." );
			}
			double h, r1, r2;
			h = result.values[0].asDouble();
			r1 = result.values[1].asDouble();
			r2 = result.values[2].asDouble();
			// +++++ check what has been specified
			if( h <= 0.0 )
			{
				throw std::invalid_argument(
					"Cylinder height must be positive" );
			}
			if( r1 == 0.0 && r2 == 0.0 )
			{
				throw std::invalid_argument(
					"At least one cylinder radius must be positive" );
			}
			if( r1 < 0.0 || r2 < 0.0 )
			{
				throw std::invalid_argument(
					"Cylinder radii must be positive" );
			}

			bool center = result.values[3].asBool();
			const gp_Pnt centerpnt( 0, 0, center ? -h/2 : 0 );
			const gp_Dir direction( 0.0, 0.0, 1.0 );
			gp_Ax2 axes( centerpnt, direction );

			if( r1 == r2 )
			{
				object = BRepPrimAPI_MakeCylinder( axes, r1, h ).Shape();
			}
			else
			{
				object = BRepPrimAPI_MakeCone( axes, r1, r2, h ).Shape();
			}
		}
		else if( node.name == "polyhedron" )
		{
			static std::vector<ArgumentSpec> argspec0{
				{ "points",    false, ArgumentSpec::Kind::ListVector3D, 0 },
				{ "faces",     false, ArgumentSpec::Kind::ListVector, 0 },
				{ "convexity", true,  ArgumentSpec::Kind::Int, 1 }
			};
			static std::vector<ArgumentSpec> argspec1{
				{ "points",    false, ArgumentSpec::Kind::ListVector3D, 0 },
				{ "triangles", false, ArgumentSpec::Kind::ListVector3D, 0 },
				{ "convexity", true,  ArgumentSpec::Kind::Int, 1 }
			};
			auto result = matchArguments( node.arguments, argspec0, environment );
			if( !result.success )
			{
				auto result = matchArguments( node.arguments, argspec1, environment );
				if( !result.success )
				{
					throw_error( node.line_nr,
							"Incompatible arguments for polyhedron." );
				}
			}

			std::vector<gp_Pnt> points;
			Value::Vector argp = result.values[0].asVector();
			if( argp.size() < 4 )
			{
				throw_error( node.line_nr,
						"A polyhedron must have at least 4 points." );
			}
			for( int i=0; i<argp.size(); i++ )
			{
				points.push_back( gp_Pnt(
					argp[i].vAsDouble( 0 ),
					argp[i].vAsDouble( 1 ),
					argp[i].vAsDouble( 2 )
				) );
			}

			const double sewingTolerance = 1.0e-7;
			BRepBuilderAPI_Sewing sewing( sewingTolerance );

			Value::Vector argf = result.values[1].asVector();
			if( argf.size() < 4 )
			{
				throw_error( node.line_nr,
						"A polyhedron must have at least 4 faces." );
			}
			for( int i=0; i<argf.size(); i++ )
			{
				int cnt = argp[i].length();
				if( cnt < 3 )
				{
					throw_error( node.line_nr, std::format(
							"face {} has less than 3 points.", i ) );
				}
				BRepBuilderAPI_MakePolygon polygon;
				for( int j=0; j<cnt; j++ )
				{
					int index = argp[i].vAsInt( j );
					if( index < 0 || index >= points.size() )
					{
						throw_error( node.line_nr, std::format(
								"face {} point {} has invalid index {}.",
								i, j, index ) );
					}
					polygon.Add( points[index] );
				}
				polygon.Close();
				if( !polygon.IsDone() )
				{
					throw_error( node.line_nr, std::format(
						"Could not create polygon for face {}", i ) );
				}
				TopoDS_Wire wire = polygon.Wire();
				BRepBuilderAPI_MakeFace faceMaker( wire );
				if( !faceMaker.IsDone() )
				{
					throw_error( node.line_nr, std::format(
						"Could not create planar face {}."
						" The face may be non-planar or degenerate.",
						i
					) );
				}
				sewing.Add( faceMaker.Face() );
			}

			sewing.Perform();
			TopoDS_Shape sewnShape = sewing.SewedShape();
			if( sewnShape.IsNull() )
			{
				throw_error( node.line_nr, "Sewing produced a null shape" );
			}

			// A single closed solid should normally produce exactly one shell.
			TopExp_Explorer shellExplorer( sewnShape, TopAbs_SHELL );
			if( !shellExplorer.More() )
			{
				throw_error( node.line_nr,
					"Sewing produced no shell."
					" The faces may not form a closed surface."
				);
			}
			TopoDS_Shell shell = TopoDS::Shell(shellExplorer.Current());
			shellExplorer.Next();
			if( shellExplorer.More() )
			{
				throw_error( node.line_nr,
					"Sewing produced multiple shells."
					" The input may contain multiple disconnected objects."
				);
			}

			BRepBuilderAPI_MakeSolid solidMaker( shell );
			if( !solidMaker.IsDone() )
			{
				throw_error( node.line_nr,
						"Could not create solid from shell" );
			}
			TopoDS_Solid solid = solidMaker.Solid();
			BRepCheck_Analyzer analyzer( solid, Standard_True );
			if( !analyzer.IsValid() )
			{
				throw_error( node.line_nr,
					"The resulting solid failed BRepCheck_Analyzer"
				);
			}
			object = solid;
		}
		else if( node.name == "polygon" )
		{
			static std::vector<ArgumentSpec> argspec0{
				{ "points",    false, ArgumentSpec::Kind::ListVector2D, 0 },
				{ "faces",     true,  ArgumentSpec::Kind::ListVector,
							std::vector<Value>{} },
				{ "convexity", true,  ArgumentSpec::Kind::Int, 1 }
			};
			auto result = matchArguments( node.arguments, argspec0, environment );
			if( !result.success )
			{
				throw_error( node.line_nr,
						"Incompatible arguments for polygon." );
			}

			std::vector<gp_Pnt> points;
			Value::Vector argp = result.values[0].asVector();
			if( argp.size() < 3 )
			{
				throw_error( node.line_nr,
						"A polygon must have at least 3 points." );
			}
			for( int i=0; i<argp.size(); i++ )
			{
				points.push_back( gp_Pnt(
					argp[i].vAsDouble( 0 ),
					argp[i].vAsDouble( 1 ),
					0.0
				) );
			}

			Value::Vector argf = result.values[1].asVector();
			std::vector<std::size_t> indices;
			if( result.supplied[1] && argf.size() > 0 )
			{
				for( int i=0; i<argf[0].length(); i++ )
				{
					int index = argf[0].vAsInt( i );
					if( index < 0 || index >= points.size() )
					{
						throw_error( node.line_nr, std::format(
								"main polygon point {} has invalid index {}.",
								i, index ) );
					}
					indices.push_back( index );
				}
			}
			else
			{
				for( int i=0; i<points.size(); i++ )
				{
					indices.push_back( i );
				}
			}
			if( indices.size() < 3 )
			{
				throw_error( node.line_nr, "polygon has less than 3 points." );
			}
			TopoDS_Wire outerWire = makeWire( points, indices, true,
										node.line_nr, 0 );
			BRepBuilderAPI_MakeFace faceMaker( outerWire );

			for( int hole=1; hole<argf.size(); hole++ )
			{
				indices.clear();
				for( int i=0; i<argf[hole].length(); i++ )
				{
					int index = argf[hole].vAsInt( i );
					if( index < 0 || index >= points.size() )
					{
						throw_error( node.line_nr, std::format(
							"polygon hole {} point {} has invalid index {}.",
							hole, i, index ) );
					}
					if( indices.size() < 3 )
					{
						throw_error( node.line_nr, std::format(
							"polygon hole {} has less than 3 points.",
							hole ) );
					}
					indices.push_back( index );
				}
				TopoDS_Wire holeWire = makeWire( points, indices, false,
											node.line_nr, hole );
				faceMaker.Add( holeWire );
			}

			if( !faceMaker.IsDone() )
			{
				throw_error( node.line_nr, std::format(
					"Could not create face with holes for polygon" ) );
			}

			object = faceMaker.Face();
		}
		else if( node.name == "circle" )
		{
			static std::vector<ArgumentSpec> argspec{
				{ "r",      true,  ArgumentSpec::Kind::Float, 0.0 },
				{ "d",      true,  ArgumentSpec::Kind::Float, 0.0 },
				{ "$fa",    true,  ArgumentSpec::Kind::Float, 0.0 },
				{ "$fs",    true,  ArgumentSpec::Kind::Float, 0.0 },
				{ "$fn",    true,  ArgumentSpec::Kind::Float, 0.0 }
			};
			auto result = matchArguments( node.arguments, argspec, environment );
			if( result.supplied[0] == result.supplied[1] )
			{
				if( !result.supplied[0] )
				{
					throw_error( node.line_nr,
						"Radius or diameter of circle must be specified." );
				}
				else
				{
					throw_error( node.line_nr,
						"Radius or diameter of circle must not both be specified." );
				}
			}
			double radius;
			if( result.supplied[0] )
			{
				radius = result.values[0].asDouble();
			}
			else
			{
				radius = 0.5 * result.values[0].asDouble();
			}
			gp_Pnt center( 0.0, 0.0, 0.0 );
			gp_Dir normal( 0.0, 0.0, 1.0 );
			gp_Ax2 axis( center, normal );
			gp_Circ circle( axis, radius );
			TopoDS_Edge edge = BRepBuilderAPI_MakeEdge( circle ).Edge();
			TopoDS_Wire wire = BRepBuilderAPI_MakeWire( edge ).Wire();
			TopoDS_Face face = BRepBuilderAPI_MakeFace( wire ).Face();

			if( face.IsNull() || face.ShapeType() != TopAbs_FACE )
			{
				throw_error( node.line_nr,
						"Constructing circle failed." );
			}
			object = face;
		}
		else if( node.name == "square" )
		{
			static std::vector<ArgumentSpec> argspec{
				{ "size",   true,  ArgumentSpec::Kind::Float, 1.0 },
				{ "center", true,  ArgumentSpec::Kind::Bool, false }
			};
			auto result = matchArguments( node.arguments, argspec, environment );
			double w, h;
			if( result.success )
			{
				w = h = result.values[0].asDouble();
			}
			else
			{
				static std::vector<ArgumentSpec> argspec{
					{ "size",   true,  ArgumentSpec::Kind::Vector2D, false },
					{ "center", true,  ArgumentSpec::Kind::Bool, false }
				};
				auto result = matchArguments( node.arguments, argspec, environment );
				if( !result.success )
				{
					std::cout << " Err: " << result.error << std::endl;
					throw_error( node.line_nr,
							"Incompatible arguments for square." );
				}
				w = result.values[0].vAsDouble( 0 );
				h = result.values[0].vAsDouble( 1 );
			}
			bool center = result.values[1].asBool();
			double x0 = center ? -0.5*w : 0.0;
			double y0 = center ? -0.5*h : 0.0;
			gp_Ax3 axes(
				gp_Pnt(  x0,  y0, 0.0 ),  // origin
				gp_Dir( 0.0, 0.0, 1.0 )   // normal
			);

			gp_Pln plane( axes );

			TopoDS_Face face = BRepBuilderAPI_MakeFace(
				plane,
				x0 / 2.0, x0 + w,
				y0 / 2.0, x0 + h
			).Face();

			if( face.IsNull() || face.ShapeType() != TopAbs_FACE )
			{
				throw_error( node.line_nr,
						"Constructing square failed." );
			}
			object = face;
		}
		else if( node.name == "text" )
		{
			static std::vector<ArgumentSpec> argspec{
				{ "text",      false, ArgumentSpec::Kind::String, "" },
				{ "size",      true,  ArgumentSpec::Kind::Float, 10.0 },
				{ "font",      true,  ArgumentSpec::Kind::String, "Liberation Sans" },
				{ "direction", true,  ArgumentSpec::Kind::String, "ltr" },
				{ "language",  true,  ArgumentSpec::Kind::String, "en" },
				{ "script",    true,  ArgumentSpec::Kind::String, "latin" },
				{ "halign",    true,  ArgumentSpec::Kind::String, "left" },
				{ "valign",    true,  ArgumentSpec::Kind::String, "baseline" },
				{ "spacing",   true,  ArgumentSpec::Kind::Float, 1.0 },
				{ "em",        true,  ArgumentSpec::Kind::Float, 0.0 },
				{ "$fa",       true,  ArgumentSpec::Kind::Float, 12.0 },
				{ "$fs",       true,  ArgumentSpec::Kind::Float, 2.0 },
				{ "$fn",       true,  ArgumentSpec::Kind::Float, 0.0 }
			};
			auto result = matchArguments( node.arguments, argspec, environment );
			if( !result.success )
			{
				throw_error( node.line_nr,
						"Incompatible arguments for text." );
			}
			std::string text = result.values[0].asString();
			double size = result.values[1].asDouble();
			std::string font_name = result.values[2].asString();
			std::string direction = result.values[3].asString();
			std::string language = result.values[4].asString();
			std::string script = result.values[5].asString();
			std::string halign = result.values[6].asString();
			std::string valign = result.values[7].asString();
			double spacing = result.values[8].asDouble();
			double em = result.values[9].asDouble();
			if( text.empty() )
			{
				throw_error( node.line_nr,
						"Text must not be empty." );
			}
			if( size <= 0 && em <= 0 )
			{
				throw_error( node.line_nr,
						"Either size or em must be greater than zero." );
			}
			if( em <= 0.0 )
			{
				em = size * (100.0 / 72.0);
			}
			if( direction == "ltr" )
			{
			}
			else
			{
				throw_error( node.line_nr,
					"Unsupported text direction: " + direction );
			}
			if( language == "en" )
			{
			}
			else
			{
				throw_error( node.line_nr,
					"Unsupported text language: " + language );
			}
			if( script == "latin" )
			{
			}
			else
			{
				throw_error( node.line_nr,
					"Unsupported text script: " + script );
			}
			Graphic3d_HorizontalTextAlignment horizontal_alignment;
			if( halign == "left" )
			{
				horizontal_alignment = Graphic3d_HTA_LEFT;
			}
			else if( halign == "center" )
			{
				horizontal_alignment = Graphic3d_HTA_CENTER;
			}
			else if( halign == "right" )
			{
				horizontal_alignment = Graphic3d_HTA_RIGHT;
			}
			else
			{
				throw_error( node.line_nr,
					"Unsupported horizontal alignment: " + halign );
			}
			Graphic3d_VerticalTextAlignment vertical_alignment;
			if( valign == "top" )
			{
				vertical_alignment = Graphic3d_VTA_TOP;
			}
			else if( valign == "center" )
			{
				vertical_alignment = Graphic3d_VTA_CENTER;
			}
			else if( valign == "bottom" )
			{
				vertical_alignment = Graphic3d_VTA_BOTTOM;
			}
			else if( valign == "baseline" )
			{
				// vertical_alignment = Graphic3d_VTA_BASELINE;
				vertical_alignment = Graphic3d_VTA_TOPFIRSTLINE;
			}
			else
			{
				throw_error( node.line_nr,
					"Unsupported vertical alignment: " + valign );
			}

			Handle(StdPrs_BRepFont) font = StdPrs_BRepFont::FindAndCreate(
					TCollection_AsciiString( font_name.c_str() ),
					Font_FontAspect_Regular,
					em,
					Font_StrictLevel_Any );

			if( font.IsNull() )
			{
				throw_error( node.line_nr,
					"Font not found: " + font_name );
			}
			NCollection_String occt_text( text.c_str() );
			StdPrs_BRepTextBuilder text_builder;
			TopoDS_Shape text2d = text_builder.Perform(
					*font,
					occt_text,
					gp_Ax3(),
					horizontal_alignment,
					vertical_alignment );

			if( text2d.IsNull() )
			{
				throw_error( node.line_nr,
					"Could not generate text geometry" );
			}
			object = text2d;
		}
		else if( node.name == "surface" )
		{
			throw_error( node.line_nr,
					"Unimplemented primitive: " + node.name );
		}
		else if( node.name == "import" )
		{
			throw_error( node.line_nr,
					"Unimplemented primitive: " + node.name );
		}
		else
		{
			throw_error( node.line_nr,
					"Unhandled primitive: " + node.name );
		}

		std::cout << indent << "." << std::endl;
		depth_dec();
		return object;
	}

	ColoredShape for_loop( const Node& node, Environment& environment )
	{
		const Argument& loop_var = node.arguments[0];
		const Expression* expr = loop_var.value.get();
		Value params = evaluator_.evaluate( *expr, environment );
		Value values;

		depth_inc();
		std::cout << indent << node.name << std::endl;

		// get a vector of values from the expression
		if( expr->kind == Expression::Kind::ColonVector )
		{
			// [ start : end ] or [ start : increment : end ]
			double start;
			double end;
			double incr = 1.0;
			if( params.isNumVector( 2 ) )
			{
				start = params.vAsDouble( 0 );
				end = params.vAsDouble( 1 );
			}
			else if( params.isNumVector( 3 ) )
			{
				start = params.vAsDouble( 0 );
				incr = params.vAsDouble( 1 );
				end = params.vAsDouble( 2 );
			}
			else
			{
				throw_error( node.line_nr, "invalid 'for' parameters" );
			}
			if( incr > 0.0 )
			{
				if( end < start )
				{
					throw_error( node.line_nr, "increment is positive but end < start" );
				}
			}
			else if( incr < 0.0 )
			{
				if( end > start )
				{
					throw_error( node.line_nr, "increment is negative but end > start" );
				}
			}
			else
			{
				throw_error( node.line_nr, "'for' increment cannot be zero" );
			}
			Value::Vector vector;
			while( start <= end )
			{
				vector.push_back( start );
				start += incr;
			}
			values = vector;
		}
		else if( expr->kind == Expression::Kind::Vector )
		{
			// just loop over the contents of that vector
			values = params;
		}
		else
		{
			// there is theoretically also the possibility of 'object'
			throw_error( node.line_nr, "invalid 'for' parameters" );
		}

		// now loop...
		std::vector<ColoredShape> shapes;
		for( int i=0; i<values.length(); i++ )
		{
			environment[loop_var.name] = values[i];
			shapes.push_back( union_visit( node, environment ) );
		}
		environment.erase( loop_var.name );

		BRepAlgoAPI_BooleanOperation operation;
		if( node.name == "for" )
		{
			operation = BRepAlgoAPI_Fuse();
		}
		else
		{
			operation = BRepAlgoAPI_Common();
		}

		ColoredShape object = bool_op_visit_execute( node.name, operation,
										shapes, environment, node.line_nr );

		std::cout << indent << "=Nface " << object.faces.size() << std::endl;
		depth_dec();
		return object;
	}

	ColoredShape bool_op_visit( const std::string& name,
						BRepAlgoAPI_BooleanOperation& operation,
						const std::vector<std::unique_ptr<Node>>& nodes,
						Environment& environment )
	{
		depth_inc();
		std::cout << indent << name << std::endl;
		std::vector<ColoredShape> shapes;
		bool first = true;
		int line_nr = 0;
		for( const auto& node : nodes )
		{
			ColoredShape shape = visit( *node, environment );
			if( shape.IsNull() )
			{
				if( node->kind == Node::Kind::BuiltinFunction ||
					node->kind == Node::Kind::Function ||
					node->kind == Node::Kind::Assignment )
				{
					// just ignore that node
					continue;
				}
				throw_error( node->line_nr, std::format(
						"Node is null ({}).", Node::strKind( node->kind ) ) );
			}
			if( first )
			{
				line_nr = node->line_nr;
				first = false;
			}
			shapes.push_back( shape );
		}
		ColoredShape object = bool_op_visit_execute( name, operation, shapes,
								environment, line_nr );
		std::cout << indent << "=Ncol " << object.faces.size() << std::endl;

		depth_dec();
		return object;
	}

	ColoredShape bool_op_visit_execute( const std::string& name,
						BRepAlgoAPI_BooleanOperation& operation,
						const std::vector<ColoredShape>& shapes,
						Environment& environment, int line_nr )
	{
		if( shapes.size() == 1 )
		{
			// only one shape, nothing to do except returning it
			return shapes[0];
		}

		TopTools_ListOfShape arguments;
		TopTools_ListOfShape tools;

		arguments.Append( shapes[0] );
		for( int i=1; i<shapes.size(); i++ )
		{
			tools.Append( shapes[i] );
		}

		operation.SetArguments( arguments );
		operation.SetTools( tools );
		operation.Build();

		if( operation.HasErrors() || !operation.IsDone() )
		{
#if 0
			std::cout << indent << "^-Error" << std::endl;
			std::cout << "  0 " << shape_type( shape ) << std::endl;
			int i=1;
			for( const TopoDS_Shape& s : tools )
			{
				if( shape.IsNull() )
				{
					continue;
				}
				std::cout << "  " << i << " " << shape_type( s ) << std::endl;
				i++;
			}
			if( operation.HasErrors() )
			{
				std::cout << "Errors:" << std::endl;
				operation.DumpErrors( std::cout );
			}
			if( operation.HasWarnings() )
			{
				std::cout << "Warnings:" << std::endl;
				operation.DumpWarnings( std::cout );
			}
			if( !operation.GetReport().IsNull() )
			{
				std::cout << "Report:" << std::endl;
				operation.GetReport()->Dump( std::cout );
			}
#endif
			throw_error( line_nr, std::format(
				"Multi-shape operation '{}' failed ({})",
				name, operation.HasErrors() ? "errors" : "not done" ) );
		}

		operation.SimplifyResult( true, true );

		ColoredShape object = { operation.Shape() };
		for( const ColoredShape& shape : shapes )
		{
			object.add_history( shape, operation );
			std::cout << indent << "+Ncol " << shape.faces.size() << std::endl;
		}
		return object;
	}

	ColoredShape union_visit_vector(
						const std::vector<std::unique_ptr<Node>>& nodes,
						Environment& environment )
	{
		BRepAlgoAPI_Fuse fuse;
		return bool_op_visit( "union", fuse, nodes, environment );
	}

	ColoredShape union_visit( const Node& node,
						Environment& environment )
	{
		BRepAlgoAPI_Fuse fuse;
		return bool_op_visit( "union", fuse, node.children, environment );
	}

	ColoredShape difference_visit( const Node& node,
						Environment& environment )
	{
		BRepAlgoAPI_Cut cut;
		return bool_op_visit( "difference", cut, node.children, environment );
	}

	ColoredShape intersection_visit( const Node& node,
						Environment& environment )
	{
		BRepAlgoAPI_Common common;
		return bool_op_visit( "intersection", common, node.children, environment );
	}

	ColoredShape hull_visit( const Node& node,
						Environment& environment )
	{
		depth_inc();
		std::cout << indent << "hull" << std::endl;
		throw_error( -1, "OpenCascade does not support hull()." );
	}

	ColoredShape minkowski_visit( const Node& node,
						Environment& environment )
	{
		depth_inc();
		std::cout << indent << "minkowski" << std::endl;
		// BRepOffsetAPI_MakeOffsetShape could do some of the functions
		throw_error( -1, "OpenCascade does not support minkowski()." );
	}

	ColoredShape linear_extrude_visit( const Node& node,
						Environment& environment )
	{
		depth_inc();
		std::cout << indent << "linear_extrude" << std::endl;
		static std::vector<ArgumentSpec> cylinder{
			{ "height",    false, ArgumentSpec::Kind::Float, 0.0 },
			{ "v",         true,  ArgumentSpec::Kind::Vector3D,
									std::vector<Value>{ 0.0, 0.0, 1.0 } },
			{ "center",    true,  ArgumentSpec::Kind::Bool, false },
			{ "convexity", true,  ArgumentSpec::Kind::Float, 10.0 },
			{ "twist",     true,  ArgumentSpec::Kind::Float, 0.0 },
			{ "slices",    true,  ArgumentSpec::Kind::Int, 0 },
			{ "scale",     true,  ArgumentSpec::Kind::Float, 1.0 },
			{ "$fn",       true,  ArgumentSpec::Kind::Float, 0.0 }
		};
		auto result = matchArguments( node.arguments, cylinder, environment );
		if( !result.success )
		{
			std::cout << " Err: " << result.error << std::endl;
			throw_error( node.line_nr,
					"Incompatible arguments for linear_extrude." );
		}
		double height = result.values[0].asDouble();
		double x = result.values[1].vAsDouble( 0 );
		double y = result.values[1].vAsDouble( 1 );
		double z = result.values[1].vAsDouble( 2 );
		bool center = result.values[2].asBool();
		// double convexity = result.values[3].asDouble();
		double twist = result.values[4].asDouble();
		// double slices = result.values[5].asDouble();
		double scale = result.values[6].asDouble();
		if( height <= 0.0 )
		{
			throw_error( -1, "linear_extrude: height must be positive" );
		}
		if( x == 0.0 && y == 0.0 && z == 0.0 )
		{
			throw_error( -1, "linear_extrude: v must not be null" );
		}
		if( scale < 0.0 )
		{
			throw_error( -1, "linear_extrude: scale must not be negative" );
		}
		if( twist != 0.0 )
		{
			throw_error( -1, "linear_extrude: twist is not supported" );
		}

		ColoredShape shape2d = union_visit( node, environment );
		if( shape2d.IsNull() )
		{
			throw Standard_DomainError( "linear_extrude: input shape is null" );
		}
		if( shape2d.ShapeType() == TopAbs_COMPOUND )
		{
			// +++
			// hmm, why do i not need to do that anymore?
			// or when exactly is it needed?
			//shape2d = compound2face( shape2d );
		}
		else if( shape2d.ShapeType() != TopAbs_FACE )
		{
			std::cout << "shape type is " << shape_type( shape2d ) << std::endl;
			throw Standard_DomainError( "linear_extrude: input shape is not a 2D face" );
			// std::cout << "linear_extrude: input shape is not a 2D face" << std::endl;
		}
		ColoredShape shape;
		if( scale == 1.0 )
		{
			// constant size
			gp_Vec extrusionVector( 0.0, 0.0, static_cast<double>(height) );
			BRepPrimAPI_MakePrism prism( shape2d, extrusionVector );
			prism.Build();
			if( !prism.IsDone() )
			{
				throw_error( -1, "linear_extrude: prism construction failed" );
			}
			shape = prism.Shape();
		}
		else if( scale == 0.0 )
		{
			// extruding to a point
			TopoDS_Face face = TopoDS::Face( shape2d );
			TopoDS_Wire baseWire = BRepTools::OuterWire( face );
			TopoDS_Vertex topPoint = BRepBuilderAPI_MakeVertex(
								gp_Pnt( 0,  0, height ) );
			BRepOffsetAPI_ThruSections loft( true, true );
			loft.AddWire( baseWire );
			loft.AddVertex( topPoint );
			loft.Build();
			shape = loft.Shape();
			if( !loft.IsDone() )
			{
				throw_error( -1, "linear_extrude: loft to point construction failed" );
			}
		}
		else
		{
			// changing size

			TopoDS_Face face = TopoDS::Face( shape2d );
			TopoDS_Wire baseWire = BRepTools::OuterWire( face );

			gp_Trsf scaleTransform;
			scaleTransform.SetScale( gp_Pnt( 0.0, 0.0, 0.0 ), scale );
			TopoDS_Wire topWire = TopoDS::Wire( BRepBuilderAPI_Transform(
							baseWire, scaleTransform, true ).Shape() );

			// Move the scaled wire to the top of the extrusion.
			gp_Trsf translateTransform;
			translateTransform.SetTranslation( gp_Vec( 0.0, 0.0, height ) );

			topWire = TopoDS::Wire( BRepBuilderAPI_Transform(
							topWire, translateTransform, true ).Shape() );

			// true  = create a solid
			// true  = use ruled surfaces between sections
			BRepOffsetAPI_ThruSections loft( true, true );
			loft.AddWire( baseWire );
			loft.AddWire( topWire );
			loft.Build();
			if( !loft.IsDone() )
			{
				throw_error( -1, "linear_extrude: loft construction failed" );
			}
			shape = loft.Shape();
		}
		if( center )
		{
			gp_Trsf translateTransform;
			translateTransform.SetTranslation( gp_Vec( 0, 0, -0.5*height ) );
			shape = BRepBuilderAPI_Transform(
						shape, translateTransform, true ).Shape();
		}
		depth_dec();
		return shape;
	}

	ColoredShape rotate_extrude_visit( const Node& node,
						Environment& environment )
	{
		depth_inc();
		std::cout << indent << "rotate_extrude" << std::endl;

		static std::vector<ArgumentSpec> cylinder{
			{ "angle",     true, ArgumentSpec::Kind::Float, 0.0 },
			{ "start",     true, ArgumentSpec::Kind::Float, 0.0 },
			{ "convexity", true, ArgumentSpec::Kind::Float, 10.0 },
			{ "$fa",       true, ArgumentSpec::Kind::Float, 0.0 },
			{ "$fs",       true, ArgumentSpec::Kind::Float, 0.0 },
			{ "$fn",       true, ArgumentSpec::Kind::Float, 0.0 }
		};
		auto result = matchArguments( node.arguments, cylinder, environment );
		if( !result.success )
		{
			std::cout << " Err: " << result.error << std::endl;
			throw_error( node.line_nr,
					"Incompatible arguments for rotate_extrude." );
		}
		double angle = d2r( result.values[0].asDouble() );
		double start = d2r( result.values[1].asDouble() );

		ColoredShape shape2d = union_visit( node, environment );
		if( shape2d.IsNull() )
		{
			throw Standard_DomainError( "rotate_extrude: input shape is null" );
		}
		if( shape2d.ShapeType() == TopAbs_COMPOUND )
		{
			shape2d = compound2face( shape2d );
		}
		else if( shape2d.ShapeType() != TopAbs_FACE )
		{
			std::cout << "shape type is " << shape_type( shape2d ) << std::endl;
			throw Standard_DomainError( "rotate_extrude: input shape is not a 2D face" );
		}
		ColoredShape shape;
		gp_Ax1 axis(
			gp_Pnt( 0.0, 0.0, 0.0 ),
			gp_Dir( 0.0, 1.0, 0.0 )
		);
		if( start != 0.0 )
		{
			// Rotate the profile into its starting angular position.
			gp_Trsf startTransform;
			startTransform.SetRotation( axis, start );

			BRepBuilderAPI_Transform transform(
				shape2d,
				startTransform,
				true // make a copy
			);
			shape2d = transform.Shape();
		}


		// Revolve the positioned profile through total_angle.
		BRepPrimAPI_MakeRevol revolved(
			shape2d,
			axis,
			angle
		);

#if 0
		depth_dec();
		return revolved.Shape();
#else
		// +++++++ is this right?
		gp_Trsf occtTransform = Transform::rotationX( 90 ).toTrsf();
		BRepBuilderAPI_Transform operation(
			revolved.Shape(),
			occtTransform,
			true   // create a transformed copy
		);

		depth_dec();
		// +++ might need to add color from the 2D object?
		return { operation.Shape() };
#endif
	}

	ColoredShape resize_visit( const Node& node,
						Environment& environment )
	{
		depth_inc();
		std::cout << indent << node.name << std::endl;
		static std::vector<ArgumentSpec> argspec1{
			{ "newsize", false, ArgumentSpec::Kind::Vector3D, 0 },
			{ "auto",    true,  ArgumentSpec::Kind::Bool, false }
		};
		static std::vector<ArgumentSpec> argspec2{
			{ "newsize", false, ArgumentSpec::Kind::Vector3D, 0 },
			{ "auto",    false, ArgumentSpec::Kind::Vector3D, 0 },
		};
		auto result = matchArguments( node.arguments, argspec1, environment );
		bool autox, autoy, autoz;
		if( result.success )
		{
			autox = autoy = autoz = result.values[1].asDouble();
		}
		else
		{
			auto result = matchArguments( node.arguments, argspec2, environment );
			if( !result.success )
			{
				throw_error( node.line_nr,
						"Incompatible arguments for resize." );
			}
			autox = result.values[1].vAsBool( 0 );
			autoy = result.values[1].vAsBool( 1 );
			autoz = result.values[1].vAsBool( 2 );
		}
		double newx = result.values[0].vAsDouble( 0 );
		double newy = result.values[0].vAsDouble( 1 );
		double newz = result.values[0].vAsDouble( 2 );
		if( newx == 0.0 && newy == 0.0 && newz == 0.0 )
		{
			throw_error( node.line_nr,
					"At least one dimension must be greater than zero." );
		}
		if( newx < 0.0 && newy < 0.0 && newz < 0.0 )
		{
			throw_error( node.line_nr,
					"Dimensions must not be negative." );
		}

		ColoredShape shape = union_visit( node, environment );

		double sizex, sizey, sizez;
		get_shape_size( shape, sizex, sizey, sizez );

		double scalex = newx*sizex != 0.0 ? newx / sizex : 1.0;
		double scaley = newy*sizey != 0.0 ? newy / sizey : 1.0;
		double scalez = newz*sizez != 0.0 ? newz / sizez : 1.0;
		if( newx == 0.0 && autox )
		{
			scalex = newy == 0.0 ? scalez : scaley;
		}
		if( newy == 0.0 && autoy )
		{
			scaley = newx == 0.0 ? scalez : scalex;
		}
		if( newz == 0.0 && autoz )
		{
			scalez = newx == 0.0 ? scaley : scalex;
		}

		Transform transform = Transform::scaling( scalex, scaley, scalez );
		ColoredShape object = apply_transform( shape, transform );

		depth_dec();
		return object;
	}

	ColoredShape projection_visit( const Node& node,
						Environment& environment )
	{
		depth_inc();
		std::cout << indent << node.name << std::endl;
		static std::vector<ArgumentSpec> argspec{
			{ "cut", true, ArgumentSpec::Kind::Bool, false }
		};
		auto result = matchArguments( node.arguments, argspec, environment );
		if( !result.success )
		{
			throw_error( node.line_nr,
					"Incompatible arguments for projection." );
		}
		bool cut = result.values[0].asBool();

		ColoredShape shape = union_visit( node, environment );
		ColoredShape object = GfxOps::projection( shape, cut );

		depth_dec();
		return object;
	}

	ColoredShape color_visit( const Node& node,
						Environment& environment )
	{
		depth_inc();
		std::cout << indent << node.name << std::endl;
		static std::vector<ArgumentSpec> argspec_v4{
			{ "c",     false, ArgumentSpec::Kind::Vector4D, 0 }
		};
		static std::vector<ArgumentSpec> argspec_v3a{
			{ "c",     false, ArgumentSpec::Kind::Vector3D, 0 },
			{ "alpha", true,  ArgumentSpec::Kind::Float, 1.0 }
		};
		static std::vector<ArgumentSpec> argspec_sa{
			{ "s",     false, ArgumentSpec::Kind::String, 0 },
			{ "alpha", true,  ArgumentSpec::Kind::Float, 1.0 }
		};
		uint8_t r, g, b, a;
		while( true )
		{
			auto result = matchArguments( node.arguments, argspec_v4, environment );
			if( result.success )
			{
				r = result.values[0].vAsDouble( 0 ) * 255;
				g = result.values[0].vAsDouble( 1 ) * 255;
				b = result.values[0].vAsDouble( 2 ) * 255;
				a = result.values[0].vAsDouble( 3 ) * 255;
				break;
			}
			result = matchArguments( node.arguments, argspec_v3a, environment );
			if( result.success )
			{
				r = result.values[0].vAsDouble( 0 ) * 255;
				g = result.values[0].vAsDouble( 1 ) * 255;
				b = result.values[0].vAsDouble( 2 ) * 255;
				a = result.values[1].asDouble() * 255;
				break;
			}
			result = matchArguments( node.arguments, argspec_sa, environment );
			if( result.success )
			{
				std::string s = result.values[0].asString();
				if( !parse_color( s, r, g, b, a ) )
				{
					throw_error( node.line_nr, std::format(
							"Unable to parse color string '{}'.", s ) );
				}
				if( result.supplied[1] )
				{
					a = result.values[1].asDouble() * 255;
				}
				break;
			}
			throw_error( node.line_nr,
					"Incompatible arguments for color." );
		}
		
		ColoredShape shape = union_visit( node, environment );
		// add colors to all faces
		for( TopExp_Explorer it( shape, TopAbs_FACE ); it.More(); it.Next() )
		{
			const TopoDS_Face& face = TopoDS::Face( it.Current() );
			ColoredFace coloredFace;
			coloredFace.face = face;
			coloredFace.color = Color( r, g, b );
			shape.faces.push_back( coloredFace );
		}

		depth_dec();
		return shape;
	}

	bool isModule( const std::string& name ) const
	{
		return modules_.find( name ) != modules_.end();
	}

	double signedArea( const std::vector<gp_Pnt>& points,
					std::vector<std::size_t> indices )
	{
		double area = 0.0;

		gp_Pnt b = points[ indices[ indices.size() - 1] ];
		for( std::size_t i=0; i<indices.size(); ++i )
		{
			gp_Pnt a = a;
			b = points[indices[i]];
			area += a.X() * b.Y() - b.X() * a.Y();
		}

		return 0.5 * area;
	}

	TopoDS_Wire makeWire( const std::vector<gp_Pnt>& points,
			std::vector<std::size_t> indices, bool counterClockwise,
			int line_nr, int hole )
	{
		// Permit the input to repeat the first index at the end.
		if( indices.size() >= 2 && indices.front() == indices.back() )
		{
			indices.pop_back();
		}

		const double area = signedArea( points, indices );
		if( area == 0.0 )
		{
			if( hole == 0 )
			{
				throw_error( line_nr, "polygon has zero area" );
			}
			else
			{
				throw_error( line_nr, std::format(
						"polygon hole {} has zero area", hole ) );
			}
		}
		const bool isCounterClockwise = area > 0.0;
		// Make the outer wire CCW and hole wires CW.
		if( isCounterClockwise != counterClockwise )
		{
			std::reverse( indices.begin(), indices.end() );
		}

		BRepBuilderAPI_MakePolygon polygon;
		for( std::size_t index : indices )
		{
			polygon.Add( points[index] );
		}
		polygon.Close();
		if( !polygon.IsDone() )
		{
			if( hole == 0 )
			{
				throw_error( line_nr, "Could not create polygon" );
			}
			else
			{
				throw_error( line_nr, std::format(
						"Could not create polygon for hole {}", hole ) );
			}
		}
		return polygon.Wire();
	}

	ColoredShape compound2face( ColoredShape& compound )
	{
		BRepBuilderAPI_MakeWire wireMaker;

		for( TopExp_Explorer it( compound, TopAbs_EDGE ); it.More(); it.Next() )
		{
			const TopoDS_Edge& edge = TopoDS::Edge( it.Current() );
			wireMaker.Add( edge );
		}
		if( !wireMaker.IsDone() )
		{
			// Edges are disconnected, invalid, or could not form one wire
			throw Standard_Failure( "Could not build wire" );
		}

		TopoDS_Wire wire = wireMaker.Wire();

		if( !BRep_Tool::IsClosed( wire ) )
		{
			throw Standard_Failure( "Boundary is not closed" );
		}
		// +++ propagate color here somehow?
		return { BRepBuilderAPI_MakeFace(wire).Face() };
	}

	bool parse_color( std::string s, uint8_t& r, uint8_t& g, uint8_t& b,
				uint8_t& a )
	{
		if( s.size() <= 1 )
		{
			// too short, needs at least 2 characters
			return false;
		}

		// is it one of the CSS color names?
		auto it = kCssColors.find( s );
		if( it != kCssColors.end() )
		{
			//const auto& [ir, ig, ib] = it->second;
			//r = ir * (1.0/255)
			//[ r, g, b ] = it->second;
			r = it->second[0];
			g = it->second[1];
			b = it->second[2];
			a = 255;
			return true;
		}

		if( s[0] != '#' )
		{
			// doesn't start with '#'
			return false;
		}
		const char* begin = s.c_str();
		char* end;
		long long value = std::strtoll( begin+1, &end, 16 );
		if( (end - begin) != s.size() )
		{
			// not all are hex characters
			return false;
		}
		// set the RGBA values
		switch( s.size()-1 )
		{
			case 3:
				r = (value >> 4) & 0xF0;
				g = (value >> 0) & 0xF0;
				b = (value << 4) & 0xF0;
				a = 255;
				break;
			case 4:
				r = (value >> 8) & 0xF0;
				g = (value >> 4) & 0xF0;
				b = (value >> 0) & 0xF0;
				a = (value << 4) & 0xF0;
				break;
			case 6:
				r = (value >> 16) & 0xFF;
				g = (value >>  8) & 0xFF;
				b = (value >>  0) & 0xFF;
				a = 255;
				break;
			case 8:
				r = (value >> 24) & 0xFF;
				g = (value >> 16) & 0xFF;
				b = (value >>  8) & 0xFF;
				a = (value >>  0) & 0xFF;
				break;
			default:
				// wrong string size
				return false;
		}
		return true;
	}

	void get_shape_size( const TopoDS_Shape& shape,
					double& sizex, double& sizey, double& sizez )
	{
		// get bounding box
		Bnd_Box box;
		box.SetGap( 0.0 );
		BRepBndLib::Add( shape, box );
		// AddOptimal might give better results?
		// BRepBndLib::AddOptimal( shape, box );

		Standard_Real xmin;
		Standard_Real ymin;
		Standard_Real zmin;
		Standard_Real xmax;
		Standard_Real ymax;
		Standard_Real zmax;

		box.Get( xmin, ymin, zmin, xmax, ymax, zmax );

		// calculate size
		sizex = xmax - xmin;
		sizey = ymax - ymin;
		sizez = zmax - zmin;
	}

	ColoredShape apply_transform( ColoredShape& shape,
						Transform& transform )
	{
		// BRepTools::Clean( shape );
		if( transform.is_simple() )
		{
			gp_Trsf occtTransform = transform.toTrsf();
			BRepBuilderAPI_Transform operation(
				shape,
				occtTransform,
				false // do not copy
			);
			return finish_transform( shape, transform, operation );
		}
		else
		{
			gp_GTrsf occtTransform = transform.toGTrsf();
			BRepBuilderAPI_GTransform operation(
				shape,
				occtTransform,
				true // do copy since shape changes
			);
			return finish_transform( shape, transform, operation );
		}
	}

	ColoredShape finish_transform( ColoredShape& shape,
					Transform& transform,
					BRepBuilderAPI_ModifyShape& operation )
	{
		if( !operation.IsDone() )
		{
			throw std::runtime_error(
				"BRepBuilderAPI_*Transform failed" );
		}
		ColoredShape object = { operation.Shape() };
		if( object.IsNull() )
		{
			throw std::runtime_error(
				"BRepBuilderAPI_*Transform returned a null shape" );
		}
#if 0
		obj_check( shape, "BRepBuilderAPI_*Transform input" );
		obj_check( object, "BRepBuilderAPI_*Transform output" );
		BRepTools::Clean( object );
		BRepMesh_IncrementalMesh mesh(
			object,
			0.01,  // deflection
			false, // relative
			0.5,   // angular deflection
			true ); // parallel
		obj_check( object, "BRepBuilderAPI_*Transform incmesh" );
#endif
		object.add_history( shape, operation );
		return object;
	}

	void obj_check( ColoredShape& shape, std::string descr )
	{
		BRepCheck_Analyzer analyzer( shape );
		if( !analyzer.IsValid() )
		{
			std::cout << descr << " " << "analyzer said invalid" << std::endl;
			// analyzer.Result( shape )->Status();
			for( const BRepCheck_Status status : analyzer.Result( shape )->Status() )
			{
				std::cout << "  - "
						  << statusToString(status)
						  << " (" << static_cast<int>(status) << ")\n";

			}
			/*
			throw std::runtime_error( std::format(
				"{} analyzer said invalid",
				descr
			) );
			*/
		}

		int solidCount = 0;
		for( TopExp_Explorer it( shape, TopAbs_SOLID ); it.More(); it.Next() )
		{
			++solidCount;
		}
		std::cout << descr << " " << solidCount << std::endl;
	}

	static const char* statusToString( BRepCheck_Status status )
	{
		switch (status)
		{
			case BRepCheck_NoError:
				return "NoError";

			case BRepCheck_InvalidPointOnCurve:
				return "InvalidPointOnCurve";

			case BRepCheck_InvalidPointOnCurveOnSurface:
				return "InvalidPointOnCurveOnSurface";

			case BRepCheck_InvalidPointOnSurface:
				return "InvalidPointOnSurface";

			case BRepCheck_No3DCurve:
				return "No3DCurve";

			case BRepCheck_Multiple3DCurve:
				return "Multiple3DCurve";

			case BRepCheck_Invalid3DCurve:
				return "Invalid3DCurve";

			case BRepCheck_NoCurveOnSurface:
				return "NoCurveOnSurface";

			case BRepCheck_InvalidCurveOnSurface:
				return "InvalidCurveOnSurface";

			case BRepCheck_InvalidCurveOnClosedSurface:
				return "InvalidCurveOnClosedSurface";

			case BRepCheck_InvalidSameRangeFlag:
				return "InvalidSameRangeFlag";

			case BRepCheck_InvalidSameParameterFlag:
				return "InvalidSameParameterFlag";

			case BRepCheck_InvalidDegeneratedFlag:
				return "InvalidDegeneratedFlag";

			case BRepCheck_FreeEdge:
				return "FreeEdge";

			case BRepCheck_InvalidMultiConnexity:
				return "InvalidMultiConnexity";

			case BRepCheck_InvalidRange:
				return "InvalidRange";

			case BRepCheck_EmptyWire:
				return "EmptyWire";

			case BRepCheck_RedundantEdge:
				return "RedundantEdge";

			case BRepCheck_SelfIntersectingWire:
				return "SelfIntersectingWire";

			case BRepCheck_NoSurface:
				return "NoSurface";

			case BRepCheck_InvalidWire:
				return "InvalidWire";

			case BRepCheck_NotClosed:
				return "NotClosed";

			case BRepCheck_NotConnected:
				return "NotConnected";

			case BRepCheck_BadOrientation:
				return "BadOrientation";

			case BRepCheck_BadOrientationOfSubshape:
				return "BadOrientationOfSubshape";

			case BRepCheck_EmptyShell:
				return "EmptyShell";

			case BRepCheck_CheckFail:
				return "CheckFail";

			default:
				return "UnknownStatus";
		}
	}

};


// ============================================================
// OpenCascade
// ============================================================

bool saveAsStep( ColoredShape& shape, const std::string& filename )
{
	// initialize the XCAF document
	Handle( XCAFApp_Application ) app = XCAFApp_Application::GetApplication();
	Handle( TDocStd_Document ) doc;
	app->NewDocument( "MDTV-XCAF", doc );
	Handle( XCAFDoc_ShapeTool ) shapeTool =
					XCAFDoc_DocumentTool::ShapeTool( doc->Main() );
	Handle( XCAFDoc_ColorTool ) colorTool =
					XCAFDoc_DocumentTool::ColorTool( doc->Main() );

	// set the shape
	TDF_Label label = shapeTool->NewShape();
	shapeTool->SetShape( label, shape );

	// set the colors
	std::cout << "number of colored faces: " << shape.faces.size() << std::endl;
	for( ColoredFace& colorFace : shape.faces )
	{
		TDF_Label faceLabel = shapeTool->AddSubShape( label, colorFace );
		colorTool->SetColor( faceLabel, colorFace, XCAFDoc_ColorSurf );
	}

	// save as step file
	STEPCAFControl_Writer writer;
	writer.SetColorMode( Standard_True );
	writer.SetNameMode( Standard_True );
	writer.SetLayerMode( Standard_True );
	writer.SetPropsMode( Standard_True );
	if( !writer.Transfer( doc, STEPControl_AsIs ) )
	{
		// throw std::runtime_error( "Could not transfer XDE document to STEP" );
		std::cout << "Could not transfer XDE document to STEP" << std::endl;
		return false;
	}
	if( writer.Write( filename.c_str() ) != IFSelect_RetDone )
	{
		// throw std::runtime_error( "Could not write STEP file" );
		std::cout << "Could not write STEP file" << std::endl;
		return false;
	}
	return true;
}

// ============================================================
// main
// ============================================================


struct Options
{
    bool debug = false;
    bool help = false;
    std::string outputFilename;
    std::string inputFilename;
    Node moduleCall;
};

Options parseArguments( int argc, char** argv )
{
    Options options;

	options.moduleCall.kind = Node::Kind::Module;
    for( int i=1; i<argc; ++i )
    {
        std::string argument = argv[i];

        if( argument == "-d" )
        {
            options.debug = true;
        }
        else if( argument == "-h" )
        {
			std::cout <<
"Usage:\n"
"  scad2step.exe [options...] input-filename\n"
"\n"
"Options:\n"
"\n"
"  -d                    Debug.\n"
"\n"
"  -h                    Print this help.\n"
"\n"
"  -m module-name        Generate this module only.\n"
"\n"
"  -o output-filename    Set the file name of the generated STEP file.\n"
"\n"
"  -p pname pvalue       Add a parameter for the module call.\n"
"\n"
"";
            options.help = true;
			return options;
        }
        else if( argument == "-m" )
        {
            if( i + 1 >= argc )
            {
                throw std::runtime_error( "-m requires a module name" );
            }
			if( !options.moduleCall.name.empty() )
			{
                throw std::runtime_error( "module name has already been specified" );
			}
            options.moduleCall.name = argv[++i];
        }
        else if( argument == "-o" )
        {
            if( i + 1 >= argc )
            {
                throw std::runtime_error( "-o requires an output filename" );
            }
			if( !options.outputFilename.empty() )
			{
                throw std::runtime_error( "output file name has already been specified" );
			}
            options.outputFilename = argv[++i];
        }
        else if( argument == "-p" )
        {
            if( i + 2 >= argc )
            {
                throw std::runtime_error( "-p requires a name and a value" );
            }
			Argument argument;
			argument.name = argv[++i];
			char* end;
			double f = std::strtod( argv[++i], &end );
			argument.value = Expression::numberValue( -1, f );
            options.moduleCall.arguments.push_back( std::move( argument ) );
        }
        else if( !argument.empty() && argument[0] == '-' )
        {
            throw std::runtime_error( "Unknown option: " + argument );
        }
        else
        {
            if( !options.inputFilename.empty() )
            {
                throw std::runtime_error( "Multiple input filenames provided" );
            }
            options.inputFilename = argument;
        }
    }

    if( options.inputFilename.empty() )
    {
        throw std::runtime_error( "No input filename provided" );
    }

	if( options.outputFilename.empty() )
	{
		std::filesystem::path inputPath( options.inputFilename );
		inputPath.replace_extension( ".step" );

		options.outputFilename = inputPath.string();
	}

    return options;
}

std::string loadFile( const std::string& filename )
{
    std::ifstream file( filename, std::ios::binary | std::ios::ate );

    if( !file )
    {
        throw std::runtime_error(
            "Could not open input file: " + filename
        );
    }

    const std::streamsize size = file.tellg();

    if( size < 0 )
    {
        throw std::runtime_error(
            "Could not determine file size: " + filename
        );
    }

    std::string buffer( static_cast<std::size_t>(size), '\0' );

    file.seekg( 0, std::ios::beg );

    if( !buffer.empty() && !file.read(buffer.data(), size) )
    {
        throw std::runtime_error(
            "Could not read input file: " + filename
        );
    }

    return buffer;
}

int main( int argc, char** argv )
{
	try
	{
		Options options = parseArguments( argc, argv );
		if( options.help )
		{
			return 0;
		}
		const std::string source = loadFile( options.inputFilename );
		Parser parser( source );
		auto tree = parser.parse();

		TreeExecutor executor( parser.get_modules() );
        ColoredShape shape = executor.execute( tree, options.moduleCall );

		if( shape.IsNull() )
		{
            std::cerr << "No 3D object has been created.\n";
            return 1;
		}
        else if( saveAsStep( shape, options.outputFilename ) )
        {
            std::cout << "STEP file saved successfully: "
                      << options.outputFilename << '\n';
        }
        else
        {
            std::cerr << "Failed to save the STEP file.\n";
            return 1;
        }
	}
	catch( const std::exception& exception )
	{
		std::cerr << "Error: " << exception.what() << '\n';
		return 1;
	}
	catch( const Standard_DomainError& e )
	{
		// std::cerr << "OCCT domain error: " << e.what() << '\n';
		std::cerr << "OCCT domain error: " << e.GetMessageString() << '\n';
	}
	catch( const Standard_Failure& e )
	{
		// Other OCCT exceptions
		// std::cerr << "OCCT error: " << e.what() << '\n';
		std::cerr << "OCCT error: " << e.GetMessageString() << '\n';
	}
	catch( ... )
	{
		std::cerr << "Error: Unknown exception type" << std::endl;
		return 1;
	}

	return 0;
}

