// SPDX-License-Identifier: 0BSD


extern "C" {
#include "enter.h"
}

#include <algorithm>
#include <bit>
#include <ctype.h>
#include <limits.h>
#include <numeric>
#include <stdlib.h>
#include <string.h>
#include <wchar.h>
#include <wctype.h>


struct character {
	wchar_t c;
	char seg[MB_LEN_MAX];
	unsigned seglen : std::bit_width(static_cast<unsigned>(MB_LEN_MAX));
	unsigned width : 2;
	bool space : 1;
	bool word : 1;
};
static_assert(MB_LEN_MAX <= ((1 << std::bit_width(static_cast<unsigned>(MB_LEN_MAX))) - 1));


extern "C" struct enter_string enter_string(const char * init, size_t initlen) {
	int y, x;
	getyx(stdscr, y, x);
	km_dokey_load_termios();

#define MEASURE(character)                 \
	mvaddnwstr(y, x, &character.c, 1);       \
	character.width = getcurx(stdscr) - x;   \
	character.space = iswspace(character.c); \
	character.word  = iswalnum(character.c)

	struct character * characters{};
	size_t characters_len{}, characters_cap{};
#define BUMPCHAR()                                                                                                            \
	do {                                                                                                                        \
		if(characters_len == characters_cap) {                                                                                    \
			characters_cap = characters_cap ? characters_cap * 2 : 16;                                                              \
			if(!(characters = reinterpret_cast<struct character *>(reallocarray(characters, characters_cap, sizeof(*characters))))) \
				return {};                                                                                                            \
		}                                                                                                                         \
	} while(false)

	while(initlen) {
		if(!characters) {
			characters_cap = initlen;
			if(!(characters = reinterpret_cast<struct character *>(reallocarray(characters, characters_cap, sizeof(*characters)))))
				return {};
		}

		struct character character = {};
		mbstate_t state            = {};
		switch(auto r = mbrtowc(&character.c, init, initlen, &state)) {
			case(size_t)-2:  // too short
				state       = {};
				character.c = -1;
				r           = std::min(initlen, (size_t)((1 << 5) - 1));
				goto dfl;
			case(size_t)-1:  // EILSEQ
				state       = {};
				character.c = -1;
				[[fallthrough]];
			case 0:
				r = 1;
				[[fallthrough]];
			default:
			dfl:
				character.seglen = r;
				memcpy(character.seg, init, r);
				initlen -= r;
				init += r;
		}

		MEASURE(character);

		BUMPCHAR();
		characters[characters_len++] = character;
	}
	size_t characters_cursor = characters_len;


#define LEFTBY(howmuch)                                \
	do {                                                 \
		int moveleft = howmuch;                            \
		while(first_on_screen && moveleft > 0)             \
			moveleft -= characters[--first_on_screen].width; \
	} while(false)
#define RIGHTBY(howmuch)                                      \
	do {                                                        \
		int moveright = howmuch;                                  \
		while(first_on_screen != characters_len && moveright > 0) \
			moveright -= characters[++first_on_screen].width;       \
	} while(false)
	// Write out from first_on_screen, so long as the cursor fits on the screen, else move by a half-screen to find it.
	// Start off with as much text as possible and cursor on the right margin; you can't return to this state; this mimicks how ≤1b did it
	size_t first_on_screen = characters_cursor;
	LEFTBY((getmaxx(stdscr) - x) - 1);

	for(wint_t input;;) {
		{
			auto screen_width         = getmaxx(stdscr) - x;
			auto all_characters_width = std::accumulate(characters, characters + characters_len, 0z, [](auto acc, auto && c) { return acc + c.width; });
			if(all_characters_width < screen_width)
				first_on_screen = 0;
			else {
				if(first_on_screen >= characters_len) {
					first_on_screen = characters_cursor - 1;
					LEFTBY(screen_width / 2);
				} else if(characters_cursor < first_on_screen)
					while(characters_cursor < first_on_screen)
						LEFTBY(screen_width / 2);
				else
					for(;;) {
						auto last = std::find_if(characters + first_on_screen, characters + characters_len, [&, acc = 0z](auto && c) mutable {
							acc += c.width;
							return acc > screen_width;
						});

						auto characters_on_screen = last - (characters + first_on_screen);
						if(first_on_screen + characters_on_screen < characters_cursor)
							RIGHTBY(screen_width / 2);
						else
							break;
					}
			}

			move(y, x);
			auto cursor_x = x;
			for(auto itr = characters + first_on_screen; itr != characters + characters_len; ++itr) {
				if(itr->width <= screen_width)
					screen_width -= itr->width;
				else
					break;
				addnwstr(&itr->c, 1);
				if(itr < characters + characters_cursor)
					cursor_x += itr->width;
			}
			clrtoeol();
			move(y, cursor_x);
		}

		switch(km_dokey(&input)) {
			case -1:
				return free(characters), (struct enter_string){};

			case OP_EDITOR_CHAR:
				if(iswprint(input)) {
					struct character character = {};
					character.c                = input;

					int newlen = wctomb(character.seg, input);
					if(newlen == -1)
						goto err;
					character.seglen = newlen;

					MEASURE(character);

					BUMPCHAR();
					memmove(characters + characters_cursor + 1, characters + characters_cursor, (characters_len - characters_cursor) * sizeof(*characters));
					++characters_len;
					characters[characters_cursor++] = character;
				} else {
				err:
					flushinp();
					beep();
				}
				break;

			case OP_EDITOR_DONE: {
				struct enter_string ret = {0, std::accumulate(characters, characters + characters_len, 0zu, [](auto acc, auto && c) { return acc + c.seglen; })};
				if(!ret.len)
					return free(characters), (struct enter_string){};
				ret.data = reinterpret_cast<char *>(malloc(ret.len + 1));
				if(!ret.data)
					return free(characters), (struct enter_string){};
				auto acc = ret.data;
				for(auto itr = characters; itr != characters + characters_len; ++itr)
					acc = reinterpret_cast<char *>(memcpy(acc, itr->seg, itr->seglen)) + itr->seglen;
				*acc = '\0';
				return free(characters), ret;
			} break;

			case OP_EDITOR_RESIZE:
				break;

			case OP_EDITOR_IDK:
				beep();
				break;

			case OP_EDITOR_BACKSPACE:
				if(!characters_cursor)
					beep();
				else {
					memmove(characters + characters_cursor - 1, characters + characters_cursor, (characters_len - characters_cursor) * sizeof(*characters));
					--characters_len;
					--characters_cursor;
				}
				break;
			case OP_EDITOR_DELETE_CHAR:
				if(characters_cursor == characters_len)
					beep();
				else {
					memmove(characters + characters_cursor, characters + characters_cursor + 1, (characters_len - (characters_cursor + 1)) * sizeof(*characters));
					--characters_len;
				}
				break;
			case OP_EDITOR_KILL_LINE:
				characters_len = characters_cursor = 0;
				break;
			case OP_EDITOR_KILL_EOL:
				characters_len = characters_cursor;
				break;
			case OP_EDITOR_KILL_WORD: {
				if(!characters_cursor)
					break;

				auto end = characters_cursor;
				--characters_cursor;
				while(characters_cursor && characters[characters_cursor - 1].space)
					--characters_cursor;
				while(characters_cursor && characters[characters_cursor - 1].word)
					--characters_cursor;

				memmove(characters + characters_cursor, characters + end, (characters_len - end) * sizeof(*characters));
				characters_len -= end - characters_cursor;
			} break;

			case OP_EDITOR_BOL:
				characters_cursor = 0;
				break;
			case OP_EDITOR_EOL:
				characters_cursor = characters_len;
				break;
			case OP_EDITOR_BACKWARD_CHAR:
				if(!characters_cursor)
					beep();
				else
					--characters_cursor;
				break;
			case OP_EDITOR_FORWARD_CHAR:
				if(characters_cursor == characters_len)
					beep();
				else
					++characters_cursor;
				break;
		}
	}
}
