#include <cstdio>
#include <cstring>
#include <algorithm>
#include <regex>
#include <set>
#include <chrono>
#include <cstdint>
#include <cinttypes>

#include <sys/select.h>

#include <fmt/format.h>

#include <kms++/kms++.h>
#include <kms++/modedb.h>
#include <kms++/mode_cvt.h>

#include <kms++util/kms++util.h>

using namespace std;
using namespace kms;

struct PropInfo {
	PropInfo(string n, uint64_t v)
		: prop(NULL), name(n), val(v) {}

	Property* prop;
	string name;
	uint64_t val;
};

struct PlaneInfo {
	Plane* plane;

	signed x;
	signed y;
	unsigned w;
	unsigned h;

	unsigned view_x;
	unsigned view_y;
	unsigned view_w;
	unsigned view_h;

	vector<Framebuffer*> fbs;

	vector<PropInfo> props;
};

struct OutputInfo {
	Connector* connector;

	Crtc* crtc;
	Videomode mode;
	vector<Framebuffer*> legacy_fbs;

	vector<PlaneInfo> planes;

	vector<PropInfo> conn_props;
	vector<PropInfo> crtc_props;
};

static bool s_use_dmt;
static bool s_use_cea;
static unsigned s_num_buffers = 1;
static bool s_flip_mode;
static bool s_flip_sync;
static bool s_cvt;
static bool s_cvt_v2;
static bool s_cvt_vid_opt;
static unsigned s_max_flips;
static bool s_print_crc;
static TestPatternOptions s_pattern_options;

__attribute__((unused)) static void print_regex_match(smatch sm)
{
	for (unsigned i = 0; i < sm.size(); ++i) {
		string str = sm[i].str();
		fmt::print("{}: {}\n", i, str);
	}
}

static void get_connector(ResourceManager& resman, OutputInfo& output, const string& str = "")
{
	Connector* conn = resman.reserve_connector(str);

	if (!conn)
		EXIT("No connector '%s'", str.c_str());

	output.connector = conn;
	output.mode = output.connector->get_default_mode();
}

static void get_default_crtc(ResourceManager& resman, OutputInfo& output)
{
	output.crtc = resman.reserve_crtc(output.connector);

	if (!output.crtc)
		EXIT("Could not find available crtc");
}

static PlaneInfo* add_default_planeinfo(OutputInfo* output)
{
	output->planes.push_back(PlaneInfo{});
	PlaneInfo* ret = &output->planes.back();
	ret->w = output->mode.hdisplay;
	ret->h = output->mode.vdisplay;
	return ret;
}

static void parse_crtc(ResourceManager& resman, Card& card, const string& crtc_str, OutputInfo& output)
{
	// @12:1920x1200i@60
	// @12:33000000,800/210/30/16/-,480/22/13/10/-,i

	const regex modename_re("(?:(@?)(\\d+):)?" // @12:
				"(?:(\\d+)x(\\d+)(i)?)" // 1920x1200i
				"(?:@([\\d\\.]+))?"); // @60

	const regex modeline_re("(?:(@?)(\\d+):)?" // @12:
				"(\\d+)," // 33000000,
				"(\\d+)/(\\d+)/(\\d+)/(\\d+)/([+-\\?])," // 800/210/30/16/-,
				"(\\d+)/(\\d+)/(\\d+)/(\\d+)/([+-\\?])" // 480/22/13/10/-
				"(?:,([i]+))?" // ,i
	);

	smatch sm;
	if (regex_match(crtc_str, sm, modename_re)) {
		if (sm[2].matched) {
			bool use_id = sm[1].length() == 1;
			unsigned num = stoul(sm[2].str());

			if (use_id) {
				Crtc* c = card.get_crtc(num);
				if (!c)
					EXIT("Bad crtc id '%u'", num);

				output.crtc = c;
			} else {
				auto crtcs = card.get_crtcs();

				if (num >= crtcs.size())
					EXIT("Bad crtc number '%u'", num);

				output.crtc = crtcs[num];
			}
		} else {
			output.crtc = output.connector->get_current_crtc();
		}

		unsigned w = stoul(sm[3]);
		unsigned h = stoul(sm[4]);
		bool ilace = sm[5].matched ? true : false;
		float refresh = sm[6].matched ? stof(sm[6]) : 0;

		if (s_cvt) {
			output.mode = videomode_from_cvt(w, h, refresh, ilace, s_cvt_v2, s_cvt_vid_opt);
		} else if (s_use_dmt) {
			try {
				output.mode = find_dmt(w, h, refresh, ilace);
			} catch (exception& e) {
				EXIT("Mode not found from DMT tables\n");
			}
		} else if (s_use_cea) {
			try {
				output.mode = find_cea(w, h, refresh, ilace);
			} catch (exception& e) {
				EXIT("Mode not found from CEA tables\n");
			}
		} else {
			try {
				output.mode = output.connector->get_mode(w, h, refresh, ilace);
			} catch (exception& e) {
				EXIT("Mode not found from the connector\n");
			}
		}
	} else if (regex_match(crtc_str, sm, modeline_re)) {
		if (sm[2].matched) {
			bool use_id = sm[1].length() == 1;
			unsigned num = stoul(sm[2].str());

			if (use_id) {
				Crtc* c = card.get_crtc(num);
				if (!c)
					EXIT("Bad crtc id '%u'", num);

				output.crtc = c;
			} else {
				auto crtcs = card.get_crtcs();

				if (num >= crtcs.size())
					EXIT("Bad crtc number '%u'", num);

				output.crtc = crtcs[num];
			}
		} else {
			output.crtc = output.connector->get_current_crtc();
		}

		unsigned clock = stoul(sm[3]);

		unsigned hact = stoul(sm[4]);
		unsigned hfp = stoul(sm[5]);
		unsigned hsw = stoul(sm[6]);
		unsigned hbp = stoul(sm[7]);

		SyncPolarity h_sync;
		switch (sm[8].str()[0]) {
		case '+': h_sync = SyncPolarity::Positive; break;
		case '-': h_sync = SyncPolarity::Negative; break;
		default: h_sync = SyncPolarity::Undefined; break;
		}

		unsigned vact = stoul(sm[9]);
		unsigned vfp = stoul(sm[10]);
		unsigned vsw = stoul(sm[11]);
		unsigned vbp = stoul(sm[12]);

		SyncPolarity v_sync;
		switch (sm[13].str()[0]) {
		case '+': v_sync = SyncPolarity::Positive; break;
		case '-': v_sync = SyncPolarity::Negative; break;
		default: v_sync = SyncPolarity::Undefined; break;
		}

		output.mode = videomode_from_timings(clock / 1000, hact, hfp, hsw, hbp, vact, vfp, vsw, vbp);
		output.mode.set_hsync(h_sync);
		output.mode.set_vsync(v_sync);

		if (sm[14].matched) {
			for (int i = 0; i < sm[14].length(); ++i) {
				char f = string(sm[14])[i];

				switch (f) {
				case 'i':
					output.mode.set_interlace(true);
					break;
				default:
					EXIT("Bad mode flag %c", f);
				}
			}
		}
	} else {
		EXIT("Failed to parse crtc option '%s'", crtc_str.c_str());
	}

	if (output.crtc)
		output.crtc = resman.reserve_crtc(output.crtc);
	else
		output.crtc = resman.reserve_crtc(output.connector);

	if (!output.crtc)
		EXIT("Could not find available crtc");
}

static void parse_plane(ResourceManager& resman, Card& card, const string& plane_str, const OutputInfo& output, PlaneInfo& pinfo)
{
	// 3:400,400-400x400
	const regex plane_re("(?:(@?)(\\d+):)?" // 3:
			     "(?:(-?\\d+),(-?\\d+)-)?" // 400,400-
			     "(\\d+)x(\\d+)"); // 400x400

	smatch sm;
	if (!regex_match(plane_str, sm, plane_re))
		EXIT("Failed to parse plane option '%s'", plane_str.c_str());

	if (sm[2].matched) {
		bool use_id = sm[1].length() == 1;
		unsigned num = stoul(sm[2].str());

		if (use_id) {
			Plane* p = card.get_plane(num);
			if (!p)
				EXIT("Bad plane id '%u'", num);

			pinfo.plane = p;
		} else {
			auto planes = card.get_planes();

			if (num >= planes.size())
				EXIT("Bad plane number '%u'", num);

			pinfo.plane = planes[num];
		}

		auto plane = resman.reserve_plane(pinfo.plane);
		if (!plane)
			EXIT("Plane id %u is not available", pinfo.plane->id());
	}

	pinfo.w = stoul(sm[5]);
	pinfo.h = stoul(sm[6]);

	if (sm[3].matched)
		pinfo.x = stol(sm[3]);
	else
		pinfo.x = output.mode.hdisplay / 2 - pinfo.w / 2;

	if (sm[4].matched)
		pinfo.y = stol(sm[4]);
	else
		pinfo.y = output.mode.vdisplay / 2 - pinfo.h / 2;
}

static void parse_prop(const string& prop_str, vector<PropInfo>& props)
{
	string name, val;

	size_t split = prop_str.find("=");

	if (split == string::npos)
		EXIT("Equal sign ('=') not found in %s", prop_str.c_str());

	name = prop_str.substr(0, split);
	val = prop_str.substr(split + 1);

	props.push_back(PropInfo(name, stoull(val, 0, 0)));
}

static void get_props(Card& card, vector<PropInfo>& props, const DrmPropObject* propobj)
{
	for (auto& pi : props)
		pi.prop = propobj->get_prop(pi.name);
}

static vector<Framebuffer*> get_default_fb(Card& card, unsigned width, unsigned height)
{
	vector<Framebuffer*> v;

	for (unsigned i = 0; i < s_num_buffers; ++i)
		v.push_back(new DumbFramebuffer(card, width, height, PixelFormat::XRGB8888));

	return v;
}

static void parse_fb(Card& card, const string& fb_str, OutputInfo* output, PlaneInfo* pinfo)
{
	unsigned w, h;
	PixelFormat format = PixelFormat::XRGB8888;

	if (pinfo) {
		w = pinfo->w;
		h = pinfo->h;
	} else {
		w = output->mode.hdisplay;
		h = output->mode.vdisplay;
	}

	if (!fb_str.empty()) {
		// XXX the regexp is not quite correct
		// 400x400-NV12
		const regex fb_re("(?:(\\d+)x(\\d+))?" // 400x400
				  "(?:-)?" // -
				  "(\\w+)?"); // NV12

		smatch sm;
		if (!regex_match(fb_str, sm, fb_re))
			EXIT("Failed to parse fb option '%s'", fb_str.c_str());

		if (sm[1].matched)
			w = stoul(sm[1]);
		if (sm[2].matched)
			h = stoul(sm[2]);
		if (sm[3].matched) {
			try {
				format = find_pixel_format_by_name(sm[3]);
			} catch (const invalid_argument& e) {
				format = fourcc_str_to_pixel_format(sm[3]);
			}
		}
	}

	vector<Framebuffer*> v;

	for (unsigned i = 0; i < s_num_buffers; ++i)
		v.push_back(new DumbFramebuffer(card, w, h, format));

	if (pinfo)
		pinfo->fbs = v;
	else
		output->legacy_fbs = v;
}

static void parse_view(const string& view_str, PlaneInfo& pinfo)
{
	const regex view_re("(\\d+),(\\d+)-(\\d+)x(\\d+)"); // 400,400-400x400

	smatch sm;
	if (!regex_match(view_str, sm, view_re))
		EXIT("Failed to parse view option '%s'", view_str.c_str());

	pinfo.view_x = stoul(sm[1]);
	pinfo.view_y = stoul(sm[2]);
	pinfo.view_w = stoul(sm[3]);
	pinfo.view_h = stoul(sm[4]);
}

static const char* usage_str =
	"Usage: kmstest [OPTION]...\n\n"
	"Show a test pattern on a display or plane\n\n"
	"Options:\n"
	"      --device=DEVICE       DEVICE is the path to DRM card to open\n"
	"  -c, --connector=CONN      CONN is <connector>\n"
	"  -r, --crtc=CRTC           CRTC is [<crtc>:]<w>x<h>[@<Hz>]\n"
	"                            or\n"
	"                            [<crtc>:]<pclk>,<hact>/<hfp>/<hsw>/<hbp>/<hsp>,<vact>/<vfp>/<vsw>/<vbp>/<vsp>[,i]\n"
	"  -p, --plane=PLANE         PLANE is [<plane>:][<x>,<y>-]<w>x<h>\n"
	"  -f, --fb=FB               FB is [<w>x<h>][-][<fmtname>|<4cc>]\n"
	"  -v, --view=VIEW           VIEW is <x>,<y>-<w>x<h>\n"
	"  -P, --property=PROP=VAL   Set PROP to VAL in the previous DRM object\n"
	"      --dmt                 Search for the given mode from DMT tables\n"
	"      --cea                 Search for the given mode from CEA tables\n"
	"      --cvt=CVT             Create videomode with CVT. CVT is 'v1', 'v2' or 'v2o'\n"
	"      --flip[=max]          Do page flipping for each output with an optional maximum flips count\n"
	"      --sync                Synchronize page flipping\n"
	"      --crc                 Print CRC16 for framebuffer contents\n"
	"  -T, --pattern=PAT         test, white, black, red, green, blue, smpte\n"
	"      --rec=REC             bt601, bt709, bt2020\n"
	"      --range=RANGE         limited, full\n"
	"\n"
	"<connector>, <crtc> and <plane> can be given by index (<idx>) or id (@<id>).\n"
	"<connector> can also be given by name.\n"
	"\n"
	"Options can be given multiple times to set up multiple displays or planes.\n"
	"Options may apply to previous options, e.g. a plane will be set on a crtc set in\n"
	"an earlier option.\n"
	"If you omit parameters, kmstest tries to guess what you mean\n"
	"\n"
	"Examples:\n"
	"\n"
	"Set eDP-1 mode to 1920x1080@60, show XR24 framebuffer on the crtc, and a 400x400 XB24 plane:\n"
	"    kmstest -c eDP-1 -r 1920x1080@60 -f XR24 -p 400x400 -f XB24\n\n"
	"XR24 framebuffer on first connected connector in the default mode:\n"
	"    kmstest -f XR24\n\n"
	"XR24 framebuffer on a 400x400 plane on the first connected connector in the default mode:\n"
	"    kmstest -p 400x400 -f XR24\n\n"
	"Test pattern on the second connector with default mode:\n"
	"    kmstest -c 1\n"
	"\n"
	"Environmental variables:\n"
	"    KMSXX_DISABLE_UNIVERSAL_PLANES    Don't enable universal planes even if available\n"
	"    KMSXX_DISABLE_ATOMIC              Don't enable atomic modesetting even if available\n";

static void usage()
{
	puts(usage_str);
}

enum class ArgType {
	Connector,
	Crtc,
	Plane,
	Framebuffer,
	View,
	Property,
};

struct Arg {
	ArgType type;
	string arg;
};

static string s_device_path;

static vector<Arg> parse_cmdline(int argc, char** argv)
{
	vector<Arg> args;

	OptionSet optionset = {
		Option("|device=",
		       [&](string s) {
			       s_device_path = s;
		       }),
		Option("c|connector=",
		       [&](string s) {
			       args.push_back(Arg{ ArgType::Connector, s });
		       }),
		Option("r|crtc=", [&](string s) {
			args.push_back(Arg{ ArgType::Crtc, s });
		}),
		Option("p|plane=", [&](string s) {
			args.push_back(Arg{ ArgType::Plane, s });
		}),
		Option("f|fb=", [&](string s) {
			args.push_back(Arg{ ArgType::Framebuffer, s });
		}),
		Option("v|view=", [&](string s) {
			args.push_back(Arg{ ArgType::View, s });
		}),
		Option("P|property=", [&](string s) {
			args.push_back(Arg{ ArgType::Property, s });
		}),
		Option("|dmt", []() {
			s_use_dmt = true;
		}),
		Option("|cea", []() {
			s_use_cea = true;
		}),
		Option("|flip?", [&](string s) {
			s_flip_mode = true;
			s_num_buffers = 2;
			if (!s.empty())
				s_max_flips = stoi(s);
		}),
		Option("|sync", []() {
			s_flip_sync = true;
		}),
		Option("|cvt=", [&](string s) {
			if (s == "v1")
				s_cvt = true;
			else if (s == "v2")
				s_cvt = s_cvt_v2 = true;
			else if (s == "v2o")
				s_cvt = s_cvt_v2 = s_cvt_vid_opt = true;
			else {
				usage();
				exit(-1);
			}
		}),
		Option("|crc", []() {
			s_print_crc = true;
		}),
		Option("T|pattern=", [&](string s) {
			s_pattern_options.pattern = s;
		}),
		Option("|rec=", [&](string s) {
			s = to_lower(s);

			if (s == "bt601")
				s_pattern_options.rec = RecStandard::BT601;
			else if (s == "bt709")
				s_pattern_options.rec = RecStandard::BT709;
			else if (s == "bt2020")
				s_pattern_options.rec = RecStandard::BT2020;
			else {
				usage();
				exit(-1);
			}
		}),
		Option("|range=", [&](string s) {
			s = to_lower(s);

			if (s == "limited")
				s_pattern_options.range = ColorRange::Limited;
			else if (s == "full")
				s_pattern_options.range = ColorRange::Full;
			else {
				usage();
				exit(-1);
			}
		}),
		Option("h|help", [&]() {
			usage();
			exit(-1);
		}),
	};

	optionset.parse(argc, argv);

	if (optionset.params().size() > 0) {
		usage();
		exit(-1);
	}

	return args;
}

static vector<OutputInfo> setups_to_outputs(Card& card, ResourceManager& resman, const vector<Arg>& output_args)
{
	vector<OutputInfo> outputs;

	OutputInfo* current_output = 0;
	PlaneInfo* current_plane = 0;

	for (auto& arg : output_args) {
		switch (arg.type) {
		case ArgType::Connector: {
			outputs.push_back(OutputInfo{});
			current_output = &outputs.back();

			get_connector(resman, *current_output, arg.arg);
			current_plane = 0;

			break;
		}

		case ArgType::Crtc: {
			if (!current_output) {
				outputs.push_back(OutputInfo{});
				current_output = &outputs.back();
			}

			if (!current_output->connector)
				get_connector(resman, *current_output);

			parse_crtc(resman, card, arg.arg, *current_output);

			current_plane = 0;

			break;
		}

		case ArgType::Plane: {
			if (!current_output) {
				outputs.push_back(OutputInfo{});
				current_output = &outputs.back();
			}

			if (!current_output->connector)
				get_connector(resman, *current_output);

			if (!current_output->crtc)
				get_default_crtc(resman, *current_output);

			current_plane = add_default_planeinfo(current_output);

			parse_plane(resman, card, arg.arg, *current_output, *current_plane);

			break;
		}

		case ArgType::Framebuffer: {
			if (!current_output) {
				outputs.push_back(OutputInfo{});
				current_output = &outputs.back();
			}

			if (!current_output->connector)
				get_connector(resman, *current_output);

			if (!current_output->crtc)
				get_default_crtc(resman, *current_output);

			if (!current_plane && card.has_atomic())
				current_plane = add_default_planeinfo(current_output);

			parse_fb(card, arg.arg, current_output, current_plane);

			break;
		}

		case ArgType::View: {
			if (!current_plane || current_plane->fbs.empty())
				EXIT("'view' parameter requires a plane and a fb");

			parse_view(arg.arg, *current_plane);
			break;
		}

		case ArgType::Property: {
			if (!current_output)
				EXIT("No object to which set the property");

			if (current_plane)
				parse_prop(arg.arg, current_plane->props);
			else if (current_output->crtc)
				parse_prop(arg.arg, current_output->crtc_props);
			else if (current_output->connector)
				parse_prop(arg.arg, current_output->conn_props);
			else
				EXIT("no object");

			break;
		}
		}
	}

	if (outputs.empty()) {
		// no outputs defined, show a pattern on all connected screens
		for (Connector* conn : card.get_connectors()) {
			if (!conn->connected())
				continue;

			OutputInfo output = {};
			output.connector = resman.reserve_connector(conn);
			EXIT_IF(!output.connector, "Failed to reserve connector %s", conn->fullname().c_str());
			output.crtc = resman.reserve_crtc(conn);
			EXIT_IF(!output.crtc, "Failed to reserve crtc for %s", conn->fullname().c_str());
			output.mode = output.connector->get_default_mode();

			outputs.push_back(output);
		}
	}

	for (OutputInfo& o : outputs) {
		get_props(card, o.conn_props, o.connector);

		if (!o.crtc)
			get_default_crtc(resman, o);

		get_props(card, o.crtc_props, o.crtc);

		if (!o.mode.valid())
			EXIT("Mode not valid for %s", o.connector->fullname().c_str());

		if (card.has_atomic()) {
			if (o.planes.empty())
				add_default_planeinfo(&o);
		} else {
			if (o.legacy_fbs.empty())
				o.legacy_fbs = get_default_fb(card, o.mode.hdisplay, o.mode.vdisplay);
		}

		for (PlaneInfo& p : o.planes) {
			if (p.fbs.empty())
				p.fbs = get_default_fb(card, p.w, p.h);
		}

		for (PlaneInfo& p : o.planes) {
			if (!p.plane) {
				if (card.has_atomic())
					p.plane = resman.reserve_generic_plane(o.crtc, p.fbs[0]->format());
				else
					p.plane = resman.reserve_overlay_plane(o.crtc, p.fbs[0]->format());

				if (!p.plane)
					EXIT("Failed to find available plane");
			}
			get_props(card, p.props, p.plane);
		}
	}

	return outputs;
}

static uint16_t crc16(uint16_t crc, uint8_t data)
{
	const uint16_t CRC16_IBM = 0x8005;

	for (uint8_t i = 0; i < 8; i++) {
		if (((crc & 0x8000) >> 8) ^ (data & 0x80))
			crc = (crc << 1) ^ CRC16_IBM;
		else
			crc = (crc << 1);

		data <<= 1;
	}

	return crc;
}

static string fb_crc(IFramebuffer* fb)
{
	uint8_t* p = fb->map(0);
	uint16_t r, g, b;

	r = g = b = 0;

	for (unsigned y = 0; y < fb->height(); ++y) {
		for (unsigned x = 0; x < fb->width(); ++x) {
			uint32_t* p32 = (uint32_t*)(p + fb->stride(0) * y + x * 4);
			RGB rgb(*p32);

			r = crc16(r, rgb.r);
			r = crc16(r, 0);

			g = crc16(g, rgb.g);
			g = crc16(g, 0);

			b = crc16(b, rgb.b);
			b = crc16(b, 0);
		}
	}

	return fmt::format("{:#06x} {:#06x} {:#06x}", r, g, b);
}

static void print_outputs(const vector<OutputInfo>& outputs)
{
	for (unsigned i = 0; i < outputs.size(); ++i) {
		const OutputInfo& o = outputs[i];

		fmt::print("Connector {}/@{}: {}", o.connector->idx(), o.connector->id(),
			   o.connector->fullname());

		for (const PropInfo& prop : o.conn_props)
			fmt::print(" {}={}", prop.prop->name(), prop.val);

		fmt::print("\n  Crtc {}/@{}", o.crtc->idx(), o.crtc->id());

		for (const PropInfo& prop : o.crtc_props)
			fmt::print(" {}={}", prop.prop->name(), prop.val);

		fmt::print(": {}\n", o.mode.to_string_long());

		if (!o.legacy_fbs.empty()) {
			auto fb = o.legacy_fbs[0];
			fmt::print("    Fb {} {}x{}-{}\n", fb->id(), fb->width(), fb->height(), pixel_format_to_fourcc_str(fb->format()));
		}

		for (unsigned j = 0; j < o.planes.size(); ++j) {
			const PlaneInfo& p = o.planes[j];
			auto fb = p.fbs[0];
			fmt::print("  Plane {}/@{}: {},{}-{}x{}", p.plane->idx(), p.plane->id(),
				   p.x, p.y, p.w, p.h);
			for (const PropInfo& prop : p.props)
				fmt::print(" {}={}", prop.prop->name(), prop.val);
			fmt::print("\n");

			fmt::print("    Fb {} {}x{}-{}\n", fb->id(), fb->width(), fb->height(),
				   pixel_format_to_fourcc_str(fb->format()));
			if (s_print_crc)
				fmt::print("      CRC16 {}\n", fb_crc(fb).c_str());
		}
	}
}

static void draw_test_patterns(const vector<OutputInfo>& outputs)
{
	for (const OutputInfo& o : outputs) {
		for (auto fb : o.legacy_fbs)
			draw_test_pattern(*fb, s_pattern_options);

		for (const PlaneInfo& p : o.planes)
			for (auto fb : p.fbs)
				draw_test_pattern(*fb, s_pattern_options);
	}
}

static void set_crtcs_n_planes_legacy(Card& card, const vector<OutputInfo>& outputs)
{
	// Disable unused crtcs
	for (Crtc* crtc : card.get_crtcs()) {
		if (find_if(outputs.begin(), outputs.end(), [crtc](const OutputInfo& o) { return o.crtc == crtc; }) != outputs.end())
			continue;

		crtc->disable_mode();
	}

	for (const OutputInfo& o : outputs) {
		int r;
		auto conn = o.connector;
		auto crtc = o.crtc;

		for (const PropInfo& prop : o.conn_props) {
			r = conn->set_prop_value(prop.prop, prop.val);
			EXIT_IF(r, "failed to set connector property %s\n", prop.name.c_str());
		}

		for (const PropInfo& prop : o.crtc_props) {
			r = crtc->set_prop_value(prop.prop, prop.val);
			EXIT_IF(r, "failed to set crtc property %s\n", prop.name.c_str());
		}

		if (!o.legacy_fbs.empty()) {
			auto fb = o.legacy_fbs[0];
			r = crtc->set_mode(conn, *fb, o.mode);
			if (r)
				fmt::print(stderr, "crtc->set_mode() failed for crtc {}: {}\n",
					   crtc->id(), strerror(-r));
		}

		for (const PlaneInfo& p : o.planes) {
			for (const PropInfo& prop : p.props) {
				r = p.plane->set_prop_value(prop.prop, prop.val);
				EXIT_IF(r, "failed to set plane property %s\n", prop.name.c_str());
			}

			auto fb = p.fbs[0];
			r = crtc->set_plane(p.plane, *fb,
					    p.x, p.y, p.w, p.h,
					    0, 0, fb->width(), fb->height());
			if (r)
				fmt::print(stderr, "crtc->set_plane() failed for plane {}: {}\n",
					   p.plane->id(), strerror(-r));
		}
	}
}

static void set_crtcs_n_planes_atomic(Card& card, const vector<OutputInfo>& outputs)
{
	int r;

	// XXX DRM framework doesn't allow moving an active plane from one crtc to another.
	// See drm_atomic.c::plane_switching_crtc().
	// For the time being, try and disable all crtcs and planes here.
	// Do not check the return value as some simple displays don't support the crtc being
	// enabled but the primary plane being disabled.

	AtomicReq disable_req(card);

	// Disable unused crtcs
	for (Crtc* crtc : card.get_crtcs()) {
		//if (find_if(outputs.begin(), outputs.end(), [crtc](const OutputInfo& o) { return o.crtc == crtc; }) != outputs.end())
		//	continue;

		disable_req.add(crtc, {
					      { "ACTIVE", 0 },
				      });
	}

	// Disable unused planes
	for (Plane* plane : card.get_planes())
		disable_req.add(plane, {
					       { "FB_ID", 0 },
					       { "CRTC_ID", 0 },
				       });

	disable_req.commit_sync(true);

	// Keep blobs here so that we keep ref to them until we have committed the req
	vector<unique_ptr<Blob>> blobs;

	AtomicReq req(card);

	for (const OutputInfo& o : outputs) {
		auto conn = o.connector;
		auto crtc = o.crtc;

		blobs.emplace_back(o.mode.to_blob(card));
		Blob* mode_blob = blobs.back().get();

		req.add(conn, {
				      { "CRTC_ID", crtc->id() },
			      });

		for (const PropInfo& prop : o.conn_props)
			req.add(conn, prop.prop, prop.val);

		req.add(crtc, {
				      { "ACTIVE", 1 },
				      { "MODE_ID", mode_blob->id() },
			      });

		for (const PropInfo& prop : o.crtc_props)
			req.add(crtc, prop.prop, prop.val);

		for (const PlaneInfo& p : o.planes) {
			auto fb = p.fbs[0];

			req.add(p.plane, {
						 { "FB_ID", fb->id() },
						 { "CRTC_ID", crtc->id() },
						 { "SRC_X", (p.view_x ?: 0) << 16 },
						 { "SRC_Y", (p.view_y ?: 0) << 16 },
						 { "SRC_W", (p.view_w ?: fb->width()) << 16 },
						 { "SRC_H", (p.view_h ?: fb->height()) << 16 },
						 { "CRTC_X", p.x },
						 { "CRTC_Y", p.y },
						 { "CRTC_W", p.w },
						 { "CRTC_H", p.h },
					 });

			for (const PropInfo& prop : p.props)
				req.add(p.plane, prop.prop, prop.val);
		}
	}

	r = req.test(true);
	if (r)
		EXIT("Atomic test failed: %d\n", r);

	r = req.commit_sync(true);
	if (r)
		EXIT("Atomic commit failed: %d\n", r);
}

static void set_crtcs_n_planes(Card& card, const vector<OutputInfo>& outputs)
{
	if (card.has_atomic())
		set_crtcs_n_planes_atomic(card, outputs);
	else
		set_crtcs_n_planes_legacy(card, outputs);
}

static bool max_flips_reached;

class FlipState : private PageFlipHandlerBase
{
public:
	FlipState(Card& card, const string& name, vector<const OutputInfo*> outputs)
		: m_card(card), m_name(name), m_outputs(outputs)
	{
	}

	void start_flipping()
	{
		m_prev_frame = m_prev_print = std::chrono::steady_clock::now();
		m_slowest_frame = std::chrono::duration<float>::min();
		m_frame_num = 0;
		queue_next();
	}

private:
	void handle_page_flip(uint32_t frame, double time)
	{
		/*
		 * We get flip event for each crtc in this flipstate. We can commit the next frames
		 * only after we've gotten the flip event for all crtcs
		 */
		if (++m_flip_count < m_outputs.size())
			return;

		m_frame_num++;
		if (s_max_flips && m_frame_num >= s_max_flips)
			max_flips_reached = true;

		auto now = std::chrono::steady_clock::now();

		std::chrono::duration<float> diff = now - m_prev_frame;
		if (diff > m_slowest_frame)
			m_slowest_frame = diff;

		if (m_frame_num % 100 == 0) {
			std::chrono::duration<float> fsec = now - m_prev_print;
			fmt::print("Connector {}: fps {:.2f}, slowest {:.2f} ms\n",
				   m_name.c_str(),
				   100.0 / fsec.count(),
				   m_slowest_frame.count() * 1000);
			m_prev_print = now;
			m_slowest_frame = std::chrono::duration<float>::min();
		}

		m_prev_frame = now;

		queue_next();
	}

	static unsigned get_bar_pos(Framebuffer* fb, unsigned frame_num)
	{
		return (frame_num * bar_speed) % (fb->width() - bar_width + 1);
	}

	static void draw_bar(Framebuffer* fb, unsigned frame_num)
	{
		int old_xpos = frame_num < s_num_buffers ? -1 : get_bar_pos(fb, frame_num - s_num_buffers);
		int new_xpos = get_bar_pos(fb, frame_num);

		draw_color_bar(*fb, old_xpos, new_xpos, bar_width);
		draw_text(*fb, fb->width() / 2, 0, to_string(frame_num), RGB(255, 255, 255));
	}

	static void do_flip_output(AtomicReq& req, unsigned frame_num, const OutputInfo& o)
	{
		unsigned cur = frame_num % s_num_buffers;

		for (const PlaneInfo& p : o.planes) {
			auto fb = p.fbs[cur];

			draw_bar(fb, frame_num);

			req.add(p.plane, {
						 { "FB_ID", fb->id() },
					 });
		}
	}

	void do_flip_output_legacy(unsigned frame_num, const OutputInfo& o)
	{
		unsigned cur = frame_num % s_num_buffers;

		if (!o.legacy_fbs.empty()) {
			auto fb = o.legacy_fbs[cur];

			draw_bar(fb, frame_num);

			int r = o.crtc->page_flip(*fb, this);
			ASSERT(r == 0);
		}

		for (const PlaneInfo& p : o.planes) {
			auto fb = p.fbs[cur];

			draw_bar(fb, frame_num);

			int r = o.crtc->set_plane(p.plane, *fb,
						  p.x, p.y, p.w, p.h,
						  0, 0, fb->width(), fb->height());
			ASSERT(r == 0);
		}
	}

	void queue_next()
	{
		m_flip_count = 0;

		if (m_card.has_atomic()) {
			AtomicReq req(m_card);

			for (auto o : m_outputs)
				do_flip_output(req, m_frame_num, *o);

			int r = req.commit(this);
			if (r)
				EXIT("Flip commit failed: %d\n", r);
		} else {
			ASSERT(m_outputs.size() == 1);
			do_flip_output_legacy(m_frame_num, *m_outputs[0]);
		}
	}

	Card& m_card;
	string m_name;
	vector<const OutputInfo*> m_outputs;
	unsigned m_frame_num;
	unsigned m_flip_count;

	chrono::steady_clock::time_point m_prev_print;
	chrono::steady_clock::time_point m_prev_frame;
	chrono::duration<float> m_slowest_frame;

	static const unsigned bar_width = 20;
	static const unsigned bar_speed = 8;
};

static void main_flip(Card& card, const vector<OutputInfo>& outputs)
{
// clang-tidy does not seem to handle FD_xxx macros
#ifndef __clang_analyzer__
	fd_set fds;

	FD_ZERO(&fds);

	int fd = card.fd();

	vector<unique_ptr<FlipState>> flipstates;

	if (!s_flip_sync) {
		for (const OutputInfo& o : outputs) {
			auto fs = unique_ptr<FlipState>(new FlipState(card, to_string(o.connector->idx()), { &o }));
			flipstates.push_back(std::move(fs));
		}
	} else {
		vector<const OutputInfo*> ois;

		string name;
		for (const OutputInfo& o : outputs) {
			name += to_string(o.connector->idx()) + ",";
			ois.push_back(&o);
		}

		auto fs = unique_ptr<FlipState>(new FlipState(card, name, ois));
		flipstates.push_back(std::move(fs));
	}

	for (unique_ptr<FlipState>& fs : flipstates)
		fs->start_flipping();

	while (!max_flips_reached) {
		int r;

		FD_SET(0, &fds);
		FD_SET(fd, &fds);

		r = select(fd + 1, &fds, NULL, NULL, NULL);
		if (r < 0) {
			fmt::print(stderr, "select() failed with {}: {}\n", errno, strerror(errno));
			break;
		} else if (FD_ISSET(0, &fds)) {
			fmt::print(stderr, "Exit due to user-input\n");
			break;
		} else if (FD_ISSET(fd, &fds)) {
			card.call_page_flip_handlers();
		}
	}
#endif
}

int main(int argc, char** argv)
{
	vector<Arg> output_args = parse_cmdline(argc, argv);

	Card card(s_device_path);

	if (!card.is_master())
		EXIT("Could not get DRM master permission. Card already in use?");

	if (!card.has_atomic() && s_flip_sync)
		EXIT("Synchronized flipping requires atomic modesetting");

	ResourceManager resman(card);

	vector<OutputInfo> outputs = setups_to_outputs(card, resman, output_args);

	if (!s_flip_mode)
		draw_test_patterns(outputs);

	print_outputs(outputs);

	set_crtcs_n_planes(card, outputs);

	fmt::print("press enter to exit\n");

	if (s_flip_mode)
		main_flip(card, outputs);
	else
		getchar();
}
