#include <psemek/app/default_application_factory.hpp>
#include <psemek/gfx/gl.hpp>
#include <psemek/gfx/painter.hpp>
#include <psemek/util/array.hpp>
#include <psemek/random/generator.hpp>
#include <psemek/random/device.hpp>
#include <psemek/random/uniform.hpp>
#include <psemek/util/hash_table.hpp>
#include <psemek/util/enum.hpp>
#include <psemek/util/clock.hpp>
#include <psemek/util/executable_path.hpp>
#include <psemek/math/box.hpp>
#include <psemek/math/camera.hpp>
#include <psemek/math/contains.hpp>
#include <psemek/math/gradient.hpp>
#include <psemek/ecs/container.hpp>
#include <psemek/ecs/declare_uuid.hpp>

#include <psemek/log/log.hpp>
#include <psemek/io/file_stream.hpp>

#include <psemek/audio/engine.hpp>
#include <psemek/audio/combine/mixer.hpp>
#include <psemek/audio/effect/compressor.hpp>
#include <psemek/audio/effect/volume.hpp>
#include <psemek/audio/wave/karplus_strong.hpp>
#include <psemek/audio/wave/sine.hpp>
#include <psemek/audio/wave/triangle.hpp>
#include <psemek/audio/effect/filter.hpp>
#include <psemek/audio/effect/fade_in.hpp>
#include <psemek/audio/effect/fade_out.hpp>
#include <psemek/audio/effect/pitch.hpp>
#include <psemek/audio/track.hpp>

#include <psemek/app/resource.hpp>

#include <boost/container/flat_set.hpp>
#include <boost/container/flat_map.hpp>

#include <map>

namespace gmtk
{

	using namespace psemek;

	psemek_declare_enum(color_type, std::uint32_t,
		(white)
		(black)
		(gray)
		(red)
		(green)
		(blue)
		(cyan)
		(magenta)
		(yellow)
	)

	psemek_declare_enum(shape_type, std::uint32_t,
		(circle)
		(square)
		(hexagon)
	)

	struct resource_type
	{
		color_type color;
		shape_type shape;

		friend bool operator == (resource_type const &, resource_type const &) = default;
		friend auto operator <=> (resource_type const &, resource_type const &) = default;
	};

	struct resource_hash
	{
		std::size_t operator()(resource_type const & r) const noexcept
		{
			return util::hash_all(r.color, r.shape);
		}
	};

	psemek_declare_enum(transformer_type, std::uint32_t,
		(mixer)
		(reshaper)
		(hue_shifter)
	)

	psemek_declare_enum(card_type, std::uint32_t,
		(mixer)
		(reshaper)
		(hue_shifter)
		(crossing)
		(zoomer)
		(eraser)
	)

	gfx::color_rgba color_of(color_type c)
	{
		switch (c)
		{
		case color_type::white: return {255, 255, 255, 255};
		case color_type::black: return {64, 64, 64, 255};
		case color_type::gray: return {160, 160, 160, 255};
		case color_type::red: return {255, 96, 96, 255};
		case color_type::green: return {96, 255, 96, 255};
		case color_type::blue: return {96, 96, 255, 255};
		case color_type::cyan: return {96, 255, 255, 255};
		case color_type::magenta: return {255, 96, 255, 255};
		case color_type::yellow: return {255, 255, 96, 255};
		}

		throw util::unknown_enum_value_exception{c};
	}

	gfx::color_rgba color_of(resource_type const & r)
	{
		return color_of(r.color);
	}

	card_type transformer_to_card(transformer_type type)
	{
		switch (type)
		{
		case transformer_type::mixer: return card_type::mixer;
		case transformer_type::reshaper: return card_type::reshaper;
		case transformer_type::hue_shifter: return card_type::hue_shifter;
		}

		throw util::unknown_enum_value_exception{type};
	}

	std::optional<transformer_type> card_to_transformer(card_type type)
	{
		switch (type)
		{
		case card_type::mixer: return transformer_type::mixer;
		case card_type::reshaper: return transformer_type::reshaper;
		case card_type::hue_shifter: return transformer_type::hue_shifter;
		default: return std::nullopt;
		}

		return std::nullopt;
	}

	struct recipe
	{
		boost::container::flat_multiset<resource_type> inputs;
		resource_type output;
	};

	static util::hash_map<transformer_type, std::vector<recipe>> const recipies
	{
		{
			transformer_type::mixer,
			{
				{{{color_type::white, shape_type::circle}, {color_type::black, shape_type::circle}}, {color_type::gray, shape_type::circle}},
				{{{color_type::white, shape_type::circle}, {color_type::red, shape_type::circle}, {color_type::green, shape_type::circle}}, {color_type::yellow, shape_type::circle}},
				{{{color_type::white, shape_type::circle}, {color_type::red, shape_type::circle}, {color_type::blue, shape_type::circle}}, {color_type::magenta, shape_type::circle}},
				{{{color_type::white, shape_type::circle}, {color_type::green, shape_type::circle}, {color_type::blue, shape_type::circle}}, {color_type::cyan, shape_type::circle}},
				{{{color_type::cyan, shape_type::square}, {color_type::magenta, shape_type::square}, {color_type::yellow, shape_type::square}}, {color_type::black, shape_type::square}},
				{{{color_type::red, shape_type::square}, {color_type::green, shape_type::square}, {color_type::blue, shape_type::square}}, {color_type::white, shape_type::square}},
				{{{color_type::white, shape_type::square}, {color_type::black, shape_type::square}}, {color_type::gray, shape_type::square}},
			},
		},
		{
			transformer_type::reshaper,
			{
				{{{color_type::gray, shape_type::circle}, {color_type::red, shape_type::circle}}, {color_type::red, shape_type::square}},
				{{{color_type::gray, shape_type::circle}, {color_type::green, shape_type::circle}}, {color_type::green, shape_type::square}},
				{{{color_type::gray, shape_type::circle}, {color_type::blue, shape_type::circle}}, {color_type::blue, shape_type::square}},
				{{{color_type::gray, shape_type::circle}, {color_type::cyan, shape_type::circle}}, {color_type::cyan, shape_type::square}},
				{{{color_type::gray, shape_type::circle}, {color_type::magenta, shape_type::circle}}, {color_type::magenta, shape_type::square}},
				{{{color_type::gray, shape_type::circle}, {color_type::yellow, shape_type::circle}}, {color_type::yellow, shape_type::square}},

				// {{{color_type::white, shape_type::square}, {color_type::red, shape_type::square}}, {color_type::red, shape_type::hexagon}},
				// {{{color_type::white, shape_type::square}, {color_type::green, shape_type::square}}, {color_type::green, shape_type::hexagon}},
				// {{{color_type::white, shape_type::square}, {color_type::blue, shape_type::square}}, {color_type::blue, shape_type::hexagon}},
				// {{{color_type::white, shape_type::square}, {color_type::cyan, shape_type::square}}, {color_type::cyan, shape_type::hexagon}},
				// {{{color_type::white, shape_type::square}, {color_type::magenta, shape_type::square}}, {color_type::magenta, shape_type::hexagon}},
				// {{{color_type::white, shape_type::square}, {color_type::yellow, shape_type::square}}, {color_type::yellow, shape_type::hexagon}},
			},
		},
		{
			transformer_type::hue_shifter,
			{
				{{{color_type::black, shape_type::circle}, {color_type::red, shape_type::circle}}, {color_type::green, shape_type::circle}},
				{{{color_type::black, shape_type::circle}, {color_type::green, shape_type::circle}}, {color_type::blue, shape_type::circle}},
				{{{color_type::black, shape_type::circle}, {color_type::blue, shape_type::circle}}, {color_type::red, shape_type::circle}},
				{{{color_type::black, shape_type::circle}, {color_type::cyan, shape_type::circle}}, {color_type::magenta, shape_type::circle}},
				{{{color_type::black, shape_type::circle}, {color_type::magenta, shape_type::circle}}, {color_type::yellow, shape_type::circle}},
				{{{color_type::black, shape_type::circle}, {color_type::yellow, shape_type::circle}}, {color_type::cyan, shape_type::circle}},
			},
		},
	};

	std::optional<transformer_type> transformer_for(resource_type const & type)
	{
		if (type == resource_type{color_type::gray, shape_type::circle})
			return transformer_type::mixer;

		if (type == resource_type{color_type::green, shape_type::circle})
			return transformer_type::hue_shifter;

		if (type == resource_type{color_type::green, shape_type::square})
			return transformer_type::reshaper;

		if (type == resource_type{color_type::blue, shape_type::square})
			return transformer_type::reshaper;

		if (type == resource_type{color_type::yellow, shape_type::square})
			return transformer_type::reshaper;

		if (type == resource_type{color_type::cyan, shape_type::square})
			return transformer_type::reshaper;

		if (type == resource_type{color_type::magenta, shape_type::square})
			return transformer_type::reshaper;

		if (type == resource_type{color_type::black, shape_type::square})
			return transformer_type::mixer;

		if (type == resource_type{color_type::white, shape_type::square})
			return transformer_type::mixer;

		return std::nullopt;
	}

	math::vector<int, 2> const neighbours[4]
	{
		{1, 0},
		{0, 1},
		{-1, 0},
		{0, -1},
	};

	struct location
	{
		int level;
		math::point<int, 2> coords;

		math::point<float, 2> center() const
		{
			float s = std::pow(3.f, -level);
			return {(coords[0] + 0.5f) * s, (coords[1] + 0.5f) * s};
		}

		math::box<float, 2> bbox(float extra = 0.f) const
		{
			float s = std::pow(3.f, -level);
			return {{{(coords[0] - extra) * s, (coords[0] + 1.f + extra) * s}, {(coords[1] - extra) * s, (coords[1] + 1.f + extra) * s}}};
		}

		location left() const
		{
			return {level, {coords[0] - 1, coords[1]}};
		}

		location right() const
		{
			return {level, {coords[0] + 1, coords[1]}};
		}

		location bottom() const
		{
			return {level, {coords[0], coords[1] - 1}};
		}

		location top() const
		{
			return {level, {coords[0], coords[1] + 1}};
		}

		location moved(math::vector<int, 2> const & delta) const
		{
			return {level, coords + delta};
		}

		location down() const
		{
			return {level + 1, {coords[0] * 3 + 1, coords[1] * 3 + 1}};
		}

		location up() const
		{
			return {level - 1, {math::idiv(coords[0], 3), math::idiv(coords[1], 3)}};
		}

		friend bool operator == (location const & x, location const & y) = default;
		friend auto operator <=> (location const & x, location const & y) = default;
	};

	struct location_hash
	{
		std::size_t operator()(location const & x) const noexcept
		{
			return util::hash_all(x.level, x.coords[0], x.coords[1]);
		}
	};

	struct vertex
	{
		psemek_ecs_declare_uuid("vertex")

		struct location location;
	};

	struct source
	{
		psemek_ecs_declare_uuid("source")

		resource_type type;
		float animate = 0.f;
	};

	struct transformer
	{
		psemek_ecs_declare_uuid("transformer")

		transformer_type type;
		float animate = 0.f;
	};

	struct crossing
	{
		psemek_ecs_declare_uuid("crossing")
	};

	struct zoomer
	{
		psemek_ecs_declare_uuid("zoomer")
	};

	struct path_vertex
	{
		psemek_ecs_declare_uuid("path_vertex")

		struct location location;

		boost::container::flat_set<ecs::handle> belts_to = {};
		boost::container::flat_set<ecs::handle> belts_from = {};
	};

	void add_belt(ecs::container & world, ecs::handle from, ecs::handle to)
	{
		world.get(from).get<path_vertex>().belts_to.insert(to);
		world.get(to).get<path_vertex>().belts_from.insert(from);
	}

	bool remove_belt(ecs::container & world, ecs::handle from, ecs::handle to)
	{
		bool removed = world.get(from).get<path_vertex>().belts_to.contains(to);
		world.get(from).get<path_vertex>().belts_to.erase(to);
		world.get(to).get<path_vertex>().belts_from.erase(from);
		return removed;
	}

	struct occupied
	{
		psemek_ecs_declare_uuid("occupied")

		ecs::handle entity;
	};

	struct item
	{
		psemek_ecs_declare_uuid("item")

		resource_type type;
		location start;
		ecs::handle target = ecs::handle::null();
		float state = 0.f;
	};

	math::point<float, 2> position(ecs::container & world, item const & i)
	{
		if (i.target)
		{
			auto end = world.get(i.target).get<path_vertex const>().location;
			return math::lerp(i.start.center(), end.center(), i.state);
		}
		else
			return i.start.center();
	}

	float scale(ecs::container & world, item const & i)
	{
		if (i.target)
		{
			auto end = world.get(i.target).get<path_vertex const>().location;
			return 3.f * std::pow(3.f, -math::lerp<float>(i.start.level, end.level, i.state));
		}
		else
			return 3.f * std::pow(3.f, -i.start.level);
	}

	bool within_grid(location const & l)
	{
		int max = std::pow(3, l.level + 1);
		return l.coords[0] >= 0 && l.coords[0] < max && l.coords[1] >= 0 && l.coords[1] < max;
	}

	bool within_tile(location const & tile, location const & l)
	{
		if (l.level < tile.level)
			return false;

		int s = std::pow(3, l.level - tile.level);

		math::interval xrange{tile.coords[0] * s, (tile.coords[0] + 1) * s - 1};
		math::interval yrange{tile.coords[1] * s, (tile.coords[1] + 1) * s - 1};

		return math::contains(xrange, l.coords[0]) && math::contains(yrange, l.coords[1]);
	}

	struct lab
	{
		psemek_ecs_declare_uuid("lab")

		float animate = 0.f;
		float animate_error = 0.f;
	};

	struct map
	{
		std::unique_ptr<ecs::container> world;

		util::hash_map<card_type, int> cards = {};

		int stage = 0;
		float stage_animation = 0.f;
		bool victory = false;

		int resource_count = 0;

		float spawn_timer = 0.f;

		bool take_card(card_type type)
		{
			if (type == card_type::eraser)
				return true;

			if (!cards.contains(type))
				return false;

			if (cards.at(type) == 0)
				return false;

			cards.at(type) -= 1;
			return true;
		}

		void put_card(card_type type)
		{
			cards[type] += 1;
		}
	};

	struct stage_info
	{
		resource_type type;
		int count;

		std::vector<card_type> cards;
		std::vector<resource_type> sources;
	};

	static stage_info stages[]
	{
		{{color_type::white, shape_type::circle}, 0, {}, {{color_type::white, shape_type::circle}}},
		{{color_type::white, shape_type::circle}, 15, {card_type::mixer}, {{color_type::black, shape_type::circle}}},
		{{color_type::gray, shape_type::circle}, 15, {card_type::hue_shifter, card_type::crossing}, {{color_type::red, shape_type::circle}}},
		{{color_type::green, shape_type::circle}, 15, {card_type::reshaper, card_type::crossing}, {}},
		{{color_type::green, shape_type::square}, 30, {card_type::hue_shifter, card_type::crossing, card_type::zoomer, card_type::zoomer}, {}},
		{{color_type::blue, shape_type::square}, 30, {card_type::mixer, card_type::crossing, card_type::zoomer}, {}},
		{{color_type::yellow, shape_type::square}, 30, {card_type::zoomer}, {}},
		{{color_type::cyan, shape_type::square}, 30, {}, {}},
		{{color_type::magenta, shape_type::square}, 30, {card_type::reshaper, card_type::reshaper, card_type::zoomer, card_type::mixer, card_type::mixer, card_type::mixer, card_type::mixer, card_type::zoomer, card_type::zoomer, card_type::crossing}, {{color_type::black, shape_type::circle}}},
		{{color_type::black, shape_type::square}, 45, {card_type::mixer, card_type::mixer, card_type::reshaper, card_type::reshaper, card_type::reshaper, card_type::zoomer, card_type::zoomer, card_type::crossing, card_type::crossing, card_type::crossing}, {{color_type::red, shape_type::circle}}},
		{{color_type::gray, shape_type::square}, 60, {}, {}},
	};

	template <typename Component, util::uuid UUID>
	struct index_base
	{
		static constexpr util::uuid uuid()
		{
			return UUID;
		}

		index_base(ecs::container & world)
			: world_(world)
		{
			world.apply<Component>([this](ecs::handle entity, Component const & v){
				index_[v.location] = entity;
			});

			world.constructor<Component>([this](ecs::handle entity, Component const & v){
				index_[v.location] = entity;
			});

			world.destructor<Component>([this](Component const & v){
				index_.erase(v.location);
			});
		}

		std::optional<ecs::handle> find(location const & l) const
		{
			if (auto it = index_.find(l); it != index_.end())
				return it->second;
			return std::nullopt;
		}

		ecs::handle get(location const & l) const
		{
			if (auto entity = find(l))
				return *entity;

			return world_.create(Component{l});
		}

	private:
		ecs::container & world_;
		util::hash_map<location, ecs::handle, location_hash> index_;
	};

	using index = index_base<vertex, util::make_uuid("vertex_index")>;
	using path_index = index_base<path_vertex, util::make_uuid("path_vertex_index")>;

	//  1 - from center to m
	// -1 - from m to center
	//  0 - no belt
	int sink_belt(ecs::container & world, location m)
	{
		if (auto entity = world.index<index>().find(m.up()); !entity || !world.get(*entity).contains<zoomer>())
			return 0;

		auto & index = world.index<path_index>();

		auto c = m.up().down();

		auto d = m.coords - c.coords;

		auto ce = index.get(c);
		auto me = index.get(m);

		auto & cv = world.get(ce).get<path_vertex>();

		if (cv.belts_to.contains(me))
		{
			auto & mv = world.get(me).get<path_vertex>();
			auto ne = *mv.belts_to.begin();

			remove_belt(world, ce, me);
			remove_belt(world, me, ne);

			auto cd = m.down();
			auto md = cd.moved(d);

			auto cde = index.get(cd);
			auto mde = index.get(md);

			add_belt(world, cde, mde);
			add_belt(world, mde, ne);

			sink_belt(world, md);

			return 1;
		}
		else if (cv.belts_from.contains(me))
		{
			auto & mv = world.get(me).get<path_vertex>();
			auto ne = *mv.belts_from.begin();

			remove_belt(world, ne, me);
			remove_belt(world, me, ce);

			auto cd = m.down();
			auto md = cd.moved(d);

			auto cde = index.get(cd);
			auto mde = index.get(md);

			add_belt(world, ne, mde);
			add_belt(world, mde, cde);

			sink_belt(world, md);

			return -1;
		}
		else
			return 0;
	}

	void sink(ecs::container & world, location & c, location & m)
	{
		if (auto entity = world.index<index>().find(m.up()); !entity || !world.get(*entity).contains<zoomer>())
			return;

		auto d = m.coords - c.coords;

		c = m.down();
		m = c.moved(d);

		sink(world, c, m);
	}

	std::optional<card_type> destroy_tile_entity(map & map, ecs::handle entity)
	{
		auto acc = map.world->get(entity);

		if (acc.contains<source>() || acc.contains<lab>())
			return std::nullopt;

		std::optional<card_type> result;

		if (acc.contains<crossing>())
		{
			result = card_type::crossing;
		}
		else if (acc.contains<zoomer>())
		{
			result = card_type::zoomer;
		}
		else if (auto t = acc.get_if<transformer>())
		{
			result = transformer_to_card(t->type);
		}

		if (result)
			map.put_card(*result);

		map.world->destroy(entity);

		return result;
	}

	void clear_tile(map & map, location const & l)
	{
		auto entity = map.world->index<index>().find(l);
		if (!entity)
			return;

		destroy_tile_entity(map, *entity);

		auto & index = map.world->index<path_index>();

		for (int y = 0; y < 3; ++y)
		{
			for (int x = 0; x < 3; ++x)
			{
				location p{l.level + 1, {3 * l.coords[0] + x, 3 * l.coords[1] + y}};

				clear_tile(map, p);

				if (auto e = index.find(p))
				{
					auto & v = map.world->get(*e).get<path_vertex>();

					for (auto to : v.belts_to)
					{
						auto & tv = map.world->get(to).get<path_vertex>();
						if (!within_tile(l, tv.location))
							remove_belt(*map.world, to, *tv.belts_to.begin());
						remove_belt(*map.world, *e, to);
					}

					for (auto from : v.belts_from)
					{
						auto & fv = map.world->get(from).get<path_vertex>();
						if (!within_tile(l, fv.location))
							remove_belt(*map.world, *fv.belts_from.begin(), from);
						remove_belt(*map.world, from, *e);
					}
				}
			}
		}

		for (int y = 0; y < 9; ++y)
		{
			for (int x = 0; x < 9; ++x)
			{
				location p{l.level + 2, {9 * l.coords[0] + x, 9 * l.coords[1] + y}};

				if (auto e = index.find(p))
				{
					auto & v = map.world->get(*e).get<path_vertex>();

					for (auto to : v.belts_to)
					{
						auto & tv = map.world->get(to).get<path_vertex>();
						if (!within_tile(l, tv.location))
							remove_belt(*map.world, to, *tv.belts_to.begin());
						remove_belt(*map.world, *e, to);
					}

					for (auto from : v.belts_from)
					{
						auto & fv = map.world->get(from).get<path_vertex>();
						if (!within_tile(l, fv.location))
							remove_belt(*map.world, *fv.belts_from.begin(), from);
						remove_belt(*map.world, from, *e);
					}
				}
			}
		}
	}

	bool place_card(map & map, card_type type, location const & l)
	{
		switch (type)
		{
		case card_type::mixer:
		case card_type::hue_shifter:
		case card_type::reshaper:
			map.world->create(
				vertex{l},
				transformer{card_to_transformer(type).value()}
			);
			return true;
		case card_type::crossing:
			map.world->create(
				vertex{l},
				crossing{}
			);
			return true;
		case card_type::zoomer:
		{
			map.world->create(
				vertex{l},
				zoomer{}
			);

			auto & index = map.world->index<path_index>();

			auto c = l.down();
			auto cd = c.down();

			if (int d = sink_belt(*map.world, c.left()))
			{
				for (int i = 0; i < 3; ++i)
				{
					auto e0 = index.get(cd.moved({-i, 0}));
					auto e1 = index.get(cd.moved({-i-1, 0}));

					if (d == 1)
						add_belt(*map.world, e0, e1);
					else
						add_belt(*map.world, e1, e0);
				}
			}
			if (int d = sink_belt(*map.world, c.right()))
			{
				for (int i = 0; i < 3; ++i)
				{
					auto e0 = index.get(cd.moved({i, 0}));
					auto e1 = index.get(cd.moved({i+1, 0}));

					if (d == 1)
						add_belt(*map.world, e0, e1);
					else
						add_belt(*map.world, e1, e0);
				}
			}
			if (int d = sink_belt(*map.world, c.bottom()))
			{
				for (int i = 0; i < 3; ++i)
				{
					auto e0 = index.get(cd.moved({0, -i}));
					auto e1 = index.get(cd.moved({0, -i-1}));

					if (d == 1)
						add_belt(*map.world, e0, e1);
					else
						add_belt(*map.world, e1, e0);
				}
			}
			if (int d = sink_belt(*map.world, c.top()))
			{
				for (int i = 0; i < 3; ++i)
				{
					auto e0 = index.get(cd.moved({0, i}));
					auto e1 = index.get(cd.moved({0, i+1}));

					if (d == 1)
						add_belt(*map.world, e0, e1);
					else
						add_belt(*map.world, e1, e0);
				}
			}
			return true;
		}
			break;
		default:
			return false;
		}

		return false;
	}

	map start_menu_map()
	{
		map result;

		result.world = std::make_unique<ecs::container>();

		for (int y = 0; y < 3; ++y)
			for (int x = 0; x < 3; ++x)
				result.world->create(
					vertex{0, {x, y}},
					zoomer{}
				);

		result.world->create(
			vertex{0, {-1, 1}},
			source{{color_type::black, shape_type::circle}}
		);

		result.world->create(
			vertex{0, {3, 1}},
			source{{color_type::black, shape_type::circle}}
		);

		result.world->create(
			vertex{1, {1, 1}},
			source{{color_type::red, shape_type::circle}}
		);

		result.world->create(
			vertex{1, {7, 7}},
			source{{color_type::yellow, shape_type::circle}}
		);

		result.world->create(
			vertex{1, {1, 4}},
			transformer{transformer_type::hue_shifter}
		);

		result.world->create(
			vertex{1, {7, 4}},
			transformer{transformer_type::hue_shifter}
		);

		result.world->create(
			vertex{1, {1, 7}},
			lab{}
		);

		result.world->create(
			vertex{1, {7, 1}},
			lab{}
		);

		auto & index = result.world->index<path_index>();

		// Left black to hue shifter
		add_belt(*result.world, index.get({1, {-2, 4}}), index.get({1, {-1, 4}}));
		add_belt(*result.world, index.get({1, {-1, 4}}), index.get({2, {0, 13}}));
		add_belt(*result.world, index.get({2, {0, 13}}), index.get({2, {1, 13}}));
		add_belt(*result.world, index.get({2, {1, 13}}), index.get({2, {2, 13}}));
		add_belt(*result.world, index.get({2, {2, 13}}), index.get({2, {3, 13}}));
		add_belt(*result.world, index.get({2, {3, 13}}), index.get({2, {4, 13}}));

		// Bottom-left red to hue shifter to left lab
		add_belt(*result.world, index.get({1, {1, -2}}), index.get({1, {1, -1}}));
		add_belt(*result.world, index.get({1, {1, -1}}), index.get({2, {4, 0}}));
		add_belt(*result.world, index.get({2, {4, 0}}), index.get({2, {4, 1}}));
		for (int y = 1; y < 22; ++y)
			add_belt(*result.world, index.get({2, {4, y}}), index.get({2, {4, y + 1}}));

		add_belt(*result.world, index.get({1, {10, 4}}), index.get({1, {9, 4}}));
		add_belt(*result.world, index.get({1, {9, 4}}), index.get({2, {26, 13}}));
		add_belt(*result.world, index.get({2, {26, 13}}), index.get({2, {25, 13}}));

		for (int y = 4; y < 22; ++y)
			add_belt(*result.world, index.get({2, {22, y + 1}}), index.get({2, {22, y}}));

		for (int x = 22; x < 25; ++x)
			add_belt(*result.world, index.get({2, {x + 1, 13}}), index.get({2, {x, 13}}));

		return result;
	}

	map campaign_map(bool challenge)
	{
		map result;

		result.world = std::make_unique<ecs::container>();

		result.world->index<index>();
		result.world->index<path_index>();

		result.world->create(
			vertex{{0, {1, -1}}},
			lab{}
		);

		result.cards[card_type::eraser] = 1000000;
		if (!challenge)
			for (auto type : card_type_values())
				result.cards[type] = 1000000;

		return result;
	}

	map sandbox_map()
	{
		map result;

		result.world = std::make_unique<ecs::container>();

		result.world->index<index>();
		result.world->index<path_index>();

		result.world->create(
			vertex{{0, {-1, 0}}},
			source{{color_type::red, shape_type::circle}}
		);

		result.world->create(
			vertex{{0, {-1, 1}}},
			source{{color_type::green, shape_type::circle}}
		);

		result.world->create(
			vertex{{0, {-1, 2}}},
			source{{color_type::blue, shape_type::circle}}
		);

		result.world->create(
			vertex{{0, {3, 0}}},
			source{{color_type::cyan, shape_type::circle}}
		);

		result.world->create(
			vertex{{0, {3, 1}}},
			source{{color_type::magenta, shape_type::circle}}
		);

		result.world->create(
			vertex{{0, {3, 2}}},
			source{{color_type::yellow, shape_type::circle}}
		);

		result.world->create(
			vertex{{0, {0, 3}}},
			source{{color_type::black, shape_type::circle}}
		);

		result.world->create(
			vertex{{0, {1, 3}}},
			source{{color_type::gray, shape_type::circle}}
		);

		result.world->create(
			vertex{{0, {2, 3}}},
			source{{color_type::white, shape_type::circle}}
		);

		result.world->create(
			vertex{{0, {1, -1}}},
			lab{}
		);

		for (auto type : card_type_values())
			result.cards[type] = 1000000;

		return result;
	}

	void draw_grid(math::box<float, 2> const & box, float view_level, gfx::painter & painter, bool solid = false)
	{
		float const grid_width = 0.025f * std::min(box[0].length() / 3.f, std::pow(3.f, -1.f - view_level));

		gfx::color_rgba color = gfx::black;
		if (!solid)
			color = gfx::light(color, 0.75f);

		for (int x = 0; x <= 3; ++x)
		{
			if (solid)
			{
				painter.line(box.corner(x / 3.f, 0.f), box.corner(x / 3.f, 1.f), grid_width, color, true);
				painter.line(box.corner(0.f, x / 3.f), box.corner(1.f, x / 3.f), grid_width, color, true);
			}
			else
			{
				for (int i = 0; i < 7; ++i)
				{
					float s = (i - 1.f / 3.f) / 6.f;
					float t = (i + 1.f / 3.f) / 6.f;

					s = std::max(s, 0.f);
					t = std::min(t, 1.f);

					painter.line(box.corner(x / 3.f, s), box.corner(x / 3.f, t), grid_width, color, true);
					painter.line(box.corner(s, x / 3.f), box.corner(t, x / 3.f), grid_width, color, true);
				}
			}
		}
	}

	void draw_item(resource_type const & type, math::point<float, 2> const & pos, float scale, gfx::painter & painter, bool selected = false, int opacity = 255)
	{
		auto color = color_of(type);
		gfx::color_rgba bcolor = selected ? gfx::white : gfx::black;

		color[3] = opacity;
		bcolor[3] = opacity;

		switch (type.shape)
		{
		case shape_type::circle:
			painter.circle(pos, 0.075f * scale, bcolor);
			painter.circle(pos, 0.05f * scale, color);
			break;
		case shape_type::square:
		{
			auto box = math::expand(math::box<float, 2>::singleton(pos), 0.075f * scale);
			painter.rect(box, bcolor);
			box = math::shrink(box, 0.025f * scale);
			painter.rect(box, color);
		}
			break;
		case shape_type::hexagon:
		{
			for (int i = 0; i < 2; ++i)
			{
				float r = (i == 0 ? 0.0875f : 0.0875f - 0.025f / std::sqrt(0.75f)) * scale;
				auto c = (i == 0) ? bcolor : color;
				for (int j = 0; j < 6; ++j)
				{
					float a = math::rad(j * 60.f);
					float b = math::rad((j + 1) * 60.f);
					painter.triangle(pos, pos + math::direction(a) * r, pos + math::direction(b) * r, c);
				}
			}
		}
			break;
		}
	}

	float animation_factor(float animate)
	{
		static math::gradient<float> const g
		{
			std::pair{0.5f, 0.f},
			math::easing_type::cubic,
			std::pair{0.875f, 1.f},
			math::easing_type::cubic,
			std::pair{1.f, 0.f},
		};

		return g(animate);
	}

	void draw_structure(math::box<float, 2> const & bbox, transformer const & t, gfx::painter & painter)
	{
		switch (t.type)
		{
		case transformer_type::mixer:
		{
			float an = animation_factor(t.animate);

			auto box = math::shrink(bbox, bbox[0].length() * 0.2f);

			float r1 = std::sqrt(2.f) * (2.f + 2.f * an) / 16.f * box[0].length();
			float r2 = std::sqrt(2.f) * (4.f + 2.f * an) / 16.f * box[0].length();

			float s = bbox[0].length() * 0.025f;
			float t = 0.6f * s;

			box = math::expand(box, an * box[0].length() * 0.125f);

			math::point<float, 2> points[]
			{
				box.corner(0.f, 0.75f),
				box.corner(0.f, 0.25f),
				box.corner(0.25f, 0.f),
				box.corner(0.75f, 0.f),
				box.corner(1.f, 0.25f),
				box.corner(1.f, 0.75f),
				box.corner(0.75f, 1.f),
				box.corner(0.25f, 1.f),
			};

			static math::triangle<std::uint32_t> const triangles[]
			{
				{0, 1, 2},
				{0, 2, 3},
				{0, 3, 4},
				{0, 4, 5},
				{0, 5, 6},
				{0, 6, 7},
			};

			for (int i = 0; i < 2; ++i)
			{
				auto color = (i == 0) ? gfx::black : gfx::white;

				if (i == 1)
				{
					points[0] += math::vector{ s, -t};
					points[1] += math::vector{ s,  t};
					points[2] += math::vector{ t,  s};
					points[3] += math::vector{-t,  s};
					points[4] += math::vector{-s,  t};
					points[5] += math::vector{-s, -t};
					points[6] += math::vector{-t, -s};
					points[7] += math::vector{ t, -s};
				}

				for (auto const & t : triangles)
				{
					auto p0 = points[t[0]];
					auto p1 = points[t[1]];
					auto p2 = points[t[2]];

					painter.triangle(p0, p1, p2, color);
				}
			}

			auto c = bbox.center();

			for (int i = 0; i < 4; ++i)
			{
				auto d = math::direction(math::rad(45.f + 90.f * i));
				painter.line(c + d * r1, c + d * r2, s, gfx::black, true);
			}
		}
			break;
		case transformer_type::reshaper:
		{
			auto box = math::shrink(bbox, bbox[0].length() * 0.2f);

			float s = bbox[0].length() * 0.025f;

			// for (int k = 0; k < 2; ++k)
			for (int k = 1; k < 2; ++k)
			{
				math::point<float, 2> points[]
				{
					box.corner(0.f, 1.f),
					box.corner(0.f, 0.75f),
					box.corner(0.25f, 0.5f),
					box.corner(0.75f, 0.5f),
					box.corner(1.f, 0.75f),
					box.corner(1.f, 1.f),
				};

				static math::triangle<std::uint32_t> const triangles[]
				{
					{0, 1, 2},
					{0, 2, 3},
					{0, 3, 4},
					{0, 4, 5},
				};

				auto c = bbox.center();

				for (int i = 0; i < 2; ++i)
				{
					auto color = (i == 0) ? gfx::black : gfx::white;

					if (i == 1)
					{
						points[0] += math::vector{s, -s};
						points[1] += math::vector{s, 0.6f * s};
						points[2] += math::vector{0.6f * s, s};
						points[3] += math::vector{- 0.6f * s, s};
						points[4] += math::vector{-s, 0.6f * s};
						points[5] += math::vector{-s, -s};
					}

					for (int j = 0; j < 2; ++j)
					{
						float d = 1.f - animation_factor(t.animate);
						d *= box[0].length() * 0.125f;
						if (j == 1)
							d = -d;

						for (auto const & t : triangles)
						{
							auto p0 = points[t[0]];
							auto p1 = points[t[1]];
							auto p2 = points[t[2]];

							if (j == 1)
							{
								float m = box[1].min + box[1].max;
								p0[1] = m - p0[1];
								p1[1] = m - p1[1];
								p2[1] = m - p2[1];
							}

							p0[1] += d;
							p1[1] += d;
							p2[1] += d;

							if (k == 0)
							{
								p0 = c + math::ort(p0 - c);
								p1 = c + math::ort(p1 - c);
								p2 = c + math::ort(p2 - c);
							}

							painter.triangle(p0, p1, p2, color);
						}
					}
				}
			}
			break;
		}
		case transformer_type::hue_shifter:
		{
			float size = bbox[0].length();
			float s = size * 0.025f;
			float r1 = size * 0.33f - s / 2.f;
			float r2 = r1 * 0.5f - s / 2.f;

			auto c = bbox.center();

			static math::gradient<float> const g
			{
				std::pair{0.5f, 1.f},
				math::easing_type::cubic,
				std::pair{1.f, 0.f},
			};

			float offset = - g(t.animate) * math::pi / 3.f;

			int n = 6;

			for (int i = 0; i < n; ++i)
			{
				float a = (i * 2.f * math::pi) / n + offset;
				float b = ((i + 1) * 2.f * math::pi) / n + offset;

				auto da = math::direction(a);
				auto db = math::direction(b);

				painter.triangle(c + da * r2, c + db * r2, c + da * r1, gfx::white);
				painter.triangle(c + db * r2, c + da * r1, c + db * r1, gfx::white);
			}

			for (int i = 0; i < n; ++i)
			{
				float a = (i * 2.f * math::pi) / n + offset;
				float b = ((i + 1) * 2.f * math::pi) / n + offset;

				auto da = math::direction(a);
				auto db = math::direction(b);

				painter.line(c + da * r1, c + db * r1, s, gfx::black, false);
				painter.line(c + da * r2, c + db * r2, s, gfx::black, false);

				if (i % (n / 6) == 0)
					painter.line(c + da * r2, c + da * r1, s, gfx::black, true);
			}
		}
			break;
		}
	}

	void draw_structure(math::box<float, 2> const & bbox, crossing const &, gfx::painter & painter)
	{
		auto wbox = math::shrink(bbox, bbox[0].length() * 0.1f);
		auto sbox = math::shrink(bbox, bbox[0].length() * 0.3f);

		gfx::color_rgba color{64, 64, 64, 255};

		painter.rect({wbox[0], sbox[1]}, color);
		painter.rect({sbox[0], wbox[1]}, color);
	}

	void draw_card(math::box<float, 2> const & bbox, card_type type, gfx::painter & painter, float animation = 0.f)
	{
		switch (type)
		{
		case card_type::crossing:
			draw_structure(bbox, crossing{}, painter);
			break;
		case card_type::mixer:
		case card_type::reshaper:
		case card_type::hue_shifter:
		{
			draw_structure(bbox, transformer{.type = card_to_transformer(type).value(), .animate = animation}, painter);
		}
			break;
		case card_type::zoomer:
			draw_grid(bbox, -1.f, painter, true);
			break;
		case card_type::eraser:
		{
			float const w = 0.075f * bbox[0].length();

			float s = 0.2f;
			float t = 0.8f;

			gfx::color_rgba color = {255, 128, 128, 255};
			painter.line(bbox.corner(s, s), bbox.corner(t, t), w, color);
			painter.line(bbox.corner(t, s), bbox.corner(s, t), w, color);
		}
			break;
		}
	}

	void draw_grids(map & map, float view_level, gfx::painter & painter)
	{
		draw_grid({{{0.f, 3.f}, {0.f, 3.f}}}, view_level, painter);

		map.world->apply<vertex const, zoomer const>([&](vertex const & v, zoomer const &)
		{
			draw_grid(v.location.bbox(), view_level, painter);
		});
	}

	void draw(map & map, gfx::painter & painter, float ui_scale, bool sandbox)
	{
		map.world->apply<vertex const, crossing const>([&](vertex const & v, crossing const & c)
		{
			draw_structure(v.location.bbox(), c, painter);
		});

		for (int i = 0; i < 2; ++i)
			map.world->apply<path_vertex const>([&](path_vertex const & vertex)
			{
				float w = (i == 0 ? 0.3f : 0.25f);
				float w0 = w * std::pow(3.f, 1.f - vertex.location.level);
				gfx::color_rgba c{192, 192, 192, 255};
				if (i == 0)
					c = {0, 0, 0, 255};

				if (vertex.location.up().down() == vertex.location && (!vertex.belts_to.empty() || !vertex.belts_from.empty()))
					painter.circle(vertex.location.center(), w0 / 2.f, c);

				for (auto b : vertex.belts_to)
				{
					auto q = map.world->get(b).get<path_vertex const>().location;
					float w1 = w * std::pow(3.f, 1.f - q.level);
					painter.line(vertex.location.center(), q.center(), w0, w1, c, c, false);
				}
			});

		map.world->apply<path_vertex const>([&](path_vertex const & vertex)
		{
			for (auto b : vertex.belts_to)
			{
				auto q = map.world->get(b).get<path_vertex const>().location;

				math::vector d = q.center() - vertex.location.center();

				auto c = vertex.location.center() + d / 2.f;

				auto n = math::ort(d);

				float s = 1.f / 6.f;

				d *= s * 0.5f;
				n *= s;

				// gfx::color_rgba color = {255, 128, 0, 255};
				gfx::color_rgba color = {64, 64, 64, 255};

				painter.triangle(c - d + n, c - d - n, c + d, color);
			}
		});

		map.world->apply<vertex const, source const>([&](vertex const & v, source const & s)
		{
			math::vector shift{0.f, 0.f};
			if (v.location.coords[0] < 0)
				shift = {1.f, 0.f};
			else if (v.location.coords[0] >= 3)
				shift = {-1.f, 0.f};
			else
				shift = {0.f, -1.f};

			auto box = v.location.bbox();

			shift *= v.location.bbox()[0].length() * animation_factor(s.animate) / 12.f;
			painter.circle(shift + box.center(), box[0].length() * 0.6f / 2.f, gfx::black, 72);
			painter.circle(shift + box.center(), box[0].length() * 0.55f / 2.f, color_of(s.type), 72);
		});

		map.world->apply<vertex const, transformer const>([&](vertex const & v, transformer const & t)
		{
			auto box = v.location.bbox();
			// box = math::expand(box, 0.125f * animation_factor(t.animate) * box[0].length());
			draw_structure(box, t, painter);
		});

		map.world->apply<vertex const, lab const>([&](vertex const & v, lab const & l)
		{
			math::vector shift{1.f, -1.f};

			shift[0] *= v.location.bbox()[0].length() * math::sqr(std::sin(l.animate_error * math::pi)) * std::sin(l.animate_error * math::pi * 5.f) / 12.f;
			shift[1] *= v.location.bbox()[0].length() * animation_factor(l.animate) / 12.f;

			painter.rect(shift + v.location.bbox(-0.2f), {0, 0, 0, 255});
			painter.rect(shift + v.location.bbox(-0.225f), {192, 192, 192, 255});

			if (!sandbox)
			{
				for (int i = -1; i <= 0; ++i)
				{
					auto pen = v.location.bbox(-0.4f).corner(0.5f, 1.f);

					int opacity = std::round(255 * (1.f - std::abs(i + map.stage_animation)));

					if (map.stage + i >= 1 && map.stage + i < std::size(stages))
						draw_item(stages[map.stage + i].type, pen + math::vector{(i + map.stage_animation) * 40.f * ui_scale, 0.f}, 1.f, painter, false, opacity);

					if (i == 0)
					{
						pen[1] -= 12.f * ui_scale;

						painter.text(pen, util::to_string(map.resource_count, "/", stages[map.stage].count), {.scale = {ui_scale, -ui_scale}, .c = {0, 0, 0, 255}});
					}
				}
			}
		});

		map.world->apply<item const>([&](item const & i)
		{
			draw_item(i.type, position(*map.world, i), scale(*map.world, i), painter);
		});
	}

	void draw_selection(math::box<float, 2> const & b, gfx::painter & painter, gfx::color_rgba const & color, bool solid = false)
	{
		float w = b[0].length() * 0.05f;

		if (solid)
		{
			painter.line(b.corner(0, 0), b.corner(1, 0), w, color, true);
			painter.line(b.corner(1, 0), b.corner(1, 1), w, color, true);
			painter.line(b.corner(1, 1), b.corner(0, 1), w, color, true);
			painter.line(b.corner(0, 1), b.corner(0, 0), w, color, true);
		}
		else
		{
			for (int i = 0; i <= 2; ++i)
			{
				float s = (i - 1.f / 3.f) / 2.f;
				float t = (i + 1.f / 3.f) / 2.f;

				s = std::max(s, 0.f);
				t = std::min(t, 1.f);

				painter.line(b.corner(s, 0), b.corner(t, 0), w, color, true);
				painter.line(b.corner(s, 1), b.corner(t, 1), w, color, true);
				painter.line(b.corner(0, s), b.corner(0, t), w, color, true);
				painter.line(b.corner(1, s), b.corner(1, t), w, color, true);
			}
		}
	}

	std::vector<std::string> card_description(card_type type)
	{
		switch (type)
		{
		case card_type::mixer:
			return {"Mixer", "Mixes input colors"};
		case card_type::hue_shifter:
			return {"Hue shifter", "Shifts the hue of", "     input colors"};
		case card_type::reshaper:
			return {"Reshaper", "Turns circles into squares"};
		case card_type::crossing:
			return {"Bridge", "Allows belts to cross", "      without merging"};
		case card_type::zoomer:
			return {"Grid", "Creates an embedded 3x3 grid", "Zoom using mouse wheel"};
		case card_type::eraser:
			return {"Eraser", "Erases placed structures"};
		}

		throw util::unknown_enum_value_exception{type};
	}

	std::uint64_t make_seed()
	{
		random::device d;
		return (std::uint64_t(d()) << 32) | d();
	}

	struct application
		: app::application
	{
		application(options const &, context const & context)
			: context_(context)
			, audio_(psemek::audio::make_engine())
		{
			mixer_ = audio::make_mixer();
			volume_ = audio::volume(mixer_);
			audio_->output()->stream(audio::compressor(volume_));

			auto sounds_root = util::executable_path().parent_path() / "sounds";

			click_low_ = audio::load_mp3(io::read_full(std::move(*app::open_resource("sounds/click_low.mp3"))));
			click_high_ = audio::load_mp3(io::read_full(std::move(*app::open_resource("sounds/click_high.mp3"))));
			key_click_ = audio::load_mp3(io::read_full(std::move(*app::open_resource("sounds/key_click.mp3"))));
			gears_ = audio::load_mp3(io::read_full(std::move(*app::open_resource("sounds/gears.mp3"))));
			machine_ = audio::load_mp3(io::read_full(std::move(*app::open_resource("sounds/machine.mp3"))));
			pop_ = audio::load_mp3(io::read_full(std::move(*app::open_resource("sounds/pop.mp3"))));
			error_ = audio::load_mp3(io::read_full(std::move(*app::open_resource("sounds/error.mp3"))));

			context_.windowed(is_windowed_);
			set_start_menu();
		}

		void on_event(app::resize_event const & event) override
		{
			screen_size_ = event.size;
		}

		void on_event(app::touch_down_event const & event) override
		{
			on_event(app::mouse_move_event{event.position, {0, 0}});
			on_event(app::mouse_button_event{app::mouse_button::left, true});
		}

		void on_event(app::touch_up_event const & event) override
		{
			on_event(app::mouse_move_event{event.position, {0, 0}});
			on_event(app::mouse_button_event{app::mouse_button::left, false});
			on_event(app::mouse_move_event{{-1000, -1000}, {0, 0}});
		}

		void on_event(app::touch_move_event const & event) override
		{
			on_event(app::mouse_move_event{event.position, {0, 0}});
		}

		void on_event(app::mouse_wheel_event const & event) override
		{
			if (!in_menu())
			{
				if (event.delta > 0)
				{
					if (selected_ && !view_transition_)
					{
						bool transitioned = false;

						if (auto entity = map_.world->index<index>().find(*selected_))
							if (map_.world->get(*entity).contains<zoomer>())
							{
								view_transition_ = {view_stack_.back()};
								view_stack_.push_back(*selected_);
								selected_ = std::nullopt;
								transitioned = true;
							}

						if (!transitioned && within_grid(*selected_) && selected_->level == view_stack_.back().level + 1 && selected_->up() != view_stack_.back())
						{
							view_transition_ = {view_stack_.back()};
							view_stack_.back() = selected_->up();
							selected_ = std::nullopt;
							transitioned = true;
						}
					}
				}

				if (event.delta < 0)
				{
					if (!view_transition_ && view_stack_.size() > 1)
					{
						view_transition_ = {view_stack_.back()};
						view_stack_.pop_back();
						selected_ = std::nullopt;
					}
				}
			}
		}

		void on_event(app::mouse_move_event const & event) override
		{
			mouse_ = event.position;
			update_selected();
		}

		void on_event(app::mouse_button_event const & event) override
		{
			if (event.down && event.button == app::mouse_button::left)
			{
				if (selected_ && active_card_)
				{
					if (auto entity = map_.world->index<index>().find(*selected_))
					{
						if (*active_card_ == card_type::eraser)
						{
							destroy_sound();
							clear_tile(map_, *selected_);
						}
						else
						{
							auto acc = map_.world->get(*entity);
							auto t = acc.get_if<transformer>();
							auto n = card_to_transformer(*active_card_);

							if (!acc.contains<zoomer>() && (!t || !n || t->type != *n))
							{
								if (map_.take_card(*active_card_))
								{
									built_sound();

									auto dcard = destroy_tile_entity(map_, *entity);

									place_card(map_, *active_card_, *selected_);

									if (*active_card_ == card_type::zoomer && dcard)
									{
										map_.take_card(*dcard);
										place_card(map_, *dcard, selected_->down());
									}

									active_card_ = std::nullopt;

									auto e = map_.world->index<path_index>().get(selected_->down());
									if (auto o = map_.world->get(e).get_if<occupied>())
									{
										map_.world->destroy(o->entity);
										map_.world->detach<occupied>(e);
									}
								}
							}
						}
					}
					else
					{
						if (map_.take_card(*active_card_))
						{
							bool built = place_card(map_, *active_card_, *selected_);

							if (built && tutorial_state_ <= 3)
								tutorial_state_ = 4;

							if (built && active_card_ == card_type::zoomer && tutorial_state_ <= 5)
								tutorial_state_ = 6;

							if (built)
							{
								active_card_ = std::nullopt;
								built_sound();

								auto e = map_.world->index<path_index>().get(selected_->down());
								if (auto o = map_.world->get(e).get_if<occupied>())
								{
									map_.world->destroy(o->entity);
									map_.world->detach<occupied>(e);
								}
							}
						}
					}
				}
				else if (selected_item_)
				{
					item_killing_spree_ = true;
					if (tutorial_state_ <= 4)
						tutorial_state_ = 5;
				}
				else if (selected_ && !belt_start_)
				{
					belt_start_ = *selected_;
					button_mouseover_sound();
				}
			}

			if (!event.down && event.button == app::mouse_button::left)
			{
				item_killing_spree_ = false;

				if (selected_button_)
				{
					if (*selected_button_ < menu_buttons_.size() && menu_buttons_[*selected_button_].action)
					{
						button_click_sound();
						menu_buttons_[*selected_button_].action();
					}
				}
				else if (selected_card_)
				{
					if (selected_card_ == active_card_)
						active_card_ = std::nullopt;
					else
						active_card_ = *selected_card_;
					button_click_sound();
				}
				else if (selected_ && belt_start_ && selected_->level == belt_start_->level)
				{
					auto d = selected_->coords - belt_start_->coords;
					if (std::abs(d[0]) + std::abs(d[1]) == 1)
					{
						auto s = belt_start_->down();

						location belt[4];
						for (int i = 0; i <= 3; ++i)
							belt[i] = s.moved(d * i);
						sink(*map_.world, belt[0], belt[1]);
						sink(*map_.world, belt[3], belt[2]);

						auto & index = map_.world->index<path_index>();

						ecs::handle e[4];
						for (int i = 0; i <= 3; ++i)
							e[i] = index.get(belt[i]);

						auto & sv = map_.world->get(e[0]).get<path_vertex>();

						if (sv.belts_to.contains(e[1]))
						{
							for (int i = 0; i < 3; ++i)
								remove_belt(*map_.world, e[i], e[i + 1]);

							remove_belt_sound();
						}
						else if (sv.belts_from.contains(e[1]))
						{
							for (int i = 0; i < 3; ++i)
								remove_belt(*map_.world, e[i + 1], e[i]);

							remove_belt_sound();
						}
						else
						{
							for (int i = 0; i < 3; ++i)
								add_belt(*map_.world, e[i], e[i + 1]);

							add_belt_sound();
						}

						if (tutorial_state_ <= 0)
							tutorial_state_ = 1;
					}
				}

				belt_start_ = std::nullopt;
			}

			if (event.down && event.button == app::mouse_button::right)
				active_card_ = std::nullopt;
		}

		void on_event(app::key_event const & event) override
		{
			if (event.down && event.key == app::keycode::NUM_1)
			{
				time_speed_ = 1.f;
			}

			if (event.down && event.key == app::keycode::NUM_2)
			{
				time_speed_ = 2.f;
			}

			if (event.down && event.key == app::keycode::NUM_3)
			{
				time_speed_ = 3.f;
			}

			if (event.down && event.key == app::keycode::ESCAPE)
			{
				if (!in_start_menu_)
				{
					in_escape_menu_ ^= true;
					if (in_escape_menu_)
						set_escape_menu_buttons();
				}
			}
		}

		bool running() const override
		{
			return running_;
		}

		void stop() override
		{
			running_ = false;
		}

		void update() override
		{
			float const frame_dt = clock_.restart().count();
			float const dt = frame_dt * time_speed_ * (in_start_menu_ ? 1.f : (1.f - menu_transition_));

			map_.spawn_timer += 3.f * dt;
			if (map_.spawn_timer >= 1.f)
			{
				map_.spawn_timer -= 1.f;

				map_.world->apply<vertex const, source>(
					[&](vertex const & v, source & s)
					{
						auto p = v.location.down();

						auto t = map_.world->index<path_index>().get(p);

						if (!map_.world->get(t).contains<occupied>())
						{
							if (!map_.world->get(t).get<path_vertex>().belts_to.empty())
							{
								s.animate += 1.f;
								item_produced_sound();
							}

							auto i = map_.world->create(
								item{s.type, p}
							);

							map_.world->attach(t, occupied{i});
						}
					}
				);
			}

			if (!in_menu() && !is_sandbox_mode_ && map_.resource_count >= stages[map_.stage].count)
			{
				if (map_.stage + 1 < std::size(stages))
				{
					for (auto card : stages[map_.stage].cards)
						map_.cards[card] += 1;

					for (auto type : stages[map_.stage].sources)
					{
						util::hash_set<location, location_hash> spots;

						auto add = [&](location l)
						{
							if (!map_.world->index<index>().find(l))
								spots.insert(l);
						};

						for (int i = 0; i < 3; ++i)
						{
							add({0, {-1, i}});
							add({0, {3, i}});
							add({0, {i, 3}});
						}

						if (!map_.cards.contains(card_type::zoomer))
						{
							spots.erase(location{0, {0, 3}});
							spots.erase(location{0, {2, 3}});
						}

						if (!spots.empty())
						{
							map_.world->create(
								vertex{random::uniform_from(map_rng_, spots)},
								source{type}
							);
						}
					}

					map_.stage += 1;
					map_.stage_animation += 1.f;
					map_.resource_count = 0;

					if (map_.stage > 1 && tutorial_state_ <= 2)
						tutorial_state_ = 3;
				}
				else if (!map_.victory)
				{
					map_.victory = true;
					in_victory_menu_ = true;
					set_victory_menu_buttons();
				}
			}

			map_.world->apply<vertex const, transformer>([&](vertex const & v, transformer & t)
			{
				boost::container::flat_multimap<resource_type, location> has_inputs;

				auto c = v.location.down();
				auto ce = map_.world->index<path_index>().get(c);

				if (map_.world->get(ce).contains<occupied>())
					return;

				for (auto n : neighbours)
				{
					auto p = c.moved(n);
					if (auto ne = map_.world->index<path_index>().find(p))
						if (map_.world->get(*ne).get<path_vertex>().belts_to.contains(ce))
							if (auto occ = map_.world->get(*ne).get_if<occupied const>())
								if (auto i = map_.world->get(occ->entity).get_if<item const>())
									if (i->start == p)
										has_inputs.insert({i->type, p});
				}

				bool crafted = false;

				for (auto const & recipe : recipies.at(t.type))
				{
					boost::container::flat_set<resource_type> input_types;

					for (auto type : recipe.inputs)
						input_types.insert(type);

					bool has_all = true;
					for (auto type : input_types)
					{
						has_all &= (has_inputs.count(type) >= recipe.inputs.count(type));
					}

					if (!has_all)
						continue;

					for (auto type : input_types)
					{
						auto it = has_inputs.lower_bound(type);

						for (int i = 0; i < recipe.inputs.count(type); ++i)
						{
							auto l = it->second;
							auto e = map_.world->index<path_index>().get(l);
							auto re = map_.world->get(e).get<occupied>().entity;

							map_.world->detach<occupied>(e);
							map_.world->destroy(re);

							++it;
						}
					}

					auto r = map_.world->create(
						item{recipe.output, c}
					);

					map_.world->attach(ce, occupied{r});

					crafted = true;

					break;
				}

				if (crafted)
				{
					t.animate += 1.f;

					switch (t.type)
					{
					case transformer_type::mixer:
						mixer_sound();
						break;
					case transformer_type::hue_shifter:
						shifter_sound();
						break;
					case transformer_type::reshaper:
						reshaper_sound();
						break;
					}
				}

				if (!crafted && !has_inputs.empty())
				{
					return;
				}
			});

			map_.world->apply<item>([&](ecs::handle entity, item & i)
			{
				if (i.target)
				{
					{
						auto & v = map_.world->get(i.target).get<path_vertex const>();
						if (v.belts_from.empty() && v.belts_to.empty())
						{
							map_.world->detach<occupied>(i.target);
							map_.world->destroy(entity);
							return;
						}
					}

					i.state += 3.f * dt;
					if (i.state < 1.f)
						return;

					i.state -= 1.f;

					i.start = map_.world->get(i.target).get<path_vertex const>().location;
					map_.world->detach<occupied>(i.target);
					i.target = ecs::handle::null();
				}
				else
				{
					auto s = map_.world->index<path_index>().get(i.start);
					map_.world->detach<occupied>(s);

					auto & v = map_.world->get(s).get<path_vertex const>();
					if (v.belts_to.empty() && v.belts_from.empty())
					{
						map_.world->destroy(entity);
						return;
					}
				}

				if (auto cell = map_.world->index<index>().find(i.start.up()))
				{
					if (auto l = map_.world->get(*cell).get_if<lab>())
					{
						if (i.type == stages[map_.stage].type || is_sandbox_mode_)
						{
							map_.resource_count += 1;
							l->animate += 1.f;
							map_.world->destroy(entity);
							item_received_sound();
						}
						else
						{
							item_error_sound();
							l->animate_error += 1.f;
							map_.world->destroy(entity);
						}
						return;
					}

					if (map_.world->get(*cell).contains<crossing>())
					{
						auto c = i.start.up().down();
						auto d = c.coords - i.start.coords;
						auto n = c.moved(d);

						auto & index = map_.world->index<path_index>();

						auto se = index.get(i.start);
						auto ce = index.get(c);
						auto ne = index.get(n);

						if (map_.world->get(se).get<path_vertex>().belts_to.contains(ce))
						{
							if (map_.world->get(ce).get<path_vertex>().belts_to.contains(ne) && !map_.world->get(ne).contains<occupied>())
							{
								i.target = ne;
								map_.world->attach(ne, occupied{entity});
							}
							else
							{
								map_.world->attach(se, occupied{entity});
							}
							return;
						}
					}

					if (map_.world->get(*cell).get_if<transformer>())
					{
						auto se = map_.world->index<path_index>().get(i.start);
						auto ce = map_.world->index<path_index>().get(i.start.up().down());

						if (map_.world->get(se).get<path_vertex>().belts_to.contains(ce))
						{
							map_.world->attach(map_.world->index<path_index>().get(i.start), occupied{entity});
							return;
						}
					}
				}

				std::vector<ecs::handle> targets;

				for (auto b : map_.world->get(map_.world->index<path_index>().get(i.start)).get<path_vertex>().belts_to)
					if (!map_.world->get(b).contains<occupied>())
						targets.push_back(b);

				if (!targets.empty())
					i.target = random::uniform_from(item_rng_, targets);

				if (i.target)
					map_.world->attach(i.target, occupied{entity});
				else
					map_.world->attach(map_.world->index<path_index>().get(i.start), occupied{entity});
			});

			map_.stage_animation = std::max(0.f, map_.stage_animation - 3.f * dt);

			map_.world->apply<source>([&](source & s)
			{
				s.animate = std::max(0.f, s.animate - 3.f * dt);
			});

			map_.world->apply<transformer>([&](transformer & t)
			{
				t.animate = std::max(0.f, t.animate - 3.f * dt);
			});

			map_.world->apply<lab>([&](lab & l)
			{
				l.animate = std::max(0.f, l.animate - 3.f * dt);
				l.animate_error = std::max(0.f, l.animate_error - 3.f * dt);
			});

			float aspect_ratio = (screen_size_[0] * 1.f) / screen_size_[1];

			if (view_transition_)
			{
				auto from = view_transition_->old.bbox(1.f / 3.f);
				auto to = view_stack_.back().bbox(1.f / 3.f);

				float t = math::smoothstep(view_transition_->timer * 2.f);

				view_box_[0].min = math::lerp(from[0].min, to[0].min, t);
				view_box_[0].max = math::lerp(from[0].max, to[0].max, t);
				view_box_[1].min = math::lerp(from[1].min, to[1].min, t);
				view_box_[1].max = math::lerp(from[1].max, to[1].max, t);

				view_transition_->timer += frame_dt;
				if (view_transition_->timer >= 0.5f)
					view_transition_ = std::nullopt;
			}
			else
			{
				view_box_ = view_stack_.back().bbox(1.f / 3.f);
			}

			view_box_[0] = math::expand(view_box_[0], (view_box_[1].length() * aspect_ratio - view_box_[0].length()) / 2.f);

			update_selected();

			for (auto & p : card_animation_)
			{
				p.second.shift += ((p.first == active_card_ ? 1.f : 0.f) - p.second.shift) * (- std::expm1(- 20.f * frame_dt));

				if (p.first == selected_card_)
				{
					p.second.phase += dt * 3.f;
					if (p.second.phase >= 1.f)
						p.second.phase -= 1.f;
				}
				else if (p.second.phase > 0.f)
				{
					p.second.phase += dt * 3.f;
					if (p.second.phase >= 1.f)
						p.second.phase = 0.f;
				}
			}

			menu_transition_ += ((in_menu() ? 1.f : 0.f) - menu_transition_) * (- std::expm1(- 20.f * frame_dt));

			for (int i = 0; i < menu_buttons_.size(); ++i)
				menu_buttons_[i].selected_state += ((selected_button_ == i ? 1.f : 0.f) - menu_buttons_[i].selected_state) * (- std::expm1(- 20.f * frame_dt));
		}

		void present() override
		{
			gl::ClearColor(1.f, 1.f, 1.f, 1.f);
			gl::Clear(gl::COLOR_BUFFER_BIT);

			gl::Viewport(0, 0, screen_size_[0], screen_size_[1]);

			float view_level = view_stack_.back().level;
			float pixel_size = view_box_[1].length() / screen_size_[1];
			float ui_scale = (screen_size_[0] > 2000 ? 4.f : 2.f) * pixel_size;

			std::vector<std::string> helper_text;
			std::optional<resource_type> helper_resource;

			if (view_transition_)
			{
				float t = math::smoothstep(view_transition_->timer * 2.f);
				view_level = math::lerp<float>(view_transition_->old.level, view_stack_.back().level, t);
			}

			draw_grids(map_, view_level, painter_);

			if (active_card_)
			{
				if (selected_)
				{
					if (auto entity = map_.world->index<index>().find(*selected_))
					{
						auto n = card_to_transformer(*active_card_);
						auto acc = map_.world->get(*entity);
						auto t = acc.get_if<transformer>();
						if (*active_card_ != card_type::eraser && (!n || !t || *n != t->type) && !acc.contains<zoomer>())
						{
							gfx::color_rgba color = {64, 64, 64, 255};
							draw_selection(selected_->bbox(), painter_, color, true);
						}
						else if (*active_card_ != card_type::eraser || (!acc.contains<source>() && !acc.contains<lab>()))
						{
							gfx::color_rgba color = {255, 128, 128, 255};
							draw_selection(selected_->bbox(), painter_, color, true);
						}
					}
					else if (*active_card_ != card_type::eraser)
					{
						gfx::color_rgba color = {64, 64, 64, 255};
						draw_selection(selected_->bbox(), painter_, color, true);
					}
				}
			}
			else
			{
				if (selected_)
				{
					gfx::color_rgba color = {128, 128, 128, 255};
					if (belt_start_)
					{
						auto d = selected_->coords - belt_start_->coords;
						if (selected_->level != belt_start_->level || std::abs(d[0]) + std::abs(d[1]) != 1)
							color = {255, 128, 128, 255};
					}
					draw_selection(selected_->bbox(), painter_, color);

					if (belt_start_)
					{
						auto d = selected_->coords - belt_start_->coords;
						if (selected_->level == belt_start_->level && std::abs(d[0]) + std::abs(d[1]) == 1)
						{
							gfx::color_rgba c{224, 224, 224, 255};

							location p0 = belt_start_->down();
							location p1 = p0.moved(d);

							sink(*map_.world, p0, p1);

							auto e0 = map_.world->index<path_index>().get(p0);
							auto e1 = map_.world->index<path_index>().get(p1);

							if (map_.world->get(e0).get<path_vertex>().belts_to.contains(e1))
								c = {244, 160, 160, 255};
							else if (map_.world->get(e0).get<path_vertex>().belts_from.contains(e1))
								c = {244, 160, 160, 255};

							float w = 0.25f * std::pow(3.f, 1.f - selected_->level) / 2.f;
							painter_.line(selected_->center(), belt_start_->center(), w, c, true);
						}
					}
				}

				if (belt_start_)
					draw_selection(belt_start_->bbox(), painter_, {64, 64, 64, 255});
			}

			draw(map_, painter_, ui_scale, is_sandbox_mode_);

			if (!in_menu())
			{
				float w = (view_box_[0].length() - view_box_[1].length()) / 2.f;
				math::vector t{view_box_[1].length() / 5.f, 0.f};
				math::vector d{w, 0.f};
				math::vector n{w + t[0], 0.f};

				auto p00 = view_box_.corner(0, 0);
				auto p01 = view_box_.corner(0, 1);
				auto p10 = view_box_.corner(1, 0);
				auto p11 = view_box_.corner(1, 1);

				float s = 0.f;
				if (view_stack_.size() > 1)
				{
					if (view_transition_ && view_transition_->old.level == -1)
						s = 2.f * view_transition_->timer;
					else
						s = 1.f;
				}
				else
				{
					if (view_transition_)
						s = 1.f - 2.f * view_transition_->timer;
					else
						s = 0.f;
				}

				gfx::color_rgba c1{255, 255, 255, std::round(224.f * s)};
				gfx::color_rgba c0{255, 255, 255, 0};

				painter_.triangle(p00, p00 + d, p01 + d, c1, c1, c1);
				painter_.triangle(p00, p01 + d, p01, c1, c1, c1);

				painter_.triangle(p00 + d, p00 + n, p01 + n, c1, c0, c0);
				painter_.triangle(p00 + d, p01 + n, p01 + d, c1, c0, c1);

				painter_.triangle(p10, p10 - d, p11 - d, c1, c1, c1);
				painter_.triangle(p10, p11 - d, p11, c1, c1, c1);

				painter_.triangle(p10 - d, p10 - n, p11 - n, c1, c0, c0);
				painter_.triangle(p10 - d, p11 - n, p11 - d, c1, c0, c1);
			}

			if (selected_item_)
			{
				auto const & i = map_.world->get(*selected_item_).get<item const>();
				draw_item(i.type, position(*map_.world, i), scale(*map_.world, i), painter_, true);
			}

			std::optional<transformer_type> shown_recipies;

			if (selected_card_ || active_card_)
			{
				if (auto type = card_to_transformer(selected_card_.value_or(*active_card_)))
					shown_recipies = *type;
			}
			else if (selected_)
			{
				if (auto entity = map_.world->index<index>().find(*selected_))
					if (auto t = map_.world->get(*entity).get_if<transformer>())
						shown_recipies = t->type;
			}

			if (shown_recipies)
			{
				float const scale = std::pow(3.f, -1.f - view_level);
				float const step = 1.5f * scale / 9.f;

				math::point<float, 2> pen = view_box_.corner(0, 1) + math::vector{9.f, -60.f} * ui_scale + math::vector{step, - 3.f * step} / 2.f;
				for (auto const & recipe : recipies.at(*shown_recipies))
				{
					int i = 0;
					for (auto type : recipe.inputs)
					{
						draw_item(type, pen + math::vector{i * step, 0.f}, scale, painter_);
						++i;
					}

					i = 3;

					float s = 0.25f;
					float t = 0.25f * std::sqrt(0.75f);

					painter_.triangle(
						pen + math::vector{(i - t) * step, -s * step},
						pen + math::vector{(i - t) * step,  s * step},
						pen + math::vector{(i + t) * step, 0.f},
						{192, 192, 192, 255}
					);

					i = 4;
					draw_item(recipe.output, pen + math::vector{i * step, 0.f}, scale, painter_);

					pen[1] -= step;
				}
			}

			auto old_selected_card = selected_card_;
			selected_card_ = std::nullopt;

			{
				float const step = 0.5f * std::pow(3.f, - 1.f - view_level);

				math::point<float, 2> pen = view_box_.corner(1, 1) - math::vector{step, step} / 2.f;

				if (map_.cards.size() > 1)
				for (auto type : card_type_values())
				{
					if (!map_.cards.contains(type))
						continue;

					math::box<float, 2> box{{{pen[0] - step, pen[0]}, {pen[1] - step, pen[1]}}};
					box[0] -= step * card_animation_[type].shift / 2.f;

					float animation = card_animation_[type].phase;

					if (math::contains(box, mouse_world_))
					{
						selected_card_ = type;
						if (old_selected_card != type)
							button_mouseover_sound();
						draw_selection(box, painter_, {128, 128, 128, 255});
					}

					if (active_card_ == type)
					{
						draw_selection(box, painter_, {64, 64, 64, 255}, true);
					}

					draw_card(box, type, painter_, 1.f - animation);

					if (map_.cards.at(type) < 1000)
						painter_.text(box.center() - math::vector{step, 0.f}, std::to_string(map_.cards.at(type)), {.scale = {ui_scale, -ui_scale}, .c = gfx::black});

					pen[1] -= step;
				}
			}

			if (selected_card_ || active_card_)
				helper_text = card_description(selected_card_.value_or(*active_card_));
			else if (selected_)
				if (auto entity = map_.world->index<index>().find(*selected_))
				{
					auto acc = map_.world->get(*entity);
					if (auto t = acc.get_if<transformer>())
						helper_text = card_description(transformer_to_card(t->type));
					else if (acc.contains<crossing>())
						helper_text = card_description(card_type::crossing);
					else if (acc.contains<zoomer>())
						helper_text = card_description(card_type::zoomer);
					else if (auto s = acc.get_if<source>())
					{
						helper_text = {"Source", "Produces "};
						helper_resource = s->type;
					}
					else if (acc.contains<lab>())
					{
						if (is_sandbox_mode_)
						{
							helper_text = {"Consumer", "Consumes items"};
						}
						else
						{
							helper_text = {"Consumer", "Consumes "};
							helper_resource = stages[map_.stage].type;
						}
					}
				}

			if (!in_menu() && helper_text.empty())
			{
				if (tutorial_state_ == 0)
				{
					helper_text.push_back("Click and drag to add");
					helper_text.push_back("       conveyor belts");
				}
				else if (tutorial_state_ == 1)
				{
					helper_text.push_back("Drag again to remove belts");
				}
				else if (tutorial_state_ == 2)
				{
					helper_text.push_back("Deliver items from");
					helper_text.push_back("source to consumer");
				}
				else if (tutorial_state_ == 3)
				{
					helper_text.push_back("Place a mixer to");
					helper_text.push_back("   combine items");
				}
				else if (tutorial_state_ == 4)
				{
					helper_text.push_back("Click on items to");
					helper_text.push_back("  remove clogging");
				}
				else if (tutorial_state_ == 5)
				{
					helper_text.push_back("Need more space?");
					helper_text.push_back("Try adding a grid!");
				}
				else if (tutorial_state_ == 6)
				{
					helper_text.push_back("Let's see how far you can get.");
					helper_text.push_back("                   Good luck!");
				}
			}

			{
				math::point<float, 2> pen = view_box_.corner(0, 1) + math::vector{9.f, -12.f} * ui_scale;

				gfx::painter::text_options opts{.scale = {ui_scale, -ui_scale}, .x = gfx::painter::x_align::left, .y = gfx::painter::y_align::top, .c = gfx::black};

				std::string extra_line;

				for (int i = 0; i < helper_text.size(); ++i)
				{
					painter_.text(pen, helper_text[i], opts);

					if (i + 1 == helper_text.size() && helper_resource)
					{
						float text_width = painter_.text_size(helper_text[i])[0] * ui_scale;
						draw_item(*helper_resource, {pen[0] + text_width + 9.f * ui_scale, pen[1] - 6.f * ui_scale}, 1.f, painter_);

						if (auto t = transformer_for(*helper_resource))
						{
							extra_line = util::to_string("(use ", *t, " to make it)");
							std::replace(extra_line.begin(), extra_line.end(), '_', ' ');
						}
					}

					pen[1] -= 16.f * ui_scale;
				}

				if (!extra_line.empty())
				{
					painter_.text(pen, extra_line, opts);
				}
			}

			if (!in_start_menu_)
			{
				math::point<float, 2> pen = view_box_.corner(0, 0) + math::vector{9.f, 12.f} * ui_scale;

				gfx::color_rgba c{127, 127, 127, 255};

				gfx::painter::text_options opts
				{
					.scale = {ui_scale, -ui_scale}, .x = gfx::painter::x_align::left, .y = gfx::painter::y_align::bottom, .c = c
				};

				painter_.text(pen + math::vector{0.f, 16.f * ui_scale}, util::to_string("Level ", map_.stage, "/", std::size(stages) - 1), opts);
				painter_.text(pen, is_sandbox_mode_ ? "Sandbox" : is_challenge_mode_ ? "Challenge" : "Campaign", opts);
			}

			auto old_selected_button = selected_button_;
			selected_button_ = std::nullopt;

			if (in_menu())
			{
				painter_.rect(view_box_, gfx::to_coloru8(gfx::color_4f{1.f, 1.f, 1.f, menu_transition_ * 0.75f}));

				float button_width = ui_scale * 250.f;
				float button_height = ui_scale * 32.f;
				float button_spacing = ui_scale * 16.f;

				float total_height = button_height * menu_buttons_.size() + button_spacing * (menu_buttons_.size() - 1.f);

				auto pen = view_box_.center() + math::vector{-button_width, total_height} / 2.f;

				for (int i = 0; i < menu_buttons_.size(); ++i)
				{
					math::box<float, 2> box;
					box[0] = {pen[0], pen[0] + button_width};
					box[1] = {pen[1] - button_height, pen[1]};

					bool active = static_cast<bool>(menu_buttons_[i].action);

					gfx::color_rgba bg_color;
					gfx::color_rgba text_color;

					if (active)
					{
						box = math::expand(box, menu_buttons_[i].selected_state * 4.f * ui_scale);

						if (math::contains(box, mouse_world_))
						{
							selected_button_ = i;
							if (old_selected_button != i)
								button_mouseover_sound();
						}

						bg_color = gfx::lerp(gfx::color_rgba{192, 192, 192, 255}, gfx::white.as_color_rgba(), menu_buttons_[i].selected_state);
						text_color = gfx::lerp(gfx::black.as_color_rgba(), gfx::color_rgba{191, 96, 0, 255}, menu_buttons_[i].selected_state);
					}
					else
					{
						bg_color = {192, 224, 255, 255};
						text_color = {0, 0, 0, 255};
					}

					painter_.rect(math::expand(box, ui_scale * 2.5f), text_color);
					painter_.rect(box, bg_color);

					painter_.text(box.center(), menu_buttons_[i].text, {.scale = {ui_scale * 2.f, - ui_scale * 2.f}, .c = text_color});

					pen[1] -= button_height;
					pen[1] -= button_spacing;
				}
			}

			painter_.render(math::orthographic_camera{view_box_}.transform());
		}

	private:
		context const & context_;
#ifdef __EMSCRIPTEN__
		bool is_windowed_ = true;
#else
		bool is_windowed_ = false;
#endif

		std::unique_ptr<audio::engine> audio_;
		audio::mixer_ptr mixer_;
		std::shared_ptr<audio::volume_control> volume_;

		audio::track_ptr click_low_;
		audio::track_ptr click_high_;
		audio::track_ptr key_click_;
		audio::track_ptr gears_;
		audio::track_ptr machine_;
		audio::track_ptr pop_;
		audio::track_ptr error_;

		bool running_ = true;

		bool in_start_menu_ = true;
		bool in_escape_menu_ = false;
		bool in_victory_menu_ = false;

		float menu_transition_ = 1.f;

		struct button
		{
			std::string text;
			std::function<void()> action;
			float selected_state = 0.f;
		};

		std::vector<button> menu_buttons_;
		std::optional<int> selected_button_;

		bool in_menu() const
		{
			return in_start_menu_ || in_escape_menu_ || in_victory_menu_;
		}

		std::uint64_t seed_ = 0;
		bool is_challenge_mode_ = false;
		bool is_sandbox_mode_ = false;

		void start_new_game(bool challenge, bool sandbox, bool new_seed)
		{
			is_challenge_mode_ = challenge;
			is_sandbox_mode_ = sandbox;

			if (new_seed)
			{
				seed_ = make_seed();
				log::info() << "New campaign seed: " << seed_;
			}
			map_rng_ = {seed_, 0xf24130ddef6fb31full};
			item_rng_ = {seed_, 0xb9fc3979f9860bbdull};

			if (sandbox)
				map_ = sandbox_map();
			else
				map_ = campaign_map(is_challenge_mode_);
			if (in_start_menu_)
			{
				view_transition_ = {view_stack_.back()};
				view_stack_.pop_back();
			}
			else if (in_escape_menu_)
			{
				view_transition_ = {view_stack_.back()};
				view_stack_.clear();
				view_stack_.push_back({-1, {0, 0}});
			}
			in_start_menu_ = false;
			in_escape_menu_ = false;
			in_victory_menu_ = false;
			reset_state();
		}

		void reset_state()
		{
			active_card_ = std::nullopt;
			selected_card_ = std::nullopt;
			selected_ = std::nullopt;
			belt_start_ = std::nullopt;
			selected_item_ = std::nullopt;;
			card_animation_.clear();
		}

		void set_start_menu()
		{
			in_start_menu_ = true;
			in_escape_menu_ = false;
			in_victory_menu_ = false;

			map_ = start_menu_map();

			if (!view_stack_.empty())
			{
				view_transition_ = {view_stack_.back()};
				view_stack_.clear();
			}
			view_stack_.push_back({-1, {0, 0}});
			view_stack_.push_back({0, {1, 1}});

			set_start_menu_buttons();
		}

		void set_start_menu_buttons()
		{
			menu_buttons_.clear();
			menu_buttons_.push_back({"Campaign mode", [this]{ start_new_game(false, false, true); }});
			menu_buttons_.push_back({"Challenge mode", [this]{ start_new_game(true, false, true); }});
			menu_buttons_.push_back({"Sandbox mode", [this]{ start_new_game(true, true, true); }});
			menu_buttons_.push_back({is_windowed_ ? "Windowed" : "Fullscreen", [this]{ is_windowed_ ^= true; context_.windowed(is_windowed_); set_start_menu_buttons(); }});
#ifndef __EMSCRIPTEN__
			menu_buttons_.push_back({"Exit", [this]{ stop(); }});
#endif
		}

		void set_escape_menu_buttons()
		{
			menu_buttons_.clear();
			menu_buttons_.push_back({"Paused", {}});
			menu_buttons_.push_back({"Continue", [this]{ in_escape_menu_ = false; }});
			menu_buttons_.push_back({"Restart", [this]{ start_new_game(is_challenge_mode_, is_sandbox_mode_, false); }});
			if (is_challenge_mode_)
				menu_buttons_.push_back({"Campaign mode", [this]{ is_challenge_mode_ = false; in_escape_menu_ = false;
					for (auto type : card_type_values())
						map_.cards[type] = 1000000;
				}});
			menu_buttons_.push_back({"Main menu", [this]{ set_start_menu(); }});
			menu_buttons_.push_back({is_windowed_ ? "Windowed" : "Fullscreen", [this]{ is_windowed_ ^= true; context_.windowed(is_windowed_); set_escape_menu_buttons(); }});
#ifndef __EMSCRIPTEN__
			menu_buttons_.push_back({"Exit", [this]{ stop(); }});
#endif
		}

		void set_victory_menu_buttons()
		{
			menu_buttons_.clear();
			menu_buttons_.push_back({"You won!", {}});
			menu_buttons_.push_back({"One more turn", [this]{ in_victory_menu_ = false; }});
			menu_buttons_.push_back({"Play again", [this]{ start_new_game(is_challenge_mode_, is_sandbox_mode_, true); }});
			menu_buttons_.push_back({"Main menu", [this]{ set_start_menu(); }});
#ifndef __EMSCRIPTEN__
			menu_buttons_.push_back({"Exit", [this]{ stop(); }});
#endif
		}

		random::generator map_rng_;
		random::generator item_rng_;
		map map_;

		util::clock<> clock_;
		float time_speed_ = 1.f;

		math::vector<int, 2> screen_size_{1, 1};
		math::point<int, 2> mouse_{0, 0};
		math::point<float, 2> mouse_world_{0, 0};
		bool item_killing_spree_ = false;

		gfx::painter painter_;

		struct view_transition
		{
			location old;
			float timer = 0.f;
		};

		std::optional<view_transition> view_transition_;

		std::vector<location> view_stack_;

		math::box<float, 2> view_box_;

		std::optional<location> selected_;
		std::optional<location> belt_start_;
		std::optional<ecs::handle> selected_item_;
		std::optional<card_type> selected_card_;
		std::optional<card_type> active_card_;

		struct card_animation_state
		{
			float shift = 0.f;
			float phase = 0.f;
		};

		util::hash_map<card_type, card_animation_state> card_animation_;

		int tutorial_state_ = 0;

		void update_selected()
		{
			mouse_world_ = view_box_.corner(
				mouse_[0] * 1.f / screen_size_[0],
				1.f - mouse_[1] * 1.f / screen_size_[1]
			);

			auto old_selected = selected_;
			selected_ = std::nullopt;
			selected_item_ = std::nullopt;

			if (!in_menu() && !view_transition_)
			{
				if (math::contains(math::shrink(view_box_[0], (view_box_[0].length() - view_box_[1].length()) / 2.f), mouse_world_[0]))
					map_.world->apply<item const>([&](ecs::handle entity, item const & i)
					{
						auto box = math::expand(math::box<float, 2>::singleton(position(*map_.world, i)), scale(*map_.world, i) / 9.f);
						if (math::contains(box, mouse_world_))
							selected_item_ = entity;
					});

				if (selected_item_ && item_killing_spree_)
				{
					auto const & i = map_.world->get(*selected_item_).get<item const>();
					if (i.target)
						map_.world->detach<occupied>(i.target);
					else
						map_.world->detach<occupied>(map_.world->index<path_index>().get(i.start));
					map_.world->destroy(*selected_item_);
					selected_item_ = std::nullopt;
					item_removed_sound();
				}

				{
					float s = std::pow(3.f, 1 + view_stack_.back().level);

					auto m = mouse_world_;

					m[0] *= s;
					m[1] *= s;

					auto l = view_stack_.back();

					location p{1 + l.level, {std::floor(m[0]), std::floor(m[1])}};

					math::interval xrange{3 * l.coords[0], 3 * l.coords[0] + 2};
					math::interval yrange{3 * l.coords[1], 3 * l.coords[1] + 2};
					auto xwrange = math::expand(xrange, 1);
					auto ywrange = math::expand(yrange, 1);

					if (math::contains(xrange, p.coords[0]) && math::contains(yrange, p.coords[1]))
						selected_ = p;
					else if (view_stack_.size() > 1 && within_grid(p) && ((math::contains(xwrange, p.coords[0]) && math::contains(yrange, p.coords[1])) || (math::contains(xrange, p.coords[0]) && math::contains(ywrange, p.coords[1]))))
					{
						selected_ = p;
						while (selected_->level > 0)
						{
							if (auto entity = map_.world->index<index>().find(p.up()))
								if (map_.world->get(*entity).contains<zoomer>())
									break;

							selected_ = std::nullopt;
							break;

							// TODO:
							selected_ = selected_->up();
						}
					}
					else if (!within_grid(p))
						if (auto entity = map_.world->index<index>().find(p))
							selected_ = p;

					if (belt_start_ && selected_ && old_selected != selected_)
					{
						button_mouseover_sound();
					}
				}
			}
		}

		void button_mouseover_sound()
		{
			mixer_->add(audio::volume(pop_->stream(), 0.125f));
		}

		void button_click_sound()
		{
			mixer_->add(audio::volume(pop_->stream(), 0.5f));
		}

		void built_sound()
		{
			mixer_->add(audio::volume(pop_->stream(), 0.5f));
		}

		void add_belt_sound()
		{
			mixer_->add(audio::volume(pop_->stream(), 0.5f));
		}

		void remove_belt_sound()
		{
			mixer_->add(audio::volume(pop_->stream(), 0.5f));
		}

		void destroy_sound()
		{
			mixer_->add(audio::volume(pop_->stream(), 0.5f));
		}

		void item_received_sound()
		{
			if (in_start_menu_) return;
			mixer_->add(audio::volume(click_high_->stream(), 0.25f));
		}

		void item_produced_sound()
		{
			if (in_start_menu_) return;
			mixer_->add(audio::volume(click_low_->stream(), 0.5f));
		}

		void mixer_sound()
		{
			if (in_start_menu_) return;
			mixer_->add(audio::volume(machine_->stream(), 0.125f));
		}

		void shifter_sound()
		{
			if (in_start_menu_) return;
			mixer_->add(audio::volume(gears_->stream(), 0.5f));
		}

		void reshaper_sound()
		{
			if (in_start_menu_) return;
			mixer_->add(audio::volume(key_click_->stream(), 0.25f));
		}

		void item_removed_sound()
		{
			if (in_start_menu_) return;
			mixer_->add(audio::volume(pop_->stream(), 0.5f));
		}

		void item_error_sound()
		{
			if (in_start_menu_) return;
			// mixer_->add(audio::volume(error_->stream(), 0.25f));
		}
	};

}

namespace psemek::app
{

	std::unique_ptr<application::factory> make_application_factory()
	{
		application::options options
		{
			.name = "Color Fractory",
		};

		return default_application_factory<gmtk::application>(options);
	}

}
