# -*- coding: utf-8 -*-

##
##  Copyright © 2012, Matthias Urlichs <matthias@urlichs.de>
##
##  This program is free software: you can redistribute it and/or modify
##  it under the terms of the GNU General Public License as published by
##  the Free Software Foundation, either version 3 of the License, or
##  (at your option) any later version.
##
##  This program is distributed in the hope that it will be useful,
##  but WITHOUT ANY WARRANTY; without even the implied warranty of
##  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
##  GNU General Public License (included; see the file LICENSE)
##  for more details.
##

"""\
		Run the current schedule, watch out for rain
		"""

from __future__ import division,absolute_import
from homevent import gevent_rpyc
gevent_rpyc.patch_all()

from traceback import format_exc,print_exc
from operator import attrgetter
import sys,signal
import gevent,rpyc
from functools import partial
from gevent.queue import Queue,Empty
from gevent.coros import Semaphore
from gevent.event import AsyncResult
from rpyc.core.service import VoidService
from optparse import make_option

from django.core.management.base import BaseCommand, CommandError
from django.core.exceptions impor
t ObjectDoesNotExist
from rainman.models import Site,Valve,Schedule,Controller,History,Level
from rainman.utils import now,str_tz
from rainman.logging import log,log_error
from datetime imp
ort datetime,time,timedelta
from django.db.models import F,Q
from django.db import transaction
from homevent.times import humandelta
import random

METER_TIME=5*60 # meters get deprecated if no value for five minutes 
METER_MAXTIME=60*60 # need to report at least hourly, otherwise I don't believe you

### database stuff
### This does not actually work because of transaction-vs.-thread issues
#def save_objs(objs):
#	for o in objs:
#		o.save()
#
#_save_q = Queue()
#def _save_job():
#	while True:
#		s = set()
#		o = _save_q.get()
#		print >>sys.stderr,"AddSave1",id(o),o
#		s.add(o)
#		try:
#			while True:
#				o = _save_q.get(timeout=1)
#				if o is None:
#					break
#				s.add(o)
#		except Empty:
#			pass
#
#		print >>sys.stderr,"Save",s
#		for i in range(3):
#			try:
#				save_objs(s)
#			except Exception:
#				print_exc()
#			else:
#				break
#		print >>sys.stderr,"SaveDone"
#				
#
#def Save(obj):
#	#_save_q.put(obj)
#	if obj is not None:
#		obj.save()

class NotConnected(RuntimeError):
	pass
class TooManyOn(RuntimeError):
	def __init__(self,valve):
		self.valve = valve
	def __str__(self):
		return u"‹%s: %s / %s: too many open valves›" % (self.__class__.__name__,self.valve.v.controller,self.valve.v)

class Command(BaseCommand):
	args = 'site'
	help = 'Run the current schedule, watch for rain, etc.'

	option_list = BaseCommand.option_list + (
		make_option('-t','--timeout',
			action='store',type='int',
			dest='timeout',
			default=600,
			help='Run main processing loop every N seconds'),
		)

	def handle(self, *args, **options):
		if len(args) != 1:
			print "Choose a site:"
			for s in Site.objects.all():
				print s.name
			sys.exit(1)
		s = Site.objects.get(name=args[0])
		for c in s.controllers.all():
			controller = SchedController(c)
			controller.connect_monitors()

		site = SchedSite(c.site)
		site.run_every(timedelta(0,options['timeout']))
		site.run_sched_task(True,reason="Command")

		if not controllers:
			raise RuntimeError("No controllers for site '%s' found." % (s.name,))
		#gevent.spawn(_save_job)
		while True:
			gevent.sleep(99999)



class RestartService(VoidService):
	def on_disconnect(self,*a,**k):
		for s in sites.itervalues():
			if s.ci._local_root is self:
				s.log("disconnected")
				s.ci = None
				s.run_main_task()
				gevent.spawn_later(10,s.maybe_restart)


class Meter(object):
	sum_it = False
	last_time = None

	def __init__(self,dev):
		self.d = dev
		self.site = SchedSite(dev.site)

	@property
	def weight(self):
		return self.d.weight

	def monitor_value(self,event=None,**k):
		"""monitor value NUMBER name…"""
		self.refresh()
		self.last_time = now()
		try:
			val = float(event[2])
			self.add_value(val)
		except Exception:
			print_exc()

	def refresh(self):
		self.d.refresh()

	def shutdown(self):
		pass

	def sync(self):
		pass

	def connect_monitors(self):
		if not self.d.var:
			return
		if self.site.ci is None:
			return
		self.mon = self.site.ci.root.monitor(self.monitor_value,"monitor","value","*",*(self.d.var.split()))

	def log(self,txt):
		self.site.log(("%s %s: "%(self.meter_type,self.d.name))+txt)

class SumMeter(Meter):
	sumval = 0
	def get_value(self):
		r,self.sumval = self.sumval,0
		return r
		
	def add_value(self,val):
		self.sumval += val

class RainMeter(SumMeter):
	meter_type="rain"

	def add_value(self,val):
		super(RainMeter,self).add_value(val)
		if val > 0:
			self.site.has_rain()

class FeedMeter(SumMeter):
	meter_type="feed"
	sum_it = True
	weight = 1
	_flow_check = None

	def __init__(self,*a,**k):
		super(FeedMeter,self).__init__(*a,**k)
		self.valves = set()
		for v in self.d.valves.all():
			valve = SchedValve(v)
			valve.feed = self
			self.valves.add(valve)

	def shutdown(self):
		super(FeedMeter,self).shutdown()
		if self._flow_check is not None:
			self._flow_check.dead()

	def check_max_flow(self,**k):
		if self._flow_check:
			raise RuntimeError("already working")
		try:
			cf = MaxFlowCheck(self)
			# Safety timer
			timer = gevent.spawn_later(self.d.db_max_flow_wait,cf.dead)

			cf.start()
			res = cf.q.get()
			self.log("End flow check: %s"%(res,))
			timer.kill()
		except Exception as ex:
			print_exc()
			try:
				log_error(self.d.site)
				if cf is not None:
					cf._unlock()
			except:
				print_exc()
				raise
		
	def do_flush(self,**k):
		if self._flow_check:
			raise RuntimeError("already working")
		try:
			cf = Flusher(self)
			# Safety timer
			timer = gevent.spawn_later(self.d.db_max_flow_wait,cf.dead)

			cf.start()
			res = cf.q.get()
			self.log("End flow check: %s"%(res,))
			timer.kill()
		except Exception as ex:
			print_exc()
			try:
				log_error(self.d.site)
				if cf is not None:
					cf._unlock()
			except:
				print_exc()
				raise
		
		
	def add_value(self,val):
		if self._flow_check:
			if self._flow_check.add_flow(val):
				return
		super(FeedMeter,self).add_value(val)

		sum_f = 0
		for valve in self.valves:
			if valve.on:
				sum_f += valve.v.flow
		if sum_f:
			# TODO: calculate flow/sec. If greater than the max flow through
			# the valves, the excess must be accounted for someplace else.
			for valve in self.valves:
				if valve.on:
					valve.add_flow(val * valve.v.flow / sum_f)

	def check_flow(self,**k):
		for valve in self.valves:
			valve.check_flow(**k)

	def connect_monitors(self):
		super(FeedMeter,self).connect_monitors()
		if self.d.var:
			self.chk = self.site.ci.root.monitor(self.check_flow,"check","flow",*(self.d.var.split()))
			self.chm = self.site.ci.root.monitor(self.check_max_flow,"check","maxflow",*(self.d.var.split()))
			self.flush = self.site.ci.root.monitor(self.do_flush,"flush",*(self.d.var.split()))
		else:
			self.flush = self.site.ci.root.monitor(self.do_flush,"flush",*(self.d.name.split()))

	
		
class AvgMeter(Meter):
	val = None
	avg = 0
	nval = 0
	ts = None

	def connect_monitors(self):
		super(AvgMeter,self).connect_monitors()
		self.ts = None

	def get_value(self):
		if self.ts is None:
			return None
		self.snapshot()
		if not self.nval:
			return None
		r = self.avg / self.nval
		self.avg = 0
		self.nval = 0
		return r

	def snapshot(self):
		n = now()
		if self.ts is not None and self.val is not None:
			w=(n-self.ts).total_seconds()
			if w > METER_TIME:
				w = METER_TIME # limit weight if the measurer dies
			self.avg += w*self.val
			self.nval += w
		self.ts = n
	
	def add_value(self,val):
		self.snapshot()
		self.val = val

class TempMeter(AvgMeter):
	meter_type="temp"

class WindMeter(AvgMeter):
	meter_type="wind"

class SunMeter(AvgMeter):
	meter_type="sun"

METERS=[]
for m in globals().values():
	if hasattr(m,"meter_type"):
		METERS.append(m)


class SchedCommon(object):
	def log(self,txt):
		raise NotImplementedError("You forgot to implement %s.log" % (self.__class__.__name__,)