#!/usr/bin/env python
from __future__ import unicode_literals, print_function

####################

leds = 0b00001000, 0b00010000
report_check_interval = 10
sar_update_interval = 20

####################


import argparse
parser = argparse.ArgumentParser(
	description='Flash parallel port LEDs according to local resource utilization.')
parser.add_argument('-s', '--stats-update', action='store', type=int, default=sar_update_interval,
	help='Averaging interval for sar data collection (default: %(default)ss).')
parser.add_argument('-l', '--led-update', action='store', type=int, default=report_check_interval,
	help='Interval between data checks and led state updates (default: %(default)ss).')
parser.add_argument('--debug', action='store_true', help='Verbose operation mode.')
argz = parser.parse_args()

import logging
logging.basicConfig(level=logging.DEBUG if argz.debug else logging.INFO)
log = logging.getLogger()


import itertools as it, operator as op, functools as ft
from time import sleep
import os, sys, signal

drain, feed = os.pipe()
led_control = os.fork()

if not led_control: # child led-control process
	os.close(feed)
	drain = os.fdopen(drain, 'rb', 0)

	import parallel

	port = parallel.Parallel()
	state = dict(it.izip(leds, it.repeat(-1))) # led_code, blip_delay (s) or 0/<0 (on/off)
	state_update = True

	def state_poll(*argz):
		global state_update
		update = drain.readline()
		if not update: sys.exit() # broken pipe
		led_id, blip = it.imap(int, update.split())
		state[leds[led_id]] = blip / 1000.0
		state_update = True
	signal.signal(signal.SIGUSR1, state_poll)

	def blip_loop():
		def state_set(val, state=port.setData):
			try: state(val) # I/O, can probably be interrupted by signal
			except KeyboardInterrupt: pass
		global state_update
		state_bin, state_dynamic = 0, dict()
		ts, state_update, state_min = 0, True, op.itemgetter(1)
		while True:
			if state_update:
				log.debug( 'Updating blip-state to:'
					' {} (dynamic-leds: {})'.format(state, state_dynamic.keys()) )
				state_bin = sum(k for k,v in state.viewitems() if v == 0)
				state_dynamic = dict((k, ts + v) for k,v in state.viewitems() if v > 0)
				log.debug( 'New leds state: static -'
					' {}, dynamic - {}'.format(state_bin, state_dynamic.keys()) )
				state_update = False
			if not state_dynamic:
				state_set(state_bin)
				state_set(state=sleep, val=3600)
				continue
			led_bin, delay = min(state_dynamic.viewitems(), key=state_min)
			delay = max(0, delay - ts)
			ts += delay
			state_set(state=sleep, val=delay)
			state_dynamic[led_bin] = ts + state[led_bin]
			state_bin ^= led_bin
			state_set(state_bin)

	blip_loop()
	sys.exit() # should never get here

else:
	os.close(drain)
	feed = os.fdopen(feed, 'wb', 0)


from twisted.internet import protocol, reactor, utils, defer
from twisted.internet.task import LoopingCall
from datetime import datetime

class DiskLoad(protocol.ProcessProtocol):
	_stats = list()
	_labels, _ts_idx, _buff = list(), 0, ''

	def connectionMade(self): self.transport.closeStdin()

	def outReceived(self, data):
		global iostat_report
		self._buff += data
		if '\n' not in data: return
		for line in it.imap(op.methodcaller('strip'), self._buff.splitlines()):
			if not line: # end of report
				iostat_report = dict(self._stats)
				self._stats = self._labels = list()
				continue
			if not self._labels: # header line
				line = line.split()
				try: self._ts_idx = line.index('DEV')
				except ValueError: pass # invalid line
				else: self._labels = line[self._ts_idx+1:]
				continue
			line = line.split(None, self._ts_idx)
			line, ts = line[-1].split(), datetime.strptime(' '.join(line[:self._ts_idx]), '%X')
			self._stats.append((line[0], dict(
				it.izip(self._labels, it.imap(float, line[1:])) )))
		self._buff = ''

	def __del__(self): os.kill(led_control, signal.SIGTERM)

iostat_report = dict()
iostat_cmd = 'sar', '-pd', unicode(argz.stats_update)
iostat = reactor.spawnProcess(DiskLoad(), bytes(iostat_cmd[0]), map(bytes, iostat_cmd))


def check_stats():
	global led_state
	state = led_state.copy()

	la = map(float, open('/proc/loadavg').read().strip().split(None, 3)[:3])
	log.debug('Load averages: {} (cpus: {})'.format(la, cpu_count))
	if la[2] > cpu_count: state[0] = 50
	elif la[2] > cpu_count - 1: state[0] = 100
	elif la[1] > cpu_count: state[0] = 200
	elif la[1] > cpu_count - 1: state[0] = 300
	else: state[0] = -1

	disk_load = len(filter(lambda data: data['%util'] > 80, iostat_report.viewvalues()))
	log.debug('Loaded blkdev: {}'.format(disk_load))
	if disk_load > 4: state[1] = 50
	elif disk_load > 2: state[1] = 100
	elif disk_load > 1: state[1] = 200
	else: state[1] = -1

	for led_id, interval in set(state.viewitems()).difference(led_state.viewitems()):
		log.debug('Sending changes. Led_ID: {}, interval: {}'.format(led_id, interval))
		os.kill(led_control, signal.SIGUSR1)
		feed.write('{} {}\n'.format(led_id, interval))
		feed.flush()
	led_state = state

cpu_count = len(filter(lambda line: line.strip() and line.strip().split()[0] == 'processor', open('/proc/cpuinfo')))
led_state = {0:-1, 1:-1}

led_update = LoopingCall(check_stats)
led_update.start(argz.led_update, now=False)


reactor.run()
