// low = 3K
// mid = 300K
// high = 16K

enum class Rating : uint8_t { low, mid, high, none };

struct RangeToPin {
  const uint8_t pin[3];
  const uint32_t low[2];
  const uint32_t mid[2];
  const uint32_t high[2];
};

// The range values are converted to unsigned long.
// They represent the measured value of type float with 3 decimal places.
constexpr RangeToPin led {
    {9, 10, 11},
    {200000, 1800999},
    {1801000, 2700999},
    {2701000, 5000000}
};

// Resistor pin 4   3K
//              5 300K
//              6  16K
constexpr RangeToPin ppm {
    {4, 5, 6},
    {0, 2000999},
    {2001000, 3000999},
    {3001000, 5000000}
};

constexpr uint8_t ORPpin = A0;   // Sensor connected to A0

template <typename T, uint8_t MAX_SIZE> class Readings {
public:
  uint8_t getIdx() { return idx; };

  const void setValue(T val) {
    values[idx] = val;
    if (++idx >= maxIdx) idx = 0;
  }

  T getAverage() {
    T tmp = 0;
    for (auto value : values) { tmp += value; }
    return tmp / maxIdx;
  }

private:
  const uint8_t maxIdx {MAX_SIZE};
  T values[MAX_SIZE];
  uint8_t idx {0};
};
Readings<float, 5> readings;

int ORPReading;
float ORPVoltage;
float ORPSalinity;

float predictSalinity(float voltage) {
  float ppm = 0;
  // Calibration points array
  float calibrationPoints[][2] = {
      {0.00,    0   }, // Assuming 0 ppm at 0 V
      {0.44985, 0.5 },
      {2.76,    80  },
      {2.97,    90  },
      {3.5657,  100 },
      {3.6377,  1000}, //  3 k ohms range
      {3.8550,  2000}, // Assuming 2000 ppm at 3.4 V
  // Continue with additional points as needed
      {4.21190, 3000}, // 300k ohms range(Most accurate range)
      {4.3256,  4000}, // 16 k ohms resistance range
      {4.8079,  5000},
  };
  int numPoints = sizeof(calibrationPoints) / sizeof(calibrationPoints[0]);

  // Find where the voltage fits within the calibration points
  for (int i = 0; i < numPoints - 1; i++) {
    if (voltage >= calibrationPoints[i][0] && voltage < calibrationPoints[i + 1][0]) {
      // Perform linear interpolation
      float slope = (calibrationPoints[i + 1][1] - calibrationPoints[i][1]) /
                    (calibrationPoints[i + 1][0] - calibrationPoints[i][0]);
      ppm = calibrationPoints[i][1] + slope * (voltage - calibrationPoints[i][0]);
      break;
    }
  }
  return ppm;
}

float readAndCalculateSalinity() {
  ORPReading = analogRead(ORPpin);
  ORPVoltage = ORPReading * (5.0 / 1023.0);
  return predictSalinity(ORPVoltage);
}

//
// Check range -> return level low, mid or high
//
Rating checkRange(float measuredValue, const RangeToPin &rtp) {
  uint32_t compare = measuredValue * 1000;   // Convert to unsigend long 
  Rating result;
  if (compare >= rtp.low[0] && compare <= rtp.low[1]) {
    result = Rating::low;
  } else if (compare >= rtp.mid[0] && compare <= rtp.mid[1]) {
    result = Rating::mid;
  } else if (compare >= rtp.high[0] && compare <= rtp.high[1]) {
    result = Rating::high;
  } else {
    result = Rating::none;
  }
  return result;
}

//
// Switch the pins (Resistor and/or led) according to the given value (low, mid or high)
//
void switchPins(Rating val, const RangeToPin &rtp) {
  for (auto pin : rtp.pin) {
    digitalWrite(pin, LOW);   // Reset LEDs before setting
  }
  switch (val) {
    case Rating::low: [[fallthrough]];
    case Rating::mid: [[fallthrough]];
    case Rating::high: digitalWrite(rtp.pin[static_cast<uint8_t>(val)], HIGH); break;
    default: break;   // out of range, do nothig.
  }
}

//
// Main program
//
void setup() {
  Serial.begin(9600);
  for (auto pin : led.pin) { pinMode(pin, OUTPUT); }
  for (auto pin : ppm.pin) { pinMode(pin, OUTPUT); }

  // Start with the middle range resistor
  digitalWrite(ppm.pin[static_cast<uint8_t>(Rating::mid)], HIGH);
}

void loop() {
  static Rating oldLevelResistor {Rating::none};
  static Rating oldLevelLed {Rating::none};

  ORPSalinity = readAndCalculateSalinity();
  readings.setValue(ORPSalinity);
  // readings.incIdx();
  float averageSalinity = readings.getAverage();

  Rating levelResistor = checkRange(ORPSalinity, ppm);
  Rating levelLed = checkRange(ORPSalinity, led);
  if (oldLevelResistor != levelResistor) {
    oldLevelResistor = levelResistor;
    switchPins(levelResistor, ppm);
  }
  if (oldLevelLed != levelLed) {
    oldLevelLed = levelLed;
    switchPins(levelLed, led);
  }
  Serial.println("=====================");
  Serial.print("Raw Reading: ");
  Serial.println(ORPReading);
  Serial.print("Voltage: ");
  Serial.println(ORPVoltage, 4);
  Serial.print("Predicted Salinity (PPM): ");
  Serial.println(ORPSalinity);
  Serial.print("Average Salinity (PPM): ");
  Serial.println(averageSalinity);
  Serial.print("Active Resistor: ");
  switch (levelResistor) {
    case Rating::low: Serial.println("3K ohms"); break;
    case Rating::mid: Serial.println("300K ohms"); break;
    case Rating::high: Serial.println("16K ohms"); break;
    default: break;
  }
  delay(1000);
}
to Sensor
Visualizes which resistor is switched on.