#include <ADuCM360.h>
#include <hal/devices.h>
#include <cmath>
#include <cassert>
#include <errno.h>
#include <cstring>
#include <hal/drivers/pwm.h>
#include <hal/drivers/flash.h>

static int position = 0;

static pwm_file pwm;

off_t pwm_lseek(off_t offset, int whence)
{
	switch (whence)
	{
	case SEEK_SET:
		if (offset > sizeof(pwm_file) || offset < 0)
		{
			errno = EINVAL;
			return -1;
		}
		else
		{
			position = offset;
		}
		break;
	case SEEK_CUR:
		if ((position + offset) > sizeof(pwm_file) || (position + offset) < 0)
		{
			errno = EINVAL;
			return -1;
		}
		else
		{
			position += offset;
		}
		break;
	case SEEK_END:
		if (offset > 0 || sizeof(pwm_file) + offset < 0)
		{
			errno = EINVAL;
			return -1;
		}
		else
		{
			position = sizeof(pwm_file) + offset;
		}
		break;
	default:
		errno = ENXIO;
		return -1;
		break;
	}

	return position;
}

/*                             UCLK              UCLK
 * PWM frequency F = ──────────────────────── = ──────
 *                    2^^PRE * 2 * (LEN + 1)      DIV
 * DIV = 2^^(PRE + 1) * (LEN + 1)
 * DIV / 2^^(PRE + 1) <= 0x10000
 *
 */
static void pwm_update(const pwm_file &file)
{
	pwm = file;

	float div = SystemCoreClock / file.freq;

	assert(div >= 8);

	int pre = 0;

	while (div / (0x2 << pre) > 0x10000)
	{
		++pre;
	}

	assert(pre <= 0x7);

	int len = lround(div / (0x2 << pre));

	PWMCON0_ENABLE_BBA = false;

	pADI_PWM->PWMCON0 = (pADI_PWM->PWMCON0 & ~PWMCON0_PRE_MSK) | (pre << 6);

	pADI_PWM->PWM0LEN = pADI_PWM->PWM1LEN = pADI_PWM->PWM2LEN = len - 1;

	pADI_PWM->PWM0COM2 = pADI_PWM->PWM0LEN >> 1; //pwm1 50% duty

	pADI_PWM->PWM1COM1 = lround(pADI_PWM->PWM0COM2 * file.setup / 100.f); //setup
	pADI_PWM->PWM1COM0 = pADI_PWM->PWM0COM2 - lround(pADI_PWM->PWM0COM2 * file.hold / 100.f); //hold

	pADI_PWM->PWM2COM1 = pADI_PWM->PWM0COM2 + pADI_PWM->PWM1COM1; //setup
	pADI_PWM->PWM2COM0 = pADI_PWM->PWM2LEN - (pADI_PWM->PWM0COM2 - pADI_PWM->PWM1COM0); //hold

	PWMCON0_PWM1INV_BBA = file.inv;

	PWMCON0_SYNC_BBA = true;

	PWMCON0_ENABLE_BBA = true;

	GP1TGL_TGL2_BBA = true;

	GP1TGL_TGL2_BBA = true;

	PWMCON0_ENABLE_BBA = false;

	PWMCON0_SYNC_BBA = false;

	PWMCON0_ENABLE_BBA = true;

	PWMCON0_LCOMP_BBA = true;
}

ssize_t pwm_read(void *buf, size_t count)
{
	if ((position + count) > sizeof(pwm_file))
	{
		count = sizeof(pwm_file) - position;
	}

	memcpy(buf, (char*) (&pwm) + position, count);

	position += count;

	return count;
}

ssize_t pwm_write(const void *buf, size_t count)
{
	pwm_file file = pwm;

	if ((position + count) > sizeof(pwm_file))
	{
		count = sizeof(pwm_file) - position;
	}

	memcpy((char*) (&file) + position, buf, count);

	position += count;

	if (file.freq <= configure_file.max_freq && file.freq >= configure_file.min_freq
	    && file.setup <= configure_file.max_setup && file.setup >= configure_file.min_setup
	    && file.hold <= configure_file.max_hold && file.hold >= configure_file.min_hold)
	{
		pwm_update(file);
	}

	return count;
}

void pwm_open(void)
{
	CLKDIS_DISPWMCLK_BBA = false;
	pADI_CLKCTL->CLKCON1 = (pADI_CLKCTL->CLKCON1 & ~CLKCON1_PWMCD_MSK) | CLKCON1_PWMCD_DIV1;

	pADI_GP1->GPCON = (pADI_GP1->GPCON & ~GP1CON_CON0_MSK) | GP1CON_CON0_PWMSYNC;
	pADI_GP1->GPCON = (pADI_GP1->GPCON & ~GP1CON_CON3_MSK) | GP1CON_CON3_PWM1;
	pADI_GP1->GPCON = (pADI_GP1->GPCON & ~GP1CON_CON4_MSK) | GP1CON_CON4_PWM2;
	pADI_GP1->GPCON = (pADI_GP1->GPCON & ~GP1CON_CON6_MSK) | GP1CON_CON6_PWM4;
	pADI_GP1->GPCON = (pADI_GP1->GPCON & ~GP1CON_CON2_MSK) | GP1CON_CON2_GPIO;

	GP1OCE_OCE0_BBA = false;
	GP1OCE_OCE3_BBA = false;
	GP1OCE_OCE4_BBA = false;
	GP1OCE_OCE6_BBA = false;
	GP1OCE_OCE2_BBA = false;

	GP1OEN_OEN0_BBA = true;
	GP1OEN_OEN3_BBA = true;
	GP1OEN_OEN4_BBA = true;
	GP1OEN_OEN6_BBA = true;
	GP1OEN_OEN2_BBA = true;

	GP1PUL_PUL0_BBA = true;
	GP1PUL_PUL3_BBA = true;
	GP1PUL_PUL4_BBA = true;
	GP1PUL_PUL6_BBA = true;
	GP1PUL_PUL2_BBA = true;

	PWMCON0_ENABLE_BBA = false;

	pADI_PWM->PWMCON0 = PWMCON0_SYNC_DIS | PWMCON0_PWM5INV_DIS | PWMCON0_PWM3INV_DIS | PWMCON0_PWM1INV_DIS
	                    | PWMCON0_PWMIEN_DIS | PWMCON0_ENA_DIS | PWMCON0_POINV_DIS | PWMCON0_HOFF_DIS | PWMCON0_LCOMP_DIS
	                    | PWMCON0_DIR_DIS | PWMCON0_MOD_DIS | PWMCON0_ENABLE_DIS;

	PWMCON1_TRIPEN_BBA = false;

	pADI_PWM->PWM0COM0 = 0x0; //pwm0 always high
	pADI_PWM->PWM0COM1 = 0xffff; //pwm0 always high
	pADI_PWM->PWM1COM2 = 0x0; //pwm3 not use
	pADI_PWM->PWM2COM2 = 0x0; //pwm5 not use

	flash_file * p_flash_file;

	decltype(flash_file::frequency) frequency;

	fseek(p_flash, (int) (&p_flash_file->frequency) - (int) (p_flash_file), SEEK_SET);
	fread(&frequency, sizeof(flash_file::frequency), 1, p_flash);

	pwm.freq = frequency;

	decltype(flash_file::setup) setup;

	fseek(p_flash, (int) (&p_flash_file->setup) - (int) (p_flash_file), SEEK_SET);
	fread(&setup, sizeof(flash_file::setup), 1, p_flash);

	pwm.setup = setup;

	decltype(flash_file::hold) hold;

	fseek(p_flash, (int) (&p_flash_file->hold) - (int) (p_flash_file), SEEK_SET);
	fread(&hold, sizeof(flash_file::hold), 1, p_flash);

	pwm.hold = hold;

	pwm.inv = false;

	pwm_update(pwm);
}

