# Copyright 2026 PostalPortal LLC
#
# Redistribution and use in source and binary forms, with or
# without modification, are permitted provided that
# the following conditions are met:
#   1. Redistributions of source code must retain
#      the above copyright notice, this list of conditions
#      and the following disclaimer.
#   2. Redistributions in binary form must reproduce
#      the above copyright notice, this list of conditions
#      and the following disclaimer in the documentation
#      and/or other materials provided with the distribution.
#   3. Neither the name of the copyright holder
#       nor the names of its contributors may be used
#       to endorse or promote products derived from
#       this software without specific prior written permission.
#
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND
# CONTRIBUTORS “AS IS” AND ANY EXPRESS OR IMPLIED WARRANTIES,
# INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
# MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
# DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR
# CONTRIBUTORS 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.


from sys import stdin, exit, print_exception
import machine
from machine import Pin, USBDevice, UART
from utime import sleep, sleep_ms, sleep_us
from micropython import const
import time

from config import *
from oledscreen import bootDisplay, clearDisplay, brightDisplay, mainDisplay, centerText
from scannerusb import initUSBHID, createAndSendBarcodeReports
from feedback import feedbackBuzzer, createLEDThread
from scanmode import isScanInhibited, setModeID, getCurrentModeID, processNewListUSBReport
from watchdog import startwatchdog, feedwatchdog

print("PostalPoint(r) Barcode Scanner")
print("Firmware version " + FIRMWARE_VERSION)

startwatchdog()

uart = UART(UART_ID, baudrate=9600, bits=8, parity=None, stop=1, tx=UART_TX_PIN, rx=UART_RX_PIN, txbuf=100, rxbuf=MAX_BARCODE_LENGTH)

triggerButton = Pin(TRIGGER_BUTTON_PIN, Pin.IN, Pin.PULL_UP)
triggerPin = Pin(TRIGGER_PIN, Pin.IN)
menuUpButton = Pin(UP_BUTTON_PIN, Pin.IN, Pin.PULL_UP)
menuDownButton = Pin(DOWN_BUTTON_PIN, Pin.IN, Pin.PULL_UP)

scanButtonState = 1

def triggerButtonHandler(pin):
    global scanButtonState
    scanButtonState = pin.value()

triggerButton.irq(handler=triggerButtonHandler, trigger=(Pin.IRQ_FALLING | Pin.IRQ_RISING))

menuUpButtonPressed = False
menuDownButtonPressed = False
menuUpButtonLastPressTime = time.ticks_ms()
menuDownButtonLastPressTime = time.ticks_ms()

def menuUpButtonHandler(pin):
    global menuUpButtonPressed, menuUpButtonLastPressTime
    if pin.value() == 0:
        # Don't allow button "wobble" to cause multiple events in quick succession,
        # ignore button presses when they're less than 100ms apart
        if time.ticks_diff(time.ticks_ms(), menuUpButtonLastPressTime) < 100:
            return
        menuUpButtonPressed = True
        menuUpButtonLastPressTime = time.ticks_ms()
        
def menuDownButtonHandler(pin):
    global menuDownButtonPressed, menuDownButtonLastPressTime
    if pin.value() == 0:
        if time.ticks_diff(time.ticks_ms(), menuDownButtonLastPressTime) < 100:
            return
        menuDownButtonPressed = True
        menuDownButtonLastPressTime = time.ticks_ms()

menuUpButton.irq(handler=menuUpButtonHandler, trigger=Pin.IRQ_FALLING)
menuDownButton.irq(handler=menuDownButtonHandler, trigger=Pin.IRQ_FALLING)

feedwatchdog()
createLEDThread()
feedwatchdog()
feedbackBuzzer("POW")
feedwatchdog()
mainDisplay(scanButtonState == 0)

if USBHID_ENABLED:
    initUSBHID()

feedwatchdog()

lastTestSend = time.ticks_ms()

uartBuffer = bytearray()
lastBufferActivity = None
bufferOverflow = False
lastBarcodeScan = time.ticks_ms()

#
# Main loop
#
while True:
    feedwatchdog()
    try:
        # Refresh OLED
        mainDisplay(scanButtonState == 0)
        feedwatchdog()
        # Handle menu buttons
        if menuUpButtonPressed:
            setModeID(getCurrentModeID() - 1)
            menuUpButtonPressed = False
        if menuDownButtonPressed:
            setModeID(getCurrentModeID() + 1)
            menuDownButtonPressed = False
            
        feedwatchdog()
        
        # Set trigger line for scan engine
        if isScanInhibited():
            triggerPin.init(Pin.IN)
        else:
            if scanButtonState == 0:
                triggerPin.init(Pin.OUT)
                triggerPin.value(0)
            else:
                triggerPin.init(Pin.IN)
        
        feedwatchdog()
        
        # Read scan data from engine
        while uart.any():
            feedwatchdog()
            data = uart.read(uart.any())
            if data:
                if not bufferOverflow:
                    uartBuffer.extend(data)
                lastBufferActivity = time.ticks_ms()
            if len(uartBuffer) > MAX_BARCODE_LENGTH:
                uartBuffer[:] = b'' # clear buffer
                bufferOverflow = True
                lastBufferActivity = None
                
        # Send scan data to host
        if lastBufferActivity is not None and time.ticks_diff(time.ticks_ms(), lastBufferActivity) > SCAN_GAP_MS:
            # Don't send if scan disabled, buffer empty, corrupt buffer data, or a code was just scanned
            if not isScanInhibited() and uartBuffer and not bufferOverflow and time.ticks_diff(time.ticks_ms(), lastBarcodeScan) > BARCODE_SCAN_INTERMISSION:
                # Stop sending trigger signal in case user is still holding the button (can cause repeat scans)
                triggerPin.init(Pin.OUT)
                triggerPin.value(0)
                if AIM_PREFIX and len(uartBuffer) <= 3:
                    # Codes must be longer than 3 chars because the AIM code is 3 chars long!
                    pass
                else:
                    feedbackBuzzer("SCAN", True)
                    createAndSendBarcodeReports(bytes(uartBuffer))
                    lastBarcodeScan = time.ticks_ms()
            uartBuffer[:] = b'' # clear buffer
            lastBufferActivity = None
            bufferOverflow = False
        
        feedwatchdog()
        
        if TESTMODE and time.ticks_diff(time.ticks_ms(), lastTestSend) > 5000:
            feedbackBuzzer("SCAN")
            createAndSendBarcodeReports(b'test barcode 12345')
            lastTestSend = time.ticks_ms()
        
        feedwatchdog()
        
        # Handle config from host if USB code queued it for processing
        processNewListUSBReport()
        
        feedwatchdog()
        # Take a tiny bit of time off
        machine.idle()
    except Exception as e:
        # Dump exceptions over serial for debugging
        print_exception(e)
        machine.reset()
