# -*- coding: utf-8 -*-
#
#    Copyright © 2019 Simon Forman
#
#    This file is part of Thun
#
#    Thun 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.
#
#    Thun 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 for more details.
#
#    You should have received a copy of the GNU General Public License
#    along with Thun.  If not see <http://www.gnu.org/licenses/>.
#
'''

Core
=====================

The core module defines a bunch of system-wide "constants" (some colors
and PyGame event groups), the message classes for Oberon-style message
passing, a "world" class that holds the main context for the system, and
a mainloop class that manages the, uh, main loop (the PyGame event queue.)

'''
from __future__ import print_function
from builtins import object, str, range
from sys import stderr
from traceback import format_exc
import pygame
from joy.joy import run
from joy.utils.stack import stack_to_string


COMMITTER = 'Joy <auto-commit@example.com>'


BLACK = FOREGROUND = 0, 0, 0
GREY = 127, 127, 127
WHITE = BACKGROUND = 255, 255, 255
BLUE = 100, 100, 255
GREEN = 70, 200, 70


MOUSE_EVENTS = frozenset({
	pygame.MOUSEMOTION,
	pygame.MOUSEBUTTONDOWN,
	pygame.MOUSEBUTTONUP
	})
'PyGame mouse events.'

ARROW_KEYS = frozenset({
	pygame.K_UP,
	pygame.K_DOWN,
	pygame.K_LEFT,
	pygame.K_RIGHT
	})
'PyGame arrow key events.'


TASK_EVENTS = tuple(range(pygame.USEREVENT, pygame.NUMEVENTS))
'Keep track of all possible task events.'

AVAILABLE_TASK_EVENTS = set(TASK_EVENTS)
'Task IDs that have not been assigned to a task.'

ALLOWED_EVENTS = [pygame.QUIT, pygame.KEYUP, pygame.KEYDOWN]
ALLOWED_EVENTS.extend(MOUSE_EVENTS)
ALLOWED_EVENTS.extend(TASK_EVENTS)
'Event "mask" for PyGame event queue, we are only interested in these event types.'


ERROR = -1
PENDING = 0
SUCCESS = 1
# 'Message status codes...  dunno if this is a good idea or not...


class Message(object):
	'''Message base class.  Contains ``sender`` field.'''
	def __init__(self, sender):
		self.sender = sender


class CommandMessage(Message):
	'''For commands, adds ``command`` field.'''
	def __init__(self, sender, command):
		Message.__init__(self, sender)
		self.command = command


class ModifyMessage(Message):
	'''
	For when resources are modified, adds ``subject`` and ``details``
	fields.
	'''
	def __init__(self, sender, subject, **details):
		Message.__init__(self, sender)
		self.subject = subject
		self.details = details


class OpenMessage(Message):
	'''
	For when resources are modified, adds ``name``, content_id``,
	``status``, and ``traceback`` fields.
	'''
	def __init__(self, sender, name):
		Message.__init__(self, sender)
		self.name = name
		self.content_id = self.thing = None
		self.status = PENDING
		self.traceback = None


class PersistMessage(Message):
	'''
	For when resources are modified, adds ``content_id`` and ``details``
	fields.
	'''
	def __init__(self, sender, content_id, **details):
		Message.__init__(self, sender)
		self.content_id = content_id
		self.details = details


class ShutdownMessage(Message):
	'''Signals that the system is shutting down.'''


# Joy Interpreter & Context


class World(object):
	'''
	This object contains the system context, the stack, dictionary, a
	reference to the display broadcast method, and the log.
	'''

	def __init__(self, stack_id, stack_holder, dictionary, notify, log):
		self.stack_holder = stack_holder
		self.dictionary = dictionary
		self.notify = notify
		self.stack_id = stack_id
		self.log = log.lines
		self.log_id = log.content_id

	def handle(self, message):
		'''
		Deal with updates to the stack and commands.
		'''
		if (isinstance(message, ModifyMessage)
			and message.subject is self.stack_holder
			):
			self._log_lines('', '%s <-' % self.format_stack())
		if not isinstance(message, CommandMessage):
			return
		c, s, d = message.command, self.stack_holder[0], self.dictionary
		self._log_lines('', '-> %s' % (c,))
		self.stack_holder[0], _, self.dictionary = run(c, s, d)
		mm = ModifyMessage(self, self.stack_holder, content_id=self.stack_id)
		self.notify(mm)

	def _log_lines(self, *lines):
		self.log.extend(lines)
		self.notify(ModifyMessage(self, self.log, content_id=self.log_id))

	def format_stack(self):
		try:
			return stack_to_string(self.stack_holder[0])
		except:
			print(format_exc(), file=stderr)
			return str(self.stack_holder[0])


def push(sender, item, notify, stack_name='stack.pickle'):
	'''
	Helper function to push an item onto the system stack with message.
	'''
	om = OpenMessage(sender, stack_name)
	notify(om)
	if om.status == SUCCESS:
		om.thing[0] = item, om.thing[0]
		notify(ModifyMessage(sender, om.thing, content_id=om.content_id))
	return om.status


def open_viewer_on_string(sender, content, notify):
	'''
	Helper function to open a text viewer on a string.
	Typically used to show tracebacks.
	'''
	push(sender, content, notify)
	notify(CommandMessage(sender, 'good_viewer_location open_viewer'))


# main loop


class TheLoop(object):
	'''
	The main loop manages tasks and the PyGame event queue
	and framerate clock.
	'''

	FRAME_RATE = 24

	def __init__(self, display, clock):
		self.display = display
		self.clock = clock
		self.tasks = {}
		self.running = False

	def install_task(self, F, milliseconds):
		'''
		Install a task to run every so many milliseconds.
		'''
		try:
			task_event_id = AVAILABLE_TASK_EVENTS.pop()
		except KeyError:
			raise RuntimeError('out of task ids')
		self.tasks[task_event_id] = F
		pygame.time.set_timer(task_event_id, milliseconds)
		return task_event_id

	def remove_task(self, task_event_id):
		'''
		Remove an installed task.
		'''
		assert task_event_id in self.tasks, repr(task_event_id)
		pygame.time.set_timer(task_event_id, 0)
		del self.tasks[task_event_id]
		AVAILABLE_TASK_EVENTS.add(task_event_id)

	def __del__(self):
		# Best effort to cancel all running tasks.
		for task_event_id in self.tasks:
			pygame.time.set_timer(task_event_id, 0)

	def run_task(self, task_event_id):
		'''
		Give a task its time to shine.
		'''
		task = self.tasks[task_event_id]
		try:
			task()
		except:
			traceback = format_exc()
			self.remove_task(task_event_id)
			print(traceback, file=stderr)
			print('TASK removed due to ERROR', task, file=stderr)
			open_viewer_on_string(self, traceback, self.display.broadcast)

	def loop(self):
		'''
		The actual main loop machinery.

		Maintain a ``running`` flag, pump the PyGame event queue and
		handle the events (dispatching to the display), tick the clock.

		When the loop is exited (by clicking the window close button or
		pressing the ``escape`` key) it broadcasts a ``ShutdownMessage``.
		'''
		self.running = True
		while self.running:
			for event in pygame.event.get():
				if event.type == pygame.QUIT:
					self.running = False
				elif event.type == pygame.KEYUP and event.key == pygame.K_ESCAPE:
					self.running = False
				elif event.type in self.tasks:
					self.run_task(event.type)
				else:
					self.display.dispatch_event(event)
			pygame.display.update()
			self.clock.tick(self.FRAME_RATE)
		self.display.broadcast(ShutdownMessage(self))
