/**
 * @file controller.c
 * @brief Implements the controller subsystem.
 */

#include <SDL3/SDL.h>
#include <akerror.h>
#include <akgl/heap.h>
#include <akgl/registry.h>
#include <akgl/game.h>
#include <akgl/controller.h>

akgl_ControlMap akgl_controlmaps[AKGL_MAX_CONTROL_MAPS];

/*
 * Keystrokes waiting for akgl_controller_poll_key() and
 * akgl_controller_poll_keystroke(). Filled by akgl_controller_handle_event()
 * before it consults the control maps, so a key that also drives an actor is
 * still delivered to a polling caller.
 *
 * head is the next slot to read, count is how many are waiting. Both index a
 * fixed array rather than a queue object, which is the whole point: a host that
 * never polls cannot make this grow.
 */
static akgl_Keystroke keybuffer[AKGL_CONTROLLER_KEY_BUFFER];
static int keybuffer_head = 0;
static int keybuffer_count = 0;

/*
 * Whether the newest entry is a key press that has not been given its composed
 * text yet. SDL reports one keystroke as two events -- the key going down, and
 * then the text it composed to, if it composed to anything -- so the text that
 * arrives next belongs to the press that arrived last. Without this flag a
 * press dropped by a full buffer would hand its text to whatever older press
 * happened to still be sitting at the end of the ring.
 */
static bool keybuffer_awaiting_text = false;

/**
 * @brief Length in bytes of the UTF-8 sequence @p src begins with, or 0.
 *
 * 0 both for a byte that cannot start a sequence and for a sequence the string
 * ends in the middle of. SDL should hand over neither, and reading past a
 * terminator to find out otherwise is not a risk worth taking for input that
 * arrives from a keyboard driver.
 *
 * @param src UTF-8 text, NUL terminated. Required.
 */
static size_t utf8_sequence_length(const char *src)
{
    unsigned char lead = (unsigned char)src[0];
    size_t len = 0;
    size_t i = 0;

    if ( lead < 0x80 ) {
	return 1;
    } else if ( (lead & 0xe0) == 0xc0 ) {
	len = 2;
    } else if ( (lead & 0xf0) == 0xe0 ) {
	len = 3;
    } else if ( (lead & 0xf8) == 0xf0 ) {
	len = 4;
    } else {
	return 0;
    }
    for ( i = 1; i < len; i++ ) {
	// A NUL fails this too, which is what stops the scan at the end of a
	// truncated string instead of past it.
	if ( ((unsigned char)src[i] & 0xc0) != 0x80 ) {
	    return 0;
	}
    }
    return len;
}

/**
 * @brief Copy as much of @p src as fits in @p dest without splitting a character.
 *
 * SDL hands over whatever the platform composed, which for an input method can
 * be several characters at once, and the ring entry holds one. Truncating on a
 * byte boundary would leave a partial UTF-8 sequence that nothing downstream
 * can render, so this stops at the last code point that fits whole.
 *
 * @param dest     Receives the prefix, always NUL terminated. Required.
 * @param destsize Bytes available in @p dest, terminator included.
 * @param src      UTF-8 text to copy. Required.
 */
static void copy_utf8_prefix(char *dest, size_t destsize, const char *src)
{
    size_t used = 0;
    size_t len = 0;

    dest[0] = '\0';
    while ( src[used] != '\0' ) {
	len = utf8_sequence_length(&src[used]);
	if ( len == 0 ) {
	    // Not valid UTF-8 from here on. Keep what is already known good.
	    break;
	}
	if ( (used + len) >= destsize ) {
	    break;
	}
	memcpy(&dest[used], &src[used], len);
	used += len;
    }
    dest[used] = '\0';
}

/**
 * @brief Record one key press, discarding it if the buffer is already full.
 *
 * Dropping the newest rather than overwriting the oldest is deliberate. A
 * caller reading a line of input wants the characters that were typed first;
 * overwriting would hand it the tail of what the user typed and silently lose
 * the head.
 *
 * The entry starts with no text. Whatever the press composes to arrives as a
 * separate event and is attached by keybuffer_attach_text().
 */
static void keybuffer_push_key(SDL_Keycode key, SDL_Keymod mod)
{
    int tail = 0;

    if ( keybuffer_count >= AKGL_CONTROLLER_KEY_BUFFER ) {
	keybuffer_awaiting_text = false;
	return;
    }
    tail = (keybuffer_head + keybuffer_count) % AKGL_CONTROLLER_KEY_BUFFER;
    keybuffer[tail].key = key;
    keybuffer[tail].mod = mod;
    keybuffer[tail].text[0] = '\0';
    keybuffer_count += 1;
    keybuffer_awaiting_text = true;
}

/**
 * @brief Give composed text to the press it belongs to, or buffer it alone.
 *
 * An entry with no keycode is not a degenerate case to be avoided: an input
 * method or a dead key finishes a character with no key press of its own, and a
 * line editor wants that character. akgl_controller_poll_keystroke() reports it;
 * akgl_controller_poll_key(), which has no keycode to give, discards it.
 */
static void keybuffer_attach_text(const char *text)
{
    int newest = 0;

    if ( (keybuffer_awaiting_text == true) && (keybuffer_count > 0) ) {
	newest = (keybuffer_head + keybuffer_count - 1) % AKGL_CONTROLLER_KEY_BUFFER;
	copy_utf8_prefix(keybuffer[newest].text, AKGL_CONTROLLER_KEYSTROKE_TEXT, text);
	keybuffer_awaiting_text = false;
	return;
    }
    if ( keybuffer_count >= AKGL_CONTROLLER_KEY_BUFFER ) {
	return;
    }
    newest = (keybuffer_head + keybuffer_count) % AKGL_CONTROLLER_KEY_BUFFER;
    keybuffer[newest].key = 0;
    keybuffer[newest].mod = 0;
    copy_utf8_prefix(keybuffer[newest].text, AKGL_CONTROLLER_KEYSTROKE_TEXT, text);
    keybuffer_count += 1;
}

/**
 * @brief Take the oldest entry, whatever it carries.
 *
 * @param dest Receives the entry. Required.
 * @return `true` when an entry was taken, `false` when the buffer was empty.
 */
static bool keybuffer_take(akgl_Keystroke *dest)
{
    if ( keybuffer_count == 0 ) {
	return false;
    }
    memcpy(dest, &keybuffer[keybuffer_head], sizeof(akgl_Keystroke));
    keybuffer_head = (keybuffer_head + 1) % AKGL_CONTROLLER_KEY_BUFFER;
    keybuffer_count -= 1;
    if ( keybuffer_count == 0 ) {
	// Nothing is left for a text event to attach itself to.
	keybuffer_awaiting_text = false;
    }
    return true;
}

akerr_ErrorContext *akgl_controller_list_keyboards(void)
{
    int count;
    SDL_KeyboardID *keyboards = SDL_GetKeyboards(&count);
    PREPARE_ERROR(errctx);

    FAIL_ZERO_RETURN(errctx, keyboards, AKERR_NULLPOINTER, "%s", SDL_GetError());

    // The array is SDL's to allocate and ours to free -- that is the contract on
    // every SDL_Get*s() enumeration. Released in CLEANUP so the loop can fail
    // without taking the array with it.
    ATTEMPT {
	for (int i = 0; i < count; i++) {
	    const char *name = SDL_GetKeyboardNameForID(keyboards[i]);
	    SDL_Log("Keyboard %d: ID %u, Name: %s\n", i, keyboards[i], name);
	}
    } CLEANUP {
	SDL_free(keyboards);
	keyboards = NULL;
    } PROCESS(errctx) {
    } FINISH(errctx, true);
    SUCCEED_RETURN(errctx);
}

akerr_ErrorContext *akgl_controller_open_gamepads(void)
{
    int count = 0;
    int i = 0;
    SDL_JoystickID *gamepads = NULL;
    SDL_Gamepad *gamepad = NULL;
    bool openfailed = false;

    PREPARE_ERROR(errctx);
    if ( SDL_HasGamepad() ) {
	gamepads = SDL_GetGamepads(&count);
	if ( count > 0 ) {
	    // SDL_free in CLEANUP, not after the loop. A gamepad that failed to
	    // open used to return straight out of here and take the enumeration
	    // array with it.
	    ATTEMPT {
		FAIL_ZERO_BREAK(errctx, gamepads, AKERR_NULLPOINTER, "%s", SDL_GetError());
		for ( i = 0; i < count ; i++ ) {
		    gamepad = SDL_OpenGamepad(gamepads[i]);
		    if ( gamepad == NULL ) {
			openfailed = true;
			break;
		    }
		    SDL_Log("Gamepad %d is %s", i, SDL_GetGamepadNameForID(gamepads[i]));
		}
		// Reported after the loop rather than inside it: a
		// FAIL_ZERO_BREAK in there would break the loop, not the block.
		FAIL_NONZERO_BREAK(errctx, openfailed, AKERR_NULLPOINTER, "%s", SDL_GetError());
	    } CLEANUP {
		if ( gamepads != NULL ) {
		    SDL_free(gamepads);
		    gamepads = NULL;
		}
	    } PROCESS(errctx) {
	    } FINISH(errctx, true);
	} else {
	    // SDL_GetGamepads allocates even when it reports none.
	    if ( gamepads != NULL ) {
		SDL_free(gamepads);
		gamepads = NULL;
	    }
	    SDL_Log("No gamepads enumerated");
	}
    } else {
	SDL_Log("No gamepads connected");
    }
    SUCCEED_RETURN(errctx);
}

akerr_ErrorContext *akgl_controller_handle_event(void *appstate, SDL_Event *event)
{
    int i = 0;
    int j = 0;
    int eventButtonComboMatch = 0;
    akgl_ControlMap *curmap = NULL;
    akgl_Control *curcontrol = NULL;
    PREPARE_ERROR(errctx);
    FAIL_ZERO_RETURN(errctx, appstate, AKERR_NULLPOINTER, "NULL appstate");
    FAIL_ZERO_RETURN(errctx, event, AKERR_NULLPOINTER, "NULL event");

    // Before the control maps, not after: a key bound to an actor is still a
    // key an interpreter polling with akgl_controller_poll_key() wants to see,
    // and the scan below returns as soon as a binding claims the event.
    if ( event->type == SDL_EVENT_KEY_DOWN ) {
	keybuffer_push_key(event->key.key, event->key.mod);
    } else if ( event->type == SDL_EVENT_TEXT_INPUT ) {
	// The character SDL composed for the press recorded a moment ago. A
	// keycode cannot express it: the layout, the shift state, a compose key
	// and a dead key all have their say between the two events.
	keybuffer_attach_text(event->text.text);
    }

    ATTEMPT {
	for ( i = 0 ; i < AKGL_MAX_CONTROL_MAPS; i++ ) {
	    curmap = &akgl_controlmaps[i];
	    if ( curmap->target == NULL ) {
		continue;
	    }
	    //SDL_Log("Control map %d maps to actor %s", i, curmap->target->name);
	    //SDL_Log("event from keyboard %d", event->key.which);
	    for ( j = 0; j < AKGL_MAX_CONTROLS; j++ ) {
		curcontrol = &curmap->controls[j];
		//SDL_Log("button/key is processed by controlmap %d control %d", i, j);
		//SDL_Log("event %d -> control on %d off %d", event->type, curcontrol->event_on, curcontrol->event_off);
		// This controlmap processes this control
		/*
		 * A map bound to device 0 hears every device of that kind.
		 *
		 * Binding a specific id is what keeps two local players on two
		 * keyboards apart, so it stays -- but a game with one player
		 * cannot ask for "the keyboard" any other way. SDL_GetKeyboards()
		 * reports what is attached; the id a key event actually carries
		 * is chosen by the video backend, and on X11 it is
		 * SDL_GLOBAL_KEYBOARD_ID (0) without XInput2 and the physical
		 * slave device's sourceid with it. Neither is the
		 * SDL_DEFAULT_KEYBOARD_ID of 1 that SDL registers when XInput2 is
		 * missing, so a game that bound SDL_GetKeyboards()[0] got a map
		 * that matched nothing and controls that did nothing.
		 *
		 * 0 is the right spelling for "any": SDL documents `which` as 0
		 * when the source is unknown or virtual, and joystick ids start
		 * at 1, so no real device is 0 and nothing is given up.
		 */
		eventButtonComboMatch = (
		    ((event->type == SDL_EVENT_GAMEPAD_BUTTON_DOWN ||
		      event->type == SDL_EVENT_GAMEPAD_BUTTON_UP) &&
		     (curmap->jsid == 0 || event->gbutton.which == curmap->jsid) &&
		     event->gbutton.button == curcontrol->button) ||
		    ((event->type == SDL_EVENT_KEY_DOWN ||
		      event->type == SDL_EVENT_KEY_UP) &&
		     (curmap->kbid == 0 || event->key.which == curmap->kbid) &&
		     event->key.key == curcontrol->key)
		    );
		if ( event->type == curcontrol->event_on && eventButtonComboMatch) {
		    CATCH(errctx, curcontrol->handler_on(curmap->target, event));
		    goto _akgl_controller_handle_event_success;
		} else if ( event->type == curcontrol->event_off && eventButtonComboMatch ) {
		    CATCH(errctx, curcontrol->handler_off(curmap->target, event));
		    goto _akgl_controller_handle_event_success;
		}
	    }
	}
    _akgl_controller_handle_event_success:
    } CLEANUP {
    } PROCESS(errctx) {
    } FINISH(errctx, true);
    SUCCEED_RETURN(errctx);
}

akerr_ErrorContext *akgl_controller_handle_button_down(void *appstate, SDL_Event *event)
{
    akgl_Actor *player = NULL;

    PREPARE_ERROR(errctx);

    FAIL_ZERO_RETURN(errctx, appstate, AKERR_NULLPOINTER, "NULL appstate");
    FAIL_ZERO_RETURN(errctx, event, AKERR_NULLPOINTER, "NULL event");
    player = SDL_GetPointerProperty(AKGL_REGISTRY_ACTOR, "player", NULL);
    FAIL_ZERO_RETURN(errctx, player, AKERR_NULLPOINTER, "Player actor does not exist");

    if ( event->gbutton.button == SDL_GAMEPAD_BUTTON_DPAD_DOWN ||
	 event->key.key == SDLK_DOWN ) {
	SDL_Log("Processing dpad down : state %d", player->state);
	AKGL_BITMASK_ADD(player->state, AKGL_ACTOR_STATE_MOVING_DOWN);
	if ( !player->movement_controls_face ) {
	    AKGL_BITMASK_DEL(player->state, AKGL_ACTOR_STATE_FACE_ALL);
	    AKGL_BITMASK_ADD(player->state, AKGL_ACTOR_STATE_FACE_DOWN);
	}
	SDL_Log("New state : %d", player->state);
    } else if ( event->gbutton.button == SDL_GAMEPAD_BUTTON_DPAD_UP ||
		event->key.key == SDLK_UP ) {
	SDL_Log("Processing dpad up");
	AKGL_BITMASK_ADD(player->state, AKGL_ACTOR_STATE_MOVING_UP);
	if ( !player->movement_controls_face ) {
	    AKGL_BITMASK_DEL(player->state, AKGL_ACTOR_STATE_FACE_ALL);
	    AKGL_BITMASK_ADD(player->state, AKGL_ACTOR_STATE_FACE_UP);
	}
	SDL_Log("New state : %d", player->state);
    } else if ( event->gbutton.button == SDL_GAMEPAD_BUTTON_DPAD_LEFT ||
		event->key.key == SDLK_LEFT ) {
	SDL_Log("Processing dpad left");
	AKGL_BITMASK_ADD(player->state, AKGL_ACTOR_STATE_MOVING_LEFT);
	if ( !player->movement_controls_face ) {
	    AKGL_BITMASK_DEL(player->state, AKGL_ACTOR_STATE_FACE_ALL);
	    AKGL_BITMASK_ADD(player->state, AKGL_ACTOR_STATE_FACE_LEFT);
	}
	SDL_Log("New state : %d", player->state);
    } else if ( event->gbutton.button == SDL_GAMEPAD_BUTTON_DPAD_RIGHT ||
		event->key.key == SDLK_RIGHT ) {
	SDL_Log("Processing dpad right");
	AKGL_BITMASK_ADD(player->state, AKGL_ACTOR_STATE_MOVING_RIGHT);
	if ( !player->movement_controls_face ) {
	    AKGL_BITMASK_DEL(player->state, AKGL_ACTOR_STATE_FACE_ALL);
	    AKGL_BITMASK_ADD(player->state, AKGL_ACTOR_STATE_FACE_RIGHT);
	}
	SDL_Log("New state : %d", player->state);
    }
    SUCCEED_RETURN(errctx);
}

akerr_ErrorContext *akgl_controller_handle_button_up(void *appstate, SDL_Event *event)
{
    akgl_Actor *player = NULL;

    PREPARE_ERROR(errctx);

    FAIL_ZERO_RETURN(errctx, appstate, AKERR_NULLPOINTER, "NULL appstate");
    FAIL_ZERO_RETURN(errctx, event, AKERR_NULLPOINTER, "NULL event");
    player = SDL_GetPointerProperty(AKGL_REGISTRY_ACTOR, "player", NULL);
    FAIL_ZERO_RETURN(errctx, player, AKERR_NULLPOINTER, "Player actor does not exist");

    if ( event->gbutton.button == SDL_GAMEPAD_BUTTON_DPAD_DOWN  ||
	 event->key.key == SDLK_DOWN ) {
	SDL_Log("processing down release");
	AKGL_BITMASK_DEL(player->state, AKGL_ACTOR_STATE_MOVING_DOWN);
	player->curSpriteFrameId = 0;
	SDL_Log("New state : %d", player->state);
    } else if ( event->gbutton.button == SDL_GAMEPAD_BUTTON_DPAD_UP  ||
		event->key.key == SDLK_UP ) {
	SDL_Log("processing up release");
	AKGL_BITMASK_DEL(player->state, AKGL_ACTOR_STATE_MOVING_UP);
	player->curSpriteFrameId = 0;
	SDL_Log("New state : %d", player->state);
    } else if ( event->gbutton.button == SDL_GAMEPAD_BUTTON_DPAD_RIGHT  ||
		event->key.key == SDLK_RIGHT) {
	SDL_Log("processing right release");
	AKGL_BITMASK_DEL(player->state, AKGL_ACTOR_STATE_MOVING_RIGHT);
	player->curSpriteFrameId = 0;
	SDL_Log("New state : %d", player->state);
    } else if ( event->gbutton.button == SDL_GAMEPAD_BUTTON_DPAD_LEFT  ||
		event->key.key == SDLK_LEFT ) {
	SDL_Log("processing left release");
	AKGL_BITMASK_DEL(player->state, AKGL_ACTOR_STATE_MOVING_LEFT);
	player->curSpriteFrameId = 0;
	SDL_Log("New state : %d", player->state);
    }
    SUCCEED_RETURN(errctx);
}

akerr_ErrorContext *akgl_controller_handle_added(void *appstate, SDL_Event *event)
{
    SDL_JoystickID which;
    SDL_Gamepad *gamepad = NULL;
    char *mapping = NULL;

    PREPARE_ERROR(errctx);
    FAIL_ZERO_RETURN(errctx, appstate, AKERR_NULLPOINTER, "NULL appstate");
    FAIL_ZERO_RETURN(errctx, event, AKERR_NULLPOINTER, "NULL event");

    which = event->gbutton.which;
    gamepad = SDL_GetGamepadFromID(which);

    if (!gamepad) {
	SDL_Log("Gamepad #%u add, but not opened: %s", (unsigned int) which, SDL_GetError());
	gamepad = SDL_OpenGamepad(which);
	SDL_Log("Gamepad #%u opened: %s", (unsigned int) which, SDL_GetError());
	mapping = SDL_GetGamepadMapping(gamepad);
	if ( mapping == NULL ) {
	    SDL_Log("Gamepad #%u has no mapping!", (unsigned int) which);
	} else if ( mapping != NULL ) {
	    SDL_Log("Gamepad #%u mapping : %s", (unsigned int) which, mapping);
	    SDL_free(mapping);
	}
    } else {
	SDL_Log("Gamepad #%u ('%s') added", (unsigned int) which, SDL_GetGamepadName(gamepad));
    }
    SUCCEED_RETURN(errctx);
}

akerr_ErrorContext *akgl_controller_handle_removed(void *appstate, SDL_Event *event)
{
    SDL_JoystickID which;
    SDL_Gamepad *gamepad = NULL;

    PREPARE_ERROR(errctx);
    FAIL_ZERO_RETURN(errctx, appstate, AKERR_NULLPOINTER, "NULL appstate");
    FAIL_ZERO_RETURN(errctx, event, AKERR_NULLPOINTER, "NULL event");

    which = event->gbutton.which;
    gamepad = SDL_GetGamepadFromID(which);

    if (gamepad) {
	SDL_CloseGamepad(gamepad);  /* the joystick was unplugged. */
    }
    SDL_Log("Gamepad #%u removed", (unsigned int) which);
    SUCCEED_RETURN(errctx);
}

akerr_ErrorContext *akgl_controller_pushmap(int controlmapid, akgl_Control *control)
{
    int newmapid = 0;
    PREPARE_ERROR(errctx);
    ATTEMPT {
	FAIL_ZERO_BREAK(errctx, control, AKERR_NULLPOINTER, "NULL Control");
	FAIL_NONZERO_BREAK(errctx, ((controlmapid < 0) || (controlmapid >= AKGL_MAX_CONTROL_MAPS)), AKERR_OUTOFBOUNDS, "Control map id %d is outside 0..%d", controlmapid, (AKGL_MAX_CONTROL_MAPS - 1));
	newmapid = akgl_controlmaps[controlmapid].nextMap;
	FAIL_ZERO_BREAK(errctx, (AKGL_MAX_CONTROLS - newmapid), AKERR_OUTOFBOUNDS, "Control map ID %d is full", controlmapid);
	memcpy((void *)&akgl_controlmaps[controlmapid].controls[newmapid], control, sizeof(akgl_Control));
	akgl_controlmaps[controlmapid].nextMap = newmapid + 1;
    } CLEANUP {
    } PROCESS(errctx) {
    } FINISH(errctx, true);
    SUCCEED_RETURN(errctx);
}

akerr_ErrorContext *akgl_controller_default(int controlmapid, char *actorname, int kbid, int jsid)
{
    akgl_ControlMap *controlmap;
    akgl_Control control;

    PREPARE_ERROR(errctx);
    ATTEMPT {
	// set up the control map
	FAIL_NONZERO_BREAK(errctx, ((controlmapid < 0) || (controlmapid >= AKGL_MAX_CONTROL_MAPS)), AKERR_OUTOFBOUNDS, "Control map id %d is outside 0..%d", controlmapid, (AKGL_MAX_CONTROL_MAPS - 1));
	memset((void *)&control, 0x00, sizeof(akgl_Control));
	controlmap = &akgl_controlmaps[controlmapid];
	controlmap->kbid = kbid;
	controlmap->jsid = jsid;

	controlmap->target = SDL_GetPointerProperty(AKGL_REGISTRY_ACTOR, actorname, NULL);
	FAIL_ZERO_BREAK(errctx, controlmap->target, AKGL_ERR_REGISTRY, "Actor %s not found in registry", actorname);

	// ---- KEYBOARD CONTROLS ----

	// Move down
	control.key = SDLK_DOWN;
	control.event_on = SDL_EVENT_KEY_DOWN;
	control.event_off = SDL_EVENT_KEY_UP;
	control.handler_on = &akgl_actor_cmhf_down_on;
	control.handler_off = &akgl_actor_cmhf_down_off;
	CATCH(errctx, akgl_controller_pushmap(controlmapid, &control));

	// Move up
	control.key = SDLK_UP;
	control.event_on = SDL_EVENT_KEY_DOWN;
	control.event_off = SDL_EVENT_KEY_UP;
	control.handler_on = &akgl_actor_cmhf_up_on;
	control.handler_off = &akgl_actor_cmhf_up_off;
	CATCH(errctx, akgl_controller_pushmap(controlmapid, &control));

	// Move left
	control.key = SDLK_LEFT;
	control.event_on = SDL_EVENT_KEY_DOWN;
	control.event_off = SDL_EVENT_KEY_UP;
	control.handler_on = &akgl_actor_cmhf_left_on;
	control.handler_off = &akgl_actor_cmhf_left_off;
	CATCH(errctx, akgl_controller_pushmap(controlmapid, &control));

	// Move right
	control.key = SDLK_RIGHT;
	control.event_on = SDL_EVENT_KEY_DOWN;
	control.event_off = SDL_EVENT_KEY_UP;
	control.handler_on = &akgl_actor_cmhf_right_on;
	control.handler_off = &akgl_actor_cmhf_right_off;
	CATCH(errctx, akgl_controller_pushmap(controlmapid, &control));

	control.key = 0;
	// ----- GAMEPAD CONTROLS
	// Move down
	control.button = SDL_GAMEPAD_BUTTON_DPAD_DOWN;
	control.event_on = SDL_EVENT_GAMEPAD_BUTTON_DOWN;
	control.event_off = SDL_EVENT_GAMEPAD_BUTTON_UP;
	control.handler_on = &akgl_actor_cmhf_down_on;
	control.handler_off = &akgl_actor_cmhf_down_off;
	CATCH(errctx, akgl_controller_pushmap(controlmapid, &control));

	// Move up
	control.button = SDL_GAMEPAD_BUTTON_DPAD_UP;
	control.event_on = SDL_EVENT_GAMEPAD_BUTTON_DOWN;
	control.event_off = SDL_EVENT_GAMEPAD_BUTTON_UP;
	control.handler_on = &akgl_actor_cmhf_up_on;
	control.handler_off = &akgl_actor_cmhf_up_off;
	CATCH(errctx, akgl_controller_pushmap(controlmapid, &control));

	// Move left
	control.button = SDL_GAMEPAD_BUTTON_DPAD_LEFT;
	control.event_on = SDL_EVENT_GAMEPAD_BUTTON_DOWN;
	control.event_off = SDL_EVENT_GAMEPAD_BUTTON_UP;
	control.handler_on = &akgl_actor_cmhf_left_on;
	control.handler_off = &akgl_actor_cmhf_left_off;
	CATCH(errctx, akgl_controller_pushmap(controlmapid, &control));

	// Move right
	control.button = SDL_GAMEPAD_BUTTON_DPAD_RIGHT;
	control.event_on = SDL_EVENT_GAMEPAD_BUTTON_DOWN;
	control.event_off = SDL_EVENT_GAMEPAD_BUTTON_UP;
	control.handler_on = &akgl_actor_cmhf_right_on;
	control.handler_off = &akgl_actor_cmhf_right_off;
	CATCH(errctx, akgl_controller_pushmap(controlmapid, &control));
    } CLEANUP {
    } PROCESS(errctx) {
    } FINISH(errctx, true);
    // The SUCCEED_RETURN used to sit at the end of the ATTEMPT block, which
    // returned past CLEANUP and left this function with no return statement at
    // all on the path that falls out of FINISH.
    SUCCEED_RETURN(errctx);
}

akerr_ErrorContext *akgl_controller_poll_key(int *keycode, bool *available)
{
    akgl_Keystroke keystroke;

    PREPARE_ERROR(errctx);
    FAIL_ZERO_RETURN(errctx, keycode, AKERR_NULLPOINTER, "NULL keycode destination");
    FAIL_ZERO_RETURN(errctx, available, AKERR_NULLPOINTER, "NULL availability destination");

    // An empty buffer is the ordinary case, not a failure: the caller is asking
    // whether a key is waiting, and the answer is no.
    *keycode = 0;
    *available = false;
    while ( keybuffer_take(&keystroke) == true ) {
	if ( keystroke.key == 0 ) {
	    // Composed text with no key behind it. This form has no way to
	    // report it, so it is dropped rather than handed back as key 0.
	    continue;
	}
	*keycode = (int)keystroke.key;
	*available = true;
	break;
    }
    SUCCEED_RETURN(errctx);
}

akerr_ErrorContext *akgl_controller_poll_keystroke(akgl_Keystroke *dest, bool *available)
{
    PREPARE_ERROR(errctx);
    FAIL_ZERO_RETURN(errctx, dest, AKERR_NULLPOINTER, "NULL keystroke destination");
    FAIL_ZERO_RETURN(errctx, available, AKERR_NULLPOINTER, "NULL availability destination");

    memset(dest, 0x00, sizeof(akgl_Keystroke));
    *available = keybuffer_take(dest);
    SUCCEED_RETURN(errctx);
}

akerr_ErrorContext *akgl_controller_flush_keys(void)
{
    PREPARE_ERROR(errctx);
    keybuffer_head = 0;
    keybuffer_count = 0;
    keybuffer_awaiting_text = false;
    SUCCEED_RETURN(errctx);
}
