#include <FastLED.h>
#include <EMFButton.h>

const uint8_t WIDTH = 8;
const uint8_t HEIGHT = 8;
#define LED_PIN 4

CRGB leds[WIDTH * HEIGHT];
const CRGB color = CRGB(0xff, 0x12, 0x10);

uint16_t XY(uint8_t x, uint8_t y)
{
  return ((HEIGHT - 1 - y) * WIDTH) + x;
}

bool showmouth = 1;

// Expression constants
enum Expression
{
  HAPPY = 0,
  SAD,
  SURPRISED,
  NEUTRAL,
  ANGRY,
  SLEEPY,

  MAX_EXPRESSIONS
};

Expression exxp = HAPPY;

template <typename T>
struct pos_t
{
  pos_t() {}
  pos_t(T xn, T yn) : x(xn), y(yn) {}
  T x, y;
};

int8_t sign(int8_t in)
{
  return (in < 0) ? -1 : (in > 0) ? 1
                                  : 0;
}

class eye
{
public:
  eye() {}
  eye(uint8_t x, uint8_t y, CRGB color) : _matrix_pos{x, y}, _color(color) {}

  void setPosition(int8_t x, int8_t y)
  {
    _pos.x = x;
    _pos.y = y;
  }

  void setPosition(pos_t<int8_t> coord)
  {
    _pos = coord;
  }

  void setOpen(uint8_t open)
  {
    _open = open;
  }

  void setAngle(int8_t angle)
  {
    _angle = angle;
  }

  void setColor(CRGB color)
  {
    _color = color;
  }

  pos_t<int8_t> getPosition()
  {
    return _pos;
  }

  uint8_t getOpen()
  {
    return _open;
  }

  int8_t getAngle()
  {
    return _angle;
  }

  CRGB getColor()
  {
    return _color;
  }

  void draw()
  {
    uint8_t open_in_pix = (_open / 64);
    uint8_t open_pix_val = (_open % 64) * 4;
    uint8_t pos_x_val = abs(_pos.x) * 2;
    uint8_t pos_y_val = abs(_pos.y) * 2;

    for (uint8_t ay = 0; ay < 3; ay++)
    {
      CRGB col = blend(CRGB(0, 0, 0), _color, (ay < open_in_pix) ? 255 : ay == open_in_pix ? open_pix_val
                                                                                           : 0);
      CRGB colP1 = blend(col, CRGB(0, 0, 0), pos_x_val);
      CRGB colP2 = blend(CRGB(0, 0, 0), col, pos_x_val);
      uint8_t X = _matrix_pos.x;
      uint8_t Y = _matrix_pos.y + ay;
      leds[XY(X, Y)] += blend(colP1, CRGB(0, 0, 0), pos_y_val);
      leds[XY(X + sign(_pos.x), Y)] += blend(colP2, CRGB(0, 0, 0), pos_y_val);
      leds[XY(X, Y + sign(_pos.y))] += blend(CRGB(0, 0, 0), colP1, pos_y_val);
      leds[XY(X + sign(_pos.x), Y + sign(_pos.y))] += blend(CRGB(0, 0, 0), colP2, pos_y_val);
    }

    for (int8_t ax = -1; ax < 2; ax++)
    {
      int16_t eyepo = (_open * 4) - (sign(ax) * _angle * 2);
      int16_t eyepospos = eyepo + (_pos.y * 2);
      uint8_t X = _matrix_pos.x + ax;
      uint8_t Y = _matrix_pos.y + (eyepospos / 255);
      if (eyepo > -(_pos.y * 2) && eyepo < 765 - (_pos.y * 2))
        leds[XY(X, Y)] = blend(leds[XY(X, Y)], _color, (eyepospos % 255));
      if (eyepo > 255 && eyepo < 1020 - (_pos.y * 2))
        leds[XY(X, Y - 1)] = blend(_color, leds[XY(X, Y - 1)], (eyepospos % 255));
    }
  }

private:
  pos_t<uint8_t> _matrix_pos;
  CRGB _color;

  pos_t<int8_t> _pos{0, 0};
  int8_t _angle = 0;
  uint8_t _open = 0;
};

class mouth
{
public:
  mouth() {}
  mouth(uint8_t x, uint8_t y, CRGB color) : _matrix_pos{x, y}, _color(color) {}

  void setOpen(uint8_t open)
  {
    _open = open;
  }

  void setPosition(int8_t pos)
  {
    _pos = pos;
  }

  void setColor(CRGB color)
  {
    _color = color;
  }

  uint8_t getOpen()
  {
    return _open;
  }

  int8_t getPosition()
  {
    return _pos;
  }

  CRGB getColor()
  {
    return _color;
  }

  void draw()
  {
    uint8_t abs_pos = (abs(_pos) * 2);
    for (uint8_t ax = 0; ax < 4; ax++)
    {
      if (ax > 0 && ax < 3)
      {
        leds[XY(_matrix_pos.x + ax, _matrix_pos.y)] += blend(CRGB(0, 0, 0), color, max(_open, (uint8_t)~abs_pos));
        leds[XY(_matrix_pos.x + ax, _matrix_pos.y + sign(_pos))] += blend(CRGB(0, 0, 0), _color, abs_pos);
      }
      else
      {
        leds[XY(_matrix_pos.x + ax, _matrix_pos.y)] += blend(CRGB(0, 0, 0), _color, max(_open, abs_pos));
      }
    }
  }

private:
  pos_t<uint8_t> _matrix_pos;
  int8_t _pos;
  uint8_t _open = 0;
  CRGB _color;
};

class face
{
public:
  face()
  {
    leftEye = eye(1, 3, color);
    rightEye = eye(6, 3, color);
    theMouth = mouth(2, 1, color);
    _left_eye_open = 255, _right_eye_open = 255;
    right_blink_dir = 1;
    left_blink_dir = 1;
    leftEye.setOpen(0);
    rightEye.setOpen(0);
    theMouth.setOpen(0);
    theMouth.setPosition(0);
  };
  bool right_blink_flag, right_blink_dir;
  bool left_blink_flag, left_blink_dir;

  uint8_t _left_eye_open, _right_eye_open;
  int8_t _left_eye_angle, _right_eye_angle;
  pos_t<int8_t> _left_eye_pos, _right_eye_pos;

  int8_t _mouth_pos;
  uint8_t _mouth_open;

  void draw()
  {
    leftEye.draw();
    rightEye.draw();
    if (showmouth)
      theMouth.draw();
  }

  void blink_event()
  {
    right_blink_flag = 1;
    left_blink_flag = 1;
  }

  void eventMaking()
  {
    EVERY_N_MILLISECONDS(random(500, 5000))
    {
      blink_event();
    }
  }

  void eventHandling()
  {
    int16_t cur_left_eye_open = leftEye.getOpen();
    int16_t cur_right_eye_open = rightEye.getOpen();

    int16_t cur_left_eye_angle = leftEye.getAngle();
    int16_t cur_right_eye_angle = rightEye.getAngle();

    pos_t<int8_t> cur_left_eye_pos = leftEye.getPosition();
    pos_t<int8_t> cur_right_eye_pos = rightEye.getPosition();

    int16_t cur_mouth_open = theMouth.getOpen();
    int16_t cur_mouth_pos = theMouth.getPosition();

    if (left_blink_flag == 1)
    {
      cur_left_eye_open += ((left_blink_dir * 2) - 1) * 4;
      if (cur_left_eye_open < 0 && !left_blink_dir)
        left_blink_dir = 1; // it can be for changeing eye
      if (cur_left_eye_open > _left_eye_open && left_blink_dir)
      {
        left_blink_flag = 0;
        left_blink_dir = 0;
      }
    }

    if (right_blink_flag == 1)
    {
      cur_right_eye_open += ((right_blink_dir * 2) - 1) * 4;
      if (cur_right_eye_open < 0 && !right_blink_dir)
        right_blink_dir = 1; // it can be for changeing eye
      if (cur_right_eye_open > _right_eye_open && right_blink_dir)
      {
        right_blink_flag = 0;
        right_blink_dir = 0;
      }
    }

    cur_left_eye_open = constrain(cur_left_eye_open, 0, 255);
    cur_right_eye_open = constrain(cur_right_eye_open, 0, 255);

    leftEye.setOpen((uint8_t)cur_left_eye_open);
    rightEye.setOpen((uint8_t)cur_right_eye_open);

    if (cur_left_eye_angle != _left_eye_angle)
    {
      cur_left_eye_angle += ((cur_left_eye_angle < _left_eye_angle) * 2) - 1;
    }

    if (cur_right_eye_angle != _right_eye_angle)
    {
      cur_right_eye_angle += ((cur_right_eye_angle < _right_eye_angle) * 2) - 1;
    }

    leftEye.setAngle(cur_left_eye_angle);
    rightEye.setAngle(cur_right_eye_angle);

    if (cur_left_eye_pos.x != _left_eye_pos.x)
    {
      cur_left_eye_pos.x += ((cur_left_eye_pos.x < _left_eye_pos.x) * 2) - 1;
    }
    if (cur_left_eye_pos.y != _left_eye_pos.y)
    {
      cur_left_eye_pos.y += ((cur_left_eye_pos.y < _left_eye_pos.y) * 2) - 1;
    }

    if (cur_right_eye_pos.x != _right_eye_pos.x)
    {
      cur_right_eye_pos.x += ((cur_right_eye_pos.x < _right_eye_pos.x) * 2) - 1;
    }
    if (cur_right_eye_pos.y != _right_eye_pos.y)
    {
      cur_right_eye_pos.y += ((cur_right_eye_pos.y < _right_eye_pos.y) * 2) - 1;
    }

    leftEye.setPosition(cur_left_eye_pos);
    rightEye.setPosition(cur_right_eye_pos);

    if (cur_mouth_open != _mouth_open)
    {
      cur_mouth_open += ((cur_mouth_open < _mouth_open) * 2) - 1;
    }

    if (cur_mouth_pos != _mouth_pos)
    {
      cur_mouth_pos += ((cur_mouth_pos < _mouth_pos) * 2) - 1;
    }

    theMouth.setOpen(cur_mouth_open);
    theMouth.setPosition(cur_mouth_pos);
  }

  void setExpression(Expression exxp)
  {
    switch (exxp)
    {
    case HAPPY:
      _left_eye_open = 255;
      _right_eye_open = 255;
      _left_eye_angle = 0;
      _right_eye_angle = 0;
      _mouth_open = 0;
      _mouth_pos = -127;
      break;
    case SAD:
      _left_eye_open = 192;
      _right_eye_open = 192;
      _left_eye_angle = -64;
      _right_eye_angle = 64;
      _mouth_open = 64;
      _mouth_pos = 127;
      break;
    case SURPRISED:
      _left_eye_open = 255;
      _right_eye_open = 255;
      _left_eye_angle = 0;
      _right_eye_angle = 0;
      _mouth_open = 255;
      _mouth_pos = 0;
      break;
    case NEUTRAL:
      _left_eye_open = 192;
      _right_eye_open = 192;
      _left_eye_angle = 0;
      _right_eye_angle = 0;
      _mouth_open = 255;
      _mouth_pos = 0;
      break;
    case ANGRY:
      _left_eye_open = 192;
      _right_eye_open = 192;
      _left_eye_angle = 64;
      _right_eye_angle = -64;
      _mouth_open = 0;
      _mouth_pos = 64;
      break;
    case SLEEPY:
      _left_eye_open = 128;
      _right_eye_open = 128;
      _left_eye_angle = 0;
      _right_eye_angle = 0;
      _mouth_open = 0;
      _mouth_pos = 0;
      break;
    default:
      _left_eye_open = 128;
      _right_eye_open = 128;
      _left_eye_angle = -32;
      _right_eye_angle = 32;
      _mouth_open = 0;
      _mouth_pos = 0;
      break;
    }
  }

private:
  eye leftEye;
  eye rightEye;
  mouth theMouth;
};

face theFace;
EMFButton btn(3);

void setup()
{
  FastLED.addLeds<NEOPIXEL, LED_PIN>(leds, WIDTH * HEIGHT);
  // Serial.begin(112500);
  FastLED.setBrightness(255);
  theFace.setExpression(exxp);
  theFace.blink_event();
}

void handleSerialInput()
{
  if (Serial.available() > 0)
  {
    String command = Serial.readStringUntil('\n');
    command.trim();

    if (command == "H")
    {
      exxp = HAPPY;
      Serial.println("Expression set to HAPPY");
    }
    else if (command == "S")
    {
      exxp = SAD;
      Serial.println("Expression set to SAD");
    }
    else if (command == "U")
    {
      exxp = SURPRISED;
      Serial.println("Expression set to SURPRISED");
    }
    else if (command == "N")
    {
      exxp = NEUTRAL;
      Serial.println("Expression set to NEUTRAL");
    }
    else if (command == "A")
    {
      exxp = ANGRY;
      Serial.println("Expression set to ANGRY");
    }
    else if (command == "L")
    {
      exxp = SLEEPY;
      Serial.println("Expression set to SLEEPY");
    }
    else
    {
      Serial.println("Unknown command");
    }
    theFace.setExpression(exxp);
    theFace.blink_event();
  }
}

void loop()
{
  theFace.eventMaking();
  theFace.eventHandling();
  FastLED.clear();
  theFace.draw();
  FastLED.show();
  handleSerialInput();
  btn.tick();
  if (btn.hasSingle())
  {
    exxp = (Expression)((int)exxp + 1);
    exxp = (Expression)((int)exxp % MAX_EXPRESSIONS);
    theFace.setExpression(exxp);
    theFace.blink_event();
  }

  if (btn.hasTriple())
  {
    showmouth = !showmouth;
  }
}