//  TM1637 Tiny Display
//  Arduino tiny library for TM1637 LED Display
//
//  Author: Jason A. Cox - @jasonacox - https://github.com/jasonacox
//  Date: 27 June 2020
//
//  Based on TM1637Display library at https://github.com/avishorp/TM1637
//
//  This library is free software; you can redistribute it and/or
//  modify it under the terms of the GNU Lesser General Public
//  License as published by the Free Software Foundation; either
//  version 2.1 of the License, or (at your option) any later version.
//
//  This library is distributed in the hope that it will be useful,
//  but WITHOUT ANY WARRANTY; without even the implied warranty of
//  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
//  Lesser General Public License for more details.
//
//  You should have received a copy of the GNU Lesser General Public
//  License along with this library; if not, write to the Free Software
//  Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301  USA

extern "C" {
  #include <stdlib.h>
  #include <string.h>
  #include <inttypes.h>
}

#include <TM1637TinyDisplay.h>
#include <Arduino.h>


//
//      A
//     ---
//  F |   | B
//     -G-
//  E |   | C
//     ---
//      D
const uint8_t digitToSegment[] PROGMEM = {
 // XGFEDCBA
  0b00111111,    // 0
  0b00000110,    // 1
  0b01011011,    // 2
  0b01001111,    // 3
  0b01100110,    // 4
  0b01101101,    // 5
  0b01111101,    // 6
  0b00000111,    // 7
  0b01111111,    // 8
  0b01101111,    // 9
  0b01110111,    // A
  0b01111100,    // b
  0b00111001,    // C
  0b01011110,    // d
  0b01111001,    // E
  0b01110001     // F
  };

// ASCII Map - Index 0 starts at ASCII 32
const uint8_t asciiToSegment[] PROGMEM = {
   0b00000000, // 032 (Space)
   0b00110000, // 033 !
   0b00100010, // 034 "
   0b01000001, // 035 #
   0b01101101, // 036 $
   0b01010010, // 037 %
   0b01111100, // 038 &
   0b00100000, // 039 '
   0b00111001, // 040 (
   0b00001111, // 041 )
   0b00100001, // 042 *
   0b01110000, // 043 +
   0b00001000, // 044 ,
   0b01000000, // 045 -
   0b00001000, // 046 .
   0b01010010, // 047 /
   0b00111111, // 048 0
   0b00000110, // 049 1
   0b01011011, // 050 2
   0b01001111, // 051 3
   0b01100110, // 052 4
   0b01101101, // 053 5
   0b01111101, // 054 6
   0b00000111, // 055 7
   0b01111111, // 056 8
   0b01101111, // 057 9
   0b01001000, // 058 :
   0b01001000, // 059 ;
   0b00111001, // 060 <
   0b01001000, // 061 =
   0b00001111, // 062 >
   0b01010011, // 063 ?
   0b01011111, // 064 @
   0b01110111, // 065 A
   0b01111100, // 066 B
   0b00111001, // 067 C
   0b01011110, // 068 D
   0b01111001, // 069 E
   0b01110001, // 070 F
   0b00111101, // 071 G
   0b01110110, // 072 H
   0b00000110, // 073 I
   0b00011110, // 074 J
   0b01110110, // 075 K
   0b00111000, // 076 L
   0b00010101, // 077 M
   0b00110111, // 078 N
   0b00111111, // 079 O
   0b01110011, // 080 P
   0b01100111, // 081 Q
   0b00110001, // 082 R
   0b01101101, // 083 S
   0b01111000, // 084 T
   0b00111110, // 085 U
   0b00011100, // 086 V
   0b00101010, // 087 W
   0b01110110, // 088 X
   0b01101110, // 089 Y
   0b01011011, // 090 Z
   0b00111001, // 091 [
   0b01100100, // 092 (backslash)
   0b00001111, // 093 ]
   0b00100011, // 094 ^
   0b00001000, // 095 _
   0b00100000, // 096 `
   0b01110111, // 097 a
   0b01111100, // 098 b
   0b01011000, // 099 c
   0b01011110, // 100 d
   0b01111001, // 101 e
   0b01110001, // 102 f
   0b01101111, // 103 g
   0b01110100, // 104 h
   0b00000100, // 105 i
   0b00011110, // 106 j
   0b01110110, // 107 k
   0b00011000, // 108 l
   0b00010101, // 109 m
   0b01010100, // 110 n
   0b01011100, // 111 o
   0b01110011, // 112 p
   0b01100111, // 113 q
   0b01010000, // 114 r
   0b01101101, // 115 s
   0b01111000, // 116 t
   0b00011100, // 117 u
   0b00011100, // 118 v
   0b00101010, // 119 w
   0b01110110, // 120 x
   0b01101110, // 121 y
   0b01011011, // 122 z
   0b00111001, // 123 {
   0b00110000, // 124 |
   0b00001111, // 125 }
   0b01000000, // 126 ~
   0b00000000  // 127 
};

static const uint8_t minusSegments = 0b01000000;
static const uint8_t degreeSegments = 0b01100011;

TM1637TinyDisplay::TM1637TinyDisplay(uint8_t pinClk, uint8_t pinDIO, unsigned int bitDelay, 
  unsigned int scrollDelay, bool flip)
{
  // Pin settings
  m_pinClk = pinClk;
  m_pinDIO = pinDIO;
  // Timing configurations
  m_bitDelay = bitDelay;
  m_scrollDelay = scrollDelay;
  // Flip 
  m_flipDisplay = flip;
  
  // Set the pin direction and default value.
  // Both pins are set as inputs, allowing the pull-up resistors to pull them up
  pinMode(m_pinClk, INPUT);
  pinMode(m_pinDIO, INPUT);
  digitalWrite(m_pinClk, LOW);
  digitalWrite(m_pinDIO, LOW);
}

void TM1637TinyDisplay::flipDisplay(bool flip)
{
  m_flipDisplay = flip;
}

void TM1637TinyDisplay::setBrightness(uint8_t brightness, bool on)
{
  m_brightness = (brightness & 0x07) | (on? 0x08 : 0x00);
  
    // Write COMM3 + brightness
  start();
  writeByte(TM1637_I2C_COMM3 + (m_brightness & 0x0f));
  stop();
}

void TM1637TinyDisplay::setScrolldelay(unsigned int scrollDelay)
{
  m_scrollDelay = scrollDelay;
}

void TM1637TinyDisplay::setSegments(const uint8_t segments[], uint8_t length, uint8_t pos)
{
  uint8_t dot = 0;

  // Write COMM1
  start();
  writeByte(TM1637_I2C_COMM1);
  stop();

  // Write COMM2 + first digit address
  start();
  if(m_flipDisplay) pos = MAXDIGITS - pos - length;
  writeByte(TM1637_I2C_COMM2 + (pos & 0x07));

  // Write the data bytes
  if(m_flipDisplay) {
    for (uint8_t k=0; k < length; k++) {
      dot = 0;
      if((length - k - 2) >= 0) {
        dot = segments[length - k - 2] & 0b10000000;
      }
      uint8_t orig = segments[length - k - 1];
      uint8_t flip = ((orig >> 3) & 0b00000111) + 
        ((orig << 3) & 0b00111000) + (orig & 0b01000000) + dot;
      writeByte(flip);
    }
  }
  else {
    for (uint8_t k=0; k < length; k++) {
      writeByte(segments[k]);
    }
  }
  stop();

}

void TM1637TinyDisplay::clear()
{
  // digits[MAXDIGITS] output array to render
  memset(digits,0,sizeof(digits));
  setSegments(digits);
}

void TM1637TinyDisplay::showNumber(int num, bool leading_zero, uint8_t length, uint8_t pos)
{
  showNumber(long(num), leading_zero, length, pos);
}

void TM1637TinyDisplay::showNumber(long num, bool leading_zero, uint8_t length, uint8_t pos)
{
  if(leading_zero) {
    showNumberDec(num, 0, leading_zero, length, pos);
  }
  else {
    showNumber(double(num), 0, length, pos);
  }
}

void TM1637TinyDisplay::showNumber(double num, uint8_t decimal_length, uint8_t length, uint8_t pos)
{
  int num_len = 0;              
  long inum = abs((long)num);  
  int decimal_places = 0;       
  double value = 0.0;
  bool negative = false; 
  bool leading_zero = false; 
  // digits[MAXDIGITS] output array to render
  memset(digits,0,sizeof(digits));

  // determine length of whole number part of num
  while(inum != 0) {  
    inum = inum / 10;
    num_len++;
  }
  if(num < 0) {
    num_len++; // make space for negative
    negative = true;
  }
  if(abs(num)<1) {
    num_len++; // make space for 0. prefix
    leading_zero = true;
  }
  // make sure we can display number otherwise show overflow
  if(num_len > length) {
    showString("----", length, pos); // overflow symbol
    return;
  }
  // how many decimal places can we show?
  decimal_places = length - num_len; 
  if(decimal_places > decimal_length) {
    decimal_places = decimal_length;
  }
  // construct whole number representation of num
  value = num;
  for(int x=0; x < decimal_places; x++) {
    value = value * 10.00;
  }
  if(num>0) value = value + 0.5; // round up
  if(num<0) value = value - 0.5; // round down
  inum = abs((long)value);

  // render display array
  if (inum == 0 && !leading_zero) {
    digits[length-1] = encodeDigit(0);
  }
  else {		
    int decimal_pos = length - 1 - decimal_places;
    for(int i = length-1; i >= 0; --i) {
      uint8_t digit = inum % 10;
      
      if (digit == 0 && inum == 0 &&
        (leading_zero == false || (i < decimal_pos))) {
        // Blank out any leading zeros except for 0.x case
        digits[i] = 0;

        // Add negative sign for negative number
        if (negative) {
          digits[i] = minusSegments;
          negative = false;
        }
      }
      else
        digits[i] = encodeDigit(digit);
      if(i == decimal_pos && decimal_length > 0) {
        digits[i] += 0b10000000; // add decimal point
      }
      inum /= 10;
    }
  }
  setSegments(digits, length, pos);
}

void TM1637TinyDisplay::showNumberDec(int num, uint8_t dots, bool leading_zero,
                                    uint8_t length, uint8_t pos)
{
  showNumberBaseEx(num < 0? -10 : 10, num < 0? -num : num, dots, leading_zero, length, pos);
}

void TM1637TinyDisplay::showNumberHex(uint16_t num, uint8_t dots, bool leading_zero,
                                    uint8_t length, uint8_t pos)
{
  showNumberBaseEx(16, num, dots, leading_zero, length, pos);
}

void TM1637TinyDisplay::showNumberBaseEx(int8_t base, uint16_t num, uint8_t dots, bool leading_zero,
                                    uint8_t length, uint8_t pos)
{
  bool negative = false;
  if (base < 0) {
    base = -base;
    negative = true;
  }

  if (num == 0 && !leading_zero) {
    // Singular case - take care separately
    for(uint8_t i = 0; i < (length-1); i++) {
      digits[i] = 0;
    }
    digits[length-1] = encodeDigit(0);
  }
  else {		
    for(int i = length-1; i >= 0; --i) {
      uint8_t digit = num % base;

      if (digit == 0 && num == 0 && leading_zero == false)
          // Leading zero is blank
        digits[i] = 0;
      else
        digits[i] = encodeDigit(digit);
        
      if (digit == 0 && num == 0 && negative) {
        digits[i] = minusSegments;
        negative = false;
      }

      num /= base;
    }
  }
  if(dots != 0) {
    showDots(dots, digits);
  }
  setSegments(digits, length, pos);
}

void TM1637TinyDisplay::showString(const char s[], uint8_t length, uint8_t pos, uint8_t dots)
{
  // digits[MAXDIGITS] output array to render
  memset(digits,0,sizeof(digits));

  // Basic Display
  if (strlen(s) <= MAXDIGITS) {
    for (int x = 0; x < strlen(s); x++) {
      digits[x] = encodeASCII(s[x]);
    }
    if(dots != 0) {
      showDots(dots, digits);
    }
    setSegments(digits, length, pos);
  }
  // Scrolling Display
  if (strlen(s) > MAXDIGITS) {
    // Scroll text on display if too long
    for (int x = 0; x < (MAXDIGITS-1); x++) {  // Scroll message on
      int y;
      for (y = 0; y < (MAXDIGITS-1); y++) {
        // shift left
        digits[y] = digits[y+1];
      }
      digits[y] = encodeASCII(s[x]);
      setSegments(digits, length, pos);
      delay(m_scrollDelay);
    }
    for (size_t x = (MAXDIGITS-1); x < strlen(s); x++) { // Scroll through string
      int y;
      for (y = 0; y < (MAXDIGITS-1); y++) {
        // shift left
        digits[y] = digits[y+1];
      }
      digits[y] = encodeASCII(s[x]);
      setSegments(digits, length, pos);
      delay(m_scrollDelay);
    }
    for (int x = 0; x < (MAXDIGITS); x++) {  // Scroll message off
          int y;
      for (y = 0; y < (MAXDIGITS-1); y++) {
        // shift left
        digits[y] = digits[y+1];
      }
      digits[y] = 0;
      setSegments(digits, length, pos);
      delay(m_scrollDelay);
    }
  }
}

void TM1637TinyDisplay::showString_P(const char s[], uint8_t length, uint8_t pos, uint8_t dots) 
{
  // digits[MAXDIGITS] output array to render
  memset(digits,0,sizeof(digits));

  // Basic Display
  if (strlen_P(s) <= MAXDIGITS) {
    for (int x = 0; x < strlen_P(s); x++) {
      digits[x] = encodeASCII(pgm_read_byte(&s[x]));
    }
    if(dots != 0) {
      showDots(dots, digits);
    }
    setSegments(digits, length, pos);
  }
  else {
    // Scroll text on display if too long
    for (int x = 0; x < (MAXDIGITS-1); x++) {  // Scroll message on
      int y;
      for (y = 0; y < (MAXDIGITS-1); y++) {
        // shift left
        digits[y] = digits[y+1];
      }
      digits[y] = encodeASCII(pgm_read_byte(&s[x]));
      setSegments(digits, length, pos);
      delay(m_scrollDelay);
    }

    for (size_t x = (MAXDIGITS-1); x < strlen_P(s); x++) { // Scroll through string
      int y;
      for (y = 0; y < (MAXDIGITS-1); y++) {
        // shift left
        digits[y] = digits[y+1];
      }
      digits[y] = encodeASCII(pgm_read_byte(&s[x]));
      setSegments(digits, length, pos);
      delay(m_scrollDelay);
    }

    for (int x = 0; x < (MAXDIGITS); x++) {  // Scroll message off
      int y;
      for (y = 0; y < (MAXDIGITS-1); y++) {
        // shift left
        digits[y] = digits[y+1];
      }
      digits[y] = 0;
      setSegments(digits, length, pos);
      delay(m_scrollDelay);
    }
  }
}

void TM1637TinyDisplay::showLevel(unsigned int level, bool horizontal) 
{
  // digits[MAXDIGITS] output array to render
  memset(digits,0,sizeof(digits));

  uint8_t digit = 0b00000000;

  if(level>100) level=100;

  if(horizontal) {
    // Must fit within 3 bars
    int bars = (int)((level*3)/100.0);
    if(bars == 0 && level > 0) bars = 1; // Only level=0 turns off display
    switch(bars) {
      case 1:
        digit = 0b00001000;
        break;
      case 2:
        digit = 0b01001000;
        break;
      case 3:
        digit = 0b01001001;
        break;
      default: // Keep at zero
        break;
    }
    for(int x = 0; x < MAXDIGITS; x++) {
      digits[x] = digit;
    }
  }
  else {
    // Must fit within (MAXDIGITS * 2) bars
    int bars = (int)((level*(MAXDIGITS*2))/100.0);
    if(bars == 0 && level > 0) bars = 1;
    for(int x = 0; x<MAXDIGITS; x++) { // for each digit
      int left = bars-(x*2);
      if(left > 1) digits[x] = 0b00110110;
      if(left == 1) digits[x] = 0b00110000;
    }
  }
  setSegments(digits);
}

void TM1637TinyDisplay::showAnimation(const uint8_t data[][4], unsigned int frames, unsigned int ms)
{
  // Animation sequence
  for (unsigned int x = 0; x < frames; x++) {
    setSegments(data[x]);
    delay(ms);
  }
}

void TM1637TinyDisplay::showAnimation_P(const uint8_t data[][4], unsigned int frames, unsigned int ms)
{
  // Animation sequence for data stored in PROGMEM flash memory
  // digits[MAXDIGITS] output array to render
  memset(digits,0,sizeof(digits));
  for (unsigned int x = 0; x < frames; x++) {
    for(unsigned int a = 0; a < 4; a++) {
          digits[a] = pgm_read_byte(&(data[x][a]));
    }
    setSegments(digits);
    delay(ms);
  }
}

void TM1637TinyDisplay::bitDelay()
{
  delayMicroseconds(m_bitDelay);
}

void TM1637TinyDisplay::start()
{
  pinMode(m_pinDIO, OUTPUT);
  bitDelay();
}

void TM1637TinyDisplay::stop()
{
  pinMode(m_pinDIO, OUTPUT);
  bitDelay();
  pinMode(m_pinClk, INPUT);
  bitDelay();
  pinMode(m_pinDIO, INPUT);
  bitDelay();
}

bool TM1637TinyDisplay::writeByte(uint8_t b)
{
  uint8_t data = b;

  // 8 Data Bits
  for(uint8_t i = 0; i < 8; i++) {
    // CLK low
    pinMode(m_pinClk, OUTPUT);
    bitDelay();

    // Set data bit
    if (data & 0x01)
      pinMode(m_pinDIO, INPUT);
    else
      pinMode(m_pinDIO, OUTPUT);

    bitDelay();

    // CLK high
    pinMode(m_pinClk, INPUT);
    bitDelay();
    data = data >> 1;
  }

  // Wait for acknowledge
  // CLK to zero
  pinMode(m_pinClk, OUTPUT);
  pinMode(m_pinDIO, INPUT);
  bitDelay();
 
  // CLK to high
  pinMode(m_pinClk, INPUT);
  bitDelay();
  uint8_t ack = digitalRead(m_pinDIO);
  if (ack == 0)
    pinMode(m_pinDIO, OUTPUT);

  bitDelay();
  pinMode(m_pinClk, OUTPUT);
  bitDelay();

  return ack;
}

void TM1637TinyDisplay::showDots(uint8_t dots, uint8_t* digits)
{
  for(int i = 0; i < MAXDIGITS; ++i)
  {
      digits[i] |= (dots & 0x80);
      dots <<= 1;
  }
}

uint8_t TM1637TinyDisplay::encodeDigit(uint8_t digit)
{
  // return digitToSegment[digit & 0x0f] using PROGMEM
  return pgm_read_byte(digitToSegment + (digit & 0x0f));
}

uint8_t TM1637TinyDisplay::encodeASCII(uint8_t chr)
{
  if(chr == 176) return degreeSegments;   // Degree mark
  if(chr > 127 || chr < 32) return 0;     // Blank
  // return asciiToSegment[chr - 32] using PROGMEM
  return pgm_read_byte(asciiToSegment + (chr - 32));
}