import os,json,pycom,machine
from network import WLAN
from utils import asciiArt
from sensors.__init__ import rgbStrip

# VRAM memory functions
def initVRAM(): # Size $7000 = 28Kbyte
    pycom.nvs_erase_all()

def readVRAM(key):
    try:
        res = pycom.nvs_get(key) / 1000 # adds support for float
    except:
        res = None
    return res

def writeVRAM(key, value):
    pycom.nvs_set(key, int(value * 1000)) # adds support for float

def readVRAMBulk(keys):
    res = {}
    for key in keys:
        try:
            res[key] = pycom.nvs_get(key) / 1000 # adds support for float
        except:
            res[key] = None
    return res


# FLash memory config.json functions
def readFlashConfig():
    try:
        read = '{}'
        with open('/flash/config.json', 'r') as f:
            read = f.read()
            if (read == ''):
                read = '{}'
            f.close()
        os.sync()
        return json.loads(read)
    except OSError as e:
        print('Error reading from config.json file: {}'.format(e))
        return {}

def clearFlashConfig(key=None):
    global FLASH_CONFIG
    print("Clearing flash configuration...", end='')
    oldLen = len(json.dumps(readFlashConfig()))
    try:
        with open('/flash/config.json', 'w') as f:
            f.seek(0)
            if key == None:
                f.write("{}" + " "*oldLen)
                FLASH_CONFIG = {}
                f.close()
            else:
                try:
                    del FLASH_CONFIG[key]
                    newWrite = json.dumps(FLASH_CONFIG)
                    f.write(newWrite + " "*(oldLen-len(newWrite)))
                    f.close()
                except:
                    print("Failed to remove key from flash config.")
                    return False
            print("cleared.")
    except:
        print("Failed to clear flash config.")
        return False
    return True

def writeFlashKey(key, value):
    try:
        config = readFlashConfig()
        oldLen = len(json.dumps(config))
        with open('/flash/config.json', 'w+') as f:
            config[key] = value
            newWrite = json.dumps(config)
            f.seek(0)
            f.write(newWrite + " "*(oldLen-len(newWrite)))
            f.close()
        os.sync()
        FLASH_CONFIG[key] = value
    except OSError as e:
        print('Error writing to config.json file: {}'.format(e))

def writeFlashConfig(values):
    try:
        config = readFlashConfig()
        oldLen = len(json.dumps(config))
        with open('/flash/config.json', 'w+') as f:
            newWrite = json.dumps(values)
            f.seek(0)
            f.write(newWrite + " "*(oldLen-len(newWrite)))
            f.close()
        os.sync()
        FLASH_CONFIG = values
    except OSError as e:
        print('Error writing to config.json file: {}'.format(e))

def cliOptionInput(prompt, choices, previousSetting=None, multi=False, default=None, altInput=False, alphaSelect=False, binaryMask=False):
    response = None
    if multi:
        response = []
        rawResponse = []
    while response == None or multi:
        if previousSetting:
            print("Previously set to {}. Input '.' to restore.")
        print(prompt + asciiArt.boldHR)
        for i, choice in enumerate(choices, start=1):
            if multi and choice in response:
                print("[x]: {}".format(choice))
            else:
                if alphaSelect:
                    print(" {} : {}".format("abcdefghijklmnopqrstuvwxyz"[i-1], choice))
                else:
                    print(" {} : {}".format(i, choice))
        altMsg = ''
        if not binaryMask:
            altMsg += ", 'a' to select all"
        if altInput:
            altMsg += ', or type in custom response'
        if multi:
            raw = input(asciiArt.thinHR + "Multiple Selection (press enter to confirm, 'a' to select all{}): ".format(altMsg))
        else:
            raw = input(asciiArt.thinHR + "Option Selection{}: ".format(altMsg))
        if multi and not binaryMask and raw == 'a':
            return choices
        if multi and len(response) >= 1 and raw == '':
            if binaryMask:
                return sum(rawResponse)
            return response
        if raw == '' and bool(default):
            return default
        if raw == '.' and bool(previousSetting):
            return previousSetting
        try:
            if alphaSelect:
                try:
                    raw = "abcdefghijklmnopqrstuvwxyz".index(raw)+1
                except:
                    if altInput:
                        return raw
                    else:
                        raw = ''
            else:
                raw = int(raw)
        except:
            raw = ''
        if raw > 0 and raw <= len(choices):
            if multi:
                print(asciiArt.clearRepl)
                if choices[raw-1] not in response:
                    response.append(choices[raw-1])
                    rawResponse.append(pow(2,raw-1))
                else:
                    response.remove(choices[raw-1])
                    rawResponse.remove(pow(2,raw-1))
            else:
                response = choices[raw-1]
            if not multi:
                return response
        elif raw != '' and altInput:
            return raw
        else:
            print(asciiArt.clearRepl)
            print("Error: Invalid input please choose a number 1-{}.".format(len(choices)))

def cliQuestion(description, previousSetting=None, options=None, multi=False, altInput=False, uppercase=True, alphaSelect=False, boolean=False, integer=False, floating=False, default=None, formatter=None, cb=None, flash=False):
    while True:
        rgbStrip.setState("warning")
        if options:
            setData = cliOptionInput(description, options, previousSetting=previousSetting, multi=multi, altInput=altInput, alphaSelect=alphaSelect, default=default)
        else:
            if previousSetting != None:
                print("Previously set to {}. Input '.' to restore.".format(previousSetting))
            setData = input(description).strip()
        if (setData == '.'):
            setData = previousSetting
            print('Restored value - {}'.format(previousSetting))
        if (setData == ""):
            if (default == None):
                print(asciiArt.clearRepl)
                print("Error: {} cannot be empty.".format(key))
                continue
            else:
                setData = default
                print("Default set - {}".format(default))
        oldData = setData
        if uppercase and not integer and not floating:
            if multi:
                for i,a in enumerate(setData): setData[i] = a.upper()
            else:
                setData = setData.upper()
        if boolean and not multi:
            if multi:
                for i,a in enumerate(setData): setData[i] = setData.upper() == "TRUE" or setData.upper() == "YES" or "Y" in setData.upper()
            else:
                setData = setData.upper() == "TRUE" or setData.upper() == "YES" or "Y" in setData.upper()
        if formatter:
            setData = formatter(setData)
        if integer and not multi:
            try:
                setData = int(setData)
            except OSError as e:
                print(asciiArt.clearRepl)
                print("Error: Invalid integer.")
                continue
        if floating and not multi:
            try:
                setData = float(setData)
            except OSError as e:
                print(asciiArt.clearRepl)
                print("Error: Invalid number.")
                continue
        rgbStrip.setState("off")
        if cb:
            cb(setData)
        return setData
    return changeFlashSetting
#
def cliSetup(key, description, previousSetting=None, options=None, multi=False, altInput=False, alphaSelect=False, overwrite=False, uppercase=True, boolean=False, integer=False, floating=False, default=None, formatter=None, cb=None, flash=False, binaryMask=False):
    global FLASH_CONFIG
    FLASH_CONFIG = readFlashConfig()
    while (key not in FLASH_CONFIG or FLASH_CONFIG[key] == None) or overwrite == True:
        rgbStrip.setState("warning")
        if options:
            setData = cliOptionInput(description, options, previousSetting=previousSetting, multi=multi, altInput=altInput, alphaSelect=alphaSelect, default=default, binaryMask=binaryMask)
        else:
            if previousSetting != None:
                print("Previously set to {}. Input '.' to restore.".format(previousSetting))
            setData = input(description).strip()
        if (setData == '.'):
            setData = previousSetting
            print('Restored value - {}'.format(previousSetting))
        if (setData == ""):
            if (default == None):
                print(asciiArt.clearRepl)
                print("Error: {} cannot be empty.".format(key))
                continue
            else:
                setData = default
                print("Default set - {}".format(default))
        oldData = setData
        if uppercase and not integer and not floating:
            if multi:
                for i,a in enumerate(setData): setData[i] = a.upper()
            else:
                setData = setData.upper()
        if boolean and not multi:
            if multi:
                for i,a in enumerate(setData): setData[i] = setData.upper() == "TRUE" or setData.upper() == "YES" or "Y" in setData.upper()
            else:
                setData = str(setData).upper() == "TRUE" or setData.upper() == "YES" or "Y" in setData.upper()
        print(setData)
        if formatter:
            setData = formatter(setData)
        if integer and not multi:
            try:
                setData = int(setData)
            except OSError as e:
                print(asciiArt.clearRepl)
                print("Error: Invalid integer.")
                continue
        if floating and not multi:
            try:
                setData = float(setData)
            except OSError as e:
                print(asciiArt.clearRepl)
                print("Error: Invalid number.")
                continue
        writeFlashKey(key,setData)
        overwrite = False
        rgbStrip.setState("off")
        if (setData != oldData):
            print('Saved as {}.'.format(setData))
        else:
            print("Saved {}.".format(setData))
        if cb:
            cb(setData)
        return True, setData
    return False, None

# Stores device id, connection settings, and behavior settings
FLASH_CONFIG = readFlashConfig()

def runFlashSetup(oldSettings={}):
    global FLASH_CONFIG
    if FLASH_CONFIG == {} or FLASH_CONFIG == None:
        print(asciiArt.clearRepl)
        print("Device Configuration"+asciiArt.boldHR)
    # Device ID
    previous = None
    if 'fipy_id' in oldSettings:
        previous = oldSettings['fipy_id']
    else:
        previous = None
    cliSetup('fipy_id', 'FiPy ID: (1001) ', formatter=lambda val: "{:0>4}".format(''.join(i for i in val if i.isdigit())), previousSetting=previous)
    if 'device_id' in oldSettings:
        previous = oldSettings['device_id']
    else:
        previous = None
    setDevice, device_id = cliSetup('device_id', 'Air Monitor ID: (1001) ', formatter=lambda val: "{:0>4}".format(''.join(i for i in val if i.isdigit())), previousSetting=previous)

    if setDevice:
        initVRAM()

    if device_id in FLASH_CONFIG:
        # TODO: Automatic device registration? Or share certificates?
        certPemAvalible = False
        privateKeyAvalible = False
        try:
            f = open('/flash/cert/FIPY-{}.cert.pem'.format(device_id), 'r')
            certPemAvalible = True
        except:
            print("/cert/FIPY-{}.cert.pem not found.".format(device_id))
        try:
            f = open('/flash/cert/FIPY-{}.private.key'.format(device_id), 'r')
            privateKeyAvalible = True
        except:
            print("/cert/FIPY-{}.private.key not found.".format(device_id))
        if not certPemAvalible or not privateKeyAvalible:
            print("ERROR: FIPY-{} certificates missing, will need to connect to AWS MQTT to register device.\n".format(device_id))

    if 'pycom_socket_version' in oldSettings:
        previous = oldSettings['pycom_socket_version']
    else:
        previous = None
    cliSetup('pycom_socket_version', 'PyCom Socket Version: ', formatter=lambda val: val if val not in {"2.1 - VC2 (21)": 21, "3.0 - PyTrack (3)": 3} else {"2.1 - VC2 (21)": 21, "3.0 - PyTrack (3)": 3}[val], options=["2.1 - VC2 (21)", "3.0 - PyTrack (3)"], altInput=True, alphaSelect=True, default=2, integer=True, previousSetting=previous)  # 2.2.5b

    # 2.2.0b
    # VOC, Temp/Humidity/Pressure, PM are present, always
    # weather_shield_option =
    #              b0 = IR_CO2 U8  IC1 $4B  AIN_0P-1N
    #              b1 = IR_CH4 U5  IC1 $4B  AIN_2P-3N
    #              b2 = PID U1     IC3 $49  AIN_2P-3N
    #              b3 = H2S U7     IC4 $4A  AIN_0P-1N
    #              b4 = Wind speed IC6 $20
    #                   Wind dir   IC3 $49 AIN_1
    if 'weather_shield_option' in oldSettings:
        previous = oldSettings['weather_shield_option']
    else:
        previous = None
        cliSetup('weather_shield_option', 'Weather Shield Option: ', options=["IR CO2", "IR Methane", "PID", "H2S", "Wind", "O3", "Loudness", "Weather Shield", "RESPR", "NH3", "SO2"], altInput=True, alphaSelect=True, integer=True, default=0, previousSetting=previous, multi=True, binaryMask=True)

    # 2.2.18
    if 'catcher_enabled' in oldSettings:
        previous = oldSettings['catcher_enabled']
    else:
        previous = None
    cliSetup('catcher_enabled', 'Catcher Enabled: (y/n) ', boolean=True, default='False', previousSetting=previous)

    # Sample Interval - minimum of 30s
    if 'sample_interval' in oldSettings:
        previous = oldSettings['sample_interval']
    else:
        previous = None
    cliSetup('sample_interval', 'Sample Interval: (30s) ', default=30, integer=True, formatter=lambda val: 30 if int(val) < 30 else val, previousSetting=previous )
    if 'send_interval' in oldSettings:
        previous = oldSettings['send_interval']
    else:
        previous = None
    cliSetup('send_interval', 'Send Interval: (3600s) ', default=3600, integer=True, formatter=lambda val: 30 if int(val) < 30 else val, previousSetting=previous )
    # cliSetup('sampling_method', 'Sampling Methods', options=['min','max','avg','raw'], multi=True)
    if 'pid_enabled' in oldSettings:
        previous = oldSettings['pid_enabled']
    else:
        previous = None
    cliSetup('pid_enabled', 'PID Enabled: (VOC PID - y/n) ', boolean=True, default='False', previousSetting=previous)
    if 'light_blinks' in oldSettings:
        previous = oldSettings['light_blinks']
    else:
        previous = None
    cliSetup('light_blinks', 'Light blinks each sample: (y/n) ', boolean=True, default='False', previousSetting=previous)
    if 'solar_size' in oldSettings:
        previous = oldSettings['solar_size']
    else:
        previous = None
    cliSetup('solar_size', 'Solar Size (W): (10) ', default=10, floating=True, previousSetting=previous)
    if 'battery_size' in oldSettings:
        previous = oldSettings['battery_size']
    else:
        previous = None
    cliSetup('battery_size', 'Battery Ah: (15.4) ', default=15.4, floating=True, previousSetting=previous)
    if 'caution_battery_voltage_threshold' in oldSettings:
        previous = oldSettings['caution_battery_voltage_threshold']
    else:
        previous = None
    cliSetup('caution_battery_voltage_threshold', 'Caution Battery Voltage: (3.8) ', default=3.6, floating=True, previousSetting=previous)
    if 'low_battery_voltage_threshold' in oldSettings:
        previous = oldSettings['low_battery_voltage_threshold']
    else:
        previous = None
    # 2.3.6 cliSetup('low_battery_voltage_threshold', 'Shutdown Battery Voltage: (3.45) ', default=3.45, floating=True, formatter=lambda val: 2.0 if val < 2.0 else val, previousSetting=previous)
    cliSetup('low_battery_voltage_threshold', 'Shutdown Battery Voltage: (3.45) ', default=3.45, floating=True, previousSetting=previous) # 2.3.6
    if 'lora_enabled' in oldSettings:
        previous = oldSettings['lora_enabled']
    else:
        previous = None
    cliSetup('lora_enabled', 'LoRa Enabled: (external LoRa antenna required - y/n) ', boolean=True, default='False', previousSetting=previous)
    if 'lte_enabled' in oldSettings:
        previous = oldSettings['lte_enabled']
    else:
        previous = None
    setLTE, lte_enabled = cliSetup('lte_enabled', 'LTE Enabled: (external LTE antenna required - y/n) ', boolean=True, default='False', previousSetting=previous)
    if lte_enabled == True:
        if 'lte_carrier' in oldSettings:
            previous = oldSettings['lte_carrier']
        else:
            previous = None
        cliSetup('lte_carrier', 'LTE Carrier', previousSetting=previous, options=['standard', 'at&t', 'verizon'], uppercase=False, default='verizon')
    if 'wifi_enabled' in oldSettings:
        previous = oldSettings['wifi_enabled']
    else:
        previous = None
    setWifi, wifi_enabled = cliSetup('wifi_enabled', 'WiFi Enabled: (y/n) ', boolean=True, default='False', previousSetting=previous)
    if FLASH_CONFIG['wifi_enabled']:
        if 'wifi_antenna' in oldSettings:
            previous = oldSettings['wifi_antenna']
        else:
            previous = None
        _, ant = cliSetup('wifi_antenna', 'External WiFi antenna: (y/n) ', boolean=True, default='False', previousSetting=previous)
        if FLASH_CONFIG['wifi_antenna']:
            wlan = WLAN(mode=WLAN.STA, antenna=WLAN.EXT_ANT)
        else:
            wlan = WLAN(mode=WLAN.STA, antenna=WLAN.INT_ANT)
        wifiAccessPoints = wlan.scan()
        wifiAccessPointNames = []
        for a in wifiAccessPoints: wifiAccessPointNames.append(a.ssid)
        if 'wifi_ssid' in oldSettings:
            previousSSID = oldSettings['wifi_ssid']
        else:
            previousSSID = None
        if 'wifi_pass' in oldSettings:
            previousPass = oldSettings['wifi_pass']
        else:
            previousPass = None
        if len(wifiAccessPointNames) != 0:
            setSSID, value = cliSetup('wifi_ssid', 'Available Networks', options=wifiAccessPointNames, default='', uppercase=False, altInput=True, previousSetting=previousSSID)
            if setSSID and value == '':
                cliSetup('wifi_ssid', 'Manual Input SSID: ', overwrite=True, uppercase=False, previousSetting=previousSSID)
                cliSetup('wifi_pass', 'WiFi password (leave blank if none): ', uppercase=False, previousSetting=previousPass)
            elif setSSID and value in wifiAccessPointNames:
                if wifiAccessPoints[wifiAccessPointNames.index(value)].sec >= 3:
                    cliSetup('wifi_pass', 'WiFi password: ', uppercase=False, previousSetting=previousPass)
                else:
                    writeFlashKey('wifi_pass','')
            elif setSSID:
                cliSetup('wifi_pass', 'WiFi password (leave blank if none): ', uppercase=False, previousSetting=previousPass)
        else:
            print("No local WiFi networks found.")
            cliSetup('wifi_ssid', 'Manual Input SSID: ', uppercase=False, previousSetting=previousSSID)
            cliSetup('wifi_pass', 'WiFi password (leave blank if none): ', uppercase=False, previousSetting=previousPass)
    else:
        writeFlashKey('wifi_antenna',False)
    if 'debugging_messages' in oldSettings:
        previousPass = oldSettings['debugging_messages']
    else:
        previous = None
    if 'ap_enabled' in oldSettings:
        previous = oldSettings['ap_enabled']
    else:
        previous = None
    cliSetup('ap_enabled', 'WiFi Access Point Enabled: (broadcast wifi - y/n) ', boolean=True, default='True', previousSetting=previous)
    if 'debugging_messages' in oldSettings:
        previous = oldSettings['debugging_messages']
    else:
        previous = None
    cliSetup('debugging_messages', 'Debugging messages: (y/n) ', boolean=True, default='False', previousSetting=previous)
    if 'offline_mode' in oldSettings:
        previous = oldSettings['offline_mode']
    else:
        previous = None
    cliSetup('offline_mode', 'Offline mode: (y/n) ', boolean=True, default='False', previousSetting=previous)
    print("\nConfigurator task complete.\n")
    print("")

def changeFlashSetting(setting, oldSettings=None):
    suc = clearFlashConfig(setting)
    if suc == False:
        print('{} not found.'.format(setting))
        return
    runFlashSetup(oldSettings)
    res = cliQuestion('Reboot? (y/n) ', boolean=True, default=True)
    if res == True:
        machine.reset()


def resetFlashConfig(oldSettings=None):
    initVRAM()
    clearFlashConfig()
    runFlashSetup(oldSettings)
    machine.reset()
