// Copyright 2005 Ben Hutchings <ben@decadentplace.org.uk>.
// See the file "COPYING" for licence details.

#include "framebuffer.hpp"

#include <cassert>
#include <cstdio>
#include <cstring>
#include <stdexcept>

#include <sys/types.h>
#include <errno.h>
#include <fcntl.h>
#include <netinet/in.h>
#include <sys/stat.h>
#include <sys/utsname.h>
#include <unistd.h>
#include <wait.h>

#include "auto_fd.hpp"
#include "temp_file.hpp"

namespace
{
    int select_display_num()
    {
	// Minimum and maximum display numbers to use.  Xvnc and ssh's
	// proxies start at 10, so we'll follow that convention.  We
	// have to put a limit on iteration somewhere, and 100
	// displays seems rather excessive so we'll stop there.
	const int min_display_num = 10;
	const int max_display_num = 99;

	// Note that we have to leave it to the X server to create the
	// lock file for the selected display number, which leaves a
	// race condition.  We could perhaps read its error stream to
	// detect the case where another server grabs the display
	// number before it.
	char lock_file_name[20];
	for (int display_num = min_display_num;
	     display_num <= max_display_num;
	     ++display_num)
	{
	    // Check whether a lock file exists for this display.  Really we
	    // should also check for stale locks, but this will probably do.
	    std::sprintf(lock_file_name, "/tmp/.X%d-lock", display_num);
	    if (access(lock_file_name, 0) == -1 && errno == ENOENT)
		return display_num;
	}

	throw std::runtime_error("did not find a free X display");
    }

    void get_random_bytes(unsigned char * buf, std::size_t len)
    {
	auto_fd random_fd(open("/dev/urandom", O_RDONLY));
	if (random_fd.get() == -1 || read(random_fd.get(), buf, len) != len)
	    throw std::runtime_error(std::strerror(errno));
    }

    std::auto_ptr<temp_file> create_temp_auth_file(int display_num)
    {
	std::auto_ptr<temp_file> auth_file(new temp_file("Xvfb-auth-"));

	// An xauth entry consists of the following fields.  All u16 fields
	// are big-endian and unaligned.  Character arrays are not null-
	// terminated.
	// u16     address family (= 256 for local socket)
	// u16     length of address
	// char[]  address (= hostname)
	// u16     length of display number
	// char[]  display number
	// u16     auth type name length
	// char[]  auth type name (= "MIT-MAGIC-COOKIE-1")
	// u16     length of auth data (= 16)
	// char[]  auth data (= random bytes)
	uint16_t family = htons(0x100);
	write(auth_file->get_fd(), &family, sizeof(family));
	utsname my_uname;
	uname(&my_uname);
	uint16_t len = htons(strlen(my_uname.nodename));
	write(auth_file->get_fd(), &len, sizeof(len));
	write(auth_file->get_fd(),
	      my_uname.nodename, strlen(my_uname.nodename));
	char display[15];
	std::sprintf(display, "%d", display_num);
	len = htons(strlen(display));
	write(auth_file->get_fd(), &len, sizeof(len));
	write(auth_file->get_fd(), display, strlen(display));
	static const char auth_type[] = "MIT-MAGIC-COOKIE-1";
	len = htons(sizeof(auth_type) - 1);
	write(auth_file->get_fd(), &len, sizeof(len));
	write(auth_file->get_fd(), auth_type, sizeof(auth_type) - 1);
	unsigned char auth_key[16];
	get_random_bytes(auth_key, sizeof(auth_key));
	len = htons(sizeof(auth_key));
	write(auth_file->get_fd(), &len, sizeof(len));
	write(auth_file->get_fd(), auth_key, sizeof(auth_key));

	return auth_file;
    }

    // Run the X server with the specified auth file, dimensions and
    // assigned display number.
    auto_kill_proc spawn_x_server(int display_num,
				  const std::string & auth_file_name,
				  int width, int height, int depth)
    {
	char display[15];
	std::sprintf(display, ":%d", display_num);
	const char * auth_file_c_str = auth_file_name.c_str();
	std::fflush(NULL);
	auto_kill_proc server_proc(fork());
	if (server_proc.get() == -1)
	    throw std::runtime_error(std::strerror(errno));

	if (server_proc.get() == 0)
	{
	    char dimensions[40];
	    std::sprintf(dimensions, "%dx%dx%d", width, height, depth);
	    execlp("Xvfb",
		   "Xvfb",
		   "-auth", auth_file_c_str,
		   "-screen", "0", dimensions,
		   display,
		   NULL);
	    _exit(128 + errno);
	}

	// Wait for the lock file to appear or the server to exit.  We can't
	// really wait on both of these, so poll at 1-second intervals.
	char lock_file_name[20];
	std::sprintf(lock_file_name, "/tmp/.X%d-lock", display_num);
	for (;;)
	{
	    if (access(lock_file_name, 0) == 0)
		break;
	    if (errno != ENOENT) // huh?
		throw std::runtime_error(std::strerror(errno));
	    if (waitpid(server_proc.get(), NULL, WNOHANG) == server_proc.get())
	    {
		server_proc.release(); // pid is now invalid
		// TODO: Get the exit status and decode it properly.
		throw std::runtime_error("X server failed to create display");
	    }
	    sleep(1);
	}

	return server_proc;
    }
}

FrameBuffer::FrameBuffer(int width, int height, int depth)
	: display_num_(select_display_num()),
	  auth_file_(create_temp_auth_file(display_num_)),
	  server_proc_(spawn_x_server(display_num_,
				      get_x_authority(),
				      width, height, depth))
{}

std::string FrameBuffer::get_x_authority() const
{
    return auth_file_->get_name();
}

std::string FrameBuffer::get_x_display() const
{
    char display[15];
    std::sprintf(display, ":%d", display_num_);
    return display;
}
