#! /usr/bin/env python
# -*- coding: utf-8 -*-
#
# Copyright (C) 2011 Sebastian Pipping <sebastian@pipping.org>
# Licensed under GPL v3 or later

from __future__ import print_function
import freitagslib.network as net
from freitagslib.commands import BuyCommand, DepositCommand
from freitagslib.encoding import asciify
from freitagslib.settings import settings, Settings

import colorama
from colorama import Fore, Style


import sys
from decimal import Decimal
import os
import time
import urllib2
import select

from display import Display
from thread import start_new_thread, allocate_lock
import time

myDisplay=None

COLOR_HINT = Fore.YELLOW + Style.BRIGHT
COLOR_ERROR = Fore.RED
COLOR_DEPOSIT = Fore.GREEN + Style.BRIGHT
COLOR_TO_GO = Fore.MAGENTA + Style.BRIGHT
COLOR_WITHDRAW = Fore.RED + Style.BRIGHT

COLOR_SOME = Fore.WHITE + Style.BRIGHT
COLOR_MUCH = Fore.YELLOW + Style.BRIGHT

COLOR_RESET = Style.RESET_ALL

display_fifo = None

scroll_line1 = None
scroll_line2 = None


offset_line1 = 0
offset_line2 = 0

brightness  = 5
screensaver = 0

SCREENSAVER_DIM     = 2* 20
SCREENSAVER_TIMEOUT = 2* 120
SCREENSAVER_OFF     = 2* 10*60

lock = allocate_lock()


def clear():
	os.system('clear')


TO_GO_ONLY = 2
TO_GO_ALL = 1
TO_GO_NONE = 0
TO_GO_PREV = -1

CODES = {
	'UNDO':('undo',),
	'COMMIT':('commit',),

	'TO GO ALL':('to_go', TO_GO_ALL),
	'TO GO NONE':('to_go', TO_GO_NONE),
	'TO GO PREV':('to_go', TO_GO_PREV),
	'TO GO ONLY':('to_go', TO_GO_ONLY),
	'TO GO ONLZ':('to_go', TO_GO_ONLY),  # Workaround for German keyboard layouts

	'DEPOSIT 0.01':('deposit', Decimal('0.01')),
	'DEPOSIT 0.05':('deposit', Decimal('0.05')),
	'DEPOSIT 0.10':('deposit', Decimal('0.10')),
	'DEPOSIT 0.50':('deposit', Decimal('0.50')),
	'DEPOSIT 1.00':('deposit', Decimal('1.00')),
	'DEPOSIT 5.00':('deposit', Decimal('5.00')),
	'DEPOSIT 10.00':('deposit', Decimal('10.00')),
	'DEPOSIT 50.00':('deposit', Decimal('50.00')),
}


def delay(what, seconds):
	for i in xrange(seconds, 0, -1):
		if i < seconds:
			sys.stdout.write('\r')
		sys.stdout.write('%s in %d Sekunden... ' % (what, i))
		sys.stdout.flush()
		time.sleep(1)


def warn_balance():
	print('Kontostand im Minus, bitte Geld aufladen.')


def error_page(error_message, hint_message=None):
	clear()
	print(COLOR_ERROR + error_message + COLOR_RESET)
	print()
	delay_seconds = 3
	if hint_message is not None:
		print(COLOR_HINT + hint_message + COLOR_RESET)
		print()
		delay_seconds += 3
	delay('Weiter', delay_seconds)


def item_info_page(item):
	indent = 4 * ' '

	clear()
	print('Diese Ware heißt')
	print()
	print(indent + COLOR_SOME + item.name + COLOR_RESET)
	print()
	print('und kostet')
	print()
	if item.deposit > 0:
		myDisplay.display_screen("PREISINFO","%s: %4.2f Euro (%4.2f Euro Pfand)" % (asciify(item.name), item.price, item.deposit))
		print(indent + '%s%4.2f Euro%s + %4.2f Euro Pfand  = %s%4.2f Euro%s .' \
				% (COLOR_SOME, item.price, COLOR_RESET, item.deposit,
				COLOR_MUCH, item.price + item.deposit, COLOR_RESET))
	else:
		myDisplay.display_screen("PREISINFO","%s: %4.2f Euro" % (asciify(item.name), item.price))
		print(indent + '%s%4.2f Euro%s .' \
				% (COLOR_MUCH, item.price, COLOR_RESET))
	print()
	print()
	print(COLOR_MUCH + 'Zum Kaufen bitte einloggen.' + COLOR_RESET)
	print()

	delay('Weiter', 6)


class Status:
	def __init__(self):
		self._reset()
		self.item_cache = dict()

	def _reset(self):
		self.auth_blob = None
		self.login_name = None
		self.balance = None
		self.transfers = None

	def dump(self):

		def sign(amount, plus='+'):
			return '-' if amount < 0 else plus

		def color(amount):
			return COLOR_WITHDRAW if amount < 0 else COLOR_DEPOSIT

		def show_total(balance, plus=' '):
			print('%3s %-40s  %s%c %6.2f Euro%s' \
					% ('', '', color(balance), sign(balance, plus),
						abs(balance), COLOR_RESET))

		def shorten(text, length):
			if len(text) <= length:
				return text
			else:
				return text[:length - 3] + '...'

		def show_item(position, diff, label, color):
			print('%2d) %-40s  %s%c %6.2f Euro%s' \
					% (position, shorten(label, 40), color, sign(diff),
					abs(diff), COLOR_RESET))

		def show_bar():
			print('%3s %-40s  %13s' % ('', '', '============='))

		if self.logged_in():
			print('Eingeloggt als:  %s%s%s' % (COLOR_SOME, self.login_name, COLOR_RESET))
			print()
			myDisplay.cmd_clear()
			myDisplay.write('Hallo %-14s' % (self.login_name[:13]+"!") )
			if self.transfers:
				initial_command, initial_balance = self.transfers[0]

				print('Geplante Änderungen:')
				show_total(initial_balance)
				i = 1
				for command, dummy in self.transfers:
					if isinstance(command, BuyCommand):
						if (command.includes_commodity()):
							show_item(i, -command.commodity_value(), command.commodity_label(), COLOR_WITHDRAW)
							i += 1
						if (command.includes_deposit()):
							show_item(i, -command.deposit_value(), command.deposit_label(), COLOR_TO_GO)
							i += 1
					else:
						show_item(i, command.difference(), command.label(), COLOR_DEPOSIT)
						i += 1
				show_bar()

				if isinstance(command, BuyCommand):
					mycmd = 0;
					if (command.includes_commodity()):
						mycmd+=1;
					if (command.includes_deposit()):
						mycmd+=2;
					if (mycmd==1):	
						mylabel=command.item_name()
					if (mycmd==2):	
						mylabel="Pfand "+command.commodity_label()[:9]
					if (mycmd==3):	
						mylabel=("%-13s" % (command.commodity_label()[:13]))+"+P"
					myDisplay.cmd_home()
					myDisplay.write('%-15s %4.2f' % (asciify(mylabel)[:15],abs(command.difference())));
					myDisplay.cmd_home()
					myDisplay.write('\nSUMME: {%02i} %8.2f' % ((i-1),initial_balance - self.balance));
				else:
					myDisplay.cmd_home()
					myDisplay.write('%-15s %4.2f' % (command.label()[:15],abs(command.difference())));
					myDisplay.cmd_home()
					myDisplay.write('\nSUMME: {%02i} %8.2f' % ((i-1),initial_balance - self.balance));

				if len(self.transfers) > 1:
					show_total(self.balance - initial_balance, plus='+')
					show_bar()
				show_total(self.balance)

				if self.balance < 0:
					warn_balance()
					myDisplay.cmd_home()
					myDisplay.write('\nKonto: %5.2f!' % (self.balance) )
					print()

				print(COLOR_MUCH + 'Committen nicht vergessen.' + COLOR_RESET)
			else:
				print('Kontostand beträgt:  %s%.2f Euro%s' % (COLOR_MUCH, self.balance, COLOR_RESET))
				myDisplay.display_screen("KONTOSTAND","%s: %.2f Euro" % (self.login_name,self.balance))
				if self.balance < 0:
					print()
					warn_balance()
		else:
			print(COLOR_MUCH + 'Bitte einloggen.' + COLOR_RESET)
			print()
			print('Scanne dazu deine ID-Karte mit dem Barcode-Leser.')
			myDisplay.display_screen("LOGIN","Bitte scanne Deinen Login-Token! *** ")
		print()

	def logged_in(self):
		return self.auth_blob is not None

	def login(self, auth_blob):
		assert(not self.logged_in())

		user_name = net.get_user_name_from_auth_blob(auth_blob)
		balance = net.get_balance(user_name)

		self.auth_blob = auth_blob
		self.login_name = user_name
		self.balance = balance
		self.transfers = list()

	def logout(self):
		# Must not fail if not logged in
		self._reset()

	def commit(self):
		assert(self.logged_in())

		def compress_deposit_commands():
			if not self.transfers:
				return

			dummy, initial_balance = self.transfers[0]
			balance_before = initial_balance
			compressed_deposit = DepositCommand(Decimal('0'))
			others = list()
			for (command, dummy) in list(self.transfers):
				if isinstance(command, DepositCommand):
					compressed_deposit.add(command)
				else:
					others.append((command, balance_before))
					balance_before += command.difference()
			if compressed_deposit.difference() != 0:
				others.append((compressed_deposit, balance_before))
			self.transfers = others

		def process_buy_commands_combined():
			if not self.transfers:
				return

			# Compress BuyCommands, use a single bulkbuy
			dummy, initial_balance = self.transfers[0]
			balance_before = initial_balance
			buy_commands = list()
			non_buy_commands = list()
			total_buy_diff = 0

			for command, dummy in self.transfers:
				if isinstance(command, BuyCommand):
					buy_commands.append(command)
				else:
					balance_before += command.difference()
					non_buy_commands.append((command, balance_before))

			try:
				net.bulk_buy(buy_commands, self.login_name)
			except urllib2.HTTPError as e:
				myDisplay.display_screen("Server error",'Server Error: %s' % str(e))
				error_page('FEHLER bei Kommunikation mit Server "%s"' % str(e))
			else:
				self.transfers = non_buy_commands

		def process_commands():
			for (command, balance_backup) in list(self.transfers):
				try:
					command.run(self.login_name)
				except urllib2.HTTPError as e:
					myDisplay.display_screen("Server error",'Server Error: %s' % str(e))
					error_page('FEHLER bei Kommunikation mit Server "%s"' % str(e))
					break
				else:
					self.transfers.pop(0)

		def finish():
			if not self.transfers:
				# Show final balance for some time
				clear()
				self.dump()
				delay('Logout', 3)

				self.logout()

		compress_deposit_commands()
		process_buy_commands_combined()
		process_commands()
		finish()

	def find(self, barcode):
		try:
			return self.item_cache[barcode]
		except KeyError:
			item = net.get_item(barcode)
			self.item_cache[barcode] = item
			return item

	def buy(self, item):
		assert(self.logged_in())
		log_entry = (BuyCommand(item), self.balance)
		self.transfers.append(log_entry)
		self.balance = self.balance - item.price

	def deposit(self, amount):
		assert(self.logged_in())
		log_entry = (DepositCommand(amount), self.balance)
		self.transfers.append(log_entry)
		self.balance = self.balance + amount

	def to_go(self, scope):
		_PRODUCT_FIRST = 'FEHLER: Bitte zuerst den betreffenden Artikel scannen.'
		if not self.transfers:
			error_page(_PRODUCT_FIRST)
			return

		if scope == TO_GO_ALL:
			"""
			Makes all BuyCommands with deposit > 0 include deposit
			...and updates future balance accordingly.
			"""
			dummy, initial_balance = self.transfers[0]
			balance_before = initial_balance

			for command, dummy in self.transfers:
				if isinstance(command, BuyCommand) \
						and not command.includes_deposit() \
						and command.deposit_value() > 0:
					command.include_deposit(True)
				balance_before += command.difference()

			self.balance = balance_before
		elif scope == TO_GO_NONE:
			"""
			Makes all BuyCommands that include commodity
			not include deposit any more
			...and updates future balance accordingly.
			"""
			first_command, initial_balance = self.transfers[0]
			balance_before = initial_balance

			for command, dummy in self.transfers:
				if isinstance(command, BuyCommand) \
						and command.includes_commodity():
					command.include_deposit(False)
				balance_before += command.difference()

			self.balance = balance_before
		elif scope == TO_GO_PREV:
			"""
			Makes the last BuyCommand include deposit
			...and updates future balance accordingly.
			"""
			prev, balance_backup = self.transfers[-1]
			if not isinstance(prev, BuyCommand):
				error_page(_PRODUCT_FIRST)
				return
			if prev.includes_deposit():
				myDisplay.cmd_clear()
				myDisplay.write('FEHLER: schon Pfand %20s' % prev.item_name()[:20])
				error_page('FEHLER: Pfand für Produkt "%s" bereits aktiviert' % prev.item_name())
				return
			if prev.deposit_value() <= 0:
				myDisplay.cmd_clear()
				myDisplay.write('FEHLER: Pfandfrei!  %20s' % prev.item_name()[:20])
				error_page('FEHLER: Produkt "%s" hat kein Pfand' % prev.item_name())
				return
			before = prev.difference()
			prev.include_deposit(True)
			after = prev.difference()
			self.balance += (after - before)
		elif scope == TO_GO_ONLY:
			"""
			Makes all BuyCommand that include commodity
			be deposit only
			...and updates future balance accordingly.
			"""
			dummy, initial_balance = self.transfers[0]
			balance_before = initial_balance

			for command, dummy in self.transfers:
				if isinstance(command, BuyCommand) \
						and command.deposit_value() > 0 \
						and command.includes_commodity():
					command.include_commodity(False)
					command.include_deposit(True)
				balance_before += command.difference()

			self.balance = balance_before

	def undo(self):
		assert(self.logged_in())
		if self.transfers:
			dummy, balance_backup = self.transfers[-1]
			self.transfers.pop()
			self.balance = balance_backup
		else:
			error_page('FEHLER: Nichts da, was ich rückgängig machen könnte.')


def print_prompt():
	sys.stdout.write(">>> ")
	sys.stdout.flush()


def handle(line, status):
	myDisplay.setIdlemessage("Mir ist langweilig!")
	if line == 'exit':
		clear()
		myDisplay.cmd_clear()
		myDisplay.terminate()
		sys.exit(0)

	if status.logged_in():
		if line in CODES:
			call = CODES[line]
			method = call[0]
			params = call[1:]
			getattr(status, method)(*params)
		else:
			try:
				item = status.find(line)
			except urllib2.HTTPError as e:
				if e.code == 404:  # URL not found == item not found with REST
					myDisplay.display_screen("FEHLER","Code ist unbekannt: '%s'" % ( line[:23]))
					error_page('FEHLER: Aktion oder Ware "%s" nicht bekannt' % line)
				else:
					myDisplay.display_screen("Server error",'%20s' % str(e)[:20])
					error_page('FEHLER bei Kommunikation mit Server "%s"' % str(e))
			else:
				status.buy(item)
	else:
		try:
			status.login(line)
			myDisplay.setIdlemessage(" Comitten nicht vergessen! ***")
		except urllib2.HTTPError as e:
			if e.code == 404:  # URL not found == user unknown
				# Try same code as a product
				item = None
				try:
					item = status.find(line)
				except urllib2.HTTPError as e:
					pass

				if item is None:
					myDisplay.display_screen("FEHLER","Nutzer ist  unbekannt: '%s' *** " % line)

					error_page('FEHLER: Produkt oder Nutzer "%s" nicht bekannt' % line,
							hint_message='Ist in der WebApp unter "Einstellungen" ' \
							'für Ihren Account Plugin "BarcodePlugin" ' \
							'als erlaubt markiert?')
				else:
					item_info_page(item)

			else:
				myDisplay.display_screen("FEHLER",'Server Error %20s' % str(e)[:20])
				error_page('FEHLER bei Kommunikation mit Server "%s"' % str(e))
		except urllib2.URLError as e:
			error_page('FEHLER bei Kommunikation mit Server "%s"' % str(e))


def read_line(f, timeout, timeout_func):
	ready_to_read, _, _ = select.select([f], [], [], timeout)
	if ready_to_read:
		return f.readline().rstrip()
	else:
		timeout_func()
		return ''


def main():
	colorama.init()

	if not settings.load("freitagskasse.conf"):
		sys.exit(1)

	status = Status()
	global scroll_line1,scroll_line2
	global myDisplay

	myDisplay = Display("/dev/ttyUSB0")
	myDisplay.start()
	myDisplay.cmd_reset()
	myDisplay.cmd_cursor_show(False)

	myDisplay.display_screen("Bitte Geduld","Initialisierung... ")
	while True:
		clear()
		status.dump()
		print_prompt()
		line = read_line(sys.stdin, timeout=3*60.0, timeout_func=status.logout)
		if line:
			myDisplay.cmd_clear()
			myDisplay.write('RFID/Barcode:\n%20s' % line[:20])
			handle(line, status)


if __name__ == '__main__':
	try:
		main()
	except KeyboardInterrupt:
		myDisplay.terminate()
		myDisplay.cmd_reset()
		myDisplay.cmd_cursor_show(False)
		pass

