#include <unistd.h>
#include <sys/types.h>
#include <sys/mman.h>
#include <stdio.h>
#include <stdlib.h>
#include <fcntl.h>
#include <assert.h>

#include "peekpoke.h"
#include "ep93xx_adc.h"

#define DATA_PAGE    0x12C00000
#define CALIB_LOC    2027          //location of calibration values
#define NUM_SAMPLES  5
#define NUM_CHANNELS 4

/* globals */
static unsigned long adc_page, syscon_page;
char *dr_page;

/*Calculate the adc value corresponding to 0V*/
//val1 is the ADC val coresponding to 0.833V
//val2 is the ADC val corresponging to 2.5V
int calcZeroVal(int val1, int val2)
{
    val2 += 0x10000;
    return (int)(val1 - (((val2 - val1) / (2.5 - 0.833)) * 0.833));
}


//return value of 1 indicates the board has no calibration values
//return value of 0 indicates the board has calibration values
int read_calibration(int buf[NUM_CHANNELS][2])
{
    int i, j, k = 0;
    unsigned short cal[NUM_CHANNELS * 2];
    // read 16 calibration bytes into buffer
    FILE *f = fopen("/etc/ADC-calibration.dat", "r");

    if (!f) { goto empty_calibration; }

    printf("Non-virgin board detected, evaluating stored "
           "calibration values\n");
    printf("Stored Calibration values [");

    if (fread(cal, NUM_CHANNELS * 4, 1, f) == 1)
    {
        fclose(f);

        for (j = 0; j < 2; j++)
            for (i = 0; i < NUM_CHANNELS; i++)
            {
                printf("0x%x", cal[k]);
                buf[i][j] = cal[k];
                k++;

                if (k < NUM_CHANNELS * 2)
                {
                    printf(", ");
                }

            }

        printf("]\n");
        return 1;
    }

empty_calibration:

    printf("/etc/ADC-calibration.dat not found or it's not readable\n");

    fclose(f);

    return 0;
}

void write_calibration(int cal[NUM_CHANNELS][2])
{
    unsigned short buf[16];
    int i, j, k = 0;
    FILE *f = fopen("/etc/ADC-calibration.dat", "w");

    //Convert 32 bit vals to 16 bit vals
    for (j = 0; j < 2; j++)
        for (i = 0; i < NUM_CHANNELS; i++)
        {
            buf[k] = (unsigned short)cal[i][j];
            k++;

        }

    if (!f) { goto unwrite_calibration; }

    if (fwrite(buf, NUM_CHANNELS * 4, 1, f) == 1) { return; }

unwrite_calibration:

    printf("Problem writing /etc/ADC-calibration.dat: %m\n");
}

static void erase_calibration()
{
    printf("Erasing calibration values...\n");
    unlink("/etc/ADC-calibration.dat");
}

int check_calibration(int cal[NUM_CHANNELS][2],
                      int stored_cal[NUM_CHANNELS][2], int state)
{
    double pcnt_diff;
    int i, j, erase_cal = 0;
    int failure = 0;

    if (state == 0) //no calibration values
    {
        printf("Virgin board detected...\n");

        for (j = 0; j < 2; j++)
        {

            for (i = 0; i < NUM_CHANNELS; i++)
            {

                if (j == 0)
                    pcnt_diff = (((double)abs(0xa000 -
                                              cal[i][j])) / 0xa000) * 100;
                else
                    pcnt_diff = (((double)abs(0x3300 -
                                              cal[i][j])) / 0x3300) * 100;

                if (pcnt_diff > 10)
                {
                    printf("Calculated calibration "
                           "values out of range...\n");

                    exit(-1);
                }

            }
        }

        write_calibration(cal);

    }
    else        //calibration values read
    {

        for (j = 0; j < 2; j++)
        {
            for (i = 0; i < NUM_CHANNELS; i++)
            {

                pcnt_diff = (((double)abs(stored_cal[i][j] -
                                          cal[i][j])) / stored_cal[i][j])
                            * 100;

                if (pcnt_diff > 0.25)
                {
                    if (!failure)
                    {
                        printf("Calibration values out"
                               "of range\n");
                        failure = 1;
                        erase_cal = 1;
                    }

                    printf("\tChannel %d: %3.3f%%\n", i
                           , pcnt_diff);
                }
            }
        }
    }

    if (erase_cal) { erase_calibration(); }

    if (failure) { return 0; }

    return 1;
}

void setDR(char *x, int n, int val)
{
    if (n < 0 || n > 8)
    {
        return;
    }

    x[0] = (x[0] & ~(1 << n)) | (val ? (1 << n) : 0);
}

void setD(char *x, int n, int val)
{
    if (n < 0 || n > 8)
    {
        return;
    }

    x[2] = (x[2] & ~(1 << n)) | (val ? (1 << n) : 0);
}

double get_volts(int val, int zero, int range)
{
    if (val <= 0x7000)
    {
        val = val + 0x10000;
    }

    val = val - zero;

    return ((double)val * 3.3) / range;
}

void calc_calibration(int calibration[NUM_CHANNELS][2],
                      int adc_result_1[NUM_CHANNELS][NUM_SAMPLES],
                      int adc_result_2[NUM_CHANNELS][NUM_SAMPLES])
{
    int i, j;

    /* zero out our calibration values */
    for (i = 0; i < NUM_CHANNELS; i++)
        for (j = 0; j < 2; j++)
        {
            calibration[i][j] = 0;
        }

    //convert 0.833V vals to 0V vals
    for (i = 0; i < NUM_CHANNELS; i++)
    {
        for (j = 0; j < NUM_SAMPLES; j++)
        {
            if (i % 2) //odd channels
                adc_result_1[i][j] =
                    calcZeroVal(adc_result_1[i][j],
                                adc_result_1[0][j]);
            else
                adc_result_2[i][j] =
                    calcZeroVal(adc_result_2[i][j],
                                adc_result_2[1][j]);
        }
    }

    //sum the readings
    for (i = 0; i < NUM_CHANNELS; i++)
    {
        for (j = 0; j < NUM_SAMPLES; j++)
        {
            if (i % 2 == 0)
            {
                //0.833 volt values
                calibration[i][0] = adc_result_2[i][j]
                                    + calibration[i][0];
                //2.5 volt values
                calibration[i][1] = adc_result_1[i][j]
                                    + calibration[i][1];
            }
            else
            {
                //0.833 volt values
                calibration[i][0] = adc_result_1[i][j]
                                    + calibration[i][0];
                //2.5 volt values
                calibration[i][1] = adc_result_2[i][j]
                                    + calibration[i][1];
            }
        }
    }

    printf("Calculated Calibration values [");

    for (j = 0; j < 2; j++)
    {
        for (i = 0; i < NUM_CHANNELS; i++)
        {
            calibration[i][j] = (calibration[i][j] / NUM_SAMPLES);

            printf("0x%x", calibration[i][j]);

            if ((i == NUM_CHANNELS - 1) && (j == 1))
            {
                printf("]\n");
            }
            else
            {
                printf(", ");
            }
        }
    }
}

/************************************************************************
 *DESCRIPTION: Read the EP93xx onboard ADC. Discard the first
 *two samples then save the next NUM_SAMPLES.
 ***********************************************************************/
static void read_7xxx_adc(int adc_result[NUM_CHANNELS][NUM_SAMPLES])
{
    int i, j, cur_ch;

    for (i = 0; i < NUM_CHANNELS; i++)
    {
        switch (i)
        {
        case 0:
            cur_ch = ADC_CH0;
            break;

        case 1:
            cur_ch = ADC_CH1;
            break;

        case 2:
            cur_ch = ADC_CH2;
            break;

        case 3:
            cur_ch = ADC_CH3;
            break;

        case 4:
            cur_ch = ADC_CH4;
            break;

        }

        //discard first two samples
        read_channel(adc_page, cur_ch);
        read_channel(adc_page, cur_ch);

        //read more samples
        for (j = 0; j < NUM_SAMPLES; j++)
        {
            hl_usleep(10000);
            adc_result[i][j] = read_channel(adc_page, cur_ch);
        }
    }
}

int test_ADC(int calibration[NUM_CHANNELS][2])
{
    int adc_result_1[NUM_CHANNELS][NUM_SAMPLES];
    int adc_result_2[NUM_CHANNELS][NUM_SAMPLES];
    int i, j, return_val;
    int failure = 0;
    double voltage;

    setD(dr_page, 0, 1);    //ADC1 = ADC3 = 0.833V
    setD(dr_page, 2, 1);    //ADC0 = ADC2 = 2.5V
    read_7xxx_adc(adc_result_1);

    setD(dr_page, 0, 0);    //ADC1 = ADC3 = 2.5V
    setD(dr_page, 2, 1);    //ADC0 = ADC2 = 0.833V
    read_7xxx_adc(adc_result_2);

    //verify results are within range
    for (i = 0; i < NUM_CHANNELS; i++)
    {
        for (j = 0; j < NUM_SAMPLES; j++)
        {
            //use the datasheet values
            voltage = get_volts(adc_result_1[i][j], 0x9E58, 0xC350);

            //even channels 2.5V(+-150mV)
            if (i % 2 == 0 &&
                    (voltage < 2.35 || voltage > 2.65))
            {
                if (!failure)
                {
                    failure = 1;
                    printf("EP93XX ADC out of range\n");
                }

                printf("\tChannel %d: %3.3fV"
                       "(expected 2.5V +- 150mV)\n", i, voltage);
                //odd channels 0.833(+-50mV)
            }
            else if (i % 2 == 1 &&
                     (voltage < 0.333 || voltage > 1.333))
            {
                if (!failure)
                {
                    failure = 1;
                    printf("EP93xx ADC out of range\n");
                }

                printf("\tChannel %d: %3.3fV"
                       "(expected 0.833V +- 50mV)\n", i, voltage);
            }


            //use the datasheet values
            voltage = get_volts(adc_result_2[i][j], 0x9E58, 0xC350);

            //odd channels 2.5V(+-150mV)
            if (i % 2 == 1 &&
                    (voltage < 2.35 || voltage > 2.65))
            {
                if (!failure)
                {
                    failure = 1;
                    printf("EP93XX ADC out of range\n");
                }

                printf("\tChannel %d: %3.3fV"
                       "(expected 2.5V +- 150mV)\n", i, voltage);
                //even channels 0.833(+-50mV)
            }
            else if (i % 2 == 0 &&
                     (voltage < 0.333 || voltage > 1.333))
            {
                if (!failure)
                {
                    failure = 1;
                    printf("EP93xx ADC out of range\n");
                }

                printf("\tChannel %d: %3.3fV"
                       "(expected 0.833V +- 50mV)\n", i, voltage);
            }
        }
    }

    calc_calibration(calibration, adc_result_1, adc_result_2);

    if (failure)
    {
        return_val = 0;
    }
    else { return_val = 1; }

    return return_val;
}


int main(void)
{
    int calibration[NUM_CHANNELS][2];
    int ret_val;
    int devmem = open("/dev/mem", O_RDWR | O_SYNC);
    assert(devmem != -1);

    dr_page = mmap(0, getpagesize(), PROT_READ | PROT_WRITE,
                   MAP_SHARED, devmem, DATA_PAGE);
    assert(&dr_page != MAP_FAILED);
    adc_page = (unsigned long)mmap(0, getpagesize(), PROT_READ | PROT_WRITE,
                                   MAP_SHARED, devmem, ADC_PAGE);
    assert(&adc_page != MAP_FAILED);
    syscon_page = (unsigned long)mmap(0, getpagesize(), PROT_READ | PROT_WRITE
                                      , MAP_SHARED, devmem, SYSCON_PAGE);
    assert(&syscon_page != MAP_FAILED);

    init_ADC(adc_page, syscon_page);
    setDR(dr_page, 0, 1);
    setDR(dr_page, 1, 0);
    setDR(dr_page, 2, 1);
    setDR(dr_page, 3, 0);

    if (test_ADC(calibration))
    {
        int state;
        int stored_calibration[NUM_CHANNELS][2];
        printf("ADC tested ok(data sheet values)\n");
        state = read_calibration(stored_calibration);

        if (check_calibration(calibration, stored_calibration, state))
        {
            ret_val = 0;
        }
        else
        {
            ret_val = 1;
        }

    }
    else
    {
        ret_val = 1;
    }

    close(devmem);

    return ret_val;
}
