/*
;    Project:       Open Vehicle Monitor System
;    Date:          14th March 2017
;
;    Changes:
;    1.0  Initial release
;
;    (C) 2011       Michael Stegen / Stegen Electronics
;    (C) 2011-2017  Mark Webb-Johnson
;    (C) 2011        Sonny Chen @ EPRO/DX
;
; 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 "ovms_log.h"
static const char *TAG = "max7317";

#include <string.h>
#include "max7317.h"
#include "ovms_command.h"
#include "ovms_peripherals.h"
#include "ovms_config.h"
#include "ovms_events.h"
#include "metrics_standard.h"

max7317::max7317(const char* name, spi* spibus, spi_host_device_t host, int clockspeed, int cspin )
  : pcp(name)
  {
  m_spibus = spibus;

  m_clockspeed = clockspeed;

  memset(&m_devcfg, 0, sizeof(spi_device_interface_config_t));
  m_devcfg.clock_speed_hz=m_clockspeed;     // Clock speed (in hz)
  m_devcfg.mode=0;                          // SPI mode 0
  m_devcfg.command_bits=0;
  m_devcfg.address_bits=0;
  m_devcfg.dummy_bits=0;
  m_devcfg.spics_io_num=cspin;
  m_devcfg.queue_size=7;                    // We want to be able to queue 7 transactions at a time

  // Use the SPI object to determine if the SPI bus has been initialized
  if (m_spibus->m_initialized == false)
    {
    esp_err_t ret = spi_bus_initialize(host, &m_spibus->m_buscfg, 0);
    assert(ret==ESP_OK);
    m_spibus->m_host = host;
    m_spibus->m_initialized = true;
    }

  esp_err_t ret = spi_bus_add_device(host, &m_devcfg, &m_spi);
  assert(ret==ESP_OK);

  m_monitor_task = NULL;
  m_monitor_timer = NULL;
  m_inputstate = ~0;
  m_outputstate = ~0;     // power up: all ports in input mode (high)
  m_monitor_ports = 0;
  *StdMetrics.ms_m_egpio_input = m_inputstate;
  *StdMetrics.ms_m_egpio_output = m_outputstate;
  *StdMetrics.ms_m_egpio_monitor = m_monitor_ports;
  }

max7317::~max7317()
  {
  if (m_monitor_timer)
    delete m_monitor_timer;
  if (m_monitor_task)
    vTaskDelete(m_monitor_task);
  }

std::bitset<10> max7317::GetConfigMonitorPorts()
  {
  std::bitset<10> ports;
  // Parse configuration: list of port numbers separated by spaces
  std::string value = MyConfig.GetParamValue("egpio", "monitor.ports");
  std::istringstream vs(value);
  std::string token;
  int port;
  while (std::getline(vs, token, ' '))
    {
    std::istringstream ts(token);
    ts >> port;
    if (port >= 0 && port <= 9)
      ports[port] = 1;
    }
  return ports;
  }

void max7317::AutoInit()
  {
  if (MyConfig.GetParamValueBool("auto", "egpio", false))
    {
    m_monitor_ports = GetConfigMonitorPorts();
    if (!CheckMonitor())
      ESP_LOGE(TAG, "AutoInit: monitoring could not be started");
    }
  }

/**
 * Output: set specific port output state
 */
void max7317::Output(uint8_t port, uint8_t level)
  {
  uint8_t buf[4];

  // Set port:
  m_spibus->spi_cmd(m_spi, buf, 0, 2, port, level);

  // Update state & framework:
  if (!m_outputstate[port] && level)
    {
    m_outputstate[port] = true;
    *StdMetrics.ms_m_egpio_output = m_outputstate;
    std::string event = "egpio.output.";
    event.append(1, '0'+port);
    event.append(".high");
    ESP_LOGD(TAG, "%s", event.c_str());
    MyEvents.SignalEvent(event, NULL);
    }
  else if (m_outputstate[port] && !level)
    {
    m_outputstate[port] = false;
    *StdMetrics.ms_m_egpio_output = m_outputstate;
    std::string event = "egpio.output.";
    event.append(1, '0'+port);
    event.append(".low");
    ESP_LOGD(TAG, "%s", event.c_str());
    MyEvents.SignalEvent(event, NULL);
    }
  }

std::bitset<10> max7317::OutputState()
  {
  return m_outputstate;
  }

/**
 * Input: read specific port input state
 */
uint8_t max7317::Input(uint8_t port)
  {
  uint8_t buf[2];
  uint8_t level;
  OvmsMutexLock lock(&m_monitor_mutex);

  // Read port state:
  //  Note: MAX7317 SPI read needs a deselect between tx and rx (see specs pg. 8).
  //  SPI always does tx & rx in parallel, so we must send a NOP (0x20,0) on the "rx"
  //  command, as the MAX7317 would interpret (0,0) as a write of level 0 to port 0.
  if (port < 8)
    {
    m_spibus->spi_cmd(m_spi, buf, 0, 2, 0x8E, 0);
    m_spibus->spi_cmd(m_spi, buf, 0, 2, 0x20, 0);
    level = (buf[1] & (1 << port)) ? 1 : 0;
    }
  else
    {
    m_spibus->spi_cmd(m_spi, buf, 0, 2, 0x8F, 0);
    m_spibus->spi_cmd(m_spi, buf, 0, 2, 0x20, 0);
    level = (buf[1] & (1 << (port-8))) ? 1 : 0;
    }

  // Update state & framework:
  if (m_inputstate[port] != level)
    {
    std::bitset<10> pstate = m_inputstate;
    pstate[port] = level;
    *StdMetrics.ms_m_egpio_input = pstate;
    if (!m_inputstate[port] && level)
      {
      std::string event = "egpio.input.";
      event.append(1, '0'+port);
      event.append(".high");
      ESP_LOGD(TAG, "%s", event.c_str());
      MyEvents.SignalEvent(event, NULL);
      }
    else if (m_inputstate[port] && !level)
      {
      std::string event = "egpio.input.";
      event.append(1, '0'+port);
      event.append(".low");
      ESP_LOGD(TAG, "%s", event.c_str());
      MyEvents.SignalEvent(event, NULL);
      }
    m_inputstate[port] = level;
    }

  return level;
  }

/**
 * Inputs: read input states for a set of ports
 */
std::bitset<10> max7317::Inputs(std::bitset<10> ports)
  {
  uint8_t buf[2];
  std::bitset<10> pstate;
  if (ports.none())
    return pstate;
  OvmsMutexLock lock(&m_monitor_mutex);

  // Read port state: see notes above
  if ((ports & std::bitset<10>(0b0011111111)).any())
    {
    m_spibus->spi_cmd(m_spi, buf, 0, 2, 0x8E, 0);
    m_spibus->spi_cmd(m_spi, buf, 0, 2, 0x20, 0);
    pstate = buf[1];
    }
  if ((ports & std::bitset<10>(0b1100000000)).any())
    {
    m_spibus->spi_cmd(m_spi, buf, 0, 2, 0x8F, 0);
    m_spibus->spi_cmd(m_spi, buf, 0, 2, 0x20, 0);
    pstate |= ((uint16_t)buf[1] << 8);
    }

  // Update state & framework:
  pstate &= ports;
  pstate |= (m_inputstate & ~ports);
  *StdMetrics.ms_m_egpio_input = pstate;
  if ((m_inputstate ^ pstate).any())
    {
    std::string event;
    for (int i=0; i<10; i++)
      {
      if (!ports[i])
        continue;
      else if (!m_inputstate[i] && pstate[i])
        {
        event = "egpio.input.";
        event.append(1, '0'+i);
        event.append(".high");
        ESP_LOGD(TAG, "%s", event.c_str());
        MyEvents.SignalEvent(event, NULL);
        }
      else if (m_inputstate[i] && !pstate[i])
        {
        event = "egpio.input.";
        event.append(1, '0'+i);
        event.append(".low");
        ESP_LOGD(TAG, "%s", event.c_str());
        MyEvents.SignalEvent(event, NULL);
        }
      }
    }
  m_inputstate = pstate;

  return pstate;
  }

std::bitset<10> max7317::InputState()
  {
  return m_inputstate;
  }

/**
 * MonitorTask: poll input state
 */
static void MonitorTaskEntry(void* me)
  {
  ((max7317*)me)->MonitorTask();
  }
void max7317::MonitorTask()
  {
  while (true)
    {
    if (m_monitor_semaphore.Take())
      Inputs(m_monitor_ports);
    }
  }

/**
 * CheckMonitor: activate/deactivate port input monitoring as necessary
 */
bool max7317::CheckMonitor()
  {
  *StdMetrics.ms_m_egpio_monitor = m_monitor_ports;
  if (m_monitor_ports.any())
    {
    // activate monitoring:
    if (!m_monitor_task)
      {
      xTaskCreatePinnedToCore(MonitorTaskEntry, "OVMS EGPIOmon",
        4*512, (void*)this, 15, &m_monitor_task, CORE(1));
      if (!m_monitor_task)
        {
        ESP_LOGE(TAG, "Monitor: unable to create task");
        return false;
        }
      }
    if (!m_monitor_timer)
      {
      m_monitor_timer = new OvmsInterval("EGPIOmon");
      if (!m_monitor_timer)
        {
        ESP_LOGE(TAG, "Monitor: unable to create timer");
        return false;
        }
      }
    if (!m_monitor_timer->IsActive())
      {
      int interval = MyConfig.GetParamValueInt("egpio", "monitor.interval",
        CONFIG_OVMS_COMP_MAX7317_MONITOR_INTERVAL);
      if (interval < 10)
        interval = 10;
      if (!m_monitor_timer->Start(interval, [&]{ m_monitor_semaphore.Give(); }))
        {
        ESP_LOGE(TAG, "Monitor: unable to start timer");
        return false;
        }
      }
    }
  else
    {
    // deactivate monitoring:
    if (m_monitor_timer && !m_monitor_timer->Stop())
      {
      ESP_LOGW(TAG, "Monitor: unable to stop timer");
      return false;
      }
    }
  return true;
  }

/**
 * Monitor: enable/disable input monitoring for specific ports
 */
bool max7317::Monitor(std::bitset<10> ports, bool enable)
  {
  OvmsMutexLock lock(&m_monitor_mutex);
  if (enable)
    m_monitor_ports |= ports;
  else
    m_monitor_ports &= ~ports;
  return CheckMonitor();
  }

bool max7317::Monitor(uint8_t port, bool enable)
  {
  OvmsMutexLock lock(&m_monitor_mutex);
  m_monitor_ports[port] = enable;
  return CheckMonitor();
  }

std::bitset<10> max7317::MonitorState()
  {
  OvmsMutexLock lock(&m_monitor_mutex);
  return m_monitor_ports;
  }


void max7317_output(int verbosity, OvmsWriter* writer, OvmsCommand* cmd, int argc, const char* const* argv)
  {
  for (int i = 0; i < argc-1; i += 2)
    {
    int port = atoi(argv[i]);
    if ((port <0)||(port>9))
      {
      writer->puts("Error: Port should be in range 0..9");
      return;
      }
    int level = atoi(argv[i+1]);
    if ((level<0)||(level>255))
      {
      writer->puts("Error: Level should be in range 0..255");
      return;
      }

    MyPeripherals->m_max7317->Output((uint8_t)port,(uint8_t)level);
    writer->printf("EGPIO port %d set to output level %d\n", port, level);
    }
  }

void max7317_pulse(int verbosity, OvmsWriter* writer, OvmsCommand* cmd, int argc, const char* const* argv)
  {
  int port = atoi(argv[0]);
  int level = atoi(argv[1]);
  int durationms = atoi(argv[2]);
  int baselevel = 1-level;

  writer->printf("EGPIO port %d set to base level %d\n", port, baselevel);
  MyPeripherals->m_max7317->Output((uint8_t)port,(uint8_t)baselevel);

  writer->printf("EGPIO port %d setting to level %d for %dms\n", port, level, durationms);
  MyPeripherals->m_max7317->Output((uint8_t)port,(uint8_t)level);

  vTaskDelay(pdMS_TO_TICKS(durationms));

  writer->printf("EGPIO port %d set back to base level %d\n", port, baselevel);
  MyPeripherals->m_max7317->Output((uint8_t)port,(uint8_t)baselevel);
  }

void max7317_input(int verbosity, OvmsWriter* writer, OvmsCommand* cmd, int argc, const char* const* argv)
  {
  for (int i = 0; i < argc; i += 1)
    {
    int port = atoi(argv[i]);
    if ((port <0)||(port>9))
      {
      writer->puts("Error: Port should be in range 0..9");
      return;
      }

    MyPeripherals->m_max7317->Output((uint8_t)port,(uint8_t)1); // set port to input
    int level = MyPeripherals->m_max7317->Input((uint8_t)port);
    writer->printf("EGPIO port %d has input level %d\n", port, level);
    }
  }

void max7317_status(int verbosity, OvmsWriter* writer, OvmsCommand* cmd,
  int argc, const char* const* argv)
  {
  max7317* me = MyPeripherals->m_max7317;

  auto printports = [writer](const char* intro, std::bitset<10> ports)
    {
    writer->printf("%s", intro);
    for (int i=0; i<10; i++)
      writer->printf(" %d:%d", i, ports.test(i));
    writer->puts("");
    };

  writer->puts("EGPIO port status:");
  printports("Output :", me->OutputState());
  printports("Input  :", me->InputState());
  printports("Monitor:", me->MonitorState());
  }

void max7317_monitor(int verbosity, OvmsWriter* writer, OvmsCommand* cmd,
  int argc, const char* const* argv)
  {
  max7317* me = MyPeripherals->m_max7317;
  std::bitset<10> ports;

  // Enable/disable port monitoring?
  if (strcmp(cmd->GetName(), "status") != 0)
    {
    bool enable = (strcmp(cmd->GetName(), "on") == 0);
    for (int i=0; i<argc; i++)
      {
      int port = atoi(argv[i]);
      if (port < 0 || port > 9)
        {
        writer->puts("Error: ports must be in range 0..9");
        }
      else
        {
        ports[port] = 1;
        }
      }
    // no ports given on command line?
    if (ports.none())
      {
      if (enable)
        {
        // enable configured ports:
        ports = me->GetConfigMonitorPorts();
        if (ports.none())
          writer->puts("ERROR: no ports configured");
        }
      else
        {
        // disable all enabled ports:
        ports = me->MonitorState();
        }
      }
    // change monitoring:
    if (ports.any())
      {
      if (enable)
        {
        // set ports to input mode:
        for (int i=0; i<10; i++)
          if (ports[i]) me->Output(i, 1);
        }
      if (me->Monitor(ports, enable))
        {
        writer->printf("OK: input monitoring %s for %d port%s.\n",
          enable ? "enabled" : "disabled",
          ports.count(),
          ports.count() == 1 ? "" : "s");
        }
      else
        {
        writer->printf("ERROR: %s input monitoring failed (see logs for details)\n",
          enable ? "enabling" : "disabling");
        }
      }
    }

  // Show monitoring status:
  ports = me->MonitorState();
  if (ports.none())
    {
    writer->puts("Status: input monitoring disabled for all ports.");
    }
  else if (ports.all())
    {
    writer->puts("Status: input monitoring enabled for all ports.");
    }
  else
    {
    writer->printf("Status: input monitoring enabled for ports:");
    for (int i=0; i<10; i++)
      {
      if (ports[i])
        writer->printf(" %d", i);
      }
    writer->puts("");
    }
  }

class Max7317Init
  {
  public: Max7317Init();
} MyMax7317Init  __attribute__ ((init_priority (4200)));

Max7317Init::Max7317Init()
  {
  ESP_LOGI(TAG, "Initialising MAX7317 EGPIO (4200)");

  MyConfig.RegisterParam("egpio", "EGPIO configuration", true, true);

  OvmsCommand* cmd_egpio = MyCommandApp.RegisterCommand(
    "egpio", "EGPIO framework", max7317_status, "", 0, 0, false);
  cmd_egpio->RegisterCommand(
    "output", "Set EGPIO output level(s)", max7317_output,
    "<port> <level> [<port> <level> ...]", 2, 20);
  cmd_egpio->RegisterCommand(
    "pulse", "Pulse EGPIO output level(s)", max7317_pulse,
    "<port> <level> <durationms>", 3, 3);
  cmd_egpio->RegisterCommand(
    "input", "Get EGPIO input level(s)", max7317_input,
    "<port> [<port> ...]", 1, 10);
  cmd_egpio->RegisterCommand(
    "status", "Show EGPIO status", max7317_status);

  OvmsCommand* cmd_mon = cmd_egpio->RegisterCommand(
    "monitor", "EGPIO input monitoring");
  cmd_mon->RegisterCommand(
    "status", "Show input monitoring status", max7317_monitor);
  cmd_mon->RegisterCommand(
    "on", "Enable input monitoring", max7317_monitor,
    "[<port> ...]\n"
    "No ports = enable as configured in egpio/monitor.ports\n"
    "Note: ports specified will be switched to input mode", 0, 10);
  cmd_mon->RegisterCommand(
    "off", "Disable input monitoring", max7317_monitor,
    "[<port> ...]\n"
    "No ports = disable all", 0, 10);
  }
