# -*- 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/>.
#
'''

Text Viewer
=================

'''
from __future__ import print_function
from __future__ import division
from builtins import object, range, str, zip
from past.builtins import basestring
from past.utils import old_div
import string
import pygame
from joy.utils.stack import expression_to_string
from joy.vui.core import (
	ARROW_KEYS,
	BACKGROUND as BG,
	FOREGROUND as FG,
	CommandMessage,
	ModifyMessage,
	OpenMessage,
	SUCCESS,
	push,
	)
from joy.vui import viewer, font_data
#reload(viewer)


MenuViewer = viewer.MenuViewer


SELECTION_COLOR = 235, 255, 0, 32
SELECTION_KEYS = {
	pygame.K_F1,
	pygame.K_F2,
	pygame.K_F3,
	pygame.K_F4,
	}
STACK_CHATTER_KEYS = {
	pygame.K_F5,
	pygame.K_F6,
	pygame.K_F7,
	pygame.K_F8,
	}


def _is_command(display, word):
	return display.lookup(word) or word.isdigit() or all(
		not s or s.isdigit() for s in word.split('.', 1)
		) and len(word) > 1


def format_stack_item(content):
	if isinstance(content, tuple):
		return '[%s]' % expression_to_string(content)
	return str(content)


class Font(object):

	IMAGE = pygame.image.load(font_data.data, 'Iosevka12.BMP')
	LOOKUP = (string.ascii_letters +
			  string.digits +
			  '''@#$&_~|`'"%^=-+*/\\<>[]{}(),.;:!?''')

	def __init__(self, char_w=8, char_h=19, line_h=19):
		self.char_w = char_w
		self.char_h = char_h
		self.line_h = line_h

	def size(self, text):
		return self.char_w * len(text), self.line_h

	def render(self, text):
		surface = pygame.Surface(self.size(text))
		surface.fill(BG)
		x = 0
		for ch in text:
			if not ch.isspace():
				try:
					i = self.LOOKUP.index(ch)
				except ValueError:
					# render a lil box...
					r = (x + 1, old_div(self.line_h, 2) - 3,
						 self.char_w - 2, old_div(self.line_h, 2))
					pygame.draw.rect(surface, FG, r, 1)
				else:
					iy, ix = divmod(i, 26)
					ix *= self.char_w
					iy *= self.char_h
					area = ix, iy, self.char_w, self.char_h
					surface.blit(self.IMAGE, (x, 0), area)
			x += self.char_w
		return surface

	def __contains__(self, char):
		assert len(char) == 1, repr(char)
		return char in self.LOOKUP


FONT = Font()


class TextViewer(MenuViewer):

	MINIMUM_HEIGHT = FONT.line_h + 3
	CLOSE_TEXT = FONT.render('close')
	GROW_TEXT = FONT.render('grow')

	class Cursor(object):

		def __init__(self, viewer):
			self.v = viewer
			self.x = self.y = 0
			self.w, self.h = 2, FONT.line_h
			self.mem = pygame.Surface((self.w, self.h))
			self.can_fade = False

		def set_to(self, x, y):
			self.fade()
			self.x, self.y = x, y
			self.draw()

		def draw(self):
			r = self.x * FONT.char_w, self.screen_y(), self.w, self.h
			self.mem.blit(self.v.body_surface, (0, 0), r)
			self.v.body_surface.fill(FG, r)
			self.can_fade = True

		def fade(self):
			if self.can_fade:
				dest = self.x * FONT.char_w, self.screen_y()
				self.v.body_surface.blit(self.mem, dest)
				self.can_fade = False

		def screen_y(self, row=None):
			if row is None: row = self.y
			return (row - self.v.at_line) * FONT.line_h

		def up(self, _mod):
			if self.y:
				self.fade()
				self.y -= 1
				self.x = min(self.x, len(self.v.lines[self.y]))
				self.draw()

		def down(self, _mod):
			if self.y < len(self.v.lines) - 1:
				self.fade()
				self.y += 1
				self.x = min(self.x, len(self.v.lines[self.y]))
				self.draw()
				self._check_scroll()

		def left(self, _mod):
			if self.x:
				self.fade()
				self.x -= 1
				self.draw()
			elif self.y:
				self.fade()
				self.y -= 1
				self.x = len(self.v.lines[self.y])
				self.draw()
				self._check_scroll()

		def right(self, _mod):
			if self.x < len(self.v.lines[self.y]):
				self.fade()
				self.x += 1
				self.draw()
			elif self.y < len(self.v.lines) - 1:
				self.fade()
				self.y += 1
				self.x = 0
				self.draw()
				self._check_scroll()

		def _check_scroll(self):
			if self.y < self.v.at_line:
				self.v.scroll_down()
			elif self.y > self.v.at_line + self.v.h_in_lines:
				self.v.scroll_up()

	def __init__(self, surface):
		self.cursor = self.Cursor(self)
		MenuViewer.__init__(self, surface)
		self.lines = ['']
		self.content_id = None
		self.at_line = 0
		self.bg = BG
		self.command = self.command_rect = None
		self._sel_start = self._sel_end = None

	def resurface(self, surface):
		self.cursor.fade()
		MenuViewer.resurface(self, surface)

		w, h = self.CLOSE_TEXT.get_size()
		self.close_rect = pygame.rect.Rect(self.w - 2 - w, 1, w, h)
		w, h = self.GROW_TEXT.get_size()
		self.grow_rect = pygame.rect.Rect(1, 1, w, h)

		self.body_surface = surface.subsurface(self.body_rect)
		self.line_w = old_div(self.body_rect.w, FONT.char_w) + 1
		self.h_in_lines = old_div(self.body_rect.h, FONT.line_h) - 1
		self.command_rect = self.command = None
		self._sel_start = self._sel_end = None

	def handle(self, message):
		if super(TextViewer, self).handle(message):
			return
		if (isinstance(message, ModifyMessage)
			and message.subject is self.lines
			):
			# TODO: check self.at_line
			self.draw_body()

	# Drawing

	def draw_menu(self):
		#MenuViewer.draw_menu(self)
		self.surface.blit(self.GROW_TEXT, (1, 1))
		self.surface.blit(self.CLOSE_TEXT,
						  (self.w - 2 - self.close_rect.w, 1))
		if self.content_id:
			self.surface.blit(FONT.render('| ' + self.content_id),
						  (self.grow_rect.w + FONT.char_w + 3, 1))
		self.surface.fill( # light grey background
			(196, 196, 196),
			(0, 0, self.w - 1, self.MINIMUM_HEIGHT),
			pygame.BLEND_MULT
			)

	def draw_body(self):
		MenuViewer.draw_body(self)
		ys = range(0, self.body_rect.height, FONT.line_h)
		ls = self.lines[self.at_line:self.at_line + self.h_in_lines + 2]
		for y, line in zip(ys, ls):
			self.draw_line(y, line)

	def draw_line(self, y, line):
		surface = FONT.render(line[:self.line_w])
		self.body_surface.blit(surface, (0, y))

	def _redraw_line(self, row):
		try: line = self.lines[row]
		except IndexError: line = ' ' * self.line_w
		else:
			n = self.line_w - len(line)
			if n > 0: line = line + ' ' * n
		self.draw_line(self.cursor.screen_y(row), line)

	# General Functionality

	def focus(self, display):
		self.cursor.v = self
		self.cursor.draw()

	def unfocus(self):
		self.cursor.fade()

	def scroll_up(self):
		if self.at_line < len(self.lines) - 1:
			self._fade_command()
			self._deselect()
			self._sel_start = self._sel_end = None
			self.at_line += 1
			self.body_surface.scroll(0, -FONT.line_h)
			row = self.h_in_lines + self.at_line
			self._redraw_line(row)
			self._redraw_line(row + 1)
			self.cursor.draw()

	def scroll_down(self):
		if self.at_line:
			self._fade_command()
			self._deselect()
			self._sel_start = self._sel_end = None
			self.at_line -= 1
			self.body_surface.scroll(0, FONT.line_h)
			self._redraw_line(self.at_line)
			self.cursor.draw()

	def command_down(self, display, x, y):
		if self.command_rect and self.command_rect.collidepoint(x, y):
			return
		self._fade_command()
		line, column, _row = self.at(x, y)
		word_start = line.rfind(' ', 0, column) + 1
		word_end = line.find(' ', column)
		if word_end == -1: word_end = len(line)
		word = line[word_start:word_end]
		if not _is_command(display, word):
			return
		r = self.command_rect = pygame.Rect(
			word_start * FONT.char_w, # x
			old_div(y, FONT.line_h) * FONT.line_h, # y
			len(word) * FONT.char_w, # w
			FONT.line_h # h
			)
		pygame.draw.line(self.body_surface, FG, r.bottomleft, r.bottomright)
		self.command = word

	def command_up(self, display):
		if self.command:
			command = self.command
			self._fade_command()
			display.broadcast(CommandMessage(self, command))

	def _fade_command(self):
		self.command = None
		r, self.command_rect = self.command_rect, None
		if r:
			pygame.draw.line(self.body_surface, BG, r.bottomleft, r.bottomright)

	def at(self, x, y):
		'''
		Given screen coordinates return the line, row, and column of the
		character there.
		'''
		row = self.at_line + old_div(y, FONT.line_h)
		try:
			line = self.lines[row]
		except IndexError:
			row = len(self.lines) - 1
			line = self.lines[row]
			column = len(line)
		else:
			column = min(old_div(x, FONT.char_w), len(line))
		return line, column, row

	# Event Processing

	def body_click(self, display, x, y, button):
		if button == 1:
			_line, column, row = self.at(x, y)
			self.cursor.set_to(column, row)
		elif button == 2:
			if pygame.KMOD_SHIFT & pygame.key.get_mods():
				self.scroll_up()
			else:
				self.scroll_down()
		elif button == 3:
			self.command_down(display, x, y)
		elif button == 4: self.scroll_down()
		elif button == 5: self.scroll_up()

	def menu_click(self, display, x, y, button):
		if MenuViewer.menu_click(self, display, x, y, button):
			return True

	def mouse_up(self, display, x, y, button):
		if MenuViewer.mouse_up(self, display, x, y, button):
			return True
		elif button == 3 and self.body_rect.collidepoint(x, y):
			self.command_up(display)

	def mouse_motion(self, display, x, y, rel_x, rel_y, button0, button1, button2):
		if MenuViewer.mouse_motion(self, display, x, y, rel_x, rel_y,
								   button0, button1, button2):
			return True
		if (button0
			and display.focused_viewer is self
			and self.body_rect.collidepoint(x, y)
			):
			bx, by = self.body_rect.topleft
			_line, column, row = self.at(x - bx, y - by)
			self.cursor.set_to(column, row)
		elif button2 and self.body_rect.collidepoint(x, y):
			bx, by = self.body_rect.topleft
			self.command_down(display, x - bx, y - by)

	def close(self):
		self._sel_start = self._sel_end = None

	def key_down(self, display, uch, key, mod):

		if key in SELECTION_KEYS:
			self._selection_key(display, key, mod)
			return
		if key in STACK_CHATTER_KEYS:
			self._stack_chatter_key(display, key, mod)
			return
		if key in ARROW_KEYS:
			self._arrow_key(key, mod)
			return

		line, i = self.lines[self.cursor.y], self.cursor.x
		modified = ()
		if key == pygame.K_RETURN:
			self._return_key(mod, line, i)
			modified = True
		elif key == pygame.K_BACKSPACE:
			modified = self._backspace_key(mod, line, i)
		elif key == pygame.K_DELETE:
			modified = self._delete_key(mod, line, i)
		elif key == pygame.K_INSERT:
			modified = self._insert_key(display, mod, line, i)
		elif uch and uch in FONT or uch == ' ':
			self._printable_key(uch, mod, line, i)
			modified = True
		else:
			print('%r %i %s' % (uch, key, bin(mod)))

		if modified:
			# The selection is fragile.
			self._deselect()
			self._sel_start = self._sel_end = None
			message = ModifyMessage(
				self, self.lines, content_id=self.content_id)
			display.broadcast(message)

	def _stack_chatter_key(self, display, key, mod):
		if key == pygame.K_F5:
			if mod & pygame.KMOD_SHIFT:
				command = 'roll<'
			else:
				command = 'swap'
		elif key == pygame.K_F6:
			if mod & pygame.KMOD_SHIFT:
				command = 'roll>'
			else:
				command = 'dup'
		elif key == pygame.K_F7:
			if mod & pygame.KMOD_SHIFT:
				command = 'tuck'
			else:
				command = 'over'
##        elif key == pygame.K_F8:
##            if mod & pygame.KMOD_SHIFT:
##                command = ''
##            else:
##                command = ''
		else:
			return
		display.broadcast(CommandMessage(self, command))

	# Selection Handling

	def _selection_key(self, display, key, mod):
		self.cursor.fade()
		self._deselect()
		if key == pygame.K_F1: # set sel start
			self._sel_start = self.cursor.y, self.cursor.x
			self._update_selection()
		elif key == pygame.K_F2: # set sel end
			self._sel_end = self.cursor.y, self.cursor.x
			self._update_selection()
		elif key == pygame.K_F3: # copy
			if mod & pygame.KMOD_SHIFT:
				self._parse_selection(display)
			else:
				self._copy_selection(display)
			self._update_selection()
		elif key == pygame.K_F4: # cut or delete
			if mod & pygame.KMOD_SHIFT:
				self._delete_selection(display)
			else:
				self._cut_selection(display)
		self.cursor.draw()

	def _deselect(self):
		if self._has_selection():
			srow, erow = self._sel_start[0], self._sel_end[0]
			# Just erase the whole selection.
			for r in range(min(srow, erow), max(srow, erow) + 1):
				self._redraw_line(r)

	def _copy_selection(self, display):
		if push(self, self._get_selection(), display.broadcast) == SUCCESS:
			return True
##        om = OpenMessage(self, 'stack.pickle')
##        display.broadcast(om)
##        if om.status == SUCCESS:
##            selection = self._get_selection()
##            om.thing[0] = selection, om.thing[0]
##            display.broadcast(ModifyMessage(
##                self, om.thing, content_id=om.content_id))

	def _parse_selection(self, display):
		if self._has_selection():
			if self._copy_selection(display):
				display.broadcast(CommandMessage(self, 'parse'))

	def _cut_selection(self, display):
		if self._has_selection():
			if self._copy_selection(display):
				self._delete_selection(display)

	def _delete_selection(self, display):
		if not self._has_selection():
			return
		self.cursor.fade()
		srow, scolumn, erow, ecolumn = self._selection_coords()
		if srow == erow:
			line = self.lines[srow]
			self.lines[srow] = line[:scolumn] + line[ecolumn:]
		else:
			left = self.lines[srow][:scolumn]
			right = self.lines[erow][ecolumn:]
			self.lines[srow:erow + 1] = [left + right]
		self.draw_body()
		self.cursor.set_to(srow, scolumn)
		display.broadcast(ModifyMessage(
			self, self.lines, content_id=self.content_id))

	def _has_selection(self):
		return (self._sel_start
				and self._sel_end
				and self._sel_start != self._sel_end)

	def _get_selection(self):
		'''Return the current selection if any as a single string.'''
		if not self._has_selection():
			return ''
		srow, scolumn, erow, ecolumn = self._selection_coords()
		if srow == erow:
			return str(self.lines[srow][scolumn:ecolumn])
		lines = []
		assert srow < erow
		while srow <= erow:
			line = self.lines[srow]
			e = ecolumn if srow == erow else len(line)
			lines.append(line[scolumn:e])
			scolumn = 0
			srow += 1
		return str('\n'.join(lines))

	def _selection_coords(self):
		(srow, scolumn), (erow, ecolumn) = (
			min(self._sel_start, self._sel_end),
			max(self._sel_start, self._sel_end)
			)
		return srow, scolumn, erow, ecolumn

	def _update_selection(self):
		if self._sel_start is None and self._sel_end:
			self._sel_start = self._sel_end
		elif self._sel_end is None and self._sel_start:
			self._sel_end = self._sel_start
		assert self._sel_start and self._sel_end
		if self._sel_start != self._sel_end:
			for rect in self._iter_selection_rectangles():
				self.body_surface.fill(
					SELECTION_COLOR,
					rect,
					pygame.BLEND_RGBA_MULT
					)

	def _iter_selection_rectangles(self, ):
		srow, scolumn, erow, ecolumn = self._selection_coords()
		if srow == erow:
			yield (
				scolumn * FONT.char_w,
				self.cursor.screen_y(srow),
				(ecolumn - scolumn) * FONT.char_w,
				FONT.line_h
				)
			return
		lines = self.lines[srow:erow + 1]
		assert len(lines) >= 2
		first_line = lines[0]
		yield (
			scolumn * FONT.char_w,
			self.cursor.screen_y(srow),
			(len(first_line) - scolumn) * FONT.char_w,
			FONT.line_h
			)
		yield (
			0,
			self.cursor.screen_y(erow),
			ecolumn * FONT.char_w,
			FONT.line_h
			)
		if len(lines) > 2:
			for line in lines[1:-1]:
				srow += 1
				yield (
					0,
					self.cursor.screen_y(srow),
					len(line) * FONT.char_w,
					FONT.line_h
					)

	# Key Handlers

	def _printable_key(self, uch, _mod, line, i):
		line = line[:i] + uch + line[i:]
		self.lines[self.cursor.y] = line
		self.cursor.fade()
		self.cursor.x += 1
		self.draw_line(self.cursor.screen_y(), line)
		self.cursor.draw()

	def _backspace_key(self, _mod, line, i):
		res = False
		if i:
			line = line[:i - 1] + line[i:]
			self.lines[self.cursor.y] = line
			self.cursor.fade()
			self.cursor.x -= 1
			self.draw_line(self.cursor.screen_y(), line + ' ')
			self.cursor.draw()
			res = True
		elif self.cursor.y:
			y = self.cursor.y
			left, right = self.lines[y - 1:y + 1]
			self.lines[y - 1:y + 1] = [left + right]
			self.cursor.x = len(left)
			self.cursor.y -= 1
			self.draw_body()
			self.cursor.draw()
			res = True
		return res

	def _delete_key(self, _mod, line, i):
		res = False
		if i < len(line):
			line = line[:i] + line[i + 1:]
			self.lines[self.cursor.y] = line
			self.cursor.fade()
			self.draw_line(self.cursor.screen_y(), line + ' ')
			self.cursor.draw()
			res = True
		elif self.cursor.y < len(self.lines) - 1:
			y = self.cursor.y
			left, right = self.lines[y:y + 2]
			self.lines[y:y + 2] = [left + right]
			self.draw_body()
			self.cursor.draw()
			res = True
		return res

	def _arrow_key(self, key, mod):
		if key == pygame.K_UP: self.cursor.up(mod)
		elif key == pygame.K_DOWN: self.cursor.down(mod)
		elif key == pygame.K_LEFT: self.cursor.left(mod)
		elif key == pygame.K_RIGHT: self.cursor.right(mod)

	def _return_key(self, _mod, line, i):
		self.cursor.fade()
		# Ignore the mods for now.
		n = self.cursor.y
		self.lines[n:n + 1] = [line[:i], line[i:]]
		self.cursor.y += 1
		self.cursor.x = 0
		if self.cursor.y > self.at_line + self.h_in_lines:
			self.scroll_up()
		else:
			self.draw_body()
			self.cursor.draw()

	def _insert_key(self, display, mod, _line, _i):
		om = OpenMessage(self, 'stack.pickle')
		display.broadcast(om)
		if om.status != SUCCESS:
			return
		stack = om.thing[0]
		if stack:
			content = format_stack_item(stack[0])
			if self.insert(content):
				if mod & pygame.KMOD_SHIFT:
					display.broadcast(CommandMessage(self, 'pop'))
				return True

	def insert(self, content):
		assert isinstance(content, basestring), repr(content)
		if content:
			self.cursor.fade()
			row, column = self.cursor.y, self.cursor.x
			line = self.lines[row]
			lines = (line[:column] + content + line[column:]).splitlines()
			self.lines[row:row + 1] = lines
			self.draw_body()
			self.cursor.y = row + len(lines) - 1
			self.cursor.x = len(lines[-1]) - len(line) + column
			self.cursor.draw()
			return True

	def append(self, content):
		self.cursor.fade()
		self.cursor.y = len(self.lines) - 1
		self.cursor.x = len(self.lines[self.cursor.y])
		self.insert(content)
