/*
 * Generic GPIO / irq buttons
 *
 * Copyright (C) 2019 - 2020 Andy Green <andy@warmcat.com>
 *
 * Permission is hereby granted, free of charge, to any person obtaining a copy
 * of this software and associated documentation files (the "Software"), to
 * deal in the Software without restriction, including without limitation the
 * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
 * sell copies of the Software, and to permit persons to whom the Software is
 * furnished to do so, subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included in
 * all copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
 * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
 * IN THE SOFTWARE.
 */
#include "private-lib-core.h"

typedef enum lws_button_classify_states {
	LBCS_IDLE,		/* nothing happening */
	LBCS_MIN_DOWN_QUALIFY,

	LBCS_ASSESS_DOWN_HOLD,
	LBCS_UP_SETTLE1,
	LBCS_WAIT_DOUBLECLICK,
	LBCS_MIN_DOWN_QUALIFY2,

	LBCS_WAIT_UP,
	LBCS_UP_SETTLE2,
} lws_button_classify_states_t;

/*
 * This is the opaque, allocated, non-const, dynamic footprint of the
 * button controller
 */

typedef struct lws_button_state {
#if defined(LWS_PLAT_TIMER_TYPE)
	LWS_PLAT_TIMER_TYPE			timer;	   /* bh timer */
	LWS_PLAT_TIMER_TYPE			timer_mon; /* monitor timer */
#endif
	const lws_button_controller_t		*controller;
	struct lws_context			*ctx;
	short					mon_refcount;
	lws_button_idx_t			enable_bitmap;
	lws_button_idx_t			state_bitmap;

	uint16_t				mon_timer_count;
	/* incremented each time the mon timer cb happens */

	/* lws_button_each_t per button overallocated after this */
} lws_button_state_t;

typedef struct lws_button_each {
	lws_button_state_t			*bcs;
	uint16_t				mon_timer_comp;
	uint16_t				mon_timer_repeat;
	uint8_t					state;
	/**^ lws_button_classify_states_t */
	uint8_t					isr_pending;
} lws_button_each_t;

#if defined(LWS_PLAT_TIMER_START)
static const lws_button_regime_t default_regime = {
	.ms_min_down			= 20,
	.ms_min_down_longpress		= 300,
	.ms_up_settle			= 20,
	.ms_doubleclick_grace		= 120,
	.flags				= LWSBTNRGMFLAG_CLASSIFY_DOUBLECLICK
};
#endif


/*
 * This is happening in interrupt context, we have to schedule a bottom half to
 * do the foreground lws_smd queueing, using, eg, a platform timer.
 *
 * All the buttons point here and use one timer per button controller.  An
 * interrupt here means, "something happened to one or more buttons"
 */
#if defined(LWS_PLAT_TIMER_START)
void
lws_button_irq_cb_t(void *arg)
{
	lws_button_each_t *each = (lws_button_each_t *)arg;

	each->isr_pending = 1;
	LWS_PLAT_TIMER_START(each->bcs->timer);
}
#endif

/*
 * This is the bottom-half scheduled via a timer set in the ISR.  From here we
 * are allowed to hold mutexes etc.  We are coming here because any button
 * interrupt arrived, we have to run another timer that tries to put whatever is
 * observed on any active button into context and either discard it or arrive at
 * a definitive event classification.
 */

#if defined(LWS_PLAT_TIMER_CB)
static LWS_PLAT_TIMER_CB(lws_button_bh, th)
{
	lws_button_state_t *bcs = LWS_PLAT_TIMER_CB_GET_OPAQUE(th);
	lws_button_each_t *each = (lws_button_each_t *)&bcs[1];
	const lws_button_controller_t *bc = bcs->controller;
	size_t n;

	/*
	 * The ISR and bottom-half is shared by all the buttons.  Each gpio
	 * IRQ has an individual opaque ptr pointing to the corresponding
	 * button's dynamic lws_button_each_t, the ISR marks the button's
	 * each->isr_pending and schedules this bottom half.
	 *
	 * So now the bh timer has fired and something to do, we need to go
	 * through all the buttons that have isr_pending set and service their
	 * state.  Intermediate states should start / bump the refcount on the
	 * mon timer.  That's refcounted so it only runs when a button down.
	 */

	for (n = 0; n < bc->count_buttons; n++) {

		if (!each[n].isr_pending)
			continue;

		/*
		 * Hide what we're about to do from the delicate eyes of the
		 * IRQ controller...
		 */

		bc->gpio_ops->irq_mode(bc->button_map[n].gpio,
				       LWSGGPIO_IRQ_NONE, NULL, NULL);

		each[n].isr_pending = 0;

		/*
		 * Force the network around the switch to the
		 * active level briefly
		 */

		bc->gpio_ops->set(bc->button_map[n].gpio,
				  !!(bc->active_state_bitmap & (1 << n)));
		bc->gpio_ops->mode(bc->button_map[n].gpio, LWSGGPIO_FL_WRITE);

		if (each[n].state == LBCS_IDLE) {
			/*
			 * If this is the first sign something happening on this
			 * button, make sure the monitor timer is running to
			 * classify its response over time
			 */

			each[n].state = LBCS_MIN_DOWN_QUALIFY;
			each[n].mon_timer_comp = bcs->mon_timer_count;

			if (!bcs->mon_refcount++) {
#if defined(LWS_PLAT_TIMER_START)
				LWS_PLAT_TIMER_START(bcs->timer_mon);
#endif
			}
		}

		/*
		 * Just for a us or two inbetween here, we're driving it to the
		 * level we were informed by the interrupt it had enetered, to
		 * force to charge on the actual and parasitic network around
		 * the switch to a deterministic-ish state.
		 *
		 * If the switch remains in that state, well, it makes no
		 * difference; if it was a pre-contact and the charge on the
		 * network was left indeterminate, this will dispose it to act
		 * consistently in the short term until the pullup / pulldown
		 * has time to act on it or the switch comes and forces the
		 * network charge state itself.
		 */
		bc->gpio_ops->mode(bc->button_map[n].gpio, LWSGGPIO_FL_READ);

		/*
		 * We could do a better job manipulating the irq mode according
		 * to the switch state.  But if an interrupt comes and we have
		 * done that, we can't tell if it's from before or after the
		 * mode change... ie, we don't know what the interrupt was
		 * telling us.  We can't trust the gpio state if we read it now
		 * to be related to what the irq from some time before was
		 * trying to tell us.  So always set it back to the same mode
		 * and accept the limitation.
		 */

		bc->gpio_ops->irq_mode(bc->button_map[n].gpio,
				       bc->active_state_bitmap & (1 << n) ?
					   LWSGGPIO_IRQ_RISING :
					   LWSGGPIO_IRQ_FALLING,
					      lws_button_irq_cb_t, &each[n]);
	}
}
#endif

#if defined(LWS_PLAT_TIMER_CB)
static LWS_PLAT_TIMER_CB(lws_button_mon, th)
{
	lws_button_state_t *bcs = LWS_PLAT_TIMER_CB_GET_OPAQUE(th);
	lws_button_each_t *each = (lws_button_each_t *)&bcs[1];
	const lws_button_controller_t *bc = bcs->controller;
	const lws_button_regime_t *regime;
	const char *event_name;
	int comp_age_ms;
	char active;
	size_t n;

	bcs->mon_timer_count++;

	for (n = 0; n < bc->count_buttons; n++) {

		if (each->state == LBCS_IDLE) {
			each++;
			continue;
		}

		if (bc->button_map[n].regime)
			regime = bc->button_map[n].regime;
		else
			regime = &default_regime;

		comp_age_ms = (bcs->mon_timer_count - each->mon_timer_comp) *
				LWS_BUTTON_MON_TIMER_MS;

		active = bc->gpio_ops->read(bc->button_map[n].gpio) ^
			       (!(bc->active_state_bitmap & (1 << n)));

		// lwsl_notice("%d\n", each->state);

		switch (each->state) {
		case LBCS_MIN_DOWN_QUALIFY:
			/*
			 * We're trying to figure out if the initial down event
			 * is a glitch, or if it meets the criteria for being
			 * treated as the definitive start of some kind of click
			 * action.  To get past this, he has to be solidly down
			 * for the time mentioned in the applied regime (at
			 * least when we sample it).
			 *
			 * Significant bounce at the start will abort this try,
			 * but if it's really down there will be a subsequent
			 * solid down period... it will simply restart this flow
			 * from a new interrupt and pass the filter then.
			 *
			 * The "brief drive on edge" strategy considerably
			 * reduces inconsistencies here.  But physical bounce
			 * will continue to be observed.
			 */

			if (!active) {
				/* We ignore stuff for a bit after discard */
				each->mon_timer_comp = bcs->mon_timer_count;
				each->state = LBCS_UP_SETTLE2;
				break;
			}

			if (comp_age_ms >= regime->ms_min_down) {

				/* We made it through the initial regime filter,
				 * the next step is wait and see if this down
				 * event evolves into a single/double click or
				 * we can call it as a long-click
				 */

				each->mon_timer_repeat = bcs->mon_timer_count;
				each->state = LBCS_ASSESS_DOWN_HOLD;
				event_name = "down";
				goto emit;
			}
			break;

		case LBCS_ASSESS_DOWN_HOLD:

			/*
			 * How long is he going to hold it?  If he holds it
			 * past the long-click threshold, we can call it as a
			 * long-click and do the up processing afterwards.
			 */
			if (comp_age_ms >= regime->ms_min_down_longpress) {
				/* call it as a longclick */
				event_name = "longclick";
				each->state = LBCS_WAIT_UP;
				goto emit;
			}

			if (!active) {
				/*
				 * He didn't hold it past the long-click
				 * threshold... we could end up classifying it
				 * as either a click or a double-click then.
				 *
				 * If double-clicks are not allowed to be
				 * classified, then we can already classify it
				 * as a single-click.
				 */
				if (!(regime->flags &
					    LWSBTNRGMFLAG_CLASSIFY_DOUBLECLICK))
					goto classify_single;

				/*
				 * Just wait for the up settle time then start
				 * looking for a second down.
				 */
				each->mon_timer_comp = bcs->mon_timer_count;
				each->state = LBCS_UP_SETTLE1;
				event_name = "up";
				goto emit;
			}

			goto stilldown;

		case LBCS_UP_SETTLE1:
			if (comp_age_ms > regime->ms_up_settle)
				/*
				 * Just block anything for the up settle time
				 */
				each->state = LBCS_WAIT_DOUBLECLICK;
			break;

		case LBCS_WAIT_DOUBLECLICK:
			if (active) {
				/*
				 * He has gone down again inside the regime's
				 * doubleclick grace period... he's going down
				 * the double-click path
				 */
				each->mon_timer_comp = bcs->mon_timer_count;
				each->state = LBCS_MIN_DOWN_QUALIFY2;
				break;
			}

			if (comp_age_ms >= regime->ms_doubleclick_grace) {
				/*
				 * The grace period expired, the second click
				 * was either not forthcoming at all, or coming
				 * quick enough to count: we classify it as a
				 * single-click
				 */

				goto classify_single;
			}
			break;

		case LBCS_MIN_DOWN_QUALIFY2:
			if (!active) {

				/*
				 * He went up again too quickly, classify it
				 * as a single-click.  It could be bounce in
				 * which case you might want to increase the
				 * ms_up_settle in the regime
				 */
classify_single:
				event_name = "click";
				each->mon_timer_comp = bcs->mon_timer_count;
				each->state = LBCS_UP_SETTLE2;
				goto emit;
			}

			if (comp_age_ms == regime->ms_min_down) {
				event_name = "down";
				goto emit;
			}

			if (comp_age_ms > regime->ms_min_down) {
				/*
				 * It's a double-click
				 */
				event_name = "doubleclick";
				each->state = LBCS_WAIT_UP;
				goto emit;
			}
			break;

		case LBCS_WAIT_UP:
			if (!active) {
				/*
				 * He has stopped pressing it
				 */
				each->mon_timer_comp = bcs->mon_timer_count;
				each->state = LBCS_UP_SETTLE2;
				event_name = "up";
				goto emit;
			}
stilldown:
			if (regime->ms_repeat_down &&
			    (bcs->mon_timer_count - each->mon_timer_repeat) *
			     LWS_BUTTON_MON_TIMER_MS > regime->ms_repeat_down) {
				each->mon_timer_repeat = bcs->mon_timer_count;
				event_name = "stilldown";
				goto emit;
			}
			break;

		case LBCS_UP_SETTLE2:
			if (comp_age_ms < regime->ms_up_settle)
				break;

			each->state = LBCS_IDLE;
			if (!(--bcs->mon_refcount)) {
#if defined(LWS_PLAT_TIMER_STOP)
				LWS_PLAT_TIMER_STOP(bcs->timer_mon);
#endif
			}
		}

		each++;
		continue;

emit:
		lws_smd_msg_printf(bcs->ctx, LWSSMDCL_INTERACTION,
				   "{\"type\":\"button\","
				   "\"src\":\"%s/%s\",\"event\":\"%s\"}",
				   bc->smd_bc_name,
				   bc->button_map[n].smd_interaction_name,
				   event_name);

		each++;
	}
}
#endif

struct lws_button_state *
lws_button_controller_create(struct lws_context *ctx,
			     const lws_button_controller_t *controller)
{
	lws_button_state_t *bcs = lws_zalloc(sizeof(lws_button_state_t) +
			(controller->count_buttons * sizeof(lws_button_each_t)),
			__func__);
	lws_button_each_t *each = (lws_button_each_t *)&bcs[1];
	size_t n;

	if (!bcs)
		return NULL;

	bcs->controller = controller;
	bcs->ctx = ctx;

	for (n = 0; n < controller->count_buttons; n++)
		each[n].bcs = bcs;

#if defined(LWS_PLAT_TIMER_CREATE)
	/* this only runs inbetween a gpio ISR and the bottom half */
	bcs->timer = LWS_PLAT_TIMER_CREATE("bcst",
			1, 0, bcs, (TimerCallbackFunction_t)lws_button_bh);
	if (!bcs->timer)
		return NULL;

	/* this only runs when a button activity is being classified */
	bcs->timer_mon = LWS_PLAT_TIMER_CREATE("bcmon", LWS_BUTTON_MON_TIMER_MS,
					       1, bcs, (TimerCallbackFunction_t)
								lws_button_mon);
	if (!bcs->timer_mon)
		return NULL;
#endif

	return bcs;
}

void
lws_button_controller_destroy(struct lws_button_state *bcs)
{
	/* disable them all */
	lws_button_enable(bcs, 0, 0);

#if defined(LWS_PLAT_TIMER_DELETE)
	LWS_PLAT_TIMER_DELETE(bcs->timer);
	LWS_PLAT_TIMER_DELETE(bcs->timer_mon);
#endif

	lws_free(bcs);
}

lws_button_idx_t
lws_button_get_bit(struct lws_button_state *bcs, const char *name)
{
	const lws_button_controller_t *bc = bcs->controller;
	int n;

	for (n = 0; n < bc->count_buttons; n++)
		if (!strcmp(name, bc->button_map[n].smd_interaction_name))
			return 1 << n;

	return 0; /* not found */
}

void
lws_button_enable(lws_button_state_t *bcs,
		  lws_button_idx_t _reset, lws_button_idx_t _set)
{
	lws_button_idx_t u = (bcs->enable_bitmap & (~_reset)) | _set;
	const lws_button_controller_t *bc = bcs->controller;
#if defined(LWS_PLAT_TIMER_START)
	lws_button_each_t *each = (lws_button_each_t *)&bcs[1];
#endif
	int n;

	for (n = 0; n < bcs->controller->count_buttons; n++) {
		if (!(bcs->enable_bitmap & (1 << n)) && (u & (1 << n))) {
			/* set as input with pullup or pulldown appropriately */
			bc->gpio_ops->mode(bc->button_map[n].gpio,
				LWSGGPIO_FL_READ |
				((bc->active_state_bitmap & (1 << n)) ?
				LWSGGPIO_FL_PULLDOWN : LWSGGPIO_FL_PULLUP));
#if defined(LWS_PLAT_TIMER_START)
			/*
			 * This one is becoming enabled... the opaque for the
			 * ISR is the indvidual lws_button_each_t, they all
			 * point to the same ISR
			 */
			bc->gpio_ops->irq_mode(bc->button_map[n].gpio,
					bc->active_state_bitmap & (1 << n) ?
						LWSGGPIO_IRQ_RISING :
							LWSGGPIO_IRQ_FALLING,
						lws_button_irq_cb_t, &each[n]);
#endif
		}
		if ((bcs->enable_bitmap & (1 << n)) && !(u & (1 << n)))
			/* this one is becoming disabled */
			bc->gpio_ops->irq_mode(bc->button_map[n].gpio,
						LWSGGPIO_IRQ_NONE, NULL, NULL);
	}

	bcs->enable_bitmap = u;
}
