/*
   A dot animation that travels along rails
   (C) Voidstar Lab LLC 2021
*/

#ifndef RIPPLE_H_
#define RIPPLE_H_

// WARNING: These slow things down enough to affect performance. Don't turn on unless you need them!
//#define DEBUG_ADVANCEMENT  // Print debug messages about ripples' movement
//#define DEBUG_RENDERING  // Print debug messages about translating logical to actual position

#include "FastLED.h"
#include "mapping.h"

enum rippleState {
  dead,
  withinNode,  // Ripple isn't drawn as it passes through a node to keep the speed consistent
  travelingUpwards,
  travelingDownwards
};

enum rippleBehavior {
  weaksauce = 0,
  feisty = 1,
  angry = 2,
  alwaysTurnsRight = 3,
  alwaysTurnsLeft = 4
};

float fmap(float x, float in_min, float in_max, float out_min, float out_max) {
  return (x - in_min) * (out_max - out_min) / (in_max - in_min) + out_min;
}

class Ripple {
  public:
    Ripple(int id) : rippleId(id) {
      Serial.print("Instanced ripple #");
      Serial.println(rippleId);
    }

    rippleState state = dead;
    unsigned long color;

    /*
       If within a node: 0 is node, 1 is direction
       If traveling, 0 is segment, 1 is LED position from bottom
    */
    int position[2];

    // Place the Ripple in a node
    void start(byte n, byte d, unsigned long c, float s, unsigned long l, byte b) {
      color = c;
      speed = s;
      lifespan = l;
      behavior = b;

      birthday = millis();
      pressure = 0;
      state = withinNode;

      position[0] = n;
      position[1] = d;

      justStarted = true;

      Serial.print("Ripple ");
      Serial.print(rippleId);
      Serial.print(" starting at node ");
      Serial.print(position[0]);
      Serial.print(" direction ");
      Serial.println(position[1]);
    }

    void advance(byte ledColors[40][14][3]) {
      unsigned long age = millis() - birthday;

      if (state == dead)
        return;

      pressure += fmap(float(age), 0.0, float(lifespan), speed, 0.0);  // Ripple slows down as it ages
      // TODO: Motion of ripple is severely affected by loop speed. Make it time invariant

      if (pressure < 1 && (state == travelingUpwards || state == travelingDownwards)) {
        // Ripple is visible but hasn't moved - render it to avoid flickering
        renderLed(ledColors, age);
      }

      while (pressure >= 1) {
#ifdef DEBUG_ADVANCEMENT
        Serial.print("Ripple ");
        Serial.print(rippleId);
        Serial.println(" advancing:");
#endif

        switch (state) {
          case withinNode: {
              if (justStarted) {
                justStarted = false;
              }
              else {
#ifdef DEBUG_ADVANCEMENT
                Serial.print("  Picking direction out of node ");
                Serial.print(position[0]);
                Serial.print(" with agr. ");
                Serial.println(behavior);
#endif

                int newDirection = -1;

                int sharpLeft = (position[1] + 1) % 6;
                int wideLeft = (position[1] + 2) % 6;
                int forward = (position[1] + 3) % 6;
                int wideRight = (position[1] + 4) % 6;
                int sharpRight = (position[1] + 5) % 6;

                if (behavior <= 2) {  // Semi-random aggressive turn mode
                  // The more aggressive a ripple, the tighter turns it wants to make.
                  // If there aren't any segments it can turn to, we need to adjust its behavior.
                  byte anger = behavior;

                  while (newDirection < 0) {
                    if (anger == 0) {
                      int forwardConnection = nodeConnections[position[0]][forward];

                      if (forwardConnection < 0) {
                        // We can't go straight ahead - we need to take a more aggressive angle
#ifdef DEBUG_ADVANCEMENT
                        Serial.println("  Can't go straight - picking more agr. path");
#endif
                        anger++;
                      }
                      else {
#ifdef DEBUG_ADVANCEMENT
                        Serial.println("  Going forward");
#endif
                        newDirection = forward;
                      }
                    }

                    if (anger == 1) {
                      int leftConnection = nodeConnections[position[0]][wideLeft];
                      int rightConnection = nodeConnections[position[0]][wideRight];

                      if (leftConnection >= 0 && rightConnection >= 0) {
#ifdef DEBUG_ADVANCEMENT
                        Serial.println("  Turning left or right at random");
#endif
                        newDirection = random(2) ? wideLeft : wideRight;
                      }
                      else if (leftConnection >= 0) {
#ifdef DEBUG_ADVANCEMENT
                        Serial.println("  Can only turn left");
#endif
                        newDirection = wideLeft;
                      }
                      else if (rightConnection >= 0) {
#ifdef DEBUG_ADVANCEMENT
                        Serial.println("  Can only turn right");
#endif
                        newDirection = wideRight;
                      }
                      else {
#ifdef DEBUG_ADVANCEMENT
                        Serial.println("  Can't make wide turn - picking more agr. path");
#endif
                        anger++;  // Can't take shallow turn - must become more aggressive
                      }
                    }

                    if (anger == 2) {
                      int leftConnection = nodeConnections[position[0]][sharpLeft];
                      int rightConnection = nodeConnections[position[0]][sharpRight];

                      if (leftConnection >= 0 && rightConnection >= 0) {
#ifdef DEBUG_ADVANCEMENT
                        Serial.println("  Turning left or right at random");
#endif
                        newDirection = random(2) ? sharpLeft : sharpRight;
                      }
                      else if (leftConnection >= 0) {
#ifdef DEBUG_ADVANCEMENT
                        Serial.println("  Can only turn left");
#endif
                        newDirection = sharpLeft;
                      }
                      else if (rightConnection >= 0) {
#ifdef DEBUG_ADVANCEMENT
                        Serial.println("  Can only turn right");
#endif
                        newDirection = sharpRight;
                      }
                      else {
#ifdef DEBUG_ADVANCEMENT
                        Serial.println("  Can't make tight turn - picking less agr. path");
#endif
                        anger--;  // Can't take tight turn - must become less aggressive
                      }
                    }

                    // Note that this can't handle some circumstances,
                    // like a node with segments in nothing but the 0 and 3 positions.
                    // Good thing we don't have any of those!
                  }
                }
                else if (behavior == alwaysTurnsRight) {
                  for (int i = 1; i < 6; i++) {
                    int possibleDirection = (position[1] + i) % 6;

                    if (nodeConnections[position[0]][possibleDirection] >= 0) {
                      newDirection = possibleDirection;
                      break;
                    }
                  }

#ifdef DEBUG_ADVANCEMENT
                  Serial.println("  Turning as rightward as possible");
#endif
                }
                else if (behavior == alwaysTurnsLeft) {
                  for (int i = 5; i >= 1; i--) {
                    int possibleDirection = (position[1] + i) % 6;

                    if (nodeConnections[position[0]][possibleDirection] >= 0) {
                      newDirection = possibleDirection;
                      break;
                    }
                  }

#ifdef DEBUG_ADVANCEMENT
                  Serial.println("  Turning as leftward as possible");
#endif
                }

#ifdef DEBUG_ADVANCEMENT
                Serial.print("  Leaving node ");
                Serial.print(position[0]);
                Serial.print(" in direction ");
                Serial.println(newDirection);
#endif

                position[1] = newDirection;
              }

              position[0] = nodeConnections[position[0]][position[1]];  // Look up which segment we're on

#ifdef DEBUG_ADVANCEMENT
              Serial.print("  and entering segment ");
              Serial.println(position[0]);
#endif

              if (position[1] == 5 || position[1] == 0 || position[1] == 1) {  // Top half of the node
#ifdef DEBUG_ADVANCEMENT
                Serial.println("  (starting at bottom)");
#endif
                state = travelingUpwards;
                position[1] = 0;  // Starting at bottom of segment
              }
              else {
#ifdef DEBUG_ADVANCEMENT
                Serial.println("  (starting at top)");
#endif
                state = travelingDownwards;
                position[1] = 13; // Starting at top of 14-LED-long strip
              }
              break;
            }

          case travelingUpwards: {
              position[1]++;

              if (position[1] >= 14) {
                // We've reached the top!
#ifdef DEBUG_ADVANCEMENT
                Serial.print("  Reached top of seg. ");
                Serial.println(position[0]);
#endif
                // Enter the new node.
                int segment = position[0];
                position[0] = segmentConnections[position[0]][0];
                for (int i = 0; i < 6; i++) {
                  // Figure out from which direction the ripple is entering the node.
                  // Allows us to exit in an appropriately aggressive direction.
                  int incomingConnection = nodeConnections[position[0]][i];
                  if (incomingConnection == segment)
                    position[1] = i;
                }
#ifdef DEBUG_ADVANCEMENT
                Serial.print("  Entering node ");
                Serial.print(position[0]);
                Serial.print(" from direction ");
                Serial.println(position[1]);
#endif
                state = withinNode;
              }
              else {
#ifdef DEBUG_ADVANCEMENT
                Serial.print("  Moved up to seg. ");
                Serial.print(position[0]);
                Serial.print(" LED ");
                Serial.println(position[1]);
#endif
              }
              break;
            }

          case travelingDownwards: {
              position[1]--;
              if (position[1] < 0) {
                // We've reached the bottom!
#ifdef DEBUG_ADVANCEMENT
                Serial.print("  Reached bottom of seg. ");
                Serial.println(position[0]);
#endif
                // Enter the new node.
                int segment = position[0];
                position[0] = segmentConnections[position[0]][1];
                for (int i = 0; i < 6; i++) {
                  // Figure out from which direction the ripple is entering the node.
                  // Allows us to exit in an appropriately aggressive direction.
                  int incomingConnection = nodeConnections[position[0]][i];
                  if (incomingConnection == segment)
                    position[1] = i;
                }
#ifdef DEBUG_ADVANCEMENT
                Serial.print("  Entering node ");
                Serial.print(position[0]);
                Serial.print(" from direction ");
                Serial.println(position[1]);
#endif
                state = withinNode;
              }
              else {
#ifdef DEBUG_ADVANCEMENT
                Serial.print("  Moved down to seg. ");
                Serial.print(position[0]);
                Serial.print(" LED ");
                Serial.println(position[1]);
#endif
              }
              break;
            }

          default:
            break;
        }

        pressure -= 1;

        if (state == travelingUpwards || state == travelingDownwards) {
          // Ripple is visible - render it
          renderLed(ledColors, age);
        }
      }

#ifdef DEBUG_ADVANCEMENT
      Serial.print("  Age is now ");
      Serial.print(age);
      Serial.print('/');
      Serial.println(lifespan);
#endif

      if (lifespan && age >= lifespan) {
        // We dead
#ifdef DEBUG_ADVANCEMENT
        Serial.println("  Lifespan is up! Ripple is dead.");
#endif
        state = dead;
        position[0] = position[1] = pressure = age = 0;
      }
    }

  private:
    float speed;  // Each loop, ripples move this many LED's.
    unsigned long lifespan;  // The ripple stops after this many milliseconds

    /*
       0: Always goes straight ahead if possible
       1: Can take 60-degree turns
       2: Can take 120-degree turns
    */
    byte behavior;

    bool justStarted = false;

    float pressure;  // When Pressure reaches 1, ripple will move
    unsigned long birthday;  // Used to track age of ripple

    static byte rippleCount;  // Used to give them unique ID's
    byte rippleId;  // Used to identify this ripple in debug output

    void renderLed(byte ledColors[40][14][3], unsigned long age) {
      int strip = ledAssignments[position[0]][0];
      int led = ledAssignments[position[0]][2] + position[1];
      FL_UNUSED(strip);
      FL_UNUSED(led);

      int red = ledColors[position[0]][position[1]][0];
      int green = ledColors[position[0]][position[1]][1];
      int blue = ledColors[position[0]][position[1]][2];

      ledColors[position[0]][position[1]][0] = byte(fl::min(255, fl::max(0, int(fmap(float(age), 0.0, float(lifespan), (color >> 8) & 0xFF, 0.0)) + red)));
      ledColors[position[0]][position[1]][1] = byte(fl::min(255, fl::max(0, int(fmap(float(age), 0.0, float(lifespan), (color >> 16) & 0xFF, 0.0)) + green)));
      ledColors[position[0]][position[1]][2] = byte(fl::min(255, fl::max(0, int(fmap(float(age), 0.0, float(lifespan), color & 0xFF, 0.0)) + blue)));

#ifdef DEBUG_RENDERING
      Serial.print("Rendering ripple position (");
      Serial.print(position[0]);
      Serial.print(',');
      Serial.print(position[1]);
      Serial.print(") at Strip ");
      Serial.print(strip);
      Serial.print(", LED ");
      Serial.print(led);
      Serial.print(", color 0x");
      for (int i = 0; i < 3; i++) {
        if (ledColors[position[0]][position[1]][i] <= 0x0F)
          Serial.print('0');
        Serial.print(ledColors[position[0]][position[1]][i], HEX);
      }
      Serial.println();
#endif
    }
};

#endif
