#include "meter_parser.h"

#include <math.h>
#include <string.h>

namespace {

enum class ObisField : uint8_t {
  None = 0,
  Energy,
  TotalPower,
  Phase1,
  Phase2,
  Phase3,
  MeterSeconds
};

struct ParseProgress {
  bool malformed_value;
  bool invalid_meter_seconds;
};

static const char *skip_space(const char *text) {
  while (*text == ' ' || *text == '\t') {
    ++text;
  }
  return text;
}

static bool has_obis_prefix(const char *line, const char *obis) {
  const char *start = skip_space(line);
  size_t len = strlen(obis);
  if (strncmp(start, obis, len) != 0) {
    return false;
  }
  char next = start[len];
  return next == '*' || next == '(';
}

static ObisField detect_obis_field(const char *line) {
  if (has_obis_prefix(line, "1-0:1.8.0")) return ObisField::Energy;
  if (has_obis_prefix(line, "1-0:16.7.0")) return ObisField::TotalPower;
  if (has_obis_prefix(line, "1-0:36.7.0")) return ObisField::Phase1;
  if (has_obis_prefix(line, "1-0:56.7.0")) return ObisField::Phase2;
  if (has_obis_prefix(line, "1-0:76.7.0")) return ObisField::Phase3;
  if (has_obis_prefix(line, "0-0:96.8.0")) return ObisField::MeterSeconds;
  return ObisField::None;
}

static bool parse_decimal(const char *begin, const char *end, float &value) {
  if (!begin || !end || begin >= end) {
    return false;
  }
  while (begin < end && (*begin == ' ' || *begin == '\t')) ++begin;
  while (end > begin && (end[-1] == ' ' || end[-1] == '\t')) --end;
  if (begin >= end) return false;

  bool negative = false;
  if (*begin == '+' || *begin == '-') {
    negative = *begin == '-';
    ++begin;
  }
  bool saw_digit = false;
  bool saw_separator = false;
  double whole = 0.0;
  double fraction = 0.0;
  double divisor = 1.0;
  for (const char *cur = begin; cur < end; ++cur) {
    char c = *cur;
    if (c >= '0' && c <= '9') {
      saw_digit = true;
      if (saw_separator) {
        divisor *= 10.0;
        fraction += static_cast<double>(c - '0') / divisor;
      } else {
        whole = whole * 10.0 + static_cast<double>(c - '0');
      }
      continue;
    }
    if ((c == '.' || c == ',') && !saw_separator) {
      saw_separator = true;
      continue;
    }
    return false;
  }
  if (!saw_digit) return false;
  double parsed = whole + fraction;
  value = static_cast<float>(negative ? -parsed : parsed);
  return isfinite(value);
}

static bool payload_bounds(const char *line, const char *&value_begin,
                           const char *&value_end, const char *&unit_begin,
                           const char *&unit_end) {
  const char *lparen = strchr(line, '(');
  if (!lparen) return false;
  const char *rparen = strchr(lparen + 1, ')');
  if (!rparen) return false;
  const char *asterisk = nullptr;
  for (const char *cur = lparen + 1; cur < rparen; ++cur) {
    if (*cur == '*') {
      asterisk = cur;
      break;
    }
  }
  value_begin = lparen + 1;
  value_end = asterisk ? asterisk : rparen;
  unit_begin = asterisk ? asterisk + 1 : rparen;
  unit_end = rparen;
  return true;
}

static bool unit_equals(const char *begin, const char *end, const char *unit) {
  while (begin < end && (*begin == ' ' || *begin == '\t')) ++begin;
  while (end > begin && (end[-1] == ' ' || end[-1] == '\t')) --end;
  size_t expected = strlen(unit);
  return static_cast<size_t>(end - begin) == expected && strncmp(begin, unit, expected) == 0;
}

static bool parse_measurement(const char *line, const char *required_unit,
                              float &value, bool allow_wh_scale) {
  const char *value_begin = nullptr;
  const char *value_end = nullptr;
  const char *unit_begin = nullptr;
  const char *unit_end = nullptr;
  if (!payload_bounds(line, value_begin, value_end, unit_begin, unit_end) ||
      !parse_decimal(value_begin, value_end, value)) {
    return false;
  }
  if (unit_equals(unit_begin, unit_end, required_unit)) {
    return true;
  }
  if (allow_wh_scale && unit_equals(unit_begin, unit_end, "Wh")) {
    value *= 0.001f;
    return true;
  }
  return false;
}

static int8_t hex_nibble(char c) {
  if (c >= '0' && c <= '9') return static_cast<int8_t>(c - '0');
  if (c >= 'A' && c <= 'F') return static_cast<int8_t>(10 + c - 'A');
  if (c >= 'a' && c <= 'f') return static_cast<int8_t>(10 + c - 'a');
  return -1;
}

static bool parse_meter_seconds(const char *line, uint32_t &value) {
  const char *value_begin = nullptr;
  const char *value_end = nullptr;
  const char *unit_begin = nullptr;
  const char *unit_end = nullptr;
  if (!payload_bounds(line, value_begin, value_end, unit_begin, unit_end)) return false;
  while (value_begin < value_end && (*value_begin == ' ' || *value_begin == '\t')) ++value_begin;
  while (value_end > value_begin && (value_end[-1] == ' ' || value_end[-1] == '\t')) --value_end;
  size_t digits = static_cast<size_t>(value_end - value_begin);
  if (digits == 0 || digits > 8) return false;
  uint32_t parsed = 0;
  for (const char *cur = value_begin; cur < value_end; ++cur) {
    int8_t nibble = hex_nibble(*cur);
    if (nibble < 0) return false;
    parsed = (parsed << 4) | static_cast<uint32_t>(nibble);
  }
  value = parsed;
  return true;
}

static void parse_line(const char *line, MeterParseResult &result, ParseProgress &progress) {
  ObisField field = detect_obis_field(line);
  float value = NAN;
  bool ok = false;
  switch (field) {
    case ObisField::Energy:
      ok = parse_measurement(line, "kWh", value, true);
      if (ok) {
        result.energy_total_kwh = value;
        result.fields_mask |= 0x01;
      }
      break;
    case ObisField::TotalPower:
      ok = parse_measurement(line, "W", value, false);
      if (ok) {
        result.total_power_w = value;
        result.fields_mask |= 0x02;
      }
      break;
    case ObisField::Phase1:
    case ObisField::Phase2:
    case ObisField::Phase3: {
      ok = parse_measurement(line, "W", value, false);
      if (ok) {
        uint8_t index = field == ObisField::Phase1 ? 0 : (field == ObisField::Phase2 ? 1 : 2);
        result.phase_power_w[index] = value;
        result.fields_mask |= static_cast<uint8_t>(0x04 << index);
      }
      break;
    }
    case ObisField::MeterSeconds:
      ok = parse_meter_seconds(line, result.meter_seconds);
      result.meter_seconds_valid = ok;
      if (!ok) progress.invalid_meter_seconds = true;
      return;
    case ObisField::None:
      return;
  }
  if (!ok) progress.malformed_value = true;
}

}  // namespace

void meter_frame_collector_init(MeterFrameCollector &collector) {
  collector.buffer[0] = '\0';
  collector.length = 0;
  collector.last_rx_ms = 0;
  collector.in_frame = false;
}

MeterFrameEvent meter_frame_collector_check_timeout(MeterFrameCollector &collector,
                                                     uint32_t now_ms,
                                                     uint32_t timeout_ms) {
  if (!collector.in_frame || now_ms - collector.last_rx_ms <= timeout_ms) {
    return MeterFrameEvent::None;
  }
  collector.length = 0;
  collector.buffer[0] = '\0';
  collector.in_frame = false;
  return MeterFrameEvent::Timeout;
}

MeterFrameEvent meter_frame_collector_feed(MeterFrameCollector &collector,
                                            char byte,
                                            uint32_t now_ms) {
  if (!collector.in_frame) {
    if (byte != '/') return MeterFrameEvent::None;
    collector.in_frame = true;
    collector.length = 1;
    collector.buffer[0] = '/';
    collector.buffer[1] = '\0';
    collector.last_rx_ms = now_ms;
    return MeterFrameEvent::None;
  }

  collector.last_rx_ms = now_ms;
  if (byte == '/') {
    collector.length = 1;
    collector.buffer[0] = '/';
    collector.buffer[1] = '\0';
    return MeterFrameEvent::Resync;
  }
  if (collector.length >= METER_FRAME_MAX) {
    collector.length = 0;
    collector.buffer[0] = '\0';
    collector.in_frame = false;
    return MeterFrameEvent::Overflow;
  }
  collector.buffer[collector.length++] = byte;
  collector.buffer[collector.length] = '\0';
  if (byte == '!') {
    collector.in_frame = false;
    return MeterFrameEvent::Complete;
  }
  return MeterFrameEvent::None;
}

bool meter_parser_parse(const char *frame, size_t len, MeterParseResult &result) {
  result = {};
  result.energy_total_kwh = NAN;
  result.total_power_w = NAN;
  result.phase_power_w[0] = NAN;
  result.phase_power_w[1] = NAN;
  result.phase_power_w[2] = NAN;
  result.status = MeterParseStatus::Empty;
  if (!frame || len == 0 || frame[0] != '/') return false;

  char line[128];
  size_t line_len = 0;
  bool terminator = false;
  ParseProgress progress = {};
  for (size_t i = 0; i < len; ++i) {
    char c = frame[i];
    if (c == '\r') continue;
    if (c == '!') {
      if (line_len > 0) {
        line[line_len] = '\0';
        parse_line(line, result, progress);
      }
      terminator = true;
      break;
    }
    if (c == '\n') {
      line[line_len] = '\0';
      parse_line(line, result, progress);
      line_len = 0;
      continue;
    }
    if (line_len + 1 >= sizeof(line)) {
      result.status = MeterParseStatus::LineTooLong;
      return false;
    }
    line[line_len++] = c;
  }

  if (!terminator) {
    result.status = MeterParseStatus::MissingTerminator;
    return false;
  }
  if (progress.invalid_meter_seconds) {
    result.status = MeterParseStatus::InvalidMeterSeconds;
    return false;
  }
  if (progress.malformed_value) {
    result.status = MeterParseStatus::MalformedValue;
    return false;
  }
  if (result.fields_mask != METER_REQUIRED_FIELDS_MASK || !result.meter_seconds_valid) {
    result.status = MeterParseStatus::MissingRequired;
    return false;
  }
  result.valid = true;
  result.status = MeterParseStatus::Valid;
  return true;
}

const char *meter_parse_status_text(MeterParseStatus status) {
  switch (status) {
    case MeterParseStatus::Valid: return "valid";
    case MeterParseStatus::Empty: return "empty";
    case MeterParseStatus::MissingTerminator: return "missing_terminator";
    case MeterParseStatus::LineTooLong: return "line_too_long";
    case MeterParseStatus::MissingRequired: return "missing_required";
    case MeterParseStatus::InvalidMeterSeconds: return "invalid_meter_seconds";
    case MeterParseStatus::MalformedValue: return "malformed_value";
  }
  return "unknown";
}
