
#define _SUPPRESS_PLIB_WARNING
#define _DISABLE_OPENADC10_CONFIGPORT_WARNING
#include <plib.h>
#include <xc.h>
#include <math.h>
#include <string.h>
#include "gpsserial.h"
#include "adc.h"
#include "alarms.h"
#include "buttons.h"
#include "Infrared.h"
#include "sleep.h"
#include "menu.h"
#include "mainmenu.h"
#include "7seg.h"
#include "Timezone/tz_coords.h"
#include "Timezone/tzmaths.h"

/* Notes & errata
   1) The menu item for calibrating the 32kHz crystal was not described in the text. Its value is between -512 and +511. Lower values make the clock run slower.
   3) The article mentions that either pushbutton or the escape button on the remote give a 5 minuute snooze when the alarm goes off, but neglects to mention that a long (1s+) press of either button, or a second press of the escape key will cancel the alarm altogether.
   4) Alarm duration has been extended to up to 15 minutes with a default of 10 seconds.
   5) While the date is showing, pressing the date button on the remote or either button on the unit will switch to showing the day of week (Sun-Sat) before switching back to the time display.
   6) A new options menu item, "GPSLCK", has been added. If set to "IGNORE", the unit will use GPS time data even if the module does not report a proper fix. This may be good enough for some indoor applications where a reliable fix is not possible.
   7) A new brightness menu item, "CUR RD", shows the minimum/current/maximum raw LDR values in 8-bit hexadecimal notation. The fourth digit decimal point lights if a new minimum or maximum has been recorded and goes out once the new data has been written to flash memory.  */

/*
 *  PIC32MX170F256B-I/P pin mapping:
 *
 *  1 MCLR
 *  2 RA0/AN0  : 3.3V -> 10k -> pin 2 -> 47k LDR -> GND
 *  3 RA1/AN1  : 12V -> 100k -> pin 3 -> 10k -> GND
 *  4 RB0/AN2  : digit select bit 1
 *  5 RB1/AN3  : infrared receiver output (enable pull-up?)
 *  6 RB2/AN4  : digit select bit 0
 *  7 RB3/AN5  : decimal point cathode drive (active high)
 *  8 VSS
 *  9 RA2/CLKI : GPS 1PPS
 * 10 RA3/CLKO : piezo buzzer drive (active high)
 * 11 RB4/SOSCI: 32kHz xtal
 * 12 RB5/SOSCO: 32kHz xtal
 * 13 VDD
 * 14 RB5/PGED3: PGED / S1 (short to GND)
 * 15 RB6/PGEC3: PGEC / S2 (short to GND)
 * 16 RB7      : 7-segment G cathode drive (active high)
 * 17 RB8      : 7-segment F cathode drive (active high)
 * 18 RB9      : 7-segment E cathode drive (active high)
 * 19 VSS
 * 20 VCAP
 * 21 RB10     : 7-segment D cathode drive (active high)
 * 22 RB11     : 7-segment A cathode drive (active high)
 * 23 RB12/AN12: 7-segment B cathode drive (active high)
 * 24 RB13/AN11: GPS serial data
 * 25 RB14/AN10: 7-segment C cathode drive (active high)
 * 26 RB15/AN9 : digit select bit 2
 * 27 AVSS
 * 28 AVDD
 *
 * Timers used: T1 for timer/stopwatch, T2/3 for button sensing, T4 for IR receiver and T5 for 7-segment display multiplexing
 */

// DEVCFG3
#pragma config PMDL1WAY = OFF           // Peripheral Module Disable Configuration (Allow only one reconfiguration)
#pragma config IOL1WAY = ON             // Peripheral Pin Select Configuration (Allow only one reconfiguration)

// DEVCFG2
#pragma config FPLLIDIV = DIV_2
#pragma config FPLLMUL = MUL_24
#pragma config FPLLODIV = DIV_4

// DEVCFG1
#pragma config FNOSC = FRC              // Oscillator Selection Bits (Fast RC Osc with PLL)
#pragma config FSOSCEN = ON             // Secondary Oscillator Enable (Enabled)
#pragma config IESO = OFF               // Internal/External Switch Over (Diasbled)
#pragma config POSCMOD = OFF            // Primary Oscillator Configuration (External clock/Primary osc disabled)
#pragma config OSCIOFNC = OFF           // CLKO Output Signal Active on the OSCO Pin (Disabled)
#pragma config FPBDIV = DIV_1           // Peripheral Clock Divisor (Pb_Clk is Sys_Clk)
#pragma config FCKSM = CSECMD           // Clock Switching and Monitor Selection (Clock Switch Enabled, FSCM Disabled)
#pragma config WDTPS = PS1024           // Watchdog Timer Postscaler (1:1024, ~1s)
#pragma config WINDIS = OFF             // Watchdog Timer Window Enable (Watchdog Timer is in Non-Window Mode)
#pragma config FWDTEN = OFF             // Watchdog Timer Enable (WDT Disabled (SWDTEN Bit Controls))
//#pragma config FWDTWINSZ = WINSZ_25     // Watchdog Timer Window Size (Window Size is 25%)

// DEVCFG0
#pragma config DEBUG = OFF              // Background Debugger Enable (Debugger is Enabled)
#pragma config JTAGEN = OFF             // JTAG Enable (JTAG Port Disabled)
#pragma config ICESEL = ICS_PGx3        // ICE/ICD Comm Channel Select (Communicate on PGEC2/PGED2)
#pragma config PWP = OFF                // Program Flash Write Protect (Disable)
#pragma config BWP = OFF                // Boot Flash Write Protect bit (Protection Disabled)
#pragma config CP = OFF                 // Code Protect (Protection Disabled)

#define SYS_FREQ (8000000L)

static int date_timer;
static unsigned long date_time;

volatile unsigned char timer_running;
static unsigned char gps_spinner_progress, gps_1pps_last_status;
static unsigned short gps_1pps_pulse_stretch;
static volatile unsigned long timer_counter, gps_spinner_timel;
static unsigned long timer_counter_lap, timer_counter_lastlap, timer_target;
static unsigned short timer_counter_lap_sub, timer_counter_lastlap_sub;
void __ISR(_TIMER_1_VECTOR,IPL2AUTO) __T1Interrupt(void) {
  if( timer_running && timer_counter < 86400 * 1000 && (!timer_target || timer_counter < timer_target) )
    ++timer_counter;
  IFS0bits.T1IF = 0;
}

union {
    struct {
        unsigned short max, min;
    } vals;
    unsigned int val;
} LDR;
unsigned short LDR_minmax_updated;
static unsigned short LDR_min_counter, LDR_max_counter, LDR_pending_min, LDR_pending_max;
#define LDR_EEPROM_ADDRESS 169

rtccTime time;
rtccDate date;
unsigned char time_set, date_set;
static unsigned char display_off, timer_alarm_off;
static int ir_timer, ir_code, ir_valid_repeat;
static unsigned long ir_time;

__attribute__((mips16)) static void setup_IOs() {
  // RA3 = piezo buzzer
  LATA = 0;
  TRISAbits.TRISA3 = 0;

  setup_7seg_IOs();
  buttons_init();
}

__attribute__((mips16)) static void setup_TMR1() {
  T1CONbits.TCS = 1;
  T1CONbits.TGATE = 1;
  T1CONbits.TCKPS = 0;
  PR1 = 32768;
  IFS0bits.T1IF = 0;
  IPC1bits.T1IP = 2;
  IEC0bits.T1IE = 1;
  T1CONbits.TON = 1;
}

__attribute__((mips16)) static void teardown_TMR1() {
  IEC0bits.T1IE = 0;
  IFS0bits.T1IF = 0;
  T1CON = 0;
}

__attribute__((mips16)) static void save_state_and_reduce_power_consumption(serial_state* pState) {
  IEC0bits.T1IE = 0;
  // turn off anode drive
  LATB |= (1<<0)|(1<<2)|(1<<15);
  // turn off cathode drive
  LATB &= ~( (1<<3)|(1<<7)|(1<<8)|(1<<9)|(1<<10)|(1<<11)|(1<<12)|(1<<14) );
  // turn off piezo buzzer
  LATA &= ~( (1<<3) );

  save_serial_state_and_close(pState);
  Infrared_Teardown();
  teardown_ADC();
  AD1CON1bits.ON = 0;
  teardown_TMR1();
  teardown_7seg_TMR();
  teardown_7seg_IOs();
  buttons_teardown();
}
__attribute__((mips16)) static void restore_state(const serial_state* pState) {
  IFS0bits.T1IF = 0;
  IEC0bits.T1IE = 1;
  T1CONbits.TON = 1;
  restore_serial_state(pState);
  Infrared_Setup();
  setup_TMR1();
  setup_7seg_IOs();
  setup_7seg_TMR();
  setup_ADC();
  buttons_init();
}

menuitem timer_set_menu = { "SETTIM", alarm_time, { .b = { .step = 1 } } };
menustate timer_set_state;

__attribute__((mips16)) static void delay_100ms() {
  IEC0bits.T1IE = 0;
  PR1 = 32768/10;
  TMR1 = 0;
  IFS0bits.T1IF = 0;
  while( !IFS0bits.T1IF )
    ;
  PR1 = 32768;
  IFS0bits.T1IF = 0;
  IEC0bits.T1IE = 1;
}
__attribute__((mips16)) static void delay_500ms() {
  IEC0bits.T1IE = 0;
  PR1 = 32768/2;
  TMR1 = 0;
  IFS0bits.T1IF = 0;
  while( !IFS0bits.T1IF )
    ;
  PR1 = 32768;
  IFS0bits.T1IF = 0;
  IEC0bits.T1IE = 1;
}

signed long tzinfo[sizeof(timezones)/sizeof(*timezones)];
daylight_savings_info dstinfo[sizeof(daylight_savings)/sizeof(*daylight_savings)];
__attribute__((mips16)) static void load_dst_override_from_flash(int first_address) {
    int i;
    memcpy(tzinfo, timezones, sizeof(tzinfo));
    memcpy(dstinfo, daylight_savings, sizeof(dstinfo));

    for( i = 0; i < sizeof(timezones)/sizeof(*timezones); ++i )
        DataEERead((unsigned int *)(tzinfo + i), first_address + i);

    for( i = 0; i < sizeof(dstinfo)/sizeof(*dstinfo); ++i ) {
        unsigned int temp;
        if( DataEERead(&temp, first_address + i + sizeof(timezones)/sizeof(*timezones)) == 0 )
            unpack_dst_info(&dstinfo[i], temp);
    }
}

clockmode curmode = show_time;
static clockmode lastmode;
static menustate curmenustate;
static int set_ir_codes_progress, set_ir_codes_timer, set_ir_codes_time;
static unsigned char ir_flash;
__attribute__((mips16)) void go_to_time_display() {
    switch(curmode) {
        case show_menu:
            while( menu_handle_keypress(mainmenu, &curmenustate, IRCODE_ESCAPE, 1, &mainmenu_callback) )
                ;
            menu_save_settings_to_flash(mainmenu, 1);
            break;
        case show_date:
        case show_dow:
            date_timer = 0;
            break;
    }
    curmode = show_time;
}

#ifdef SHOW_UTC_FEATURE
unsigned char show_UTC;
#endif
__attribute__((mips16)) static void handle_ir_button_press(int ir_code, unsigned char emulated) {
    if( ir_code == IRCODE_DISPON && curmode != set_ir_codes ) {
        display_off = !display_off;
    } else if( (ir_code&~IR_REPEAT) == IRCODE_BRIUP ) {
        DISPLAY_BRIGHTNESS += 3;
        if( DISPLAY_BRIGHTNESS > 100 )
            DISPLAY_BRIGHTNESS = 100;
    } else if( (ir_code&~IR_REPEAT) == IRCODE_BRIDN ) {
        DISPLAY_BRIGHTNESS -= 3;
        if( DISPLAY_BRIGHTNESS < 3 )
            DISPLAY_BRIGHTNESS = 3;
    } else {
        display_off = 0;
        if( ir_code == IRCODE_ALA_ON && curmode != set_timer && (curmode != show_menu || menu_currentitem_handles_alarm_on_key(mainmenu, &curmenustate)) ) {
            toggle_global_alarm_flag();
        } else switch(curmode) {
            case show_menu:
                if( !menu_handle_keypress(mainmenu, &curmenustate, ir_code, emulated, &mainmenu_callback) ) {
                    curmode = show_time;
                    menu_save_settings_to_flash(mainmenu, 1);
                }
                ir_timer = 0;
                break;
            case show_time:
                if( ir_code == IRCODE_SELECT ) {
                    mainmenu_set_mode(tz_override != -1 ? tz_override : timezone_found, tzinfo, dstinfo);
                    curmode = show_menu;
                } else if( ir_code == IRCODE_CNTDN ) {
                    curmode = set_timer;
                    lastmode = show_timer_down;
                    timer_set_state.sub[1] = 0;
                    timer_set_state.level = 1;
                } else if( ir_code == IRCODE_CNTUP ) {
                    timer_counter = 0;
                    timer_counter_lastlap = 0;
                    timer_counter_lastlap_sub = 0;
                    timer_running = 1;
                    timer_target = 0;
                    curmode = lastmode = show_timer_up;
                } else if( ir_code == IRCODE_DATE ) {
                    date_timer = 5;
                    date_time = RtccGetTime();
                    curmode = show_date;
#ifdef SHOW_UTC_FEATURE
                } else if( ir_code == IRCODE_SHOUTC ) {
                    show_UTC ^= 1;
#endif
                } else if( alarm_timer && alarm_is_daily_type && ir_code == IRCODE_ESCAPE ) {
                    if( alarm_is_snoozed() )
                        alarm_cancel();
                    else
                        alarm_snooze(&date);
                }
                break;
            case show_date:
                if( ir_code == IRCODE_SELECT ) {
                    mainmenu_set_mode(tz_override != -1 ? tz_override : timezone_found, tzinfo, dstinfo);
                    curmode = show_menu;
                } else if( ir_code == IRCODE_DATE ) {
                    curmode = show_dow;
                    date_timer = 5;
                } else if( ir_code == IRCODE_ESCAPE ) {
                    date_timer = 0;
                    curmode = show_time;
                }
                break;
            case show_dow:
                if( ir_code == IRCODE_SELECT ) {
                    mainmenu_set_mode(tz_override != -1 ? tz_override : timezone_found, tzinfo, dstinfo);
                    curmode = show_menu;
                } else if( ir_code == IRCODE_ESCAPE || ir_code == IRCODE_DATE ) {
                    date_timer = 0;
                    curmode = show_time;
                }
                break;
            case show_ir_code:
                if( ir_code == (IRCODE_ESCAPE|IR_REPEAT) ) {
                    curmode = show_menu;
                }
                break;
            case set_timer:
                if( ir_code == IRCODE_ALA_ON ) {
                    timer_alarm_off = !timer_alarm_off;
                } else if( !menu_handle_keypress(&timer_set_menu, &timer_set_state, ir_code, emulated, 0) || timer_set_state.level == 0 ) {
                    curmode = show_time;
                    if( ir_code != IRCODE_ESCAPE ) {
                        int temp, temp2;
                        temp = timer_set_menu.data.b.c.val / 100;
                        timer_target = (timer_set_menu.data.b.c.val - temp * 100);
                        if( timer_target > 59 )
                            timer_target = 59;
                        temp2 = temp / 100;
                        temp -= temp2 * 100;
                        if( temp > 59 )
                            timer_target += 59 * 60;
                        else
                            timer_target += temp*60;
                        timer_target += temp2 * 3600;

                        timer_counter = 0;
                        timer_counter_lastlap = 0;
                        timer_counter_lastlap_sub = 0;
                        timer_running = 1;
                        curmode = lastmode;
                    }
                }
                break;
            case show_timer_up:
            case show_timer_down:
            case show_timer_lap:
                if( (ir_code&~IR_REPEAT) == IRCODE_TI_ADD ) {
                    __builtin_disable_interrupts();
                    if( lastmode == show_timer_down ) {
                        if( timer_counter >= 60 )
                            timer_counter -= 60;
                    } else {
                        if( timer_counter < 1000 * 86400 - 60 )
                            timer_counter += 60;
                    }
                    __builtin_enable_interrupts();
                } else if( (ir_code&~IR_REPEAT) == IRCODE_TI_SUB ) {
                    __builtin_disable_interrupts();
                    if( lastmode == show_timer_down ) {
                        if( timer_counter < 1000 * 86400 - 60 && timer_counter < timer_target - 60 )
                            timer_counter += 60;
                    } else {
                        if( timer_counter >= 60 )
                            timer_counter -= 60;
                    }
                    __builtin_enable_interrupts();
                } else if( ir_code == IRCODE_TI_PAU ) {
                    T1CONbits.ON = 0;
                    curmode = lastmode;
                    break;
                } else if( ir_code == IRCODE_TI_RES ) {
                    T1CONbits.ON = 1;
                    curmode = lastmode;
                    break;
                } else if( ir_code == IRCODE_ESCAPE ) {
                    timer_running = 0;
                    alarm_cancel();
                    curmode = show_time;
                    T1CONbits.ON = 1;
                } else if( ir_code == IRCODE_TI_LAP ) {
                    timer_counter_lastlap = timer_counter_lap;
                    timer_counter_lastlap_sub = timer_counter_lap_sub;
                    timer_counter_lap = timer_counter;
                    timer_counter_lap_sub = TMR1;
                    if( TMR1 < 8192 && timer_counter > timer_counter_lap )
                        ++timer_counter_lap;
                    curmode = show_timer_lap;
                } else if( curmode == show_timer_up && timer_target == 0 && timer_counter < 5 ) {
                    int i;
                    for( i = 0; i < 10; ++i )
                        if( ir_code == ir_codes_submenu[i].data.b.c.val )
                            break;
                    if( i < 10 || ir_code == IRCODE_UP || ir_code == IRCODE_DOWN || ir_code == IRCODE_LEFT || ir_code == IRCODE_RIGHT ) {
                        timer_running = 0;
                        lastmode = show_timer_up;
                        curmode = set_timer;
                        timer_set_state.sub[1] = 0;
                        timer_set_state.level = 1;
                        handle_ir_button_press(ir_code, emulated);
                    }
                }
                break;
        }
    }
}

void __pic32_software_reset() {
    SoftReset();
}

__attribute__((mips16)) static void display_ir_code(char* new_display, unsigned long ir_code) {
    if( !(ir_code&(IR_RC5|IR_NEC)) ) {
        memcpy(new_display, " NONE ", 6);
    } else {
        new_display[0] = (ir_code&IR_RC5) ? 'P' : ( (ir_code&IR_NEC) ? '^' : ' ' ); // N
        new_display[1] = (ir_code&IR_RC5) ? 'H' : ( (ir_code&IR_NEC) ? 'E' : ' ' );
        new_display[2] = hexdig((ir_code>>12)&15);
        new_display[3] = hexdig((ir_code>> 8)&15);
        new_display[4] = hexdig((ir_code>> 4)&15);
        new_display[5] = hexdig((ir_code    )&15);
    }
}

static const unsigned char qsine[64] = { 0, 6, 12, 18, 25, 31, 37, 43, 49, 56, 62, 68, 74, 80, 86, 92, 97, 103, 109, 115, 120, 126, 131, 136, 142, 147, 152, 157, 162, 167, 171, 176, 181, 185, 189, 193, 197, 201, 205, 209, 212, 216, 219, 222, 225, 228, 231, 234, 236, 238, 241, 243, 244, 246, 248, 249, 251, 252, 253, 254, 254, 255, 255, 255 };
__attribute__((mips16)) static unsigned char u8sine(unsigned char pos, int scale) {
    int ret, mult = 1;

    if( pos >= 128 ) {
        mult = -1;
        pos -= 128;
    }
    if( pos >= 64 )
        ret = qsine[127 - pos];
    else
        ret = qsine[pos];
    ret = ret * scale >> 8;
    return 128 + ((ret * mult) / 2);
}

__attribute__((mips16)) static void update_display() {
    char new_display[6];
    unsigned char new_dps, new_dim_dps, new_colons;
    int new_brightness;

    new_brightness = DISPLAY_BRIGHTNESS * 256;
    if( LDR.vals.min < LDR.vals.max - 64 ) {
        int ulimit = AUTODIM_ULIMIT;
        int llimit = AUTODIM_LLIMIT;

        if( ulimit > llimit ) {
            int ldr_val = get_LDR_val();

            llimit = (LDR.vals.max - LDR.vals.min) * llimit / 100 + LDR.vals.min;
            ulimit = (LDR.vals.max - LDR.vals.min) * ulimit / 100 + LDR.vals.min;
            if( ldr_val < llimit )
                ldr_val = llimit;
            else if( ldr_val > ulimit )
                ldr_val = ulimit;
            ldr_val = 256 - (ldr_val - llimit) * 256 / (ulimit - llimit);
            new_brightness = new_brightness * (AUTODIM_MINBR + (100 - AUTODIM_MINBR) * ldr_val / 256) / 100;
        }
    }

    if( timezone_found != -1 && gps_fixgoodfor < 3 ) {
        // lost GPS lock but had a valid timezone at some time in the past so pulsate the display
        new_brightness = (new_brightness * (u8sine(TMR3&255, 192)+56)) >> 8;
    }

    brightness = new_brightness / 100;

    new_colons = 0;
    new_dps = 0;
    new_dim_dps = 0;
    switch(curmode) {
        case show_time:
            if( gps_detected && timezone_found == -1 ) {
                memcpy(new_display, "GPS   ", 6);
                if( gps_numsats == 0 ) {
                    if( gps_spinner_progress < 4 )
                        new_display[5] = INDIVIDUAL_SEG_OFFSET + gps_spinner_progress;
                    else
                        new_display[4] = INDIVIDUAL_SEG_OFFSET + (gps_spinner_progress == 7 ? 0 : gps_spinner_progress - 1);

                    if( RtccGetTime() != gps_spinner_timel ) {
                        gps_spinner_timel = RtccGetTime();
                        gps_spinner_progress = (gps_spinner_progress+1)&7;
                    }
                } else if( gps_fixgood ) {
                    if( gps_datetimelatlongood ) {
                        new_display[4] = 'S';
                        new_display[5] = 'E';
                    } else {
                        new_display[4] = 'F';
                        new_display[5] = '1';
                    }
                } else {
                    int tens = gps_numsats / 10;
                    new_display[4] = '0' + (tens);
                    new_display[5] = '0' + (gps_numsats - tens*10);//% 10);
                }

                new_colons = 0;
                if( !PORTAbits.RA2 && gps_1pps_last_status )
                    gps_1pps_pulse_stretch = 2048;
                else if( gps_1pps_pulse_stretch )
                    --gps_1pps_pulse_stretch;
                gps_1pps_last_status = PORTAbits.RA2;
                new_dps = gps_1pps_pulse_stretch ? 4 : 0;
            } else {
                if( _24HR_MODE ) {
                    new_display[0] = '0' + (time.hour >> 4);
                    new_display[1] = '0' + (time.hour & 15);
                } else {
                    int hour = time.hour;
                    if( hour == 0x00 ) {
                        hour = 0x12;
                    } else if( hour > 0x12 ) {
                        if( hour >= 0x20 )
                            hour -= 0x18;
                        else
                            hour -= 0x12;

                        if( hour >= 0x0A )
                            hour += 0x06;
                    }
                    new_display[0] = '0' + (hour >> 4);
                    new_display[1] = '0' + (hour & 15);
                }
                if( LEADING_ZERO_BLANKING && new_display[0] == '0' )
                    new_display[0] = ' ';
                new_display[2] = '0' + (time.min  >> 4);
                new_display[3] = '0' + (time.min  & 15);
                new_display[4] = '0' + (time.sec  >> 4);
                new_display[5] = '0' + (time.sec  & 15);
                if( !time_set && RTCCONbits.HALFSEC )
                    memset(new_display, ' ', 6);

                new_colons = COLONS_ON ? (COLONS_FLASH ? RTCCONbits.HALFSEC : 1) : 0;
#ifdef SHOW_UTC_FEATURE
                new_dps = (show_UTC ? 1 : 0) ^ ((DPS_ON ? (!DPS_FLASH || RTCCONbits.HALFSEC ? 10 : 0) : 0) ^ (!_24HR_MODE && PM_DP_MODE && time.hour >= 0x12 ? PM_DP_MODE : 0));
#else
                new_dps = ((DPS_ON ? (!DPS_FLASH || RTCCONbits.HALFSEC ? 10 : 0) : 0) ^ (!_24HR_MODE && PM_DP_MODE && time.hour >= 0x12 ? PM_DP_MODE : 0));
#endif
            }
            if( alarm_timer && RTCCONbits.HALFSEC )
                memset(new_display, ' ', 6);

            break;
        case show_date:
            if( DATE_FMT_YYMMDD ) {
                new_display[0] = '0' + (date.year >> 4);
                new_display[1] = '0' + (date.year & 15);
                new_display[2] = '0' + (date.mon  >> 4);
                new_display[3] = '0' + (date.mon  & 15);
                new_display[4] = '0' + (date.mday >> 4);
                new_display[5] = '0' + (date.mday & 15);
            } else {
                if( DATE_FMT_DDMMYY ) {
                    new_display[0] = '0' + (date.mday >> 4);
                    new_display[1] = '0' + (date.mday & 15);
                    new_display[2] = '0' + (date.mon  >> 4);
                    new_display[3] = '0' + (date.mon  & 15);
                } else {
                    new_display[0] = '0' + (date.mon  >> 4);
                    new_display[1] = '0' + (date.mon  & 15);
                    new_display[2] = '0' + (date.mday >> 4);
                    new_display[3] = '0' + (date.mday & 15);
                }
                new_display[4] = '0' + (date.year >> 4);
                new_display[5] = '0' + (date.year & 15);
            }
            if( !date_set && RTCCONbits.HALFSEC )
                memset(new_display, ' ', 6);

            new_colons = 0;
            new_dps = 10;
            break;
        case show_dow:
            memcpy(new_display, &day_cycle[date.wday].altname, 6);
            if( !date_set && RTCCONbits.HALFSEC )
                memset(new_display, ' ', 6);
            break;
        case show_menu:
        {
            char flashing[6];
            menu_show(mainmenu, &curmenustate, new_display, &new_dps, flashing);
            if( RTCCONbits.HALFSEC ) {
                int i;
                for( i = 0; i < 6; ++i )
                    if( flashing[i] )
                        new_display[i] = ' ';
            } else {
                int i;
                for( i = 0; i < 6; ++i )
                    if( flashing[i] && new_display[i] == ' ' )
                        new_display[i] = '0';
            }
            break;
        }
        case show_ir_code:
            if( ir_timer ) {
                display_ir_code(new_display, ir_code);
            } else {
                new_display[0] = 'I';
                new_display[1] = 'R';
                memset((char*)new_display+2, ' ', 4);
            }
            break;
        case set_ir_code:
        {
            int code_show;

            if( ir_timer ) {
                code_show = ir_code;
            } else {
                menuitem* pitem = menu_get_current_item(mainmenu, &curmenustate, 1);
                code_show = pitem->data.b.c.val;
            }

            new_display[0] = (code_show&IR_RC5) ? 'P' : ( (code_show&IR_NEC) ? '^' : ' ' ); // N
            new_display[1] = (code_show&IR_RC5) ? 'H' : ( (code_show&IR_NEC) ? 'E' : ' ' );
            new_display[2] = hexdig((code_show>>12)&15);
            new_display[3] = hexdig((code_show>> 8)&15);
            new_display[4] = hexdig((code_show>> 4)&15);
            new_display[5] = hexdig((code_show    )&15);
            break;
        }
        case show_timer_up:
        case show_timer_down:
        case show_timer_lap:
        {
            unsigned char neg;
            int counter, sub_counter;

            neg = 0;
            if( curmode == show_timer_lap ) {
                counter = timer_counter_lap - timer_counter_lastlap;
                sub_counter = timer_counter_lap_sub - timer_counter_lastlap_sub;
                if( sub_counter < 0 ) {
                    --counter;
                    sub_counter += 32768;
                }
                if( counter < 0 ) {
                    counter = -counter;
                    sub_counter = 32767 - sub_counter;
                    neg = 1;
                }
            } else if( curmode == show_timer_down ) {
                counter = timer_target - timer_counter - 1;
                sub_counter = 32768 - TMR1;
                if( sub_counter >= 32768 ) {
                    ++counter;
                    sub_counter -= 32768;
                }
                if( counter < 0 ) {
                    counter = 0;
                    sub_counter = 0;
                }
            } else {
                counter = timer_counter;
                sub_counter = TMR1;
            }
            if( timer_target && timer_counter == timer_target ) {
                sub_counter = 0;
                if( !alarm_timer ) {
                    if( timer_alarm_off )
                        go_to_time_display();
                    else
                        alarm_trigger(&time, 0);
                }
            }

            if( counter < 3600 ) {
                sprintf(new_display, "%02d%02d%02d", counter / 60, counter % 60, sub_counter * 100 / 32768);
                new_dps = (sub_counter >= 16384 ? 2 : 0)|8;
            } else if( counter < 36000 ) {
                sprintf(new_display, "%01d%02d%02d%01d", counter / 3600, (counter / 60) % 60, counter % 60, sub_counter * 10 / 32768);
                new_dps = (sub_counter >= 16384 ? 5 : 0)|16;
            } else if( counter < 86400 ) {
                sprintf(new_display, "%02d%02d%02d", counter / 3600, (counter / 60) % 60, counter % 60);
                new_dps = (sub_counter >= 16384 ? 10 : 0);
            } else if( counter < 86400*10 ) {
                sprintf(new_display, "%01dD%02d%02d", counter / 86400, (counter / 3600) % 24, (counter / 60) % 60);
                new_dps = (sub_counter >= 16384 ? 40 : 0);
            } else if( counter < 86400*100 ) {
                sprintf(new_display, "%02dD%02d%01d", counter / 86400, (counter / 3600) % 24, (counter / 600) % 6);
                new_dps = (sub_counter >= 16384 ? 16 : 0);
            } else {
                sprintf(new_display, "%03dD%02d", counter / 86400, (counter / 3600) % 24);
                new_dps = (sub_counter >= 16384 ? 32 : 0);
            }
            if( neg && new_display[0] == '0' )
                new_display[0] = '-';

            new_colons = 0;
            break;
        }
        case set_timer:
        {
            char flashing[6];
            menu_show(&timer_set_menu, &timer_set_state, new_display, &new_dps, flashing);
            if( RTCCONbits.HALFSEC ) {
                int i;
                for( i = 0; i < 6; ++i )
                    if( flashing[i] )
                        new_display[i] = ' ';
            } else {
                int i;
                for( i = 0; i < 6; ++i )
                    if( flashing[i] && new_display[i] == ' ' )
                        new_display[i] = '0';
            }
            new_dps = (timer_alarm_off ? 0 : 32);
            break;
        }
        case set_ir_codes:
            if( set_ir_codes_progress == 0 ) {
                memcpy(new_display, "IRSE7T", 6);
            } else if( set_ir_codes_timer ) {
                if( ir_codes_submenu[set_ir_codes_progress-1].data.b.c.val ) {
                    display_ir_code(new_display, ir_codes_submenu[set_ir_codes_progress-1].data.b.c.val);
                } else {
                    memcpy(new_display, " SKIP ", 6);
                }
                if( set_ir_codes_time != RtccGetTime() ) {
                    if( --set_ir_codes_timer == 0 ) {
                        ++set_ir_codes_progress;
                    }
                    set_ir_codes_time = RtccGetTime();
                }
            } else {
                if( set_ir_codes_progress == sizeof(ir_codes_submenu)/sizeof(*ir_codes_submenu) )
                    memcpy(new_display, "IR FIN", 6);
                else
                    memcpy(new_display, ir_codes_submenu[set_ir_codes_progress-1].name, 6);
            }

            if( RTCCONbits.HALFSEC && !set_ir_codes_timer )
                memset(new_display, ' ', 6);
            break;
    }
    if( GLOBAL_ALARM_FLAG && curmode != show_timer_up && curmode != show_timer_down && curmode != show_timer_lap && curmode != set_timer ) {
        if( time_set && date_set && alarm_is_in_next_24_hours(&time, &date) )
            new_dps ^= 32;//(PM_DP_MODE == 32 ? 2 : 32);
        else
            new_dim_dps = 32;
    }
    if( ir_flash ) {
        new_dps ^= 32;
        --ir_flash;
    }

    if( display_off ) {
        memset(new_display, ' ', 6);
        new_colons = 0;
        new_dps = 0;
    }

    __builtin_disable_interrupts();
    colons = new_colons;
    dps = new_dps;
    dim_dps = new_dim_dps;
    memcpy((char*)display, new_display, 6);
    __builtin_enable_interrupts();
}

__attribute__((mips16)) static void handle_button_press() {
    if( alarm_timer && alarm_is_daily_type ) {
        if( lbutton_long_press || rbutton_long_press || bbutton_long_press )
            alarm_cancel();
        else
            alarm_snooze(&date);
        lbutton_short_press = lbutton_long_press = rbutton_short_press = rbutton_long_press = bbutton_short_press = bbutton_long_press = bbutton_shortlong_press_lr = bbutton_shortlong_press_rl = 0;
    } else if( curmode == show_menu || curmode == set_timer ) {
        int ir_code = 0;
        unsigned char editing = 0;

        menuitem* item;
        if( curmode == show_menu )
            item = menu_get_submenu(mainmenu, &curmenustate, &editing);
        else
            item = menu_get_submenu(&timer_set_menu, &timer_set_state, &editing);

        if( lbutton_short_press ) {
            lbutton_short_press = 0;
            ir_code = editing ? IRCODE_DOWN : IRCODE_UP;
        } else if( rbutton_short_press ) {
            rbutton_short_press = 0;
            ir_code = editing ? IRCODE_UP : IRCODE_DOWN;
        } else if( lbutton_long_press ) {
            lbutton_long_press = 0;
            if( editing )
                ir_code = IRCODE_SELECT;
            else
                ir_code = IRCODE_ESCAPE;
        } else if( rbutton_long_press ) {
            rbutton_long_press = 0;
            if( editing )
                ir_code = IRCODE_RIGHT;
            else
                ir_code = IRCODE_SELECT;
        } else if( bbutton_long_press ) {
            bbutton_long_press = 0;
            ir_code = IRCODE_ESCAPE;
        } else if( bbutton_short_press ) {
            bbutton_short_press = 0;
            ir_code = IRCODE_ALA_ON;
        } else {
            bbutton_shortlong_press_lr = bbutton_shortlong_press_rl = 0;
        }

        if( ir_code )
            handle_ir_button_press(ir_code, 1);
    } else if( curmode == show_time || curmode == show_date || curmode == show_dow ) {
        int ir_code = 0;

        if( lbutton_short_press || rbutton_short_press ) {
            lbutton_short_press = 0;
            rbutton_short_press = 0;

            if( curmode == show_time ) {
                date_timer = 5;
                date_time = RtccGetTime();
                curmode = show_date;
            } else if( curmode == show_date ) {
                date_timer = 5;
                curmode = show_dow;
            } else {
                date_timer = 0;
                curmode = show_time;
            }
        } else if( rbutton_long_press ) {
            rbutton_long_press = 0;

            mainmenu_set_mode(tz_override != -1 ? tz_override : timezone_found, tzinfo, dstinfo);
            curmode = show_menu;
        } else if( bbutton_long_press ) {
            curmode = set_ir_codes;
            set_ir_codes_progress = 0;
            set_ir_codes_timer = 0;

            bbutton_long_press = 0;
        } else if( bbutton_short_press || bbutton_shortlong_press_lr ) {
            ir_code = IRCODE_CNTUP;
            bbutton_shortlong_press_lr = 0;
            bbutton_short_press = 0;
        } else if( bbutton_shortlong_press_rl || lbutton_long_press ) {
            ir_code = IRCODE_CNTDN;
            bbutton_shortlong_press_rl = 0;
            lbutton_long_press = 0;
        }

        if( ir_code )
            handle_ir_button_press(ir_code, 1);
    } else if( curmode == show_timer_up || curmode == show_timer_down ) {
        int ir_code = 0;

        if( lbutton_short_press ) {
            lbutton_short_press = 0;
            ir_code = IRCODE_UP;
        } else if( rbutton_short_press ) {
            rbutton_short_press = 0;
            ir_code = IRCODE_DOWN;
        } else if( lbutton_long_press ) {
            lbutton_long_press = 0;
            ir_code = IRCODE_SELECT;
        } else if( rbutton_long_press ) {
            rbutton_long_press = 0;
            ir_code = IRCODE_RIGHT;
        } else if( bbutton_long_press ) {
            bbutton_long_press = 0;
            ir_code = IRCODE_ESCAPE;
        } else if( bbutton_short_press ) {
            bbutton_short_press = 0;
            ir_code = IRCODE_ALA_ON;
        } else if( bbutton_shortlong_press_lr ) {
            bbutton_shortlong_press_lr = 0;
            ir_code = T1CONbits.ON ? IRCODE_TI_PAU : IRCODE_TI_RES;
        } else if ( bbutton_shortlong_press_rl )  {
            bbutton_shortlong_press_rl = 0;
            ir_code = IRCODE_TI_LAP;
        }

        if( ir_code )
            handle_ir_button_press(ir_code, 1);
    } else if( curmode == set_ir_codes ) {
        if( lbutton_short_press ) {
            if( set_ir_codes_progress > 0 )
                --set_ir_codes_progress;
            lbutton_short_press = 0;
        } else if( rbutton_short_press ) {
            if( set_ir_codes_progress == sizeof(ir_codes_submenu)/sizeof(*ir_codes_submenu) ) {
                menu_save_settings_to_flash(mainmenu, 1);
                curmode = show_time;
            } else {
                if( set_ir_codes_progress > 0 )
                    ir_codes_submenu[set_ir_codes_progress-1].data.b.c.val = set_ir_codes_progress; // this will not be identified as a valid IR code but is still a distinct code for emulation
                ++set_ir_codes_progress;
            }

            rbutton_short_press = 0;
        } else if( bbutton_long_press ) {
            menu_save_settings_to_flash(mainmenu, 1);
            curmode = show_time;

            bbutton_long_press = 0;
        } else {
            lbutton_long_press = rbutton_long_press = bbutton_short_press = bbutton_shortlong_press_lr = bbutton_shortlong_press_rl = 0;
        }
    } else if( curmode == set_ir_code ) {
        if( rbutton_short_press ) {
            menuitem* pitem = menu_get_current_item(mainmenu, &curmenustate, 1);
            pitem->data.b.c.val = (pitem - ir_codes_submenu) + 1; // this will not be identified as a valid IR code but is still a distinct code for emulation
            curmode = show_menu;
        } else if( bbutton_long_press ) {
            curmode = show_menu;
        } else {
            lbutton_short_press = lbutton_long_press = rbutton_long_press = bbutton_short_press = bbutton_shortlong_press_lr = bbutton_shortlong_press_rl = 0;
        }
    }
}

extern void (*ir_cn_interrupt_chain)(void);
unsigned char gps_skip_second_disable;
static unsigned char last_RA2, last_1PPS_time_valid;
static rtccTime last_1PPS_time;
static rtccDate last_1PPS_date;
void cn_interrupt_chain(void) {
    unsigned char RA2 = PORTAbits.RA2;
    if( RA2 != last_RA2 ) {
        if( RA2 ) {
            rtccTime time;
            rtccDate date;
            int halfsec;

            while(1) {
                time.l = RTCTIME;
                date.l = RTCDATE;
                halfsec = RTCCONbits.HALFSEC;
                if( time.l == RTCTIME && date.l == RTCDATE && halfsec == RTCCONbits.HALFSEC )
                    break;
            }

            if( gps_skip_second_disable )
                --gps_skip_second_disable;
            if( halfsec && last_1PPS_time_valid ) {
                if( last_1PPS_time.l == time.l ) {
                    // if we're getting pulses before the clock has rolled over, speed the clock up a bit
                    if( !gps_skip_second_disable ) {
                        time_date_add_one_second(&time, &date);
                        if( RTCCONbits.HALFSEC && !RTCCONbits.RTCSYNC ) {
                            RTCTIME = time.l;
                            RTCDATE = date.l;
                            gps_skip_second_disable = 60;
                            if( OPTS_XTAL_TRIM < 511 )
                                RtccSetCalibration(++OPTS_XTAL_TRIM);
                        }
                    }
                } else if( !gps_skip_second_disable ) {
                    // if we're getting pulses after the clock has rolled over, slow the clock up a bit
                    if( times_are_one_second_apart(&last_1PPS_time, &last_1PPS_date, &time, &date) ) {
                        if( OPTS_XTAL_TRIM > -512 )
                            RtccSetCalibration(--OPTS_XTAL_TRIM);
                        gps_skip_second_disable = 60;
                    }
                }
            }

            last_1PPS_time.l = time.l;
            last_1PPS_date.l = date.l;
            last_1PPS_time_valid = 1;
        }
    }
    IFS1bits.CNAIF = 0;
}

__attribute__((mips16)) static int realmain() {
  SYSTEMConfigPerformance(SYS_FREQ);
  INTEnableSystemMultiVectoredInt();
  INTEnableInterrupts();

  mOSCEnableSOSC();

  setup_IOs();
  setup_TMR1();
  setup_7seg_TMR();

  display_test(&delay_500ms);
  LATAbits.LATA3 = 1;
  delay_100ms();
  LATAbits.LATA3 = 0;

  while( !OSCCONbits.SOSCRDY )
      ;
  RtccInit();
  while( RtccGetClkStat() != RTCC_CLK_ON )
      ;

  time.l = 0;
  date.l = 0;
  date.mday = 1;
  date.mon = 1;
  RtccFixupDOW(&date);
  RtccSetTimeDate(time.l, date.l);

  setup_serial(0, 9600);
  Infrared_Setup();
  setup_ADC();
  LDR.vals.min = 1023;
  DataEERead(&LDR.val, LDR_EEPROM_ADDRESS);

  // pull down for 1PPS input
  CNPDAbits.CNPDA2 = 1;
  // change notification for 1PPS input
  ir_cn_interrupt_chain = &cn_interrupt_chain;
  CNENAbits.CNIEA2 = 1;
  CNCONAbits.ON = 1;
  IFS1bits.CNAIF = 0;
  IEC1bits.CNAIE = 1;

  DataEEInit();
  menu_init(mainmenu, &curmenustate);
  menu_load_settings_from_flash(mainmenu, 1);
  load_dst_override_from_flash(MAX_MENU_STATE_SIZE+1);

  // load time zone override if set
  mainmenu_set_mode(tz_override != -1 ? tz_override : timezone_found, tzinfo, dstinfo);
  mainmenu_callback(menu_updownlist_change, &mainmenu[5]);

  RtccSetCalibration(OPTS_XTAL_TRIM);

  while(1) {
    (void)serial_read(32, tzinfo, dstinfo);
    RtccGetTimeDate(&time, &date);
#ifdef SHOW_UTC_FEATURE
    if( (tz_override != -1 || timezone_found >= 0) && !show_UTC ) {
#else
    if( (tz_override != -1 || timezone_found >= 0) ) {
#endif
        int gtime, gdate, orig_gdate;
        bcd_time_date_to_decimal(&gtime, &gdate, &time, &date);
        orig_gdate = gdate;
        apply_timezone(&gtime, &gdate, tz_override == -1 ? timezone_found : tz_override, tzinfo, dstinfo);
        decimal_time_date_to_bcd(&time, &date, gtime, gdate);
        if( orig_gdate != gdate )
            RtccFixupDOW(&date);
    }
    if( !time_set ) {
        time.hour = 0x12;
        time.min = 0;
        time.sec = 0;
    }
    if( !date_set ) {
        date.year = 0x16;
        date.mon = 0x01;
        date.mday = 0x01;
        date.wday = 5;
    }

    if( ADCResultsUpdated ) {
        int LDR_val = get_LDR_val();
        ADCResultsUpdated = 0;
        if( LDR_val < LDR.vals.min ) {
            if( LDR_min_counter == 0 || LDR_val < LDR_pending_min )
                LDR_pending_min = LDR_val;
            if( ++LDR_min_counter == 100 ) {
                LDR.vals.min = LDR_val;
                LDR_min_counter = 0;
                LDR_minmax_updated = 1024;
            }
        } else {
            LDR_min_counter = 0;
        }
        if( LDR_val > LDR.vals.max ) {
            if( LDR_max_counter == 0 || LDR_val > LDR_pending_max )
                LDR_pending_max = LDR_val;
            if( ++LDR_max_counter == 100 ) {
                LDR.vals.max = LDR_val;
                LDR_max_counter = 0;
                LDR_minmax_updated = 1024;
            }
        } else {
            LDR_max_counter = 0;
        }
        if( LDR_minmax_updated && --LDR_minmax_updated == 0 ) {
            DataEEWrite(LDR.val, LDR_EEPROM_ADDRESS);
        }
    }
#ifndef __DEBUG
    if( get_supply_voltage() < 10000 /* 10V */ ) {
      serial_state state;
      save_state_and_reduce_power_consumption(&state);
      do {
          teardown_ADC();
          LowPowerSleep();
          setup_ADC();
      } while( get_supply_voltage() < 11000 /* 11V */ );

      ResumeNormalOscillator();
      teardown_ADC();
      restore_state(&state);
      curmode = show_time;
    }
#endif
    if( ir_final_code ) {
        ir_flash = 150;

        if( (curmode == set_ir_code || curmode == set_ir_codes) && ir_final_code == (ir_code|IR_REPEAT) ) {
            if( ++ir_valid_repeat == 5 ) {
                if( curmode == set_ir_code ) {
                    menuitem* pitem = menu_get_current_item(mainmenu, &curmenustate, 1);
                    pitem->data.b.c.val = ir_code;
                    curmode = show_menu;
                } else {
                    ir_codes_submenu[set_ir_codes_progress-1].data.b.c.val = ir_code;
                    set_ir_codes_time = RtccGetTime();
                    set_ir_codes_timer = 3;
                }
            } else {
                ir_final_code &= ~IR_REPEAT;
            }
        } else {
            ir_valid_repeat = 0;
        }

        ir_timer = 5;
        ir_time = time.l;
        ir_code = ir_final_code;
        ir_final_code = 0;
        handle_ir_button_press(ir_code, 0);
    }
    if( ir_timer && ir_time != time.l ) {
        ir_time = time.l;
        --ir_timer;
    }
    if( date_timer && date_time != RtccGetTime() ) {
        date_time = RtccGetTime();
        if( --date_timer == 0 && (curmode == show_date || curmode == show_dow) )
            curmode = show_time;
    }
    LATAbits.LATA3 = alarm_get_piezo_state(&time, &date);
    buttons_handle_presses();
    if( lbutton_short_press || lbutton_long_press || rbutton_short_press || rbutton_long_press || bbutton_short_press || bbutton_long_press || bbutton_shortlong_press_lr || bbutton_shortlong_press_rl )
        handle_button_press();
    update_display();
  }
  return (EXIT_SUCCESS);
}

int main() {
  if( (RCON & 0x18) == 0x18 ) {
    // The WDT caused a wake from sleep
    RCONCLR = 0x18;
    asm volatile ( "eret" ); // return from interrupt
  }
  if( (RCON & 0x14) == 0x14 ) {
    // The WDT caused a wake from idle
    RCONCLR = 0x14;
    asm volatile ( "eret" ); // return from interrupt
  }
  return realmain();
}
