/* ----------------------------------------------------------------------------
 *         SAM Software Package License 
 * ----------------------------------------------------------------------------
 * Copyright (c) 2011, Atmel Corporation
 *
 * All rights reserved.
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions are met:
 *
 * - Redistributions of source code must retain the above copyright notice,
 * this list of conditions and the disclaimer below.
 *
 * Atmel's name may not be used to endorse or promote products derived from
 * this software without specific prior written permission.
 *
 * DISCLAIMER: THIS SOFTWARE IS PROVIDED BY ATMEL "AS IS" AND ANY EXPRESS OR
 * IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
 * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT ARE
 * DISCLAIMED. IN NO EVENT SHALL ATMEL BE LIABLE FOR ANY DIRECT, INDIRECT,
 * INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
 * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA,
 * OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
 * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
 * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE,
 * EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 * ----------------------------------------------------------------------------
 */
 
/** \file */

/*----------------------------------------------------------------------------
 *        Headers
 *----------------------------------------------------------------------------*/

/** \addtogroup tsd_module
 *@{
 */
     

#include <board.h>
#include <string.h>

/*----------------------------------------------------------------------------
 *        Local definitions
 *----------------------------------------------------------------------------*/

/** Size in pixels of calibration points. */
#define POINTS_SIZE         4
/** Maximum difference in pixels between the test point and the measured point.
 */
#define POINTS_MAX_XERROR   10
/** Maximum difference in pixels between the test point and the measured point.
 */
#define POINTS_MAX_YERROR   8

/** Delay at the end of calibartion for result display (positive or negative) */
#define DELAY_RESULT_DISPLAY 4000000

/** Clear Strings on LCD */
#if 1
#define CLEAR_STRING()  LCDD_Fill(COLOR_WHITE)
#else
#define CLEAR_STRING()  \
    LCDD_DrawFilledRectangle(strX -  3*strW, strY, \
                             strX + 20*strW, strY + 6*strH, COLOR_WHITE)
#endif

/*----------------------------------------------------------------------------
 *         Local types
 *----------------------------------------------------------------------------*/

/**
 * Point used during the touchscreen calibration process.
 */
typedef struct _CalibrationPoint {

    /** Coordinate of point along the X-axis of the screen. */
    uint32_t x;
    /** Coordinate of point along the Y-axis of the screen. */
    uint32_t y;
    /** Calibration data of point. */
    uint32_t data[2];

} CalibrationPoint;

/*----------------------------------------------------------------------------
 *         Local variables
 *----------------------------------------------------------------------------*/

/** Calibration display title */
static const char* strTitle = "LCD Calibration";

/** indicates if the touch screen has been calibrated.
    If not, Callback functions are not called */
static volatile uint8_t bCalibrationOk = 0;
/** Slope for interpoling touchscreen measurements along the X-axis. */
static int32_t xSlope;
/** Slope for interpoling touchscreen measurements along the Y-axis. */
static int32_t ySlope;

/** Calibration points */
static CalibrationPoint calibrationPoints[] = {

    /* Top-left corner calibration point */
    {
        BOARD_LCD_WIDTH / 10,
        BOARD_LCD_HEIGHT / 10,
        {0, 0}
    },
    /* Top-right corner calibration point */
    {
        BOARD_LCD_WIDTH - BOARD_LCD_WIDTH / 10,
        BOARD_LCD_HEIGHT / 10,
        {0, 0}
    },
    /* Bottom-right corner calibration point */
    {
        BOARD_LCD_WIDTH - BOARD_LCD_WIDTH / 10,
        BOARD_LCD_HEIGHT - BOARD_LCD_HEIGHT / 10,
        {0, 0}
    },
    /* Bottom-left corner calibration point */
    {
        BOARD_LCD_WIDTH / 10,
        BOARD_LCD_HEIGHT - BOARD_LCD_HEIGHT / 10,
        {0, 0}
    }
};

/** Test point */
static const CalibrationPoint testPoint = {
    BOARD_LCD_WIDTH / 2,
    BOARD_LCD_HEIGHT / 2,
    {0, 0}
};

/*----------------------------------------------------------------------------
 *         External functions
 *----------------------------------------------------------------------------*/

/*----------------------------------------------------------------------------
 *         Local functions
 *----------------------------------------------------------------------------*/

/**
 * Display a calibration point on the given buffer.
 * \param pPoint  Calibration point to display.
 */
static void DrawCalibrationPoint(
    const CalibrationPoint *pPoint)
{
    LCDD_DrawFilledRectangle(pPoint->x - POINTS_SIZE / 2,
                             pPoint->y - POINTS_SIZE / 2,
                             pPoint->x + POINTS_SIZE,
                             pPoint->y + POINTS_SIZE,
                             COLOR_RED);
}

/**
 * Clears a calibration point from the given buffer.
 * \param pLcdBuffer  LCD buffer to draw on.
 * \param pPoint  Calibration point to clear.
 */
static void ClearCalibrationPoint(
    const CalibrationPoint *pPoint)
{
    LCDD_DrawFilledRectangle(pPoint->x - POINTS_SIZE,
                             pPoint->y - POINTS_SIZE,
                             pPoint->x + POINTS_SIZE,
                             pPoint->y + POINTS_SIZE,
                             COLOR_WHITE);
}

/*----------------------------------------------------------------------------
 *        Exported functions
 *----------------------------------------------------------------------------*/

/**
 * Indicates if the calibration of the touch screen is Ok
 * \return 1 calibration Ok, 0 if not
 */
uint8_t TSDCom_IsCalibrationOk(void)
{
    return bCalibrationOk;
}

/**
 * Interpolates the provided raw measurements using the previously calculated
 * slope. The resulting x and y coordinates are stored in an array.
 * \param pData  Raw measurement data, as returned by TSD_GetRawMeasurement().
 * \param pPoint  Array in which x and y will be stored.
 */
void TSDCom_InterpolateMeasurement(const uint32_t *pData, uint32_t *pPoint)
{
    pPoint[0] = calibrationPoints[0].x
                - (((int32_t) calibrationPoints[0].data[0] - (int32_t) pData[0]) * 1024)
                / xSlope;

    pPoint[1] = calibrationPoints[0].y
                - (((int32_t) calibrationPoints[0].data[1] - (int32_t) pData[1]) * 1024)
                / ySlope;
    /* Is pPoint[0] negative ? */
    if(pPoint[0] & 0x80000000)        pPoint[0] = 0;
    /* Is pPoint[0] bigger than the LCD width ? */
    if(pPoint[0] > BOARD_LCD_WIDTH)   pPoint[0] = BOARD_LCD_WIDTH;
    /* Is pPoint[1] negative ? */
    if(pPoint[1] & 0x80000000)        pPoint[1] = 0;
    /* Is pPoint[1] bigger than the LCD width ? */
    if(pPoint[1] > BOARD_LCD_HEIGHT)  pPoint[1] = BOARD_LCD_HEIGHT;
}

/**
 * Performs the calibration process using the provided buffer to display
 * information.
 * \param pLcdBuffer  LCD buffer to display.
 * \return True if calibration was successful; otherwise false.
 */
uint8_t TSDCom_Calibrate(void)
{
    uint32_t i; // to keep the tempo with gcc code optimisation
    int32_t slope1, slope2;
    CalibrationPoint measuredPoint;
    uint8_t xOk, yOk;
    int32_t xDiff, yDiff;
    uint32_t strX = BOARD_LCD_WIDTH / 2 - 75, strY = 60;
    uint32_t strW, strH;

    LCDD_GetStringSize("P", &strW, &strH);
    /* Calibration setup */
    LCDD_Fill(COLOR_WHITE);
    LCDD_Flush_CurrentCanvas();
    LCDD_DrawString(strX, strY, strTitle, COLOR_BLACK);
    LCDD_Flush_CurrentCanvas();
    LCDD_DrawString(strX - 2*strW, strY + 3*strH,
        " Touch the dots to\ncalibrate the screen", COLOR_DARKBLUE);
    LCDD_Flush_CurrentCanvas();
    /* Calibration points */
    for (i = 0; i < 4; i++) {

        DrawCalibrationPoint(&calibrationPoints[i]);
        LCDD_Flush_CurrentCanvas();
        /* Wait for touch & end of conversion */
        TSD_WaitPenPressed();
        TSD_GetRawMeasurement(calibrationPoints[i].data);
        ClearCalibrationPoint(&calibrationPoints[i]);
        LCDD_Flush_CurrentCanvas();
        /* Wait for contact loss */
        TSD_WaitPenReleased();
        printf("P%d: (%d,%d)\n\r", (unsigned int)i, (unsigned int)calibrationPoints[i].data[0], (unsigned int)calibrationPoints[i].data[1]);
    }

    /* Calculate slopes using the calibration data
     * Theory behind those calculations:
     *   - We suppose the touchscreen measurements are linear, so the following equations are true (simple
     *     linear regression) for any two 'a' and 'b' points of the screen:
     *       dx = (a.data[0] - b.data[0]) / (a.x - b.x)
     *       dy = (a.data[1] - b.data[1]) / (a.y - b.y)
     *
     *   - We calculate dx and dy (called xslope and yslope here) using the calibration points.
     *
     *   - We can then use dx and dy to infer the position of a point 'p' given the measurements performed
     *     by the touchscreen ('c' is any of the calibration points):
     *       dx = (p.data[0] - c.data[0]) / (p.x - c.x)
     *       dy = (p.data[1] - c.data[1]) / (p.y - c.y)
     *     Thus:
     *       p.x = c.x - (p.data[0] - c.data[0]) / dx
     *       p.y = c.y - (p.data[1] - c.data[1]) / dy
     *
     *   - Since there are four calibration points, dx and dy can be calculated twice, so we average
     *     the two values.
     */
    slope1 = ((int32_t) calibrationPoints[0].data[0]) - ((int32_t) calibrationPoints[1].data[0]);
    slope1 *= 1024;
    slope1 /= ((int32_t) calibrationPoints[0].x) - ((int32_t) calibrationPoints[1].x);
    slope2 = ((int32_t) calibrationPoints[2].data[0]) - ((int32_t) calibrationPoints[3].data[0]);
    slope2 *= 1024;
    slope2 /= ((int32_t) calibrationPoints[2].x) - ((int32_t) calibrationPoints[3].x);
    xSlope = (slope1 + slope2) / 2;

    slope1 = ((int32_t) calibrationPoints[0].data[1]) - ((int32_t) calibrationPoints[2].data[1]);
    slope1 *= 1024;
    slope1 /= ((int32_t) calibrationPoints[0].y) - ((int32_t) calibrationPoints[2].y);
    slope2 = ((int32_t) calibrationPoints[1].data[1]) - ((int32_t) calibrationPoints[3].data[1]);
    slope2 *= 1024;
    slope2 /= ((int32_t) calibrationPoints[1].y) - ((int32_t) calibrationPoints[3].y);
    ySlope = (slope1 + slope2) / 2;

    printf("Slope: %d, %d\n\r", (unsigned int)xSlope, (unsigned int)ySlope);

    /* Test point */
    CLEAR_STRING();
    LCDD_DrawString(strX, strY, strTitle, COLOR_BLACK);
    LCDD_DrawString(strX - 2*strW, strY + 3*strH,
        " Touch the point to\nvalidate calibration", COLOR_DARKBLUE);
    LCDD_Flush_CurrentCanvas();
    DrawCalibrationPoint(&testPoint);
    LCDD_Flush_CurrentCanvas();
    /* Wait for touch & end of conversion */
    TSD_WaitPenPressed();

    TSD_GetRawMeasurement(measuredPoint.data);
    TSDCom_InterpolateMeasurement(measuredPoint.data, (uint32_t *) &measuredPoint);
    DrawCalibrationPoint(&measuredPoint);
    LCDD_Flush_CurrentCanvas();
    /* Check resulting x and y */
    xDiff = (int32_t) measuredPoint.x - (int32_t) testPoint.x;
    yDiff = (int32_t) measuredPoint.y - (int32_t) testPoint.y;
    xOk = (xDiff >= -POINTS_MAX_XERROR) && (xDiff <= POINTS_MAX_XERROR);
    yOk = (yDiff >= -POINTS_MAX_YERROR) && (yDiff <= POINTS_MAX_YERROR);

    /* Wait for contact loss */
    TSD_WaitPenReleased();

    printf("TP: %d, %d -> %d, %d\n\r",
        (unsigned int)measuredPoint.data[0], (unsigned int)measuredPoint.data[1],
        (unsigned int)measuredPoint.x, (unsigned int)measuredPoint.y);

    /* Check calibration result */
    if (xOk && yOk) {

        bCalibrationOk = 1;
        CLEAR_STRING();
        LCDD_DrawString(strX, strY, strTitle, COLOR_BLACK);
        LCDD_DrawString(strX + 3*strW, strY + 2*strH, "Success !", COLOR_GREEN);
        LCDD_Flush_CurrentCanvas();
    }
    else {

        bCalibrationOk = 0;
        CLEAR_STRING();
        LCDD_DrawString(strX, strY, strTitle, COLOR_BLACK);
        LCDD_DrawString(strX + strW, strY + 2*strH, "Error too big", COLOR_RED);
        LCDD_Flush_CurrentCanvas();
        TRACE_WARNING("X %u, Y %u; Diff %d, %d\n\r",
            (unsigned int)(measuredPoint.x), (unsigned int)(measuredPoint.y), (unsigned int)xDiff, (unsigned int)yDiff);
    }

    /* Slight delay */
    for (i = 0; i < DELAY_RESULT_DISPLAY; i++);
    LCDD_Flush_CurrentCanvas();
    return (xOk && yOk);
}

/**
 * Read calibrate data to buffer.
 * \param pBuffer  Data buffer.
 * \param size     Size of data buffer in bytes.
 */
void TSDCom_ReadCalibrateData(void *pBuffer, uint32_t size)
{
    uint8_t *pDest = (uint8_t *)pBuffer;
    
    memcpy(pDest, (void const *)&bCalibrationOk, sizeof(bCalibrationOk));
    pDest += sizeof(bCalibrationOk);
    memcpy(pDest, &xSlope, sizeof(xSlope));
    pDest += sizeof(xSlope);
    memcpy(pDest, &ySlope, sizeof(ySlope));
    pDest += sizeof(ySlope);
    memcpy(pDest, &calibrationPoints[0].data, sizeof(calibrationPoints[0].data));
    pDest += sizeof(calibrationPoints[0].data);
}

/**
 * Restore calibrate data with buffer data.
 * \param pBuffer  Data buffer.
 * \param size     Size of data buffer in bytes.
 */
void TSDCom_RestoreCalibrateData(void *pBuffer, uint32_t size)
{
    uint8_t *pSrc = (uint8_t *)pBuffer;

    memcpy((void *)&bCalibrationOk, pSrc, sizeof(bCalibrationOk));
    pSrc += sizeof(bCalibrationOk);
    memcpy(&xSlope, pSrc, sizeof(xSlope));
    pSrc += sizeof(xSlope);
    memcpy(&ySlope, pSrc, sizeof(ySlope));
    pSrc += sizeof(ySlope);
    memcpy(&calibrationPoints[0].data, pSrc, sizeof(calibrationPoints[0].data));
    pSrc += sizeof(calibrationPoints[0].data);
}

/**@}*/

