import numpy as np

import json
import os
import serial
import subprocess
import threading
import time

SERIAL_PATH = "/dev/ttyUSB0"
SERIAL_BAUD = 115200

SAMPLE_PERIOD = 0.2
DECIMATION_RATE = 5

is_mock = os.environ['MOCK']
if is_mock:
  process = subprocess.Popen(["/usr/bin/env", "python", "/home/kelvin/src/shroom-server/shroom_pipe.py"], stdin=subprocess.PIPE, stdout=subprocess.PIPE)
else:
  process = subprocess.Popen(["ssh", "shrooms@threefortiethofonehamster.com", "/usr/bin/env", "python", "/home/shrooms/go/src/shroom-server/shroom-pipe.py"], stdin=subprocess.PIPE, stdout=subprocess.PIPE)

def send_update(msg):
  global process
  process.stdin.write(bytes(json.dumps(msg) + "\n", "utf8"))
  process.stdin.flush()

class MockSerial:
  def __init__(self):
    self.humidity = np.zeros(100)
    self.humidifier_on = False
    self.humidity[:] = 0.80
    self.humidity[-1] = 0.20
    self.humidity[0] = 0.20

  def write(self, msg):
    if msg == b'h':
      print("mock hum toggle")
      self.humidifier_on = not self.humidifier_on

  def read(self, _):
    t = time.time()
    temp = 25 + np.sin(0.01*2*np.pi*t) + 0.5*np.sin(0.0001*2*np.pi*t + 7)

    # very janky model of humidity diffusion
    # fix end conditions
    for _ in range(20):
      self.humidity[-1] = 0.2*0.20 + 0.8*self.humidity[-2]
      self.humidity[0] = 0.20
      if self.humidifier_on:
        self.humidity[20] = 2
      # use the gradient to determine the change in humidity
      avg = 0.5*(self.humidity[:-2] + self.humidity[2:])
      self.humidity[1:-1] += 0.10*(avg - self.humidity[1:-1])
    #print(self.humidity)

    humidity = self.humidity[60] + np.random.random()*0.003
    if self.humidifier_on:
      hv = 3.3
    else:
      hv = 0.0
    return bytes("{},{},{}\n".format(humidity, temp, hv), "utf8")

if is_mock:
  s = MockSerial()
else:
  s = serial.Serial(SERIAL_PATH, SERIAL_BAUD, timeout = 10)

def reset_serial():
  if not is_mock:
    s.close()
    s = serial.Serial(SERIAL_PATH, SERIAL_BAUD, timeout = 10)
  time.sleep(10)

class Humidifier:
  def __init__(self):
    self.on = False
    self.history = np.zeros(10)
    self.switch_timeout = 0

  @property
  def off(self):
    return not self.on

  def update(self, volts):
    self.history[1:] = self.history[:-1]
    self.history[0] = volts
    #print(self.history)
    avg = np.sum(self.history)/self.history.shape[0]
    if self.on:
      if avg < 0.2:
        self.on = False
        print("send status off")
        send_update({"status": {"humidifier": False}})
        self.switch_timeout = time.time() + 1
    else:
      if avg > 2.6:
        self.on = True
        print("send status on")
        send_update({"status": {"humidifier": True}})
        self.switch_timeout = time.time() + 1

  def toggle(self, s):
    if time.time() > self.switch_timeout:
      s.write(b"h")
      self.switch_timeout = time.time() + 7

humidifier = Humidifier()
target_lower = 0.85
target_upper = 0.90
feedforward_coeff = 50

exiting = False
# run thread to process data from process's stdout
def stdout_loop():
  global process, target_lower, target_upper, feedforward_coeff
  while not exiting:
    msg = process.stdout.readline()
    if len(msg) == 0:
      continue
    print("got message ", msg)
    try:
      msg_js = json.loads(msg)
      if "query_params" in msg_js:
        if msg_js["query_params"]:
          send_update({"params": {
            "target_lower": target_lower,
            "target_upper": target_upper,
            "feedforward_coeff": feedforward_coeff
            }
          })
      elif "set_params" in msg_js:
        if type(msg_js["set_params"]) is dict:
          set_params = msg_js["set_params"]
          if "name" in set_params and "value" in set_params:
            if type(set_params["value"]) is float:
              if set_params["name"] == "target_lower":
                target_lower = set_params["value"]
              elif set_params["name"] == "target_upper":
                target_upper = set_params["value"]
              elif set_params["name"] == "feedforward_coeff":
                feedforward_coeff = set_params["value"]
    except json.JSONDecodeError as e:
      print("received bad json ", msg)
stdout_thread = threading.Thread(target=stdout_loop)
stdout_thread.start()


humidifier_history = np.zeros(30)
first_sample = False
frame_num = 0
try:
  last_sample = 0
  while True:
    now = time.time()
    if now - last_sample < SAMPLE_PERIOD:
      time.sleep(SAMPLE_PERIOD - (now - last_sample) + 0.001)
      continue
    last_sample = now

    s.write(b"s")
    resp = s.read(120)
    if len(resp) == 0:
      reset_serial()
      time.sleep(5)
      continue
    parts = resp.split(b",")
    humidity = float(parts[0])
    temp = float(parts[1])
    volts = float(parts[2])
    if first_sample:
      humidifier_history[:] = humidity
      first_sample = False
    else:
      humidifier_history[:-1] = humidifier_history[1:]
      humidifier_history[-1] = humidity

    # compensate for the slow response time by adding a little feed forward
    # using the slope of the humidifier data
    slope = (humidifier_history[-1] - humidifier_history[0])/humidifier_history.shape[0]
    comp_humidity = humidity + feedforward_coeff*slope

    try:
      humidifier.update(volts)
      if comp_humidity < target_lower and humidifier.off:
        humidifier.toggle(s)
      elif comp_humidity > target_upper and humidifier.on:
        humidifier.toggle(s)

      if frame_num == 0:
        print(humidity, temp, volts)
        update = {
            "data": {
              "time": int(now*1000),
              "temp": temp,
              "hum": humidity,
              "hv": volts
            }
        }
        send_update(update)
      frame_num = (frame_num + 1) % DECIMATION_RATE
    except Exception as e:
      print("pipe errored out, restarting: ", e)
      # restart the process I guess
      exiting = True
      process.kill()
      time.sleep(0.1)
      stdout_thread.join()
      exiting = False

      process = subprocess.Popen(["/usr/bin/env", "python", "/home/kelvin/src/shroom-server/shroom_pipe.py"], stdin=subprocess.PIPE, stdout=subprocess.PIPE)

      stdout_thread = threading.Thread(target=stdout_loop)
      stdout_thread.start()

finally:
  # kill ssh connection
  exiting = True
  process.kill()
  stdout_thread.join()