// This Esphome TM1651 component for use with Mini Battery Displays (7 LED levels)
// and removes the Esphome dependency on the TM1651 Arduino library.
// It was largely based on the work of others as set out below.
// @mrtoy-me July 2025
// ==============================================================================================
// Original Arduino TM1651 library:
// Author:Fred.Chu
// Date:14 August, 2014
// Applicable Module: Battery Display v1.0
// 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.
// Modified record:
// Author:  Detlef Giessmann Germany
// Mail:    mydiyp@web.de
// Demo for the new 7 LED Battery-Display 2017
// IDE:     Arduino-1.6.5
// Type:    OPEN-SMART CX10*4RY68  4Color
// Date:    01.05.2017
// ==============================================================================================
// Esphome component using arduino TM1651 library:
// MIT License
// Copyright (c) 2019 freekode
// ==============================================================================================
// Library and command-line (python) program to control mini battery displays on Raspberry Pi:
// MIT License
// Copyright (c) 2020 Koen Vervloese
// ==============================================================================================
// MIT License
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
// The above copyright notice and this permission notice shall be included in all
// copies or substantial portions of the Software.
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
// SOFTWARE.

#include "tm1651.h"
#include "esphome/core/log.h"

namespace esphome::tm1651 {

static const char *const TAG = "tm1651.display";

static const bool LINE_HIGH = true;
static const bool LINE_LOW = false;

// TM1651 maximum frequency is 500 kHz (duty ratio 50%) = 2 microseconds / cycle
static const uint8_t CLOCK_CYCLE = 8;

static const uint8_t HALF_CLOCK_CYCLE = CLOCK_CYCLE / 2;
static const uint8_t QUARTER_CLOCK_CYCLE = CLOCK_CYCLE / 4;

static const uint8_t ADDR_FIXED = 0x44;  // fixed address mode
static const uint8_t ADDR_START = 0xC0;  // address of the display register

static const uint8_t DISPLAY_OFF = 0x80;
static const uint8_t DISPLAY_ON = 0x88;

static const uint8_t MAX_DISPLAY_LEVELS = 7;

static const uint8_t PERCENT100 = 100;
static const uint8_t PERCENT50 = 50;

static const uint8_t TM1651_BRIGHTNESS_DARKEST = 0;
static const uint8_t TM1651_BRIGHTNESS_TYPICAL = 2;
static const uint8_t TM1651_BRIGHTNESS_BRIGHTEST = 7;

static const uint8_t TM1651_LEVEL_TAB[] = {0b00000000, 0b00000001, 0b00000011, 0b00000111,
                                           0b00001111, 0b00011111, 0b00111111, 0b01111111};

// public

void TM1651Display::setup() {
  this->clk_pin_->setup();
  this->clk_pin_->pin_mode(gpio::FLAG_OUTPUT);

  this->dio_pin_->setup();
  this->dio_pin_->pin_mode(gpio::FLAG_OUTPUT);

  this->brightness_ = TM1651_BRIGHTNESS_TYPICAL;

  // clear display
  this->display_level_();
  this->update_brightness_(DISPLAY_ON);
}

void TM1651Display::dump_config() {
  ESP_LOGCONFIG(TAG, "Battery Display");
  LOG_PIN("  CLK: ", clk_pin_);
  LOG_PIN("  DIO: ", dio_pin_);
}

void TM1651Display::set_brightness(uint8_t new_brightness) {
  this->brightness_ = this->remap_brightness_(new_brightness);
  if (this->display_on_) {
    this->update_brightness_(DISPLAY_ON);
  }
}

void TM1651Display::set_level(uint8_t new_level) {
  if (new_level > MAX_DISPLAY_LEVELS)
    new_level = MAX_DISPLAY_LEVELS;
  this->level_ = new_level;
  if (this->display_on_) {
    this->display_level_();
  }
}

void TM1651Display::set_level_percent(uint8_t percentage) {
  this->level_ = this->calculate_level_(percentage);
  if (this->display_on_) {
    this->display_level_();
  }
}

void TM1651Display::turn_off() {
  this->display_on_ = false;
  this->update_brightness_(DISPLAY_OFF);
}

void TM1651Display::turn_on() {
  this->display_on_ = true;
  // display level as it could have been changed when display turned off
  this->display_level_();
  this->update_brightness_(DISPLAY_ON);
}

// protected

uint8_t TM1651Display::calculate_level_(uint8_t percentage) {
  if (percentage > PERCENT100)
    percentage = PERCENT100;
  // scale 0-100% to 0-7 display levels
  // use integer arithmetic with rounding
  uint16_t initial_scaling = (percentage * MAX_DISPLAY_LEVELS) + PERCENT50;
  return (uint8_t) (initial_scaling / PERCENT100);
}

void TM1651Display::display_level_() {
  this->start_();
  this->write_byte_(ADDR_FIXED);
  this->stop_();

  this->start_();
  this->write_byte_(ADDR_START);
  this->write_byte_(TM1651_LEVEL_TAB[this->level_]);
  this->stop_();
}

uint8_t TM1651Display::remap_brightness_(uint8_t new_brightness) {
  if (new_brightness <= 1)
    return TM1651_BRIGHTNESS_DARKEST;
  if (new_brightness == 2)
    return TM1651_BRIGHTNESS_TYPICAL;

  // new_brightness >= 3
  return TM1651_BRIGHTNESS_BRIGHTEST;
}

void TM1651Display::update_brightness_(uint8_t on_off_control) {
  this->start_();
  this->write_byte_(on_off_control | this->brightness_);
  this->stop_();
}

// low level functions

bool TM1651Display::write_byte_(uint8_t data) {
  // data bit written to DIO when CLK is low
  for (uint8_t i = 0; i < 8; i++) {
    this->half_cycle_clock_low_((bool) (data & 0x01));
    this->half_cycle_clock_high_();
    data >>= 1;
  }

  // start 9th cycle, setting DIO high and look for ack
  this->half_cycle_clock_low_(LINE_HIGH);
  return this->half_cycle_clock_high_ack_();
}

void TM1651Display::half_cycle_clock_low_(bool data_bit) {
  // first half cycle, clock low and write data bit
  this->clk_pin_->digital_write(LINE_LOW);
  delayMicroseconds(QUARTER_CLOCK_CYCLE);

  this->dio_pin_->digital_write(data_bit);
  delayMicroseconds(QUARTER_CLOCK_CYCLE);
}

void TM1651Display::half_cycle_clock_high_() {
  // second half cycle, clock high
  this->clk_pin_->digital_write(LINE_HIGH);
  delayMicroseconds(HALF_CLOCK_CYCLE);
}

bool TM1651Display::half_cycle_clock_high_ack_() {
  // second half cycle, clock high and check for ack
  this->clk_pin_->digital_write(LINE_HIGH);
  delayMicroseconds(QUARTER_CLOCK_CYCLE);

  this->dio_pin_->pin_mode(gpio::FLAG_INPUT);
  // valid ack on DIO is low
  bool ack = (!this->dio_pin_->digital_read());

  this->dio_pin_->pin_mode(gpio::FLAG_OUTPUT);

  // ack should be set DIO low by now
  // if its not, set DIO low before the next cycle
  if (!ack) {
    this->dio_pin_->digital_write(LINE_LOW);
  }
  delayMicroseconds(QUARTER_CLOCK_CYCLE);

  // begin next cycle
  this->clk_pin_->digital_write(LINE_LOW);

  return ack;
}

void TM1651Display::start_() {
  // start data transmission
  this->delineate_transmission_(LINE_HIGH);
}

void TM1651Display::stop_() {
  // stop data transmission
  this->delineate_transmission_(LINE_LOW);
}

void TM1651Display::delineate_transmission_(bool dio_state) {
  // delineate data transmission
  // DIO changes its value while CLK is high

  this->dio_pin_->digital_write(dio_state);
  delayMicroseconds(HALF_CLOCK_CYCLE);

  this->clk_pin_->digital_write(LINE_HIGH);
  delayMicroseconds(QUARTER_CLOCK_CYCLE);

  this->dio_pin_->digital_write(!dio_state);
  delayMicroseconds(QUARTER_CLOCK_CYCLE);
}

}  // namespace esphome::tm1651
