/***************************************************************************
 *             __________               __   ___.
 *   Open      \______   \ ____   ____ |  | _\_ |__   _______  ___
 *   Source     |       _//  _ \_/ ___\|  |/ /| __ \ /  _ \  \/  /
 *   Jukebox    |    |   (  <_> )  \___|    < | \_\ (  <_> > <  <
 *   Firmware   |____|_  /\____/ \___  >__|_ \|___  /\____/__/\_ \
 *                     \/            \/     \/    \/            \/
 * $Id$
 *
 * Copyright (C) 2012 by Amaury Pouly
 *
 * This program is free software; you can redistribute it and/or
 * modify it under the terms of the GNU General Public License
 * as published by the Free Software Foundation; either version 2
 * of the License, or (at your option) any later version.
 *
 * This software is distributed on an "AS IS" basis, WITHOUT WARRANTY OF ANY
 * KIND, either express or implied.
 *
 ****************************************************************************/

#include "config.h"
#include "kernel.h"
#include "touchscreen-imx233.h"
#include "stdlib.h"
#ifdef SAMSUNG_YPZ5
#include "pinctrl-imx233.h"
#endif

/* Description:
 * the driver basically has 2 modes:
 * - wait mode: use the hardware touch detect mechanism to wait for an edge
 * - measurement mode: use pull up/down and adc to measure X/Y
 * In measurement mode, we start by measuring X then Y then we check if
 * the hardware still detect a touch. When no touch is detected anymore, we
 * go back to wait mode.
 * For each axis, we handle stabilization by repeatedly measuring the position
 * until it is stable. We consider a set of measures stable when
 * no measure is further away from the average than DEBOUNCE_THRESHOLD and
 * we have at least SAMPLES_THRESHOLD measures. This avoids using magic
 * timing constants specific to the touchscreen stabilization time and
 * it is much more robust.
 *
 * Inspired by linux touchscreen driver for the stmp37xx.
 */

enum touch_state_t
{
    TOUCH_STATE_WAIT, /* wait for touch */
    TOUCH_STATE_MEASURE_X, /* measure x */
    TOUCH_STATE_MEASURE_Y, /* measure y */
    TOUCH_STATE_VERIFY, /* verify touch */
};

#define NR_SAMPLES  10
#define DELAY       1

static enum touch_state_t touch_state;
static int touch_chan = -1;
static int touch_delay = -1;
static int touch_x, touch_y;
/* once a touch is confirmed, the parameters are copied to these value for
 * instant readout by button code. */
static bool old_touch_detect = false;
static int old_touch_x, old_touch_y;

static void process(void);

void INT_TOUCH_DETECT(void)
{
    process();
}

static void touch_channel_irq(int chan)
{
    imx233_icoll_enable_interrupt(INT_SRC_LRADC_CHx(chan), false);
    imx233_lradc_enable_channel_irq(chan, false);
    process();
}

#ifdef SAMSUNG_YPZ5
/* On this target we need to manually setup pulldown pins,
 * using specific GPIO lines
 */
static void pulldown_setup(bool xminus_enable, bool yminus_enable,
    bool xplus_enable, bool yplus_enable)
{
    /* TX+ */
    imx233_pinctrl_set_gpio(0, 25, xplus_enable);
    /* TX- */
    imx233_pinctrl_set_gpio(3, 15, xminus_enable);
    /* TY+ */
    imx233_pinctrl_set_gpio(0, 26, yplus_enable);
    /* TY- */
    imx233_pinctrl_set_gpio(1, 21, yminus_enable);
}
#endif

static void kick_measure(bool pull_x, bool pull_y, bool detect, int src)
{
    if(touch_chan >= 0)
        imx233_lradc_release_channel(touch_chan);
    touch_chan = imx233_lradc_acquire_channel(src, TIMEOUT_NOBLOCK);
    if(touch_chan < 0)
        panicf("touchscreen: cannot get adc channel");
    /* enable interrupt */
    imx233_lradc_set_channel_irq_callback(touch_chan, &touch_channel_irq);
    imx233_icoll_enable_interrupt(INT_SRC_LRADC_CHx(touch_chan), true);
    imx233_lradc_enable_channel_irq(touch_chan, true);
    /* setup measurement: x- pull down and x+ pull up */
#ifdef SAMSUNG_YPZ5
    pulldown_setup(pull_x, pull_y, pull_x, pull_y);
#endif
    imx233_lradc_setup_touch(pull_x, pull_y, pull_x, pull_y, detect);
    imx233_lradc_enable_touch_detect_irq(false);
    imx233_lradc_enable_channel_irq(touch_chan, true);
    /* measure channel, no accumulation */
    imx233_lradc_setup_source(touch_chan, false, src);
    imx233_lradc_setup_sampling(touch_chan, true, NR_SAMPLES - 1);
    imx233_lradc_clear_channel(touch_chan);
    /* use a delay */
    imx233_lradc_setup_delay(touch_delay, 1 << touch_chan, 1 << touch_delay,
        NR_SAMPLES -1, DELAY);
    imx233_lradc_kick_delay(touch_delay);
}

static void enter_state(enum touch_state_t state)
{
    touch_state = state;
    switch(state)
    {
        case TOUCH_STATE_WAIT:
#ifdef SAMSUNG_YPZ5
            pulldown_setup(false, false, false, false);
#endif
            imx233_lradc_setup_touch(false, false, false, false, true);
            imx233_lradc_enable_touch_detect_irq(true);
            break;
        case TOUCH_STATE_MEASURE_X:
            kick_measure(true, false, false, LRADC_SRC_YPLUS);
            break;
        case TOUCH_STATE_MEASURE_Y:
            kick_measure(false, true, false, LRADC_SRC_XPLUS);
            break;
        case TOUCH_STATE_VERIFY:
            kick_measure(false, false, true, LRADC_SRC_YPLUS);
            break;
    }
}

static void process(void)
{
    switch(touch_state)
    {
        case TOUCH_STATE_WAIT:
            /* a touch has happened, check if it's still valid */
            if(imx233_lradc_read_touch_detect())
                enter_state(TOUCH_STATE_MEASURE_X);
            else
            {
                old_touch_detect = false;
                /* clear detect interrupt */
                imx233_lradc_clear_touch_detect_irq();
            }
            break;
        case TOUCH_STATE_MEASURE_X:
            touch_x = imx233_lradc_read_channel(touch_chan) / NR_SAMPLES;
            enter_state(TOUCH_STATE_MEASURE_Y);
            break;
        case TOUCH_STATE_MEASURE_Y:
                touch_y = imx233_lradc_read_channel(touch_chan) / NR_SAMPLES;
                enter_state(TOUCH_STATE_VERIFY);
            break;
        case TOUCH_STATE_VERIFY:
            if(imx233_lradc_read_touch_detect())
            {
                old_touch_detect = true;
                old_touch_x = touch_x;
                old_touch_y = touch_y;
                enter_state(TOUCH_STATE_MEASURE_X);
            }
            else
            {
                old_touch_detect = false;
                enter_state(TOUCH_STATE_WAIT);
            }
            break;
    }
}

void imx233_touchscreen_init(void)
{
    touch_delay = imx233_lradc_acquire_delay(TIMEOUT_NOBLOCK);
    if(touch_delay < 0)
        panicf("Cannot acquire channel and delays for touchscreen measurement");
    imx233_touchscreen_enable(false);
}

void imx233_touchscreen_enable(bool enable)
{
    if(enable)
        enter_state(TOUCH_STATE_WAIT);
    imx233_icoll_enable_interrupt(INT_SRC_TOUCH_DETECT, enable);
}

bool imx233_touchscreen_get_touch(int *raw_x, int *raw_y)
{
    if(raw_x) *raw_x = old_touch_x;
    if(raw_y) *raw_y = old_touch_y;
    return old_touch_detect;
}
