/*
  ngc_params.c - get/set NGC parameter value by id or name

  Part of grblHAL

  Copyright (c) 2021 Terje Io

  Grbl 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.

  Grbl 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 Grbl.  If not, see <http://www.gnu.org/licenses/>.
*/

/*
  All predefined parameters defined in NIST RS274NGC version 3 (ref section 3.2.1) are implemented.
  Most additional predefined parameters defined by LinuxCNC (ref section 5.2.3.1) are implemented.
*/

#include <stdint.h>
#include <stdbool.h>
#include <stdlib.h>
#include <string.h>
#include <math.h>

#include "system.h"
#include "settings.h"
#include "ngc_params.h"

#define MAX_PARAM_LENGTH 20

typedef float (*ngc_param_get_ptr)(ngc_param_id_t id);
typedef float (*ngc_named_param_get_ptr)(void);

typedef struct {
    ngc_param_id_t id_min;
    ngc_param_id_t id_max;
    ngc_param_get_ptr get;
} ngc_ro_param_t;

typedef struct ngc_rw_param {
    ngc_param_id_t id;
    float value;
    struct ngc_rw_param *next;
} ngc_rw_param_t;

typedef struct {
    const char *name;
    ncg_name_param_id_t id;
    ngc_named_param_get_ptr get;
} ngc_named_ro_param_t;

typedef struct ngc_named_rw_param {
    char name[MAX_PARAM_LENGTH + 1];
    float value;
    struct ngc_named_rw_param *next;
} ngc_named_rw_param_t;

ngc_rw_param_t *rw_params = NULL;
ngc_named_rw_param_t *rw_global_params = NULL;

static float _relative_pos (uint_fast8_t axis)
{
    float value;

    if(axis < N_AXIS) {
        value = sys.position[axis] / settings.axis[axis].steps_per_mm - gc_get_offset(axis);
        if(settings.flags.report_inches)
            value *= 25.4f;
    } else
        value = 0.0f;

    return value;
}

// numbered parameters

static float probe_coord (ngc_param_id_t id)
{
    uint_fast8_t axis = id % 10;

    return axis <= N_AXIS ? sys.probe_position[axis - 1] : 0.0f;
}

static float scaling_factors (ngc_param_id_t id)
{
    float *factors = gc_get_scaling();
    uint_fast8_t axis = id % 10;

    return axis <= N_AXIS ? factors[axis - 1] : 0.0f;
}

static float probe_result (ngc_param_id_t id)
{
    return sys.flags.probe_succeeded ? 1.0f : 0.0f;
}
/*
static float home_pos (ngc_param_id_t id)
{
    uint_fast8_t axis = id % 10;

    return axis <= N_AXIS ? sys.home_position[axis - 1] : 0.0f;
}
*/
static float m66_result (ngc_param_id_t id)
{
    return (float)sys.var5399;
}

static float tool_number (ngc_param_id_t id)
{
    return (float)gc_state.tool->tool;
}

static float tool_offset (ngc_param_id_t id)
{
    uint_fast8_t axis = id % 10;

    return axis <= N_AXIS ? gc_state.tool_length_offset[axis] : 0.0f;
}

static float g28_home (ngc_param_id_t id)
{
    float value = 0.0f;
    uint_fast8_t axis = id % 10;
    coord_system_t data;

    if(axis <= N_AXIS && settings_read_coord_data(CoordinateSystem_G28, &data.xyz))
        value = data.xyz[axis - 1];

    return value;
}

static float g30_home (ngc_param_id_t id)
{
    float value = 0.0f;
    uint_fast8_t axis = id % 10;
    coord_system_t data;

#if COMPATIBILITY_LEVEL > 1
    if(id <= CoordinateSystem_G59) {
#endif
    if (axis <= N_AXIS && settings_read_coord_data(CoordinateSystem_G30, &data.xyz))
        value = data.xyz[axis - 1];
#if COMPATIBILITY_LEVEL > 1
    }
#endif

    return value;
}

static float coord_system (ngc_param_id_t id)
{
    return (float)gc_state.modal.coord_system.id;
}

static float coord_system_offset (ngc_param_id_t id)
{
    float value = 0.0f;
    uint_fast8_t axis = id % 10;
    coord_system_t data;

    id = (id - 5220 - axis - (id == 0 ? 10 : 0)) / 20;

    if (axis > 0 && axis <= N_AXIS && settings_read_coord_data((coord_system_id_t)id, &data.xyz))
        value = data.xyz[axis - 1];

    return value;
}

static float g92_offset_applied (ngc_param_id_t id)
{
    return (float)gc_state.g92_coord_offset_applied;
}

static float g92_offset (ngc_param_id_t id)
{
    uint_fast8_t axis = id % 10;

    return axis <= N_AXIS ? gc_state.g92_coord_offset [axis - 1] : 0.0f;
}

static float work_position (ngc_param_id_t id)
{
    float value = 0.0f;
    uint_fast8_t axis = id % 10;

    if(axis < N_AXIS)
        value = _relative_pos(axis);

    return value;
}

PROGMEM static const ngc_ro_param_t ngc_ro_params[] = {
    { .id_min = 5061, .id_max = 5069, .get = probe_coord },        // LinuxCNC
    { .id_min = 5070, .id_max = 5070, .get = probe_result },       // LinuxCNC
    { .id_min = 5161, .id_max = 5169, .get = g28_home },
    { .id_min = 5181, .id_max = 5189, .get = g30_home },
    { .id_min = 5191, .id_max = 5199, .get = scaling_factors },    // Mach3
    { .id_min = 5210, .id_max = 5210, .get = g92_offset_applied }, // LinuxCNC
    { .id_min = 5211, .id_max = 5219, .get = g92_offset },
    { .id_min = 5220, .id_max = 5220, .get = coord_system },
    { .id_min = 5221, .id_max = 5230, .get = coord_system_offset },
    { .id_min = 5241, .id_max = 5250, .get = coord_system_offset },
    { .id_min = 5261, .id_max = 5270, .get = coord_system_offset },
    { .id_min = 5281, .id_max = 5290, .get = coord_system_offset },
    { .id_min = 5301, .id_max = 5310, .get = coord_system_offset },
    { .id_min = 5321, .id_max = 5230, .get = coord_system_offset },
    { .id_min = 5341, .id_max = 5350, .get = coord_system_offset },
    { .id_min = 5361, .id_max = 5370, .get = coord_system_offset },
    { .id_min = 5381, .id_max = 5390, .get = coord_system_offset },
    { .id_min = 5399, .id_max = 5399, .get = m66_result },         // LinuxCNC
    { .id_min = 5400, .id_max = 5400, .get = tool_number },        // LinuxCNC
    { .id_min = 5401, .id_max = 5409, .get = tool_offset },        // LinuxCNC
    { .id_min = 5420, .id_max = 5428, .get = work_position }       // LinuxCNC
};

bool ngc_param_get (ngc_param_id_t id, float *value)
{
    bool found = id > 0 && id < ngc_ro_params[0].id_min;
    uint_fast8_t idx = sizeof(ngc_ro_params) / sizeof(ngc_ro_param_t);

    *value = 0.0f;

    if(found) {
        ngc_rw_param_t *rw_param = rw_params;
        while(rw_param) {
            if(rw_param->id == id) {
                *value = rw_param->value;
                rw_param = NULL;
            } else
                rw_param = rw_param->next;
        }
    } else do {
        idx--;
        if((found = id >= ngc_ro_params[idx].id_min && id <= ngc_ro_params[idx].id_max))
            *value = ngc_ro_params[idx].get(id);
    } while(idx && !found);

    return found;
}

bool ngc_param_is_rw (ngc_param_id_t id)
{
    return id > 0 && id < ngc_ro_params[0].id_min;
}

bool ngc_param_exists (ngc_param_id_t id)
{
    return id > 0 && id <= ngc_ro_params[(sizeof(ngc_ro_params) / sizeof(ngc_ro_param_t)) - 1].id_max;
}

bool ngc_param_set (ngc_param_id_t id, float value)
{
    bool ok = id > 0 && id < ngc_ro_params[0].id_min;

    if(ok) {

        ngc_rw_param_t *rw_param = rw_params, *rw_param_last = rw_params;

        while(rw_param) {
            if(rw_param->id == id) {
                break;
            } else {
                rw_param_last = rw_param;
                rw_param = rw_param->next;
            }
        }

        if(rw_param == NULL && value != 0.0f && (rw_param = malloc(sizeof(ngc_rw_param_t)))) {
            rw_param->id = id;
            rw_param->next = NULL;
            if(rw_params == NULL)
                rw_params = rw_param;
            else
                rw_param_last->next = rw_param;
        }

        if(rw_param)
            rw_param->value = value;
        else
            ok = value == 0.0f;
    }

    return ok;
}
PROGMEM static const ngc_named_ro_param_t ngc_named_ro_param[] = {
    { .name = "_vmajor",              .id = NGCParam_vmajor },
    { .name = "_vminor",              .id = NGCParam_vminor },
    { .name = "_line",                .id = NGCParam_line },
    { .name = "_motion_mode",         .id = NGCParam_motion_mode },
    { .name = "_plane",               .id = NGCParam_plane },
    { .name = "_ccomp",               .id = NGCParam_ccomp },
    { .name = "_metric",              .id = NGCParam_metric },
    { .name = "_imperial",            .id = NGCParam_imperial },
    { .name = "_absolute",            .id = NGCParam_absolute },
    { .name = "_incremental",         .id = NGCParam_incremental },
    { .name = "_inverse_time",        .id = NGCParam_inverse_time },
    { .name = "_units_per_minute",    .id = NGCParam_units_per_minute },
    { .name = "_units_per_rev",       .id = NGCParam_units_per_rev },
    { .name = "_coord_system",        .id = NGCParam_coord_system },
    { .name = "_tool_offset",         .id = NGCParam_tool_offset },
    { .name = "_retract_r_plane",     .id = NGCParam_retract_r_plane },
    { .name = "_retract_old_z",       .id = NGCParam_retract_old_z },
    { .name = "_spindle_rpm_mode",    .id = NGCParam_spindle_rpm_mode },
    { .name = "_spindle_css_mode",    .id = NGCParam_spindle_css_mode },
    { .name = "_ijk_absolute_mode",   .id = NGCParam_ijk_absolute_mode },
    { .name = "_lathe_diameter_mode", .id = NGCParam_lathe_diameter_mode },
    { .name = "_lathe_radius_mode",   .id = NGCParam_lathe_radius_mode },
    { .name = "_spindle_on",          .id = NGCParam_spindle_on },
    { .name = "_spindle_cw",          .id = NGCParam_spindle_cw },
    { .name = "_mist",                .id = NGCParam_mist },
    { .name = "_flood",               .id = NGCParam_flood },
    { .name = "_speed_override",      .id = NGCParam_speed_override },
    { .name = "_feed_override",       .id = NGCParam_feed_override },
    { .name = "_adaptive_feed",       .id = NGCParam_adaptive_feed },
    { .name = "_feed_hold",           .id = NGCParam_feed_hold },
    { .name = "_feed",                .id = NGCParam_feed },
    { .name = "_rpm",                 .id = NGCParam_rpm },
    { .name = "_x",                   .id = NGCParam_x },
    { .name = "_y",                   .id = NGCParam_y },
    { .name = "_z",                   .id = NGCParam_z },
    { .name = "_a",                   .id = NGCParam_a },
    { .name = "_b",                   .id = NGCParam_b },
    { .name = "_c",                   .id = NGCParam_c },
    { .name = "_u",                   .id = NGCParam_u },
    { .name = "_v",                   .id = NGCParam_v },
    { .name = "_w",                   .id = NGCParam_w },
    { .name = "_current_tool",        .id = NGCParam_current_tool },
    { .name = "_current_pocket",      .id = NGCParam_current_pocket },
    { .name = "_selected_tool",       .id = NGCParam_selected_tool },
    { .name = "_selected_pocket",     .id = NGCParam_selected_pocket },
};


// Named parameters

float ngc_named_param_get_by_id (ncg_name_param_id_t id)
{
    float value;

    switch(id) {

        case NGCParam_vmajor:
            value = 1.1f;
            break;

        case NGCParam_vminor:
            value = 0.0f; // TODO: derive from version letter?
            break;

        case NGCParam_line:
            value = (float)gc_state.line_number;
            break;

        case NGCParam_motion_mode:
            value = (float)(gc_state.modal.motion * 10); // TODO: Fix G38.x
            break;

        case NGCParam_plane:
            value = (float)(170 + gc_state.modal.plane_select * 10);
            break;

        case NGCParam_ccomp:
            value = 400.0f;
            break;

        case NGCParam_metric:
            value = gc_state.modal.units_imperial ? 0.0f : 1.0f;
            break;

        case NGCParam_imperial:
            value = gc_state.modal.units_imperial ? 1.0f : 0.0f;
            break;

        case NGCParam_absolute:
            value = gc_state.modal.distance_incremental ? 0.0f : 1.0f;
            break;

        case NGCParam_incremental:
            value = gc_state.modal.distance_incremental ? 1.0f : 0.0f;
            break;

        case NGCParam_inverse_time:
            value = gc_state.modal.feed_mode == FeedMode_InverseTime ? 1.0f : 0.0f;
            break;

        case NGCParam_units_per_minute:
            value = gc_state.modal.feed_mode == FeedMode_UnitsPerMin ? 1.0f : 0.0f;
            break;

        case NGCParam_units_per_rev:
            value = gc_state.modal.feed_mode == FeedMode_UnitsPerRev ? 1.0f : 0.0f;
            break;

        case NGCParam_coord_system:
            {
                uint_fast16_t id = gc_state.modal.coord_system.id * 10;

                if(id > (CoordinateSystem_G59 * 10))
                    id = (CoordinateSystem_G59 * 10) + gc_state.modal.coord_system.id - CoordinateSystem_G59;

                value = (float)(540 + id);
            }
            break;

        case NGCParam_tool_offset:
            value = gc_state.modal.tool_offset_mode >= ToolLengthOffset_Enable ? 1.0f : 0.0f;
            break;

        case NGCParam_retract_r_plane:
            value = gc_state.modal.retract_mode == CCRetractMode_Previous ? 1.0f : 0.0f;
            break;

        case NGCParam_retract_old_z:
            value = gc_state.modal.retract_mode == CCRetractMode_RPos ? 1.0f : 0.0f;
            break;

        case NGCParam_spindle_rpm_mode:
            value = gc_state.modal.spindle_rpm_mode == SpindleSpeedMode_RPM ? 1.0f : 0.0f;
            break;

        case NGCParam_spindle_css_mode:
            value = gc_state.modal.spindle_rpm_mode == SpindleSpeedMode_CSS ? 1.0f : 0.0f;
            break;

        case NGCParam_ijk_absolute_mode:
            value = 0.0f;
            break;

        case NGCParam_lathe_diameter_mode:
            value = gc_state.modal.diameter_mode ? 1.0f : 0.0f;
            break;

        case NGCParam_lathe_radius_mode:
            value = gc_state.modal.diameter_mode ? 0.0f : 1.0f;
            break;

        case NGCParam_spindle_on:
            value = gc_state.modal.spindle.on ? 1.0f : 0.0f;
            break;

        case NGCParam_spindle_cw:
            value = gc_state.modal.spindle.ccw ? 1.0f : 0.0f;
            break;

        case NGCParam_mist:
            value = gc_state.modal.coolant.mist ? 1.0f : 0.0f;
            break;

        case NGCParam_flood:
            value = gc_state.modal.coolant.flood ? 1.0f : 0.0f;
            break;

        case NGCParam_speed_override:
            value = gc_state.modal.override_ctrl.spindle_rpm_disable ? 0.0f : 1.0f;
            break;

        case NGCParam_feed_override:
            value = gc_state.modal.override_ctrl.feed_rate_disable ? 0.0f : 1.0f;
            break;

        case NGCParam_adaptive_feed:
            value = 0.0f;
            break;

        case NGCParam_feed_hold:
            value = gc_state.modal.override_ctrl.feed_hold_disable ? 0.0f : 1.0f;
            break;

        case NGCParam_feed:
            value = gc_state.feed_rate;
            break;

        case NGCParam_rpm:
            value = gc_state.spindle.rpm;
            break;

        case NGCParam_x:
            //no break
        case NGCParam_y:
            //no break
        case NGCParam_z:
            //no break
        case NGCParam_a:
            //no break
        case NGCParam_b:
            //no break
        case NGCParam_c:
            //no break
        case NGCParam_u:
            //no break
        case NGCParam_v:
            //no break
        case NGCParam_w:
            value = _relative_pos(id - NGCParam_x);
            break;

        case NGCParam_current_tool:
            value = (float)gc_state.tool->tool;
            break;

        case NGCParam_current_pocket:
            value = 0.0f;
            break;

        case NGCParam_selected_tool:
            value = gc_state.tool_change ? (float)gc_state.tool_pending : -1.0f;
            break;

        case NGCParam_selected_pocket:
            value = 0.0f;
            break;

        default:
            value = NAN;
    }

    return value;
}

bool ngc_named_param_get (char *name, float *value)
{
    char c, *s = name;
    bool found = false;
    uint_fast8_t idx = sizeof(ngc_named_ro_param) / sizeof(ngc_named_ro_param_t);

    // Lowercase name
    while((c = *s))
        *s++ = LCAPS(c);

    *value = 0.0f;

     if(*name == '_') do {
        idx--;
        if((found = !strcmp(name, ngc_named_ro_param[idx].name)))
            *value = ngc_named_param_get_by_id(ngc_named_ro_param[idx].id);
    } while(idx && !found);

    if(!found) {
        ngc_named_rw_param_t *rw_param = rw_global_params;
        while(rw_param && !found) {
            if((found = !strcmp(rw_param->name, name)))
                *value = rw_param->value;
            else
                rw_param = rw_param->next;
        }
    }

    return found;
}

bool ngc_named_param_exists (char *name)
{
    char c, *s1 = name, *s2 = name;
    bool ok = false;
    uint_fast8_t idx = sizeof(ngc_named_ro_param) / sizeof(ngc_named_ro_param_t);

    // Lowercase name, remove control characters and spaces
    while((c = *s1++) && c > ' ')
        *s2++ = LCAPS(c);

    *s2 = '\0';

    // Check if name is supplied, return false if not.
    if((*name == '_' ? *(name + 1) : *name) == '\0')
        return false;

    // Check if it is a (read only) predefined parameter.
    if(*name == '_') do {
        idx--;
        ok = !strcmp(name, ngc_named_ro_param[idx].name);
    } while(idx && !ok);

    // If not predefined attempt to find it.
    if(!ok && rw_global_params && strlen(name) < MAX_PARAM_LENGTH) {

        ngc_named_rw_param_t *rw_param = rw_global_params;

        while(rw_param) {
            if((ok = !strcmp(rw_param->name, name)))
                break;
            rw_param = rw_param->next;
         }
     }

    return ok;
}

bool ngc_named_param_set (char *name, float value)
{
    char c, *s1 = name, *s2 = name;
    bool ok = false;
    uint_fast8_t idx = sizeof(ngc_named_ro_param) / sizeof(ngc_named_ro_param_t);

    // Lowercase name, remove control characters and spaces
    while((c = *s1++) && c > ' ')
        *s2++ = LCAPS(c);

    *s2 = '\0';

    // Check if name is supplied, return false if not.
    if((*name == '_' ? *(name + 1) : *name) == '\0')
        return false;

    // Check if it is a (read only) predefined parameter.
    if(*name == '_') do {
        idx--;
        ok = !strcmp(name, ngc_named_ro_param[idx].name);
    } while(idx && !ok);

    // If not predefined attempt to set it.
    if(!ok && (ok = strlen(name) < MAX_PARAM_LENGTH)) {

        ngc_named_rw_param_t *rw_param = rw_global_params, *rw_param_last = rw_global_params;

         while(rw_param) {
             if(!strcmp(rw_param->name, name)) {
                 break;
             } else {
                 rw_param_last = rw_param;
                 rw_param = rw_param->next;
             }
         }

         if(rw_param == NULL && (rw_param = malloc(sizeof(ngc_named_rw_param_t)))) {
             strcpy(rw_param->name, name);
             rw_param->next = NULL;
             if(rw_global_params == NULL)
                 rw_global_params = rw_param;
             else
                 rw_param_last->next = rw_param;
         }

         if((ok = rw_param != NULL))
             rw_param->value = value;
     }

    return ok;
}
