#
# BearLibTerminal
# Copyright (C) 2013-2017 Cfyz
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
# of the Software, and to permit persons to whom the Software is furnished to do
# so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in all
# copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
# FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
# COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
# IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
# CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
#

import sys, ctypes, atexit
from ctypes import c_int, c_uint32, c_char_p, c_wchar_p, c_void_p, POINTER

_version3 = sys.version_info >= (3, 0)
_integer = int if _version3 else (int, long)
_c_byte_p = ctypes.POINTER(ctypes.c_byte)

def _load_library():
	import os
	
	# Figure out where the library binary should be
	places = [sys.argv[0]]
	try:
		# Try to search near the wrapper module
		places.insert(0, __file__)
	except NameError:
		# There may be no module (e. g. a standalone app)
		pass
	places = [os.path.dirname(os.path.abspath(place)) for place in places]
	
	# Construct a platform-specific name of the library binary file
	if 'win32' in sys.platform:
		name = 'BearLibTerminal.dll'
	elif 'linux' in sys.platform:
		name = 'libBearLibTerminal.so'
	elif 'darwin' in sys.platform:
		name = 'libBearLibTerminal.dylib'
	else:
		raise RuntimeError('Unsupported platform: ' + sys.platform)
	
	# Now actually try to load the library binary
	for place in places:
		try:
			return ctypes.CDLL(os.path.join(place, name))
		except OSError:
			pass
	raise RuntimeError('Cannot load BearLibTerminal library: no {} found in {}'.format(name, places))

_library = _load_library()

# wchar_t size may vary
_wchar_size = ctypes.sizeof(ctypes.c_wchar)
_tchar_size = _wchar_size if _version3 else 1
if _wchar_size == 4:
	_wset = _library.terminal_set32
	_wprint_ext = _library.terminal_print_ext32
	_wmeasure_ext = _library.terminal_measure_ext32
	_read_wstr = _library.terminal_read_str32
	_color_from_wname = _library.color_from_name32
	_wget = _library.terminal_get32
	_wfont = _library.terminal_font32
else:
	_wset = _library.terminal_set16
	_wprint_ext = _library.terminal_print_ext16
	_wmeasure_ext = _library.terminal_measure_ext16
	_read_wstr = _library.terminal_read_str16
	_color_from_wname = _library.color_from_name16
	_wget = _library.terminal_get16
	_wfont = _library.terminal_font16

# color/bkcolor accept uint32, color_from_name returns uint32
_library.terminal_color.argtypes = [c_uint32]
_library.terminal_bkcolor.argtypes = [c_uint32]
_library.color_from_name8.restype = c_uint32
_color_from_wname.restype = c_uint32

def color_from_name(s):
	if _version3 or isinstance(s, unicode):
		return _color_from_wname(s)
	else:
		return _library.color_from_name8(s)

def open():
	if _library.terminal_open() == 0:
		return False
	set('terminal.encoding-affects-put=false')
	# Try to register a terminal ipython integration.
	try:
		from IPython.lib import inputhook
		def bearlibterminal_inputhook():
			has_input()
			while not inputhook.stdin_ready():
				delay(5)
			return 0
		inputhook.inputhook_manager.set_inputhook(bearlibterminal_inputhook)
	except:
		pass
	return True

# Try to register a kernel-based ipython/jupyter integration.
try:
    from ipykernel.eventloops import register_integration
    @register_integration('blt')
    def loop_blt(kernel):
        interval = int(kernel._poll_interval * 1000) # to milliseconds
        while True:
            kernel.do_one_iteration()
            delay(interval)
except:
    pass

close = _library.terminal_close
close.restype = None

def set(s):
	if _version3 or isinstance(s, unicode):
		return _wset(s) == 1
	else:
		return _library.terminal_set8(s) == 1

def setf(s, *args):
	return set(s.format(*args))

refresh = _library.terminal_refresh
refresh.restype = None

clear = _library.terminal_clear
clear.restype = None

clear_area = _library.terminal_clear_area
clear_area.restype = None

crop = _library.terminal_crop
crop.restype = None

layer = _library.terminal_layer
layer.restype = None

def color(v):
	if isinstance(v, _integer):
		_library.terminal_color(v)
	else:
		_library.terminal_color(color_from_name(v))

def bkcolor(v):
	if isinstance(v, _integer):
		_library.terminal_bkcolor(v)
	else:
		_library.terminal_bkcolor(color_from_name(v))

composition = _library.terminal_composition
composition.restype = None

_afont = _library.terminal_font8
_afont.restype = None
_afont.argtypes = [c_char_p]
_wfont.restype = None
_wfont.argtypes = [c_wchar_p]
def font(name):
	if _version3 or isinstance(name, unicode):
		_wfont(name)
	else:
		_afont(name)

def put(x, y, c):
	if not isinstance(c, _integer):
		c = ord(c)
	_library.terminal_put(x, y, c)

def put_ext(x, y, dx, dy, c, corners=None):
	if not isinstance(c, _integer):
		c = ord(c)
	if corners is None:
		_library.terminal_put_ext(x, y, dx, dy, c, None)
	else:
		for i in range(0, 4):
			put_ext.corners[i] = corners[i]
		_library.terminal_put_ext(x, y, dx, dy, c, ctypes.cast(put_ext.corners, ctypes.POINTER(ctypes.c_uint)))
put_ext.corners = (c_uint32 * 4)()

def pick(x, y, z = 0):
	return _library.terminal_pick(x, y, z);

def pick_color(x, y, z = 0):
	return _library.terminal_pick_color(x, y, z);

pick_bkcolor = _library.terminal_pick_bkcolor
pick_bkcolor.restype = c_uint32

_aprint_ext = _library.terminal_print_ext8
_aprint_ext.argtypes = [c_int, c_int, c_int, c_int, c_int, c_char_p, POINTER(c_int), POINTER(c_int)]
_aprint_ext.restype = None
_wprint_ext.argtypes = [c_int, c_int, c_int, c_int, c_int, c_wchar_p, POINTER(c_int), POINTER(c_int)]
_wprint_ext.restype = None
def puts(x, y, s, width=0, height=0, align=0):
	out_width = c_int()
	out_height = c_int()
	if _version3 or isinstance(s, unicode):
		_wprint_ext(x, y, width, height, align, s, ctypes.byref(out_width), ctypes.byref(out_height))
	else:
		_aprint_ext(x, y, width, height, align, s, ctypes.byref(out_width), ctypes.byref(out_height))
	return (out_width.value, out_height.value)

print_ = puts

if _version3:
	setattr(sys.modules[__name__], "print", puts)

def printf(x, y, s, *args):
	return puts(x, y, s.format(*args))

_ameasure_ext = _library.terminal_measure_ext8
_ameasure_ext.argtypes = [c_int, c_int, c_char_p, POINTER(c_int), POINTER(c_int)]
_ameasure_ext.restype = None
_wmeasure_ext.argtypes = [c_int, c_int, c_wchar_p, POINTER(c_int), POINTER(c_int)]
_wmeasure_ext.restype = None
def measure(s, width=0, height=0):
	out_width = c_int()
	out_height = c_int()
	if _version3 or isinstance(s, unicode):
		_wmeasure_ext(width, height, s, ctypes.byref(out_width), ctypes.byref(out_height))
	else:
		_ameasure_ext(width, height, s, ctypes.byref(out_width), ctypes.byref(out_height))
	return (out_width.value, out_height.value)

def measuref(s, *args):
	return measure(s.format(*args))

def has_input():
	return _library.terminal_has_input() == 1

state = _library.terminal_state

def check(state):
	return _library.terminal_state(state) > 0

read = _library.terminal_read

peek = _library.terminal_peek

def read_str(x, y, s, max):
	if _version3 or isinstance(s, unicode):
		p = ctypes.create_unicode_buffer(s, max+1)
		rc = _read_wstr(x, y, p, max)
		return rc, p.value
	else:
		p = ctypes.create_string_buffer(s, max+1)
		rc = _library.terminal_read_str8(x, y, p, max)
		return rc, p.value

delay = _library.terminal_delay
delay.restype = None

_library.terminal_get8.restype = c_char_p
_wget.restype = c_wchar_p

def get(s, default_value=None):
	if _version3:
		return _wget(s, default_value)
	elif isinstance(s, unicode):
		return unicode(_wget(s, default_value));
	else:
		return str(_library.terminal_get8(s, default_value))

def color_from_argb(a, r, g, b):
	result = a
	result = result * 256 + r
	result = result * 256 + g
	result = result * 256 + b
	return result

def _prepare_field_layout(array_dt, field_desc, field_name):
	parts = field_desc.split(':')
	dt_name = parts[0]
	format = parts[1] if len(parts) > 1 else None
	if dt_name not in array_dt.fields:
		raise RuntimeError("There is no field with name '{}' in {}".format(dt_name, dt))
	dt, offset = array_dt.fields[dt_name]
	if field_name == 'code':
		if dt.kind in 'iu' and dt.itemsize == 4:
			format = 'char32'
		else:
			raise RuntimeError("Invalid tile code field '{}' data type {}".format(dt_name, dt))
	elif field_name in ['color', 'back_color']:
		if format and format not in ['rgb', 'bgr', 'rgba', 'bgra']:
			raise RuntimeError("Invalid tile color format '{}'".format(format))
		if dt.kind in 'iu' and dt.itemsize == 4:
			if not format:
				format = 'bgra'
			elif format not in ['rgba', 'bgra']:
				raise RuntimeError("Invalid tile color format '{}' for data type {}".format(format, dt))
		elif dt.subdtype and (dt.subdtype[0].kind in 'iu' and dt.subdtype[0].itemsize == 1) and dt.subdtype[1] in [(3,), (4,)]:
			size = dt.subdtype[1][0]
			if not format:
				format = 'rgb' if size == 3 else 'rgba'
			elif (size == 3 and format not in ['rgb', 'bgr']) or (size == 4 and format not in ['rgba', 'bgra']):
				raise RuntimeError("Invalid tile color format '{}' for data type {}".format(format, dt))
		else:
			raise RuntimeError("Invalid tile color field '{}' data type {}".format(dt_name, dt))
	else:
		raise RuntimeError("Unknown field '{}'".format(field_name))
	return '{}={}:{}'.format(field_name, offset, format)

def _prepare_tile_layout(array_dt, tile_code, tile_color, tile_back_color):
	fields = []
	fields.append(_prepare_field_layout(array_dt, tile_code, 'code'))
	if tile_color:
		fields.append(_prepare_field_layout(array_dt, tile_color, 'color'))
	if tile_back_color:
		fields.append(_prepare_field_layout(array_dt, tile_back_color, 'back_color'))
	return ','.join(fields)

_library.terminal_put_array.restype = c_int
_library.terminal_put_array.argtypes = [c_int, c_int, c_int, c_int, _c_byte_p, c_int, c_int, c_void_p, c_int]

def put_np_array(x, y, arr, tile_code, tile_color=None, tile_back_color=None):
	key = (arr.dtype, tile_code, tile_color, tile_back_color)
	layout = put_np_array.layouts.get(key)
	if not layout:
		layout = _prepare_tile_layout(arr.dtype, tile_code, tile_color, tile_back_color)
		put_np_array.layouts[key] = layout
	w, h = arr.shape
	column_stride, row_stride = arr.strides
	return _library.terminal_put_array(x, y, w, h, ctypes.cast(arr.ctypes.data, ctypes.POINTER(ctypes.c_byte)), row_stride, column_stride, layout, _tchar_size)

put_np_array.layouts = dict()

@atexit.register
def _cleanup():
	close()

# Keyboard scancodes for events/states.
TK_A                = 0x04
TK_B                = 0x05
TK_C                = 0x06
TK_D                = 0x07
TK_E                = 0x08
TK_F                = 0x09
TK_G                = 0x0A
TK_H                = 0x0B
TK_I                = 0x0C
TK_J                = 0x0D
TK_K                = 0x0E
TK_L                = 0x0F
TK_M                = 0x10
TK_N                = 0x11
TK_O                = 0x12
TK_P                = 0x13
TK_Q                = 0x14
TK_R                = 0x15
TK_S                = 0x16
TK_T                = 0x17
TK_U                = 0x18
TK_V                = 0x19
TK_W                = 0x1A
TK_X                = 0x1B
TK_Y                = 0x1C
TK_Z                = 0x1D
TK_1                = 0x1E
TK_2                = 0x1F
TK_3                = 0x20
TK_4                = 0x21
TK_5                = 0x22
TK_6                = 0x23
TK_7                = 0x24
TK_8                = 0x25
TK_9                = 0x26
TK_0                = 0x27
TK_RETURN           = 0x28
TK_ENTER            = 0x28
TK_ESCAPE           = 0x29
TK_BACKSPACE        = 0x2A
TK_TAB              = 0x2B
TK_SPACE            = 0x2C
TK_MINUS            = 0x2D
TK_EQUALS           = 0x2E
TK_LBRACKET         = 0x2F
TK_RBRACKET         = 0x30
TK_BACKSLASH        = 0x31
TK_SEMICOLON        = 0x33
TK_APOSTROPHE       = 0x34
TK_GRAVE            = 0x35
TK_COMMA            = 0x36
TK_PERIOD           = 0x37
TK_SLASH            = 0x38
TK_F1               = 0x3A
TK_F2               = 0x3B
TK_F3               = 0x3C
TK_F4               = 0x3D
TK_F5               = 0x3E
TK_F6               = 0x3F
TK_F7               = 0x40
TK_F8               = 0x41
TK_F9               = 0x42
TK_F10              = 0x43
TK_F11              = 0x44
TK_F12              = 0x45
TK_PAUSE            = 0x48
TK_INSERT           = 0x49
TK_HOME             = 0x4A
TK_PAGEUP           = 0x4B
TK_DELETE           = 0x4C
TK_END              = 0x4D
TK_PAGEDOWN         = 0x4E
TK_RIGHT            = 0x4F
TK_LEFT             = 0x50
TK_DOWN             = 0x51
TK_UP               = 0x52
TK_KP_DIVIDE        = 0x54
TK_KP_MULTIPLY      = 0x55
TK_KP_MINUS         = 0x56
TK_KP_PLUS          = 0x57
TK_KP_ENTER         = 0x58
TK_KP_1             = 0x59
TK_KP_2             = 0x5A
TK_KP_3             = 0x5B
TK_KP_4             = 0x5C
TK_KP_5             = 0x5D
TK_KP_6             = 0x5E
TK_KP_7             = 0x5F
TK_KP_8             = 0x60
TK_KP_9             = 0x61
TK_KP_0             = 0x62
TK_KP_PERIOD        = 0x63
TK_SHIFT            = 0x70
TK_CONTROL          = 0x71
TK_ALT              = 0x72

# Mouse events/states.
TK_MOUSE_LEFT       = 0x80 # Buttons
TK_MOUSE_RIGHT      = 0x81
TK_MOUSE_MIDDLE     = 0x82
TK_MOUSE_X1         = 0x83
TK_MOUSE_X2         = 0x84
TK_MOUSE_MOVE       = 0x85 # Movement event
TK_MOUSE_SCROLL     = 0x86 # Mouse scroll event
TK_MOUSE_X          = 0x87 # Cusor position in cells
TK_MOUSE_Y          = 0x88
TK_MOUSE_PIXEL_X    = 0x89 # Cursor position in pixels
TK_MOUSE_PIXEL_Y    = 0x8A
TK_MOUSE_WHEEL      = 0x8B # Scroll direction and amount
TK_MOUSE_CLICKS     = 0x8C # Number of consecutive clicks

# If key was released instead of pressed, it's code will be OR'ed with VK_KEY_RELEASED.
TK_KEY_RELEASED     = 0x100

# Virtual key-codes for internal terminal states/variables.
# These can be accessed via terminal_state function.
TK_WIDTH            = 0xC0 # Terminal width in cells
TK_HEIGHT           = 0xC1 # Terminal height in cells
TK_CELL_WIDTH       = 0xC2 # Cell width in pixels
TK_CELL_HEIGHT      = 0xC3 # Cell height in pixels
TK_COLOR            = 0xC4 # Current foregroung color
TK_BKCOLOR          = 0xC5 # Current background color
TK_LAYER            = 0xC6 # Current layer
TK_COMPOSITION      = 0xC7 # Current composition state
TK_CHAR             = 0xC8 # Translated ANSI code of last produced character
TK_WCHAR            = 0xC9 # Unicode codepoint of last produced character
TK_EVENT            = 0xCA # Last dequeued event
TK_FULLSCREEN       = 0xCB # Fullscreen state

# Other events.
TK_CLOSE            = 0xE0
TK_RESIZED          = 0xE1

# Generic mode enum. Used in Terminal.composition call only.
TK_OFF              =    0
TK_ON               =    1

# Input result codes for terminal_read_str function.
TK_INPUT_NONE       =    0
TK_INPUT_CANCELLED  =   -1

# Text printing alignment.
TK_ALIGN_DEFAULT    =    0
TK_ALIGN_LEFT       =    1
TK_ALIGN_RIGHT      =    2
TK_ALIGN_CENTER     =    3
TK_ALIGN_TOP        =    4
TK_ALIGN_BOTTOM     =    8
TK_ALIGN_MIDDLE     =   12
