/*********************************************************************
Written by Dave Astels.
MIT license, check LICENSE for more information
All text above must be included in any redistribution
*********************************************************************/

#include <SPI.h>
#include <Wire.h>
#include <Adafruit_GFX.h>
#include <Adafruit_SSD1306.h>
#include <RTClib.h>
#include <WiFi101.h>
#include <ArduinoJson.h>
#include <Adafruit_NeoPixel.h>
#include "Adafruit_MQTT.h"
#include "Adafruit_MQTT_Client.h"

#include "secrets.h"

#define BUTTON_A 9
#define BUTTON_B 6
#define BUTTON_C 5
#define MOTION_PIN 10
#define LIGHT_PIN A1
#define NEOPIXEL_PIN A5

#define ROOM_ID "4"

#define OLED_RESET 4
Adafruit_SSD1306 display(OLED_RESET);

Adafruit_NeoPixel pixel = Adafruit_NeoPixel(1, NEOPIXEL_PIN, NEO_GRB + NEO_KHZ800);

RTC_DS3231 rtc;

WiFiClient client;

// Setup MQTT
#define AIO_SERVER      "io.adafruit.com"
#define AIO_SERVERPORT  1883

Adafruit_MQTT_Client mqtt(&client, AIO_SERVER, AIO_SERVERPORT, AIO_USER, AIO_KEY);
Adafruit_MQTT_Publish photocell_feed(&mqtt, AIO_USER "/feeds/hue-controller.hue-photocell");
Adafruit_MQTT_Publish motion_feed(&mqtt, AIO_USER "/feeds/hue-controller.hue-motion");
Adafruit_MQTT_Publish control_feed(&mqtt, AIO_USER "/feeds/hue-controller.hue-control");

DynamicJsonBuffer jsonBuffer(8500);

const char *hue_ip = NULL;
uint8_t *light_numbers = NULL;
boolean last_motion = false;

DateTime *sunrise = NULL;
DateTime *sunset = NULL;

// hardcoded day start/end times

DateTime wakeup = DateTime(0, 0, 0, 8, 0, 0);
DateTime bedtime = DateTime(0, 0, 0, 23, 30, 0);

boolean need_sunrise_sunset_times = false;

//#define TRACE 1

void init_log()
{
#ifdef TRACE
  Serial.begin(9600);
  while (!Serial) {}
  Serial.println("Starting");
#endif
}

void log(const char *msg)
{
#ifdef TRACE
  Serial.print(msg);
#endif
}

void log(const int i)
{
#ifdef TRACE
  Serial.print(i);
#endif
}

void logln(const char *msg)
{
#ifdef TRACE
  Serial.println(msg);
#endif
}


const char *fetch_hue_ip() {
  logln("Getting HUE IP");
  display.println("Getting HUE IP");
  if (!client.connectSSL("www.meethue.com", 443)) {
    logln("COULD NOT CONNECT");
    display.println("COULD NOT CONNECT");
    return NULL;
  } 
  client.println("GET /api/nupnp HTTP/1.1");
  client.println("Host: www.meethue.com");
  client.println("Connection: close");
  if (!client.println()) {
    client.stop();
    logln("CONNECTION ERROR");
    display.println("CONNECTION ERROR");
    return NULL;
  }

  char status[32] = {0};
  client.readBytesUntil('\r', status, sizeof(status));
  if (strcmp(status, "HTTP/1.1 200 OK") != 0) {
    client.stop();
    logln(status);
    display.println(status);
    return NULL;
  }

  char endOfHeaders[] = "\r\n\r\n";
  if (!client.find(endOfHeaders)) {
    client.stop();
    display.println("Getting HUE IP");
    logln("HEADER ERROR");
    display.println("HEADER ERROR");
    return NULL;
  }

  JsonArray& root = jsonBuffer.parseArray(client);
  client.stop();

  if (!root.success()) {
    logln("JSON PARSE ERROR");
    display.println("JSON PARSE ERROR");
    return NULL;
  }

  return strdup(root[0][F("internalipaddress")]);
}


boolean fetch_sunrise_sunset(long *sunrise, long *sunset)
{
  logln("Contacting DarkSky");
  display.println("Contacting DarkSky");
  if (!client.connectSSL("api.darksky.net", 443)) {
    logln("COULD NOT CONNECT");
    display.println("COULD NOT CONNECT");
    return false;
  }

  client.print("GET /forecast/");
  client.print(DARKSKY_KEY);
  client.print("/42.9837,-81.2497?units=ca&exclude=currently,minutely,hourly,alerts,flags&language=en");
  client.println(" HTTP/1.1");

  client.print("Host: ");
  client.println("api.darksky.net");
  client.println("Connection: close");
  if (!client.println()) {
    client.stop();
    logln("CONNECTION ERROR");
    display.println("CONNECTION ERROR");
    return false;
  }

  char status[32] = {0};
  client.readBytesUntil('\r', status, sizeof(status));
  if (strcmp(status, "HTTP/1.1 200 OK") != 0) {
    client.stop();
    display.println(status);
    return false;
  }

  char endOfHeaders[] = "\r\n\r\n";
  if (!client.find(endOfHeaders)) {
    client.stop();
    logln("HEADER ERROR");
    display.println("HEADER ERROR");
    return false;
  }

  JsonObject& root = jsonBuffer.parseObject(client);
  client.stop();

  if (!root.success()) {
    logln("JSON PARSE ERROR");
    display.println("JSON PARSE ERROR");
    return false;
  }

  JsonObject& data = root["daily"]["data"][0];
  long start_of_day = data["time"];
  long raw_sunrise_time = data["sunriseTime"];
  long raw_sunset_time = data["sunsetTime"];

  *sunrise = raw_sunrise_time - start_of_day;
  *sunset = raw_sunset_time - start_of_day;

  return true;
}


boolean update_sunrise_sunset()
{
  long sunrise_seconds, sunset_seconds;
  if (!fetch_sunrise_sunset(&sunrise_seconds, &sunset_seconds)) {
    return false;
  }
  
  if (sunrise) {
    delete sunrise;
  }
  sunrise = new DateTime(0, 0, 0, sunrise_seconds / 3600, (sunrise_seconds / 60) % 60, 0);

  if (sunset) {
    delete sunset;
  }
  sunset = new DateTime(0, 0, 0, sunset_seconds / 3600, (sunset_seconds / 60) % 60, 0);

  return true;
}

uint8_t *lights_for_group(const char *group_number)
{
  logln("Finding lights");
  display.println("Finding lights");
  if (!client.connect(hue_ip, 80)) {
    display.println("COULD NOT CONNECT");
    return NULL;
  } 

  client.print("GET /api/");
  client.print(HUE_USER);
  client.print("/groups/");
  client.print(group_number);
  client.println(" HTTP/1.1");

  client.print("Host: ");
  client.println(hue_ip);
  client.println("Connection: close");
  if (!client.println()) {
    client.stop();
    display.println("CONNECTION ERROR");
    return NULL;
  }

  char status[32] = {0};
  client.readBytesUntil('\r', status, sizeof(status));
  if (strcmp(status, "HTTP/1.1 200 OK") != 0) {
    client.stop();
    display.println(status);
    return NULL;
  }

  char endOfHeaders[] = "\r\n\r\n";
  if (!client.find(endOfHeaders)) {
    client.stop();
    display.println("HEADER ERROR");
    return NULL;
  }

  JsonObject& group = jsonBuffer.parseObject(client);
  client.stop();

  if (!group.success()) {
    display.println("JSON PARSE ERROR");
    return NULL;
  }

  JsonArray& lights = group["lights"];
  uint8_t *light_numbers = (uint8_t*)malloc(lights.size() + 1);
  light_numbers[0] = (uint8_t)lights.size();
  for (uint i = 0; i < lights.size(); i++) {
    light_numbers[i+1] = (uint8_t)atoi((const char *)lights[i]);
  }
  return light_numbers;
}


void update_light(uint8_t light_number, boolean on_off, uint8_t brightness)
{
  if (!client.connect(hue_ip, 80)) {
    return;
  } 

  log("Turning light ");
  log(light_number);
  logln(on_off ? " on" : " off");

  log("PUT /api/");
  log(HUE_USER);
  log("/lights/");
  log(light_number);
  logln("/state HTTP/1.1");

  char content[32];
  sprintf(content, "{\"on\":%s,\"bri\":%d}", on_off ? "true" : "false", brightness);

  client.print("PUT /api/");
  client.print(HUE_USER);
  client.print("/lights/");
  client.print(light_number);
  client.println("/state HTTP/1.1");

  client.print("Host: ");
  client.println(hue_ip);

  client.println("Connection: close");

  client.print("Content-Type: ");
  client.println("application/json");
  client.println("User-Agent: FeatherM0Sender");
  client.print("Content-Length: ");
  client.println(strlen(content));
  client.println();

  client.println(content);
  client.stop();
}


void update_all_lights(uint8_t *light_numbers, boolean on_off, uint8_t brightness)
{
  if (light_numbers != NULL) {
    uint8_t num_lights = light_numbers[0];
    for (int i = 0; i < num_lights; i++) {
      update_light(light_numbers[i+1], on_off, brightness);
    }
  }
}


boolean is_between(DateTime *now, DateTime *start, DateTime *end)
{
  // now > start || now > end
  if (now->hour() < start->hour()) return false;
  if (now->hour() == start->hour() && now->minute() < start->minute()) return false;
  if (now->hour() > end->hour()) return false;
  if (now->hour() == end->hour() && now->minute() > end->minute()) return false;
  return true;
}


void MQTT_connect()
{
  if (mqtt.connected()) {
    return;
  }
  
  while (mqtt.connect() != 0) { // connect will return 0 for connected
    mqtt.disconnect();
    delay(5000);  // wait 5 seconds
  }
}


void setup()
{
  init_log();
  
  pinMode(BUTTON_A, INPUT_PULLUP);
  pinMode(BUTTON_B, INPUT_PULLUP);
  pinMode(BUTTON_C, INPUT_PULLUP);
  pinMode(MOTION_PIN, INPUT_PULLUP);
  
  
  display.begin(SSD1306_SWITCHCAPVCC, 0x3C);  // initialize with the I2C addr 0x3C (for the 128x32)
  display.display();
  delay(2000);
  display.setTextSize(1);
  display.setTextColor(WHITE);
  
  WiFi.setPins(8,7,4,2);

  // // attempt to connect to WiFi network:
  int status = WL_IDLE_STATUS;

  display.print(WIFI_SSID);
  while (status != WL_CONNECTED) {
    display.print(".");
    delay(500);
    status = WiFi.begin(WIFI_SSID, WIFI_PASS);
  }

  if (! rtc.begin()) {
    while (1);
  }

  if (rtc.lostPower()) {
    rtc.adjust(DateTime(F(__DATE__), F(__TIME__)));
  }

  // Clear the buffer.
  display.clearDisplay();
  display.setCursor(0, 0);

  hue_ip = fetch_hue_ip();
  if (hue_ip == NULL) {
    while (true) {
    }
  }
  light_numbers = lights_for_group(ROOM_ID);
  if (light_numbers == NULL) {
    while (true) {
    }
  }
  
  pixel.begin();
  pixel.setPixelColor(0, 0, 0, 0);
  pixel.show();

  if (!update_sunrise_sunset()) {
    sunrise = new DateTime(0, 0, 0, 7, 0, 0);
    sunset = new DateTime(0, 0, 0, 16, 30, 0);
  }
}

long ping_time = 0;
void ping_if_time(DateTime now)
{
  if (now.secondstime() >= ping_time) {
    ping_time = now.secondstime() + 250;
    if (!mqtt.ping()) {
      logln("No MQTT ping");
      mqtt.disconnect();
    }
  }
}


void loop()
{
  display.clearDisplay();
  display.setCursor(0, 0);
  
  DateTime now = rtc.now();
  ping_if_time(now);
  MQTT_connect();


  boolean is_motion = digitalRead(MOTION_PIN);
  int32_t light_level = analogRead(LIGHT_PIN);
  char buf[22];
  sprintf(buf, "%d/%02d/%02d   %02d:%02d:%02d", now.year(), now.month(), now.day(), now.hour(), now.minute(), now.second());

  if (now.hour() == 0) {
    if (need_sunrise_sunset_times) {
      if (!update_sunrise_sunset()) {
        while (true) {
        }
      }
      need_sunrise_sunset_times = false;
    }
  } else {
    need_sunrise_sunset_times = true;
  }
  
  boolean motion_started = is_motion && !last_motion;
  boolean motion_ended = !is_motion && last_motion;
  last_motion = is_motion;

  if (motion_started) {
    logln("Publishing motion start");
    if (!motion_feed.publish((const char*)"started")) {
      logln("\n***** MQTT motion publish failed\n");
    }     
    if (!photocell_feed.publish(light_level)) {
      logln("\n***** MQTT photocell publish failed\n");
    }
    pixel.setPixelColor(0, 16, 0, 0);
  } else if (motion_ended) {
    if (!motion_feed.publish((const char*)"ended")) {
          logln("\n***** MQTT motion publish failed\n");
    }
    pixel.setPixelColor(0, 0, 0, 0);
  }
  pixel.show();
    
  display.println(buf);
  if (is_motion) {
    display.print("  ");
  } else {
    display.print("No");
  }
  display.print(" Motion  Light: ");
  display.println(light_level);
  display.print(is_between(&now, sunrise, sunset) ? "Light" : "Dark");
  display.println(" out now");
  display.print("You should be ");
  display.println(is_between(&now, &wakeup, &bedtime) ? "awake" : "asleep");
  display.display();

  if (!digitalRead(BUTTON_A) || (motion_started && (light_level < 50 || !is_between(&now, sunrise, sunset)))) {
    if (!control_feed.publish((const char*)"on")) {
      logln("\n***** MQTT control publish failed\n");
    }
    update_all_lights(light_numbers, true, is_between(&now, &wakeup, &bedtime) ? 100 : 10);
  }
  if (!digitalRead(BUTTON_C) || motion_ended) {
    if (!control_feed.publish((const char*)"off")) {
      logln("\n*****MQTT control publish failed\n");
    }
    update_all_lights(light_numbers, false, 0);
  }
  
  delay(400);
}
