/**
 * Marlin 3D Printer Firmware
 * Copyright (c) 2019 MarlinFirmware [https://github.com/MarlinFirmware/Marlin]
 *
 * Based on Sprinter and grbl.
 * Copyright (c) 2011 Camiel Gubbels / Erik van der Zalm
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * This program 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 General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
 *
 */

#include "../../../inc/MarlinConfig.h"

#if HAS_DRIVER(L6470)

#include "../../gcode.h"
#include "../../../libs/L6470/L6470_Marlin.h"
#include "../../../module/stepper/indirection.h"
#include "../../../module/planner.h"

#define DEBUG_OUT ENABLED(L6470_CHITCHAT)
#include "../../../core/debug_out.h"

/**
 *
 * M906: report or set KVAL_HOLD which sets the maximum effective voltage provided by the
 *       PWMs to the steppers
 *
 * J - select which driver(s) to monitor on multi-driver axis
 *     0 - (default) monitor all drivers on the axis or E0
 *     1 - monitor only X, Y, Z or E1
 *     2 - monitor only X2, Y2, Z2 or E2
 *     3 - monitor only Z3 or E3
 *     4 - monitor only E4
 *     5 - monitor only E5
 * Xxxx, Yxxx, Zxxx, Exxx - axis to be monitored with displacement
 *     xxx (1-255) is distance moved on either side of current position
 *
 * I - over current threshold
 *     optional - will report current value from driver if not specified
 *
 * K - value for KVAL_HOLD (0 - 255) (optional)
 *     optional - will report current value from driver if not specified
 *
 */

/**
 * Sets KVAL_HOLD wich affects the current being driven through the stepper.
 *
 * L6470 is used in the STEP-CLOCK mode.  KVAL_HOLD is the only KVAL_xxx
 * that affects the effective voltage seen by the stepper.
 *
 */

/**
 * MACRO to fetch information on the items associated with current limiting
 * and maximum voltage output.
 *
 * L6470 can be setup to shutdown if either current threshold is exceeded.
 *
 * L6470 output current can not be set directly.  It is set indirectly by
 * setting the maximum effective output voltage.
 *
 *  Effective output voltage is set by PWM duty cycle.
 *
 *  Maximum effective output voltage is affected by MANY variables.  The main ones are:
 *    KVAL_HOLD
 *    KVAL_RUN
 *    KVAL_ACC
 *    KVAL_DEC
 *    Vs compensation (if enabled)
 */

void L6470_report_current(L6470 &motor, const uint8_t axis) {
  if (L6470.spi_abort) return;  // don't do anything if set_directions() has occurred
  const uint16_t status = motor.getStatus() ;
  const uint8_t overcurrent_threshold = (uint8_t)motor.GetParam(L6470_OCD_TH),
                stall_threshold = (uint8_t)motor.GetParam(L6470_STALL_TH),
                motor_status = (status  & (STATUS_MOT_STATUS)) >> 13,
                adc_out = motor.GetParam(L6470_ADC_OUT),
                adc_out_limited = constrain(adc_out, 8, 24);
  const float comp_coef = 1600.0f / adc_out_limited;
  const int microsteps = _BV(motor.GetParam(L6470_STEP_MODE) & 0x07);
  char temp_buf[80];
  L6470.say_axis(axis);
  #if ENABLED(L6470_CHITCHAT)
    sprintf_P(temp_buf, PSTR("   status: %4x   "), status);
    DEBUG_ECHO(temp_buf);
    print_bin(status);
  #endif
  sprintf_P(temp_buf, PSTR("\n...OverCurrent Threshold: %2d (%4d mA)"), overcurrent_threshold, (overcurrent_threshold + 1) * 375);
  SERIAL_ECHO(temp_buf);

  char numstr[11];
  dtostrf((stall_threshold + 1) * 31.25, 1, 2, numstr);
  sprintf_P(temp_buf, PSTR("   Stall Threshold: %2d (%s mA)"), stall_threshold, numstr);
  SERIAL_ECHO(temp_buf);

  SERIAL_ECHOPGM("   Motor Status: ");
  const char *stat_str;
  switch (motor_status) {
    default:
    case 0: stat_str = PSTR("stopped"); break;
    case 1: stat_str = PSTR("accelerating"); break;
    case 2: stat_str = PSTR("decelerating"); break;
    case 3: stat_str = PSTR("at constant speed"); break;
  }
  serialprintPGM(stat_str);
  SERIAL_EOL();

  SERIAL_ECHOPAIR("...microsteps: ", microsteps);
  SERIAL_ECHOPAIR("   ADC_OUT: ", adc_out);
  SERIAL_ECHOPGM("   Vs_compensation: ");
  serialprintPGM((motor.GetParam(L6470_CONFIG) & CONFIG_EN_VSCOMP) ? PSTR("ENABLED ") : PSTR("DISABLED"));

  SERIAL_ECHOLNPAIR("   Compensation coefficient: ", dtostrf(comp_coef * 0.01f, 7, 2, numstr));
  SERIAL_ECHOPAIR("...KVAL_HOLD: ", motor.GetParam(L6470_KVAL_HOLD));
  SERIAL_ECHOPAIR("   KVAL_RUN : ", motor.GetParam(L6470_KVAL_RUN));
  SERIAL_ECHOPAIR("   KVAL_ACC: ", motor.GetParam(L6470_KVAL_ACC));
  SERIAL_ECHOPAIR("   KVAL_DEC: ", motor.GetParam(L6470_KVAL_DEC));
  SERIAL_ECHOPGM("   V motor max =  ");
  float val;
  PGM_P suf;
  switch (motor_status) {
    case 0:
      val = motor.GetParam(L6470_KVAL_HOLD);
      suf = PSTR("(KVAL_HOLD)");
      break;
    case 1:
      val = motor.GetParam(L6470_KVAL_RUN);
      suf = PSTR("(KVAL_RUN)");
      break;
    case 2:
      val = motor.GetParam(L6470_KVAL_ACC);
      suf = PSTR("(KVAL_ACC)");
      break;
    case 3:
      val = motor.GetParam(L6470_KVAL_DEC);
      suf = PSTR("(KVAL_DEC)");
      break;
  }
  SERIAL_ECHO(dtostrf(val * 100 / 256, 10, 2, numstr));
  SERIAL_ECHOPGM("%% ");
  serialprintPGM(suf);
  SERIAL_EOL();
}

void GcodeSuite::M906() {
  #define L6470_SET_KVAL_HOLD(Q) stepper##Q.SetParam(L6470_KVAL_HOLD, value)

  DEBUG_ECHOLNPGM("M906");

  bool report_current = true;

  #if HAS_DRIVER(L6470)
    const uint8_t index = parser.byteval('I');
  #endif

  LOOP_XYZE(i) if (uint8_t value = parser.byteval(axis_codes[i])) {

    report_current = false;

    if (planner.has_blocks_queued() || planner.cleaning_buffer_counter) {
      SERIAL_ECHOLNPGM("!Can't set KVAL_HOLD with steppers moving");
      return;
    }

    switch (i) {
      case X_AXIS:
        #if AXIS_DRIVER_TYPE_X(L6470)
          if (index == 0) L6470_SET_KVAL_HOLD(X);
        #endif
        #if AXIS_DRIVER_TYPE_X2(L6470)
          if (index == 1) L6470_SET_KVAL_HOLD(X2);
        #endif
        break;
      case Y_AXIS:
        #if AXIS_DRIVER_TYPE_Y(L6470)
          if (index == 0) L6470_SET_KVAL_HOLD(Y);
        #endif
        #if AXIS_DRIVER_TYPE_Y2(L6470)
          if (index == 1) L6470_SET_KVAL_HOLD(Y2);
        #endif
        break;
      case Z_AXIS:
        #if AXIS_DRIVER_TYPE_Z(L6470)
          if (index == 0) L6470_SET_KVAL_HOLD(Z);
        #endif
        #if AXIS_DRIVER_TYPE_Z2(L6470)
          if (index == 1) L6470_SET_KVAL_HOLD(Z2);
        #endif
        #if AXIS_DRIVER_TYPE_Z3(L6470)
          if (index == 2) L6470_SET_KVAL_HOLD(Z3);
        #endif
        break;
      case E_AXIS: {
        const int8_t target_extruder = get_target_extruder_from_command();
        if (target_extruder < 0) return;
        switch (target_extruder) {
          #if AXIS_DRIVER_TYPE_E0(L6470)
            case 0: L6470_SET_KVAL_HOLD(E0); break;
          #endif
          #if AXIS_DRIVER_TYPE_E1(L6470)
            case 1: L6470_SET_KVAL_HOLD(E1); break;
          #endif
          #if AXIS_DRIVER_TYPE_E2(L6470)
            case 2: L6470_SET_KVAL_HOLD(E2); break;
          #endif
          #if AXIS_DRIVER_TYPE_E3(L6470)
            case 3: L6470_SET_KVAL_HOLD(E3); break;
          #endif
          #if AXIS_DRIVER_TYPE_E4(L6470)
            case 4: L6470_SET_KVAL_HOLD(E4); break;
          #endif
          #if AXIS_DRIVER_TYPE_E5(L6470)
            case 5: L6470_SET_KVAL_HOLD(E5); break;
          #endif
        }
      } break;
    }
  }

  if (report_current) {
    #define L6470_REPORT_CURRENT(Q) L6470_report_current(stepper##Q, Q)

    L6470.spi_active = true;    // let set_directions() know we're in the middle of a series of SPI transfers

    #if AXIS_DRIVER_TYPE_X(L6470)
      L6470_REPORT_CURRENT(X);
    #endif
    #if AXIS_DRIVER_TYPE_X2(L6470)
      L6470_REPORT_CURRENT(X2);
    #endif
    #if AXIS_DRIVER_TYPE_Y(L6470)
      L6470_REPORT_CURRENT(Y);
    #endif
    #if AXIS_DRIVER_TYPE_Y2(L6470)
      L6470_REPORT_CURRENT(Y2);
    #endif
    #if AXIS_DRIVER_TYPE_Z(L6470)
      L6470_REPORT_CURRENT(Z);
    #endif
    #if AXIS_DRIVER_TYPE_Z2(L6470)
      L6470_REPORT_CURRENT(Z2);
    #endif
    #if AXIS_DRIVER_TYPE_Z3(L6470)
      L6470_REPORT_CURRENT(Z3);
    #endif
    #if AXIS_DRIVER_TYPE_E0(L6470)
      L6470_REPORT_CURRENT(E0);
    #endif
    #if AXIS_DRIVER_TYPE_E1(L6470)
      L6470_REPORT_CURRENT(E1);
    #endif
    #if AXIS_DRIVER_TYPE_E2(L6470)
      L6470_REPORT_CURRENT(E2);
    #endif
    #if AXIS_DRIVER_TYPE_E3(L6470)
      L6470_REPORT_CURRENT(E3);
    #endif
    #if AXIS_DRIVER_TYPE_E4(L6470)
      L6470_REPORT_CURRENT(E4);
    #endif
    #if AXIS_DRIVER_TYPE_E5(L6470)
      L6470_REPORT_CURRENT(E5);
    #endif

    L6470.spi_active = false;   // done with all SPI transfers - clear handshake flags
    L6470.spi_abort = false;
  }
}

#endif // HAS_DRIVER(L6470)
