#include <ArduinoJson.h>
#include <JsonLogic.h>

using json_logic::Json;
using json_logic::JsonLogic;

namespace
{
struct AutomationRule
{
  const char* name;
  const char* action;
  const char* logic;
};

// Sensor data: {"temperature":31.5,"humidity":72,"soil_moisture":25,"rain_detected":false,"occupied":true,"motion":true,"ambient_light":120,"leak_detected":false,"time":{"h":22,"m":15,"s":30}}
// - Warm temperature: TRUE -> Temperature is above 20 C
// - Ventilation: TRUE -> Turn ventilation fan on
// - Irrigation: TRUE -> Open irrigation valve
// - Heating: false
// - Night motion light: TRUE -> Turn lights on
// - Leak alarm: false
// - Night schedule: TRUE -> The time is between 21:00 and 09:00

const AutomationRule rules[] = {
    {
        "Warm temperature",
        "Temperature is above 20 C",
        R"({">":[{"var":["temperature",0]},20]})",
    },
    {
        "Ventilation",
        "Turn ventilation fan on",
        R"({"and":[{">":[{"var":["temperature",0]},28]},{">":[{"var":["humidity",0]},65]}]})",
    },
    {
        "Irrigation",
        "Open irrigation valve",
        R"({"and":[{"<":[{"var":["soil_moisture",100]},30]},{"!":{"var":["rain_detected",true]}}]})",
    },
    {
        "Heating",
        "Turn heater on",
        R"({"and":[{"<":[{"var":["temperature",25]},18]},{"var":["occupied",false]}]})",
    },
    {
        "Night motion light",
        "Turn lights on",
        R"({"and":[{"var":["motion",false]},{"<":[{"var":["ambient_light",100000]},200]}]})",
    },
    {
        "Leak alarm",
        "Close water valve and send an alarm",
        R"({"==":[{"var":["leak_detected",false]},true]})",
    },
    {
        "Night schedule",
        "The time is between 21:00 and 09:00",
        R"({"or":[{">=":[{"var":["time.h",0]},21]},{"<":[{"var":["time.h",0]},9]}]})",
    },
};

void evaluateRules(const Json& sensorData)
{
  JsonLogic* engine = JsonLogic::GetInstance();

  Serial.println();
  Serial.print("Sensor data: ");
  serializeJson(sensorData, Serial);
  Serial.println();

  for (const AutomationRule& ruleDefinition : rules)
  {
    Json rule;
    const DeserializationError parseError =
        deserializeJson(rule, ruleDefinition.logic);

    Serial.print("- ");
    Serial.print(ruleDefinition.name);
    Serial.print(": ");

    if (parseError)
    {
      Serial.print("invalid rule: ");
      Serial.println(parseError.c_str());
      continue;
    }

    const Json result = engine->Apply(rule, sensorData);
    if (engine->HasError())
    {
      Serial.print("evaluation error: ");
      Serial.println(engine->GetLastError().c_str());
      continue;
    }

    if (result.as<bool>())
    {
      Serial.print("TRUE -> ");
      Serial.println(ruleDefinition.action);
    }
    else
    {
      Serial.println("false");
    }
  }
}

void evaluateJsonLine(const String& line)
{
  Json sensorData;
  const DeserializationError error = deserializeJson(sensorData, line);
  if (error)
  {
    Serial.print("Invalid sensor JSON: ");
    Serial.println(error.c_str());
    return;
  }

  evaluateRules(sensorData);
}
}

void setup()
{
  Serial.begin(115200);
  delay(1500);

  Serial.println();
  Serial.println("JsonLogic IoT rule demo");
  Serial.println("Send one JSON object per line to evaluate new sensor data.");

  Json sampleData;
  deserializeJson(
      sampleData,
      R"({"temperature":24.5,"humidity":72,"soil_moisture":25,"rain_detected":false,"occupied":true,"motion":true,"ambient_light":120,"leak_detected":false,"time":{"h":1,"m":1,"s":1}})");
  evaluateRules(sampleData);
}

void loop()
{
  if (!Serial.available()) return;

  const String line = Serial.readStringUntil('\n');
  if (line.length() > 0) evaluateJsonLine(line);
}
