#include <FastLED.h>

const uint8_t   XRES          = 32;
const uint8_t   YRES          = 8;
const uint16_t  NUM_LEDS      = XRES * YRES;
const uint8_t   BRIGHTNESS    = 255;
const uint8_t   LED_PIN       = 6;

CRGB            leds[NUM_LEDS];

// map x, y to pixel index
uint16_t pixel(uint8_t x, uint8_t y) {
  return NUM_LEDS - XRES - (y * XRES) + x;
}

template <int16_t W, int16_t H>
class FireMatrix {
  private:
    struct Flare {
      int16_t x;
      int16_t y;
      uint8_t heat;
    };
    // width of the fire matrix
    static const int16_t WIDTH  = W > 8 ? W : 8;
    // height of the fire matrix
    static const int16_t HEIGHT = H > 8 ? H : 8;
    // max number of flares
    static const uint8_t FLARES = W / 4;
    // array of flares
    Flare flare[FLARES];
    // fire matrix 4 bit heat points
    uint8_t heatPoint[WIDTH / 2][HEIGHT];
    // number of flares
    uint8_t flares = 0;

    // store a 4 bit heat point
    void putHeat(int16_t x, int16_t y, uint8_t heat) {
      uint8_t h;
      if (x < 0 || x >= WIDTH || y < 0 || y >= HEIGHT) return;
      x >>= 1; // divide x by 2
      h = heatPoint[x][y];
      // if y is odd put heat in lower 4 bits else in upper 4 bits
      h = y & 1 ? (h & 0xf0) | (heat & 0xf) : (h & 0xf) | (heat << 4);
      heatPoint[x][y] = h;
    }

    // retrieve a 4 bit heat point
    uint8_t getHeat(int16_t x, int16_t y) {
      uint8_t h;
      if (x < 0 || x >= WIDTH || y < 0 || y >= HEIGHT) return 0;
      x >>= 1; // divide x by 2
      h = heatPoint[x][y];
      // if y is odd get heat from lower 4 bits else from upper 4 bits
      h = y & 1 ? h & 0xf : (h >> 4) & 0xf;
      return h;
    }

  protected:
    // heat-up flare and update heat points
    void heatFlare(uint8_t index) {
      Flare f = flare[index];
      int16_t b = f.heat * 10 / cooling + 1;
      for (int16_t x = (f.x - b); x < (f.x + b); x++) {
        for (int16_t y = (f.y - b); y < (f.y + b); y++) {
          if (x >= 0 && x < WIDTH && y >= 0 && y < HEIGHT) {
            int16_t d = (cooling * sqrt16((f.x - x) * (f.x - x) + (f.y - y) * (f.y - y)) + 5) / 10;
            uint8_t n = f.heat > d ? f.heat - d : 0;
            if (n > getHeat(x, y)) { // can only get brighter
              putHeat(x, y, n);
            }
          }
        }
      }
    }

    // cool down flare and delete it from flare array if out
    void coolFlare(uint8_t index) {
      Flare f = flare[index];
      if (f.heat > 0) {
        f.heat--;
        flare[index] = f;
      } else {
        // flare is out
        for (int16_t i = index + 1; i < flares; i++) {
          flare[i - 1] = flare[i];
        }
        flares--;
      }
    }

    // try to ignite new flare if there is room in flare array
    void sparkFlare() {
      if (flares < FLARES && random8(100) < sparking) {
        Flare f;
        f.x = random16(0, WIDTH);
        f.y = random16(0, (HEIGHT / 8) + 1);
        f.heat = 10;
        flare[flares] = f;
        flares++;
      }
    }

  public:
    // default palette based on FastLED HeatColors
    CRGBPalette16 palette = CRGBPalette16(
      0x000000, 0x330000, 0x660000, 0x990000, 0xCC0000, 0xFF0000, 0xFF3300, 0xFF6600, 
      0xFF9900, 0xFFCC00, 0xFFFF00, 0xFFFF33, 0xFFFF66, 0xFFFF99, 0xFFFFCC, 0xFFFFFF
      );
    // flame cool-off rate, default 16
    uint8_t cooling = 16;
    // chance of sparking new flame in pecent, default 50
    uint8_t sparking = 50;

    // call periodicly to update fire matrix
    void update() {
      int16_t x, y;
      // move all existing heat points up the display and cool off
      for (y = HEIGHT - 1; y > 0; y--) {
        for (x = 0; x < WIDTH; x++) {
          uint8_t h = getHeat(x, y - 1);
          putHeat(x, y, h > 0 ? h - 1 : 0);
        }
      }
      // heat-up the bottom row
      for (x = 0; x < WIDTH; x++) {
        putHeat(x, 0, random8(5, 9));
      }
      // glow and cool off flares
      for (x = 0; x < flares; x++) {
        // heat-up flare
        heatFlare(x);
        // cool-down flare
        coolFlare(x);
      }
      // try spark a new flare
      sparkFlare();
    }

    // get color from fire matrix
    CRGB color(int16_t x, int16_t y) {
      if (x >=0 && x < WIDTH && y >= 0 && y < WIDTH) {
        return ColorFromPalette(palette, 24 * getHeat(x, y));
      } else {
        return CRGB::Black;
      }
    }
};

uint8_t fps = 60;
FireMatrix<XRES, YRES> fire;

void setup() {
  //Serial.begin(115200);
  FastLED.addLeds<NEOPIXEL, LED_PIN>(leds, NUM_LEDS);
  FastLED.setBrightness(BRIGHTNESS);
  FastLED.clear();
  FastLED.show();
}

void loop() {
  fire.update();
  for (uint8_t x = 0; x < XRES; x++) {
    for (uint8_t y = 0; y < YRES; y++) {
      leds[pixel(x, y)] = fire.color(x, y);
    }
  }
  FastLED.show();
  delay(1000 / fps);
}