/*
   This code implements the control of an 4x5 led-matrix. An ATtiny44 controls it with 10 transistors.
   The µC is battery powered, so it sleeps until it is activated by a hardware interrupt with a button.
   ATtiny44, internal clock 1 Mhz
   Felix Husemann, Dec 2020
*/

#include <avr/sleep.h>
#include <avr/interrupt.h>
#include <avr/io.h>
#include <avr/pgmspace.h>

#define button        0
#define balls         6
#define s1            7
#define s2            8
#define s3            9
#define s4            10
#define z1            4
#define z2            3
#define z3            2
#define z4            1
#define z5            5
#define perRow        2000  // Time per line in µs
#define perColumn     100   // Time per column in ms, change this if text is moving to fast
#define holdEnd       5000
#define BRIGHTNESS    5     // Speed of changing the brightness
#define BEAT          13
#define SPEEDDROP     150   // Speed of the snowflakes falling down
#define LETITSNOW     30000 // Duration of the snowing
#define SNOWING_ON    true

volatile unsigned long timer1 = 0;
volatile unsigned long timer2 = 0;
unsigned long timerBlink = 0;
unsigned int timeBlink = random(400, 1000);
bool blinking = false;
bool rising = true;

const int columns[] = {7, 8, 9, 10};
const int rows[] = {4, 3, 2, 1, 5};

/*
   Here the letters are stored. Each 8-bit boolean represents one line of the matrix, so 5 8-bit booleans are one letter.
   (-> Each line in this format is one letter. One 0b00000000 after each letter is needed)
*/
// INSERT COPIED DATA FROM PYTHON SCRIPT HERE ------------------------------------------------
const char data1[] PROGMEM = {
  0b11100000,            0b01000000,            0b01000000,            0b01000000,            0b11100000,            0b00000000,
  0b10010000,            0b11010000,            0b10110000,            0b10010000,            0b10010000,            0b00000000,
  0b01110000,            0b10000000,            0b01100000,            0b00010000,            0b11100000,            0b00000000,
  0b11110000,            0b01000000,            0b01000000,            0b01000000,            0b01000000,            0b00000000,
  0b11100000,            0b10010000,            0b11100000,            0b10100000,            0b10010000,            0b00000000,
  0b10010000,            0b10010000,            0b10010000,            0b10010000,            0b01100000,            0b00000000,
  0b01110000,            0b10000000,            0b10000000,            0b10000000,            0b01110000,            0b00000000,
  0b11110000,            0b01000000,            0b01000000,            0b01000000,            0b01000000,            0b00000000,
  0b01100000,            0b10010000,            0b11110000,            0b10010000,            0b10010000,            0b00000000,
  0b11100000,            0b10010000,            0b11100000,            0b10010000,            0b11100000,            0b00000000,
  0b10000000,            0b10000000,            0b10000000,            0b10000000,            0b11110000,            0b00000000,
  0b11110000,            0b10000000,            0b11100000,            0b10000000,            0b11110000,            0b00000000,
  0b01110000,            0b10000000,            0b01100000,            0b00010000,            0b11100000,            0b00000000,
};
// --------------------------------------------------------------------------------------------

const char data2[] PROGMEM = {    //Let it snow
  0b00000000,            0b00000000,            0b00000000,            0b00000000,            0b00000000,            0b00000000,
  0b10000000,            0b10000000,            0b10000000,            0b10000000,            0b11110000,            0b00000000,
  0b11110000,            0b10000000,            0b11100000,            0b10000000,            0b11110000,            0b00000000,
  0b11110000,            0b01000000,            0b01000000,            0b01000000,            0b01000000,            0b00000000,
  0b00000000,            0b00000000,            0b00000000,            0b00000000,            0b00000000,            0b00000000,
  0b11100000,            0b01000000,            0b01000000,            0b01000000,            0b11100000,            0b00000000,
  0b11110000,            0b01000000,            0b01000000,            0b01000000,            0b01000000,            0b00000000,
  0b00000000,            0b00000000,            0b00000000,            0b00000000,            0b00000000,            0b00000000,
  0b01110000,            0b10000000,            0b01100000,            0b00010000,            0b11100000,            0b00000000,
  0b10010000,            0b11010000,            0b10110000,            0b10010000,            0b10010000,            0b00000000,
  0b01100000,            0b10010000,            0b10010000,            0b10010000,            0b01100000,            0b00000000,
  0b10010000,            0b10010000,            0b10010000,            0b11110000,            0b10010000,            0b00000000,
  0b00000000,            0b00000000,            0b00000000,            0b00000000,            0b00000000,            0b00000000,
  0b00000000,            0b00000000,            0b00000000,            0b00000000,            0b00000000,            0b00000000
};

const char *const address1 PROGMEM = data1;
const char *const address2 PROGMEM = data2;
const int dataLength1 = sizeof(data1) / 6;
const int dataLength2 = sizeof(data2) / 6;

char led[5][5] = {0};
int dataCounter = 0;
char dataPointer = 0;
char width = 6;

uint8_t hbval = 12;
int8_t hbdelta = 4;

void setup() {
  initTimer0();

  ADCSRA  &= 0b01111111;                      // Disable ADC, use the minimum of energy
  ACSR    |= 0b10000000;                      // Disable Analog Comparator
  MCUCR   &= ~((1 << ISC01) | (1 << ISC00));  // level interrupt INT0 (low level)

  pinMode(button, INPUT);
  pinMode(balls, OUTPUT);
  digitalWrite(balls, LOW);
  pinMode(s1, OUTPUT);
  pinMode(s2, OUTPUT);
  pinMode(s3, OUTPUT);
  pinMode(s4, OUTPUT);
  digitalWrite(s1, HIGH);
  digitalWrite(s2, HIGH);
  digitalWrite(s3, HIGH);
  digitalWrite(s4, HIGH);
  pinMode(z1, OUTPUT);
  pinMode(z2, OUTPUT);
  pinMode(z3, OUTPUT);
  pinMode(z4, OUTPUT);
  pinMode(z5, OUTPUT);
  digitalWrite(z1, LOW);
  digitalWrite(z2, LOW);
  digitalWrite(z3, LOW);
  digitalWrite(z4, LOW);
  digitalWrite(z5, LOW);
}

void loop() {

  enterSleep();                                         //Sleep until the button is pressed

  TCCR1A |= (1 << COM1A1) | (1 << WGM10);               //Enable PWM on pin 6 with prescaler 64
  TCCR1B |= (1 << CS11) | (1 << CS10) | (1 << WGM12);
  OCR1A = 12;

  timer1 = 0;                                           // FIRST SEQUENCE (Text)
  timer2 = 0;
  initMatrix(address1);
  while (dataCounter < dataLength1) {
    writeMatrix();                                      //Show each row for a short time
    if (timer1 > perColumn)  {
      timer1 = 0;
      shiftMatrix(address1);                            //Shift Matrix left and add data on the right
    }
    if (timer2 > BEAT) {                                //Pulsing green leds
      if (hbval > 240) hbdelta = -hbdelta;
      if (hbval < 12) hbdelta = -hbdelta;
      hbval += hbdelta;
      OCR1A = hbval;
      timer2 = 0;
    }
  }
  timer1 = 0;
  timer2 = 0;
  while (timer1 < holdEnd)  {                           // Green leds on some time after text
    if (timer2 > BEAT) {
      if (hbval > 240) hbdelta = -hbdelta;
      if (hbval < 12) hbdelta = -hbdelta;
      hbval += hbdelta;
      OCR1A = hbval;
      timer2 = 0;
    }
  }

  if (SNOWING_ON) {
    enterSleep();

    timer1 = 0;                                         // SECOND SEQUENCE (Text and snowfall)
    timer2 = 0;
    initMatrix(address2);
    while (dataCounter < dataLength2) {
      writeMatrix();
      if (timer1 > perColumn)  {
        timer1 = 0;
        shiftMatrix(address2);                          //Shift Matrix left and add data on the right
      }
      if (timer2 > timeBlink) {
        digitalWrite(balls, HIGH);
        blinking = true;
        timer2 = 0;
      }
      if (blinking && (timer2 > 100))  {
        digitalWrite(balls, LOW);
        blinking = false;
        timeBlink = random(500, 1500);
        timer2 = 0;
      }
    }
    timer1 = 0;
    timer2 = 0;
    writeMatrix();
    for (int i = 0; i < 5; i++)  {
      for (int j = 0; j < 4; j++)  led[i][j] = 0;
    }
    digitalWrite(balls, LOW);
    delay(1000);
    while (timer1 < LETITSNOW) {                        //Snowfall
      writeMatrix();
      letItSnow();
    }
  }
}

/*
   For each line of the matrix for the duration of "perRow":
   Turn on all columns defined in the led matrix and then one row -> Columns are multiplexed
*/
void writeMatrix(void)  {
  for (int x = 0; x < 5; x++)  {
    for (int y = 0; y < 4; y++)  {
      digitalWrite(columns[y], !(led[x][y]));
    }
    digitalWrite(rows[x], HIGH);
    delayMicroseconds(perRow);
    digitalWrite(rows[x], LOW);
  }
}

void shiftMatrix(char *const address)  {
  char temp;
  for (int i = 0; i < 4; i++)  {        // Shift first four columns to the left
    for (int j = 0; j < 5; j++)  {
      led[j][i] = led[j][i + 1];
    }
  }
  for (int i = 0; i < 5; i++) {         // Get new column of data
    strcpy_P(&temp, (address + dataCounter * 6 + i));
    led[i][4] = (temp >> (7 - dataPointer)) & 0b00000001;
  }
  dataPointer ++;
  if (dataPointer == width)  {
    dataPointer = 0;
    dataCounter ++;
  }
}

/*
   Generate randomly snowflakes in the top row and let them fall down one row each step
*/
void letItSnow(void)  {
  if (timer2 > SPEEDDROP)  {
    for (int j = 4; j > 0;)  {          //Columns
      j--;
      for (int i = 5; i > 0;) {         //Lines
        i--;
        if (led[i][j] == 1) {
          led[i][j] = 0;
          if (i != 4)  led[i + 1][j] = 1;
          //else         led[0][1] = 1;
          break;
        }
      }
    }
    led[0][0] = random(0, 255) > 200;
    led[0][1] = random(0, 255) > 200;
    led[0][2] = random(0, 255) > 200;
    led[0][3] = random(0, 255) > 200;
    timer2 = 0;
  }
}

void initMatrix(char *const address) {
  char temp;
  dataCounter = 0;
  dataPointer = 0;
  for (int i = 0; i < 5; i++) {
    for (int j = 0; j < 5; j++) {         // Get new column of data
      strcpy_P(&temp, (address + j));
      led[j][i] = (temp >> (7 - dataPointer)) & 0b00000001;
    }
    dataPointer ++;
  }
  delay(500);
  timer1 = 0;
  timer2 = 0;
}

void enterSleep(void) {                 // If not activated, go to sleep to consume the minimum energy (~ 4nA)
  digitalWrite(balls, LOW);
  digitalWrite(z1, LOW);
  digitalWrite(z2, LOW);
  digitalWrite(z3, LOW);
  digitalWrite(z4, LOW);
  digitalWrite(z5, LOW);
  digitalWrite(s1, HIGH);
  digitalWrite(s2, HIGH);
  digitalWrite(s3, HIGH);
  digitalWrite(s4, HIGH);
  TCCR0B &= ~(1 << CS01);               // Turn off timer
  TIMSK0 &= ~(1 << OCIE0A);             // Timer interrupt off
  PCMSK0 |= (1 << PCINT0);              // Enable Pin Change on Button Pin
  GIMSK |= (1 << PCIE0);                // External Interrupt Request 0 Enable
  set_sleep_mode(SLEEP_MODE_PWR_DOWN);  // Set Sleep Mode to deepest: Power Down
  sleep_enable();                       // Enable Sleep Mode
  sleep_bod_disable();                  // Disable Brown-Out Detector
  sleep_mode();                         // Enter Sleep Mode
  sleep_disable();                      // Disable Sleep Mode
  TCCR0B |= (1 << CS01);                // Turn on timer
  TIMSK0 |= (1 << OCIE0A);              // Timer interrupt on
}

void initTimer0(void)
{
  cli();
  TCCR0A = 0;
  TCCR0B = 0;
  TCCR0A |= (1 << WGM01);               // CTC Modus
  TCCR0B |= (1 << CS01);                // Prescaler 8
  OCR0A = 124;                          // ((1000000/8)/1000) = 125
  TIMSK0 |= (1 << OCIE0A);              // Interups enabled
  sei();
}

ISR(PCINT0_vect)  {                     // If the button is pressed
  // Disable external interrupt here, in case the external low pulse is too long
  GIMSK &= ~(1 << PCIE0);
  PCMSK0 &= ~(1 << PCINT0);
}

ISR(TIM0_COMPA_vect)                    //Count up the two timers
{
  timer1++;
  timer2++;
}
