"""
plotters_combined_with_scd30.py

Combines all the plotters into one program, recording values over a 24hr period (by default).
Allows you to switch page by waving your hand over the screen.
Plotting PMS5003 and SCD30 is likely to use more memory than is available on Feather M4 (192kB) -
Feather nRF52840 Express (256kB) works well.
"""
interval = 540 # full screen of reading spans 24hrs
#interval = 1 # uncomment for 1 reading per second
#interval = 60 # uncomment for 1 reading per minute
#interval = 3600 # uncomment for 1 reading per hour

# the higher the threshold value the less sensitive, we've found this to be a good default through testing
mic_threshold = 3100

# the threshold for the proximity detection, the higher the less sensitive
prox_threshold = 100

# Setup
import time
import math
import gc

import adafruit_bme280
import analogio
import board
import busio
import displayio
import pulseio
import terminalio
from adafruit_display_text import label

import pimoroni_physical_feather_pins
from pimoroni_circuitpython_adapter import not_SMBus
from pimoroni_envirowing import gas, screen
from pimoroni_envirowing.screen import plotter
from pimoroni_ltr559 import LTR559
from pimoroni_pms5003 import PMS5003

# optional Adafruit library for SCD30
try:
    from adafruit_scd30 import SCD30
except ImportError:
    pass

#print("(", gc.mem_free(), ")")

# set up the connection with the bme280
i2c = busio.I2C(board.SCL, board.SDA)
bme280 = adafruit_bme280.Adafruit_BME280_I2C(i2c, address=0x76)

# average global sea level pressure, for more accurate readings change this to your local sea level pressure (measured in hPa)
bme280.sea_level_pressure = 1013.25

# set up the pms5003
pms5003 = PMS5003()
try:
    pms5003.read()
    is_pms5003 = True
except Exception as e:
    print(e)
    print("You probably don't have a pms5003 connected, continuing without particulate logging")
    is_pms5003 = False

# set up the scd30
try:
    pressure = bme280.pressure
    last_pressure = round(pressure)
    scd30 = SCD30(i2c, ambient_pressure=last_pressure)
    is_scd30 = True
except (NameError, ValueError, OSError) as ex:
    print(ex)
    print("You probably don't have an scd30 connected, continuing without CO2 logging")
    is_scd30 = False

# set up connection with the ltr559
i2c_dev = not_SMBus(I2C=i2c)
ltr559 = LTR559(i2c_dev=i2c_dev)

# setup screen
screen = screen.Screen(backlight_control=False)

# define our pwm pin (for changing the screen brightness)
pwm = pulseio.PWMOut(pimoroni_physical_feather_pins.pin21())

# start the screen at 50% brightness
pwm.duty_cycle = 2**15

# set up mic input
mic = analogio.AnalogIn(pimoroni_physical_feather_pins.pin8())

# colours for the plotter are defined as rgb values in hex, with 2 bytes for each colour
red = 0xFF0000
green = 0x00FF00
blue = 0x0000FF

# Setup bme280 screen plotter
# the max value is set to 70 as it is the screen height in pixels after the labels (top_space) (this is just to make a calculation later on easier)
bme280_splotter = plotter.ScreenPlotter([red, green, blue, red+green+blue], max_value=70, min_value=0, top_space=10, display=screen)

# add a colour coded text label for each reading
bme280_splotter.group.append(label.Label(terminalio.FONT, text="{:0.1f} C".format(bme280.temperature), color=red, x=0, y=5, max_glyphs=15))
bme280_splotter.group.append(label.Label(terminalio.FONT, text="{:0.1f} hPa".format(bme280.pressure), color=green, x=50, y=5, max_glyphs=15))
bme280_splotter.group.append(label.Label(terminalio.FONT, text="{:0.1f} %".format(bme280.humidity), color=blue, x=120, y=5, max_glyphs=15))
#bme280_splotter.group.append(label.Label(terminalio.FONT, text="{:0.2f} m".format(bme280.altitude), color=red+green+blue, x=40, y=20, max_glyphs=15)) # uncomment for altitude estimation

# if the pms5003 is connected
if is_pms5003:
    # Set up the pms5003 screen plotter
    # the max value is set to 1000 as a nice number to work with
    pms5003_splotter = plotter.ScreenPlotter([green, blue, red+blue+green], max_value=1000, min_value=0, top_space=10, display=screen)

    # add a colour coded text label for each reading
    pms5003_splotter.group.append(label.Label(terminalio.FONT, text="PM2.5: {:d}", color=green, x=0, y=5, max_glyphs=15))
    pms5003_splotter.group.append(label.Label(terminalio.FONT, text="PM10: {:d}", color=blue, x=80, y=5, max_glyphs=15))
    #pms5003_splotter.group.append(label.Label(terminalio.FONT, text="PM1.0: {:d}", color=red, x=0, y=20, max_glyphs=15)) # uncomment to enable PM1.0 measuring

    # red line for the WHO guideline (https://en.wikipedia.org/wiki/Air_quality_guideline)
    # made in a new bitmap so it doesn't get overwritten
    pms5003_splotter.redline_bm = displayio.Bitmap(160, 1, 1)
    pms5003_splotter.redline_pl = displayio.Palette(1)
    pms5003_splotter.redline_pl[0] = red
    pms5003_splotter.redline_tg = displayio.TileGrid(pms5003_splotter.redline_bm, pixel_shader=pms5003_splotter.redline_pl, x=0, y=44)
    pms5003_splotter.group.append(pms5003_splotter.redline_tg)

# if an scd30 is connected
if is_scd30:
    # Set up the scd30 screen plotter
    scd30_splotter = plotter.ScreenPlotter([green], max_value=3000, min_value=0, top_space=10, display=screen)

    # add a colour coded text label for each reading
    scd30_splotter.group.append(label.Label(terminalio.FONT, text="CO2: {:.0f} ppm", color=green, x=0, y=5, max_glyphs=15))

# Set up the gas screen plotter
# the max value is set to 3.3 as its the max voltage the feather can read
gas_splotter = plotter.ScreenPlotter([red, green, blue], max_value=3.3, min_value=0.5, top_space=10, display=screen)

# add a colour coded text label for each reading
gas_splotter.group.append(label.Label(terminalio.FONT, text="OX: {:.0f}", color=red, x=0, y=5, max_glyphs=15))
gas_splotter.group.append(label.Label(terminalio.FONT, text="RED: {:.0f}", color=green, x=50, y=5, max_glyphs=15))
gas_splotter.group.append(label.Label(terminalio.FONT, text="NH3: {:.0f}", color=blue, x=110, y=5, max_glyphs=15))

# from https://stackoverflow.com/a/49955617
def human_format(num, round_to=0):
    magnitude = 0
    while abs(num) >= 1000:
        magnitude += 1
        num = round(num / 1000.0, round_to)
    return '{:.{}f}{}'.format(round(num, round_to), round_to, ['', 'K', 'M', 'G', 'T', 'P'][magnitude])

# set up the light&sound plotter
lightandsound_splotter = plotter.ScreenPlotter([green, blue], display=screen, max_value=1, top_space=10)

# add a colour coded text label for each reading
lightandsound_splotter.group.append(label.Label(terminalio.FONT, text="Sound", color=green, x=0, y=5, max_glyphs=15))
lightandsound_splotter.group.append(label.Label(terminalio.FONT, text="Light", color=blue, x=80, y=5, max_glyphs=15))

# record the time that the sampling starts
last_reading = time.monotonic()
last_sec = time.monotonic()

# initialise counters at 0
reading = 0
readings = 0

# init the available pages
available_pages = {
    1: bme280_splotter,
    2: gas_splotter,
    3: lightandsound_splotter
}

# if pms5003 detected, add to available pages
if is_pms5003:
    available_pages.update({0:pms5003_splotter})

# if scd30 detected, add to available pages
if is_scd30:
    available_pages.update({4:scd30_splotter})

current_page = 3

# create a generator that will give the next page index, looping back to 0 once it reaches the end
def page_turner(available_pages):
    while True:
        for i in sorted(available_pages.keys()):
            yield i

# init the generator
page = page_turner(available_pages)

#print("(", gc.mem_free(), ")")

while True:
    # if 1 second has passed
    if last_sec + 1 < time.monotonic():

        # take the light reading
        lux = ltr559.get_lux()

        # take a proximity reading
        prox = ltr559.get_proximity()

        # if proximity confidence above threshold, change page
        if prox > prox_threshold:
            current_page = next(page)
            available_pages[current_page].draw(full_refresh=True, show=True)
        else:
            # change screen brightness according to the amount of light detected
            pwm.duty_cycle = int(min(lightandsound_splotter.remap(lux, 0, 400, 0, (2**16 - 1)), (2**16 - 1)))

        # if interval time has passed since last reading
        if last_reading + interval < time.monotonic():

            # take readings
            temperature = bme280.temperature
            pressure = bme280.pressure
            humidity = bme280.humidity
            #altitude = bme280.altitude # uncomment for altitude estimation

            # update the line graph
            bme280_splotter.update(
                # scale to 70 as that's the number of pixels height available
                bme280_splotter.remap(temperature, 0, 50, 0, 70),
                bme280_splotter.remap(pressure, 975, 1025, 0, 70),
                bme280_splotter.remap(humidity, 0, 100, 0, 70),
                #bme280_splotter.remap(altitude, 0, 1000, 0, 70), # uncomment for altitude estimation
                draw=False
            )

            # update the labels
            bme280_splotter.group[1].text = "{:0.1f} C".format(temperature)
            bme280_splotter.group[2].text = "{:0.1f} hPa".format(pressure)
            bme280_splotter.group[3].text = "{:0.1f} %".format(humidity)
            #bme280_splotter.group[4].text = "{:0.2f} m".format(altitude) # uncomment for altitude estimation

            gc.collect()

            # take readings
            gas_reading = gas.read_all()

            # update the line graph
            # the value plotted on the graph is the voltage drop over each sensor, not the resistance, as it graphs nicer
            gas_splotter.update(
                gas_reading._OX.value * (gas_reading._OX.reference_voltage/65535),
                gas_reading._RED.value * (gas_reading._RED.reference_voltage/65535),
                gas_reading._NH3.value * (gas_reading._NH3.reference_voltage/65535),
                draw=False
            )

            # update the labels
            gas_splotter.group[1].text = "OX:{}".format(human_format(gas_reading.oxidising))
            gas_splotter.group[2].text = "RED:{}".format(human_format(gas_reading.reducing))
            gas_splotter.group[3].text = "NH3:{}".format(human_format(gas_reading.nh3))

            gc.collect()

            # get the sound readings (number of samples over the threshold / total number of samples taken) and apply a logarithm function to them to represent human hearing
            sound_level = math.log((reading/readings) + 1, 2)
            # weight the result using a -2x^3 + 3x^2 curve to emphasise changes around the midpoint (0.5)
            sound_level = (-2* sound_level**3 + 3* sound_level**2)
            # then weight the result using a x^3 - 3x^2 + 3x curve to make the results fill the graph and not sit at the bottom
            sound_level = (sound_level**3 - 3*sound_level**2 + 3*sound_level)

            # update the line graph
            lightandsound_splotter.update(
                sound_level,
                lightandsound_splotter.remap(lux, 0, 1000, 0, 1),
                draw=False
            )

            # update the labels
            lightandsound_splotter.group[1].text = "Sound: {:1.2f}".format(sound_level)
            lightandsound_splotter.group[2].text = "Light: {:.0f}".format(lux)

            gc.collect()

            if is_pms5003:
                # take readings
                pms_reading = pms5003.read()
                pm2 = pms_reading.data[1]
                pm10 = pms_reading.data[2]
                #pm1 = pms_reading.data[0] # uncomment to enable PM1.0 measuring

                # update the line graph
                pms5003_splotter.update(
                    pms5003_splotter.remap(pm2, 0, 50, 0, 1000),
                    pms5003_splotter.remap(pm10, 0, 100, 0, 1000),
                    #pms5003_splotter.remap(pm1, 0, 100, 0, 1000), # uncomment to enable PM1.0 measuring
                    draw=False
                )

                # update the labels
                pms5003_splotter.group[1].text = "PM2.5: {:d}".format(pm2)
                pms5003_splotter.group[2].text = "PM10: {:d}".format(pm10)
                #pms5003_splotter.group[3].text = "PM1.0: {:d}".format(pm1) # uncomment to enable PM1.0 measuring

                gc.collect()

            if is_scd30:
                # take readings
                if last_pressure != round(pressure):
                    last_pressure = round(pressure)
                    scd30.ambient_pressure = last_pressure
                co2ppm = scd30.CO2
                scd30_splotter.update(co2ppm,
                                      draw=False)
                scd30_splotter.group[1].text = "CO2: {:.0f} ppm".format(co2ppm)

            available_pages[current_page].draw()

            # reset the sound readings counters
            reading = 0
            readings = 0
            # record the time that this reading was taken
            last_reading = time.monotonic()

            #print("(", gc.mem_free(), ")")

        # update the last_sec time
        last_sec = time.monotonic()

    # take a sample of the current mic voltage
    sample = abs(mic.value - 32768)
    readings += 1
    if sample > mic_threshold:
        reading += 1