// Project: pa night-light
// Copyright (c) 2023-2024, Framework Labs

#include "proto_activities.h"

#include <Adafruit_NeoPixel.h>

// Types

/// The top-level modes of the system.
enum class Mode {
  OFF, STANDBY, ON
};

// Common activities

/// Stops after the given number of ticks.
pa_activity (Delay, pa_ctx(unsigned i), unsigned ticks) {
  pa_self.i = ticks;
  while (pa_self.i > 0) {
    --pa_self.i;
    pa_pause;
  }
} pa_end;

/// Detects when `value` changes and reports it in the output parameter.
pa_activity (ChangeDetector, pa_ctx(int prevValue), int value, bool& didChange) {
    didChange = true;
    pa_self.prevValue = value;
    pa_pause;

    pa_always {
      didChange = pa_self.prevValue != value;
      pa_self.prevValue = value;
    } pa_always_end;
} pa_end;

/// Detects the rising edge of the signal.
pa_activity (RisingEdgeDetector, pa_ctx(), bool signal, bool& edge) {
  edge = false;
  pa_every (signal) {
    edge = true;
    pa_pause;
    edge = false;
    pa_await (!signal);
  } pa_every_end;
} pa_end

/// Extends a detected event for the given duration.
pa_activity (EventExtender, pa_ctx(pa_use(Delay)), bool event, unsigned int duration, bool& prolongedEvent) {
  prolongedEvent = false;
  pa_every (event) {
    prolongedEvent = true;
    pa_when_reset (event, Delay, duration);
    prolongedEvent = false;
  } pa_every_end;
} pa_end;

// EMA filter

static int ema(int value, int average, float alpha) {
  return static_cast<int>(alpha * value + (1 - alpha) * average);
}

/// A simple Exponential-Moving-Average low pass filter.
pa_activity (EMAFilter, pa_ctx(int average), int inValue, float alpha, int& outValue) {
  outValue = pa_self.average = inValue;
  pa_pause;

  pa_always {
    outValue = pa_self.average = ema(inValue, pa_self.average, alpha);
  } pa_always_end;
} pa_end;

// Button

static constexpr int BUTTON_PIN = 4;

static void initButton() {
  pinMode(BUTTON_PIN, INPUT_PULLUP);
}

static bool readButton() {
  return digitalRead(BUTTON_PIN) == LOW;
}

/// Continuously reads and returns the value of the button pin.
pa_activity (ButtonReader, pa_ctx(), bool& isPressed) {
  initButton();

  pa_always {
    isPressed = readButton();
  } pa_always_end;
} pa_end;

/// Returns the rising edge of a button press.
pa_activity (ButtonRecognizer, pa_ctx(pa_co_res(2); pa_use(ButtonReader); pa_use(RisingEdgeDetector); bool rawPressed), 
                               bool& isPressed) {
  pa_co(2) {
    pa_with (ButtonReader, pa_self.rawPressed);
    pa_with (RisingEdgeDetector, pa_self.rawPressed, isPressed);
  } pa_co_end;
} pa_end;

// Indicator Led

static constexpr int LED_PIN = 2;

static void initLed() {
  pinMode(LED_PIN, OUTPUT);
}

static void setLed() {
  digitalWrite(LED_PIN, HIGH);
}

static void clearLed() {
  digitalWrite(LED_PIN, LOW);
}

/// Blinks the led with given on-tick and off-tick intervals.
pa_activity (Blinker, pa_ctx(pa_use(Delay)), 
                      unsigned onTicks, unsigned offTicks) {
  initLed();

  pa_repeat {
    setLed();
    pa_run (Delay, onTicks);

    clearLed();
    pa_run (Delay, offTicks);
  }
} pa_end;

// Photon Sensor

static constexpr int PHOTON_PIN = 14;

static void initPhotoResistor() {
  pinMode(PHOTON_PIN, INPUT);
}

static int readPhotoResistor() {
  return analogRead(PHOTON_PIN);
}

/// Reads and filters the photoresistor.
pa_activity (PhotonReader, pa_ctx(), int& level) {
  initPhotoResistor();

  pa_always {
    const auto value = readPhotoResistor();
    level = map(value, 0, 1023, 0, 31);
  } pa_always_end;
} pa_end;

/// Returns true if a threshold is suprassed.
pa_activity (PhotonThresholder, pa_ctx(), int level, int threshold, bool& triggered) {
  pa_always {
    triggered = level <= threshold;
  } pa_always_end;
} pa_end;

/// Reads and filters the photo-ressistor and returns true if the threshold is surpassed.
pa_activity (PhotonSensor, pa_ctx(pa_co_res(3); pa_use(PhotonReader); pa_use(EMAFilter); pa_use(PhotonThresholder); int rawLevel; int level), 
                           bool& triggered) {
  pa_co(3) {
    pa_with (PhotonReader, pa_self.rawLevel);
    pa_with (EMAFilter, pa_self.rawLevel, 0.4, pa_self.level);
    pa_with (PhotonThresholder, pa_self.level, 15, triggered);
  } pa_co_end;
} pa_end;

// Brightness Dial

static constexpr int DIAL_PIN = 15;

static void initPotentiometer() {
  pinMode(DIAL_PIN, INPUT);
}

static int readPhotentiometer() {
  return analogRead(DIAL_PIN);
}

/// Reads the Potentiometer.
pa_activity (BrightnessReader, pa_ctx(), int& brightness) {
  initPotentiometer();

  pa_always {
    const auto value = readPhotentiometer();
    brightness = map(value, 0, 4095, 0, 255);
  } pa_always_end;
} pa_end;

// Reads and filters the Potentiometer value.
pa_activity (BrightnessDial, pa_ctx(pa_co_res(2); pa_use(BrightnessReader); pa_use(EMAFilter); int rawBrightness), int& brightness) {
  pa_co(2) {
    pa_with (BrightnessReader, pa_self.rawBrightness);
    pa_with (EMAFilter, pa_self.rawBrightness, 0.4, brightness);
  } pa_co_end;
} pa_end;

// Light Ring

static constexpr int RING_PIN = 12;

static Adafruit_NeoPixel strip = Adafruit_NeoPixel(16, RING_PIN, NEO_GRB + NEO_KHZ800);

static void initStrip() {
  pinMode(RING_PIN, OUTPUT);
  strip.begin();
}

/// Calculates the brightness for the light from the ambient light, the day-night condition and if the user is turning the dial.
pa_activity (LightBrightnessCalculator, pa_ctx(), int ambientBrightness, bool isDay, bool isChanging, int& lightBrightness) {
  pa_always {
    if (isChanging) {
      lightBrightness = ambientBrightness;
    } else if (isDay) {
      lightBrightness = 0;
    } else {
      lightBrightness = ambientBrightness;
    }
  } pa_always_end;
} pa_end;

/// Drives the LED strip with the given brightness.
pa_activity (LightDriver, pa_ctx(int prevBrightness), int brightness) {
  initStrip();

  pa_repeat {
    strip.setBrightness(brightness);
    for (uint16_t i = 0; i < strip.numPixels(); ++i) {
      strip.setPixelColor(i, strip.Color(255, 255, 255));
    }
    strip.show();

    pa_self.prevBrightness = brightness;
    pa_await (brightness != pa_self.prevBrightness);
  }
} pa_end;

/// Manages the Light by detecting if the user is changing the potentiometer and sets the light according
/// to the ambient brightness and the day-night situation.
pa_activity (Light, pa_ctx(pa_co_res(4); pa_use(ChangeDetector); pa_use(EventExtender); pa_use(LightBrightnessCalculator); pa_use(LightDriver); 
                           bool didChange; bool isChanging; int lightBrightness), 
                    int ambientBrightness, bool isDay) {
  pa_co(4) {
    pa_with (ChangeDetector, ambientBrightness, pa_self.didChange);
    pa_with (EventExtender, pa_self.didChange, 20, pa_self.isChanging);
    pa_with (LightBrightnessCalculator, ambientBrightness, isDay, pa_self.isChanging, pa_self.lightBrightness);
    pa_with (LightDriver, pa_self.lightBrightness);
  } pa_co_end;
} pa_end;

static void clearLight() {
  strip.setBrightness(0);
  strip.show();
}

// Mode Controller

/// Maps `isDay` bool to `Mode`.
pa_activity (ModeMapper, pa_ctx(), bool isDay, Mode& mode) {
  pa_always {
    mode = isDay ? Mode::STANDBY : Mode::ON;
  } pa_always_end;
} pa_end;

/// Controls the ON mode by reading the sensors, and controling the light brightness with their values.
pa_activity (OnModeController, pa_ctx(pa_co_res(4); pa_use(Light); pa_use(BrightnessDial); pa_use(PhotonSensor); pa_use(ModeMapper);
                                      int brightness; bool isDay),
                               Mode& mode) {
  pa_co(4) {
    pa_with (PhotonSensor, pa_self.isDay);
    pa_with (BrightnessDial, pa_self.brightness);
    pa_with (Light, pa_self.brightness, pa_self.isDay);
    pa_with (ModeMapper, pa_self.isDay, mode);
  } pa_co_end;
} pa_end;

/// Controlls the OFF mode by shutting off the light.
pa_activity (OffModeController, pa_ctx()) {
  clearLight();
  pa_halt;
} pa_end;

/// Toggles between ON mode and OFF mode on button press.
pa_activity (ModeController, pa_ctx(pa_use(OnModeController); pa_use(OffModeController)), 
                             bool isPressed, Mode& mode) {
  pa_repeat {
    pa_when_abort (isPressed, OnModeController, mode);

    mode = Mode::OFF;
    pa_when_abort (isPressed, OffModeController);
  }
} pa_end;

// Mode Indicator

/// Applies the mode to the indication LED.
pa_activity (ModeIndicationApplier, pa_ctx(pa_use(Blinker)), Mode mode) {
  if (mode == Mode::OFF) {
    clearLed();
    pa_halt;
  } else if (mode == Mode::STANDBY) {
    pa_run (Blinker, 10, 10);
  } else { // mode == Mode::ON
    setLed();
    pa_halt;
  }
} pa_end;

/// Drives an LED to indicate the mode.
pa_activity (ModeIndicator, pa_ctx(pa_use(ModeIndicationApplier); Mode prevMode), Mode mode) {
  initLed();

  pa_repeat {
    pa_self.prevMode = mode;
    pa_when_abort (mode != pa_self.prevMode, ModeIndicationApplier, mode);
  }
} pa_end;

// Main

/// The main activity which sets up the button recognizer, mode-controller and mode-indicator.
pa_activity (Main, pa_ctx(pa_use(ButtonRecognizer); pa_use(ModeController); pa_use(ModeIndicator);
                          pa_co_res(3); bool isPressed; Mode mode)) {
  pa_co(3) {
    pa_with (ButtonRecognizer, pa_self.isPressed);
    pa_with (ModeController, pa_self.isPressed, pa_self.mode);
    pa_with (ModeIndicator, pa_self.mode);
  } pa_co_end;
} pa_end;

pa_use(Main);

// Setup and Loop

void setup() {
  Serial.begin(9600);
}

void loop() {
  pa_tick(Main);
  delay(100); // 10Hz
}