/*
 * File:   neopixel.c
 * Author: Nicholas
 *
 * Created on 30 September 2025, 1:51 PM
 * 
 * Controls:
 * VR1 - adjust brightness in full bright light.
 * VR2 - adjust time between effects.
 * S1 - short press cycles to next enabled effect, long press enables automatic effect cycling (which is on initially by default)
 * S2 - short press cycles to next enabled palette, long press enables automatic palette cycling (which is on initially by default)
 * hold S2, then press and release S1 -> short press disables current effect, long press enables all effects (all on initially by default)
 * hold S1, then press and release S2 -> short press disables current palette, long press enables all palettes (all on initially by default)
 * S3 - short press sets minimum brightness to the currently setting of VR1, long press disables automatic brightness adjustment
 * 
 * The LDR range is self-calibrating; it needs to see darkness and bright light at least once to operate properly
 * 
 * Pin mappings:
 *  Analog pins:
 *      VR1: RA4
 *      VR2: RA5
 *      LDR: RC5
 *  Digital pins:
 *      LEDs: RA2
 *      S1: RC0
 *      S2: RC1
 *      S3: RC2
 */

/* Todo: * Test S3 functions */

#include <xc.h>
#include <string.h>
#include "config.h"
#include "effects.h"

unsigned char RGBBufs[2][80*3];
unsigned char* RGBBufPtr;
unsigned char* RGBBufEnd;

unsigned short ButtonStateChange[3];
unsigned char ButtonShortPress[3], ButtonLongPress[3], ButtonShortPressWithOtherButtonDown[3], ButtonLongPressWithOtherButtonDown[3], IgnoreOtherButtonPresses;

static unsigned short readTMR0() {
    unsigned char low  = TMR0L;
    unsigned char high = TMR0H;        // latch
    return ((unsigned short)high << 8) | low;
}

#define BUTTON_DEBOUNCE 5
#define BUTTON_LONGPRESS 250

void __interrupt() isr(void){
    if( PIR2bits.TMR4IF ) {
        SSP1BUF = *RGBBufPtr;
        if( ++RGBBufPtr == RGBBufEnd ) {
            PIE2bits.TMR4IE = 0;
            T3CONbits.ON = 1;
        }
        PIR2bits.TMR4IF = 0;
    } else {
        unsigned short T0 = readTMR0();

        if( IOCCFbits.IOCCF0 ) {
            IOCCFbits.IOCCF0 = 0;
            if( PORTCbits.RC0 && T0-ButtonStateChange[0] > BUTTON_DEBOUNCE ) {
                if( IgnoreOtherButtonPresses ) {
                    --IgnoreOtherButtonPresses;
                } else if( !PORTCbits.RC1 || !PORTCbits.RC2 ) {
                    IgnoreOtherButtonPresses = 1;
                    if( T0-ButtonStateChange[0] > BUTTON_LONGPRESS )
                        ++ButtonLongPressWithOtherButtonDown[0];
                    else
                        ++ButtonShortPressWithOtherButtonDown[0];
                } else {
                    if( T0-ButtonStateChange[0] > BUTTON_LONGPRESS )
                        ++ButtonLongPress[0];
                    else
                        ++ButtonShortPress[0];
                }
            }
            ButtonStateChange[0] = T0;
        }
        if( IOCCFbits.IOCCF1 ) {
            IOCCFbits.IOCCF1 = 0;
            if( PORTCbits.RC1 && T0-ButtonStateChange[1] > BUTTON_DEBOUNCE ) {
                if( IgnoreOtherButtonPresses ) {
                    --IgnoreOtherButtonPresses;
                } else if( !PORTCbits.RC0 || !PORTCbits.RC2 ) {
                    IgnoreOtherButtonPresses = 1;
                    if( T0-ButtonStateChange[1] > BUTTON_LONGPRESS )
                        ++ButtonLongPressWithOtherButtonDown[1];
                    else
                        ++ButtonShortPressWithOtherButtonDown[1];
                } else {
                    if( T0-ButtonStateChange[1] > BUTTON_LONGPRESS )
                        ++ButtonLongPress[1];
                    else
                        ++ButtonShortPress[1];
                }
            }
            ButtonStateChange[1] = T0;
        }
        if( IOCCFbits.IOCCF2 ) {
            if( PORTCbits.RC2 && T0-ButtonStateChange[2] > BUTTON_DEBOUNCE ) {
                if( IgnoreOtherButtonPresses ) {
                    --IgnoreOtherButtonPresses;
                } else if( !PORTCbits.RC0 || !PORTCbits.RC2 ) {
                    IgnoreOtherButtonPresses = 1;
                    if( T0-ButtonStateChange[2] > BUTTON_LONGPRESS )
                        ++ButtonLongPressWithOtherButtonDown[2];
                    else
                        ++ButtonShortPressWithOtherButtonDown[2];
                } else {
                    if( T0-ButtonStateChange[2] > BUTTON_LONGPRESS )
                        ++ButtonLongPress[2];
                    else
                        ++ButtonShortPress[2];
                }
            }
            IOCCFbits.IOCCF2 = 0;
            ButtonStateChange[2] = T0;
        }
    }
}

static unsigned char IsButtonHeldDown(unsigned char mask, unsigned short loops) {
    unsigned short timer = loops;
    while(timer--) {
        if( PORTC & mask )
            return 0;
    }
    timer = loops;
    while(timer--) {
        if( !(PORTC & mask) )
            timer = loops;
    }
    return 1;
}

static void StartTransmission(unsigned char* start, unsigned char* end) {
    while( PIE2bits.TMR4IE || !PIR2bits.TMR3IF )
        ;
    RGBBufPtr = start;
    RGBBufEnd = end;
    TMR2 = 0;
    TMR4 = 0;
    PIE2bits.TMR4IE = 1;
    PIR2bits.TMR3IF = 0;
    TMR3H = 248; // this sets up TMR3 to overflow after 2240 clocks which, at 8MHz, is 280us
    TMR3L = 64;
}

#define VR1_ADC_CHAN 0x04
#define VR2_ADC_CHAN 0x05
#define LDR_ADC_CHAN 0x15
static void ADCInit() {
    ADCON0bits.CS = 1;
    ADCON0bits.FM = 1;
    ADCON0bits.IC = 0;
    ADCON0bits.ON = 1;
}

static unsigned short ReadADC(unsigned char c){   //get result from selected channel
    unsigned short result;
    ADPCH=c;
    ADCON0bits.ADGO=1;              //start a reading
    while(ADCON0bits.ADGO){}        //wait till done
    result = ADRESL;                // read low first
    return result | ((unsigned short)ADRESH) << 8;
}

static void IOInit() {
    ANSELA = 0x00;
    ANSELC = 0x00;

    // Analog inputs: RA4, RA5, RC5
    ANSELAbits.ANSA1 = 0;
    ANSELAbits.ANSA4 = 1;
    ANSELAbits.ANSA5 = 1;
    ANSELCbits.ANSC0 = 0;
    ANSELCbits.ANSC1 = 0;
    ANSELCbits.ANSC2 = 0;
    ANSELCbits.ANSC5 = 1;

    // Directions
    TRISAbits.TRISA4 = 1;   // RA4 input (analog)
    TRISAbits.TRISA5 = 1;   // RA5 input (analog)
    TRISAbits.TRISA2 = 0;   // RA2 output (digital)

    TRISCbits.TRISC5 = 1;   // RC5 input (analog)
    TRISCbits.TRISC0 = 1;   // RC0 input (digital)
    TRISCbits.TRISC1 = 1;   // RC1 input (digital)
    TRISCbits.TRISC2 = 1;   // RC2 input (digital)

    // Weak pull-ups on RC0..RC2 (only effective when pin is digital input)
    WPUCbits.WPUC0 = 1;
    WPUCbits.WPUC1 = 1;
    WPUCbits.WPUC2 = 1;

    // Use Schmitt trigger threshold on RC0..RC2
    INLVLCbits.INLVLC0 = 1;
    INLVLCbits.INLVLC1 = 1;
    INLVLCbits.INLVLC2 = 1;

    // Make sure no pull-ups on the analog pins
    WPUAbits.WPUA4 = 0;
    WPUAbits.WPUA5 = 0;
    WPUCbits.WPUC5 = 0;
    
    // Set up pin change interrupts for buttons (RC0-RC2)
    PIE0bits.IOCIE = 1;
    IOCCPbits.IOCCP0 = 1;
    IOCCPbits.IOCCP1 = 1;
    IOCCPbits.IOCCP2 = 1;
    IOCCNbits.IOCCN0 = 1;
    IOCCNbits.IOCCN1 = 1;
    IOCCNbits.IOCCN2 = 1;
    IOCCFbits.IOCCF0 = 0;
    IOCCFbits.IOCCF1 = 0;
    IOCCFbits.IOCCF2 = 0;
}

static void ScaleBuffer(unsigned char* start, unsigned char* end, unsigned char scale) {
    while( start < end ) {
        *start = (unsigned short)*start * scale / 256;
        ++start;
    }
}

#define AUTO_CYCLE_EFFECTS 1
#define AUTO_CYCLE_PALETTE 2

__eeprom unsigned char AutoCycling = AUTO_CYCLE_EFFECTS | AUTO_CYCLE_PALETTE;
__eeprom unsigned short DisabledEffects = 0, MinLDRValueSeen = 4095, MaxLDRValueSeen = 0;
__eeprom unsigned char DisabledPalettes = 0, MinBrightness = 32, AutoBrightnessControl = 1;
__eeprom unsigned char InitEffect = 1, InitPalette = 1;
unsigned char CurrentEffect, CurrentPalette;
static const unsigned char MaxEffect = 12, MaxPalette = 4;

static void GoToNextEffect() {
    ResetEffect(readTMR0());
    do {
        if( ++CurrentEffect > MaxEffect )
            CurrentEffect = 1;
    } while( DisabledEffects & (1<<CurrentEffect) );
}

static void GoToNextPalette() {
    do {
        if( ++CurrentPalette > MaxPalette )
            CurrentPalette = 1;
    } while( DisabledPalettes & (1<<CurrentPalette) );
}

void main(void) {
    unsigned short VR1, VR2, LDR, LastLDR;
    unsigned short NextEffectTimer = 0;

    CurrentEffect = InitEffect;
    CurrentPalette = InitPalette;

    IOInit();
    ADCInit();

    if( IsButtonHeldDown(1<<2, 1000) ) {
        // S3 held down at boot -> reset EEPROM config
        AutoCycling = AUTO_CYCLE_EFFECTS | AUTO_CYCLE_PALETTE;
        DisabledEffects = 0; MinLDRValueSeen = 4095; MaxLDRValueSeen = 0;
        DisabledPalettes = 0; MinBrightness = 32; AutoBrightnessControl = 1;
        CurrentEffect = InitEffect = 1; CurrentPalette = InitPalette = 1;
        IOCCFbits.IOCCF2 = 0;
    } else if( IsButtonHeldDown(1<<1, 1000) ) {
        // S2 held down at boot -> reset brightness
        MinBrightness = 32; AutoBrightnessControl = 1;
        IOCCFbits.IOCCF1 = 0;
    } else if( IsButtonHeldDown(1<<0, 1000) ) {
        // S1 held down at boot -> reset LDR
        MinLDRValueSeen = 4095; MaxLDRValueSeen = 0;
        IOCCFbits.IOCCF0 = 0;
    }

    // Run Timer 0 off LFINTOSC giving a count of 32768 per second (rolls over at 0.5Hz)
    T0CON1bits.T0CS = 4; // LFINTOSC
    T0CON1bits.CKPS = 6;
    T0CON0bits.MD16 = 1;
    T0CON0bits.T0EN = 1;
    PIR0bits.TMR0IF = 0;

    // set up Timer 2 & CCP1 to generate PWM
    T2CLKCONbits.CS = 1;
    T2CONbits.OUTPS = 0;
    T2CONbits.CKPS = 0;
    PR2 = 4;
    CCPR1 = 8;
    CCP1CONbits.MODE = 15;
    CCP1CONbits.FMT = 0;
    CCP1CONbits.EN = 1;

    CCPTMRS0bits.C1TSEL = 1;
    T2CONbits.ON = 1;

    TRISAbits.TRISA2 = 0;
    ANSELAbits.ANSA2 = 0;

    // set up MSSP for SPI
    SSP1CON1bits.SSPM = 3;
    SSP1ADD = 9;
    SSP1CON1bits.CKP = 0;
    SSP1CON1bits.SSPEN = 1;

    // set up CLC1 (any CLC will work as long as it can drive the selected pin)
    CLCSELECT=0;    //CLC1
    CLCnCONbits.MODE = 0; // AND -> OR
    CLCnPOL = 0;
    CLCnSEL0 = 42; // SCK1
    CLCnSEL1 = 20; // CCP1 output
    CLCnSEL2 = 42; // SCK1
    CLCnSEL3 = 41; // SDO1
    CLCnGLS0 = 2;
    CLCnGLS1 = 8;
    CLCnGLS2 = 32;
    CLCnGLS3 = 128;
    CLCnCONbits.EN = 1;
    unsigned char intcon=INTCON;
    GIE=0;
    PPSLOCK = 0x55; //Required sequence
    PPSLOCK = 0xAA; //Required sequence
    PPSLOCKbits.PPSLOCKED = 0; //clear PPSLOCKED bit
    RA2PPS = 0x1; // CLC1OUT
    PPSLOCK = 0x55; //Required sequence
    PPSLOCK = 0xAA; //Required sequence
    PPSLOCKbits.PPSLOCKED = 1; //set PPSLOCKED bit
    INTCON=intcon;

    // Timer 3 will be used to generate the 280us reset pulse after updating the WS2812BV5 LEDs
    T3CLK = 1; // Fosc / 4 = 8MHz
    T3CON = 0; // leave it off for now, it'll be switched on later
    PIR2bits.TMR3IF = 1;

    // Timer 4 is used as the millisecond counter for effects
    T4CLKCONbits.CS = 1;
    T4CONbits.OUTPS = 0;
    T4CONbits.CKPS = 0;
    PR4 = 82;
    T4CONbits.ON = 1;
    PIR2bits.TMR4IF = 0;
    INTCONbits.GIE = 1;
    INTCONbits.PEIE = 1;

    unsigned char which_buf = 0, last_tmr0h = 0;//, update_ldr = 0;
    ResetEffect(readTMR0());
    LDR = LastLDR = 0;
    while(1) {
        unsigned char brightness;

        VR1 = ReadADC(VR1_ADC_CHAN);
        VR2 = ReadADC(VR2_ADC_CHAN);
        LDR = (LastLDR + ReadADC(LDR_ADC_CHAN))>>1;
        LastLDR = LDR;
        if( LDR < MinLDRValueSeen )
            MinLDRValueSeen = LDR;
        if( LDR > MaxLDRValueSeen )
            MaxLDRValueSeen = LDR;
        brightness = (unsigned char)(VR1 / 16);
        if( MaxLDRValueSeen > MinLDRValueSeen && AutoBrightnessControl ) {
//            unsigned char ldrval = MinBrightness + (unsigned char)((unsigned long)(LDR - MinLDRValueSeen) * (255 - MinBrightness) / MaxLDRValueSeen);
//            brightness = (unsigned short)brightness * ldrval / 256;
            if( brightness < MinBrightness ) {
                brightness = MinBrightness;
            } else {
                unsigned char ldrval = (unsigned char)((unsigned long)(LDR - MinLDRValueSeen) * 255 / (MaxLDRValueSeen - MinLDRValueSeen));
                unsigned char MaxBrightness = brightness;
                brightness = MinBrightness + (unsigned short)(MaxBrightness - MinBrightness) * ldrval / 256;
            }
        }

        TMR0L;
        if( TMR0H != last_tmr0h ) {
          last_tmr0h = TMR0H;
          if( AutoCycling && ++NextEffectTimer >= VR2/4 ) {
            NextEffectTimer = 0;
            if( AutoCycling & AUTO_CYCLE_EFFECTS ) {
              GoToNextEffect();
              // no need to worry about repeating the same palette with a new effect
              if( AutoCycling & AUTO_CYCLE_PALETTE ) {
                do {
                  CurrentPalette = (unsigned char)(random(MaxPalette)+1);
                } while( DisabledPalettes & (1<<CurrentPalette) );
              }
            } else {
              GoToNextPalette();
            }
          }
        }

        if( ButtonShortPress[0] ) {
            AutoCycling &= ~AUTO_CYCLE_EFFECTS;
            GoToNextEffect();
            InitEffect = CurrentEffect;
            --ButtonShortPress[0];
        }
        if( ButtonLongPress[0] ) {
            AutoCycling |= AUTO_CYCLE_EFFECTS;
            --ButtonLongPress[0];
        }
        if( ButtonShortPressWithOtherButtonDown[0] ) {
            if( (DisabledEffects | (1<<CurrentEffect)) != ((1<<1)|(1<<2)|(1<<3)|(1<<4)|(1<<5)|(1<<6)|(1<<7)|(1<<8)|(1<<9)|(1<<10)|(1<<11)|(1<<12)) ) {
                DisabledEffects |= (1<<CurrentEffect);
                GoToNextEffect();
            }
            --ButtonShortPressWithOtherButtonDown[0];
        }
        if( ButtonLongPressWithOtherButtonDown[0] ) {
            DisabledEffects = 0;
            --ButtonLongPressWithOtherButtonDown[0];
        }
        if( ButtonShortPress[1] ) {
            AutoCycling &= ~AUTO_CYCLE_PALETTE;
            GoToNextPalette();
            InitPalette = CurrentPalette;
            --ButtonShortPress[1];
        }
        if( ButtonLongPress[1] ) {
            AutoCycling |= AUTO_CYCLE_PALETTE;
            --ButtonLongPress[1];
        }
        if( ButtonShortPressWithOtherButtonDown[1] ) {
            if( (DisabledPalettes | (1<<CurrentPalette)) != ((1<<1)|(1<<2)|(1<<3)|(1<<4)) ) {
                DisabledPalettes |= (1<<CurrentPalette);
                GoToNextPalette();
            }
            --ButtonShortPressWithOtherButtonDown[1];
        }
        if( ButtonLongPressWithOtherButtonDown[1] ) {
            DisabledPalettes = 0;
            --ButtonLongPressWithOtherButtonDown[1];
        }
        if( ButtonShortPress[2] ) {
            MinBrightness = brightness/(unsigned char)(VR1 / 16);
            if( MinBrightness < 4 )
                MinBrightness = 4;
            AutoBrightnessControl = 1;
            --ButtonShortPress[2];
        }
        if( ButtonLongPress[2] ) {
            AutoBrightnessControl = !AutoBrightnessControl;
            --ButtonLongPress[2];
        }
        effect((rgb*)RGBBufs[which_buf], CurrentEffect, CurrentPalette, readTMR0());
        ScaleBuffer(RGBBufs[which_buf], RGBBufs[which_buf]+sizeof(RGBBufs[which_buf]), brightness);
        StartTransmission(RGBBufs[which_buf], RGBBufs[which_buf]+sizeof(RGBBufs[which_buf]));
        which_buf ^= 1;
    }
}