from __future__ import annotations
from typing import List, Optional, TYPE_CHECKING

import colors
from floor_map import FloorMap
from inventory import Inventory
from useable import BaseUseable

if TYPE_CHECKING:
	from engine import Engine
	from entity import Entity

class BaseAction:
	"""Base type for the various actions to apply to a specified entity."""
	entity: Entity

	def __init__(self, entity: Entity):
		self.entity = entity

	def perform(self) -> bool:
		"""return True if the game state should be progressed"""
		raise NotImplementedError()


class QuitAction(BaseAction):
	def __init__(self):
		"""override the base __init__, as no entity is needed"""
		pass

	def perform(self) -> bool:
		raise SystemExit()


class WaitAction(BaseAction):
	def perform(self) -> bool:
		return True


class MovementAction(BaseAction):
	"""Move an Entity within the map"""
	def __init__(self, entity, xdir: int, ydir: int):
		super().__init__(entity)
		self.xdir = xdir
		self.ydir = ydir

	def perform(self) -> bool:
		dest_x = self.entity.x + self.xdir
		dest_y = self.entity.y + self.ydir

		floor_map: FloorMap = self.entity.floor_map

		#bounds and collision checks
		if not floor_map.in_bounds(dest_x, dest_y):
			return False
		if not floor_map.tiles["walkable"][dest_x, dest_y]:
			return False
		if floor_map.get_all_entities_at(dest_x, dest_y, unwalkable_only=True):
			return False

		self.entity.set_pos(dest_x, dest_y)
		return True

class MeleeAction(BaseAction):
	"""Melee attack from the Entity towards a target"""
	def __init__(self, entity, xdir: int, ydir: int):
		super().__init__(entity)
		self.xdir = xdir
		self.ydir = ydir

	def perform(self) -> bool:
		dest_x = self.entity.x + self.xdir
		dest_y = self.entity.y + self.ydir

		targets = self.entity.floor_map.get_all_entities_at(dest_x, dest_y, unwalkable_only=True)

		if not targets:
			return False

		target = targets.pop()

		if not target or not target.stats:
			return False

		#TODO: better combat system

		#calculate damage
		damage = self.entity.stats.attack - target.stats.defense

		#calculate message output
		engine: Engine = self.entity.floor_map.engine
		msg: str = f"{self.entity.name} attacked {target.name}"

		if damage > 0:
			msg += f" for {damage} damage"
		else:
			msg += f" but was ineffective"

		engine.message_log.add_message(msg)

		#performing the actual change here, so the player's death event is at the bottom of the message log
		target.stats.current_hp -= damage

		return True

class BumpAction(BaseAction):
	"""Move an Entity within the map, or attack a target if one is found"""
	def __init__(self, entity, xdir: int, ydir: int):
		super().__init__(entity)
		self.xdir = xdir
		self.ydir = ydir

	def perform(self) -> bool:
		dest_x = self.entity.x + self.xdir
		dest_y = self.entity.y + self.ydir

		if self.entity.floor_map.get_all_entities_at(dest_x, dest_y, unwalkable_only=True):
			return MeleeAction(self.entity, self.xdir, self.ydir).perform()
		else:
			return MovementAction(self.entity, self.xdir, self.ydir).perform()


class PickupAction(BaseAction):
	"""Pickup an item at the entity's location"""
	def perform(self) -> bool:
		x = self.entity.x
		y = self.entity.y

		floor_map: FloorMap = self.entity.floor_map
		engine: Engine = floor_map.engine

		item_pile: List[Entity] = floor_map.get_all_entities_at(x, y, items_only=True)

		if len(item_pile) == 0:
			return False
		elif len(item_pile) == 1:
			item: Entity = item_pile.pop()

			msg: str = f"you picked up a(n) {item.name}"
			if item.useable.current_stack > 1:
				msg = f"you picked up a stack of {item.useable.current_stack} {item.name}"

			floor_map.entities.remove(item)
			self.entity.inventory.insert(item)
			engine.message_log.add_message(msg, color=colors.terminal_light)
		else:
			from event_handlers import OptionSelector #circular imports are a pain

			#build an options list
			options: List[str] = []
			for item in item_pile:
				options.append(item.name)

			engine.event_handler = OptionSelector(
				engine=floor_map.engine,
				parent_handler=engine.event_handler,
				title="Pick Up Item",
				options=options,
				callback=lambda x: self.pickup_callback(engine, floor_map, self.entity, item_pile[x])
			)

		return True

	#utils
	def pickup_callback(self, engine: Engine, floor_map: FloorMap, entity: Entity, item: Entity) -> None:
		msg: str = f"you picked up a(n) {item.name}"
		if item.useable.current_stack > 1:
			msg = f"you picked up a stack of {item.useable.current_stack} {item.name}"

		floor_map.entities.remove(item)
		entity.inventory.insert(item)
		engine.message_log.add_message(msg, color=colors.terminal_light)

class DropAction(BaseAction):
	"""Drop an item from an entity's inventory at the entity's location"""
	index: int
	display_callback: function

	def __init__(self, entity: Entity, index: int, display_callback: function):
		"""override the base __init__"""
		super().__init__(entity)
		self.index = index
		self.display_callback = display_callback

	def perform(self) -> bool:
		x = self.entity.x
		y = self.entity.y

		inventory: Inventory = self.entity.inventory
		floor_map: FloorMap = self.entity.floor_map
		engine: Engine = floor_map.engine

		item: Entity = inventory.withdraw(self.index)

		item.x = x
		item.y = y

		#TODO: Check for floorpile stack merging
		floor_map.entities.add(item)

		if self.display_callback: #adjust the cursor
			self.display_callback(-1)

		msg: str = f"you dropped a(n) {item.name}"
		if item.useable.current_stack > 1:
			msg = f"you dropped a stack of {item.useable.current_stack} {item.name}"

		engine.message_log.add_message(msg, color=colors.terminal_light)

		return True


class DropPartialStackAction(BaseAction):
	"""Drop part of a stack from an entity's inventory at the entity's location"""
	index: int
	amount: int
	display_callback: function

	def __init__(self, entity: Entity, index: int, amount: int, display_callback: function):
		"""override the base __init__"""
		super().__init__(entity)
		self.index = index
		self.amount = amount
		self.display_callback = display_callback

	def perform(self) -> bool:
		x = self.entity.x
		y = self.entity.y

		inventory: Inventory = self.entity.inventory
		floor_map: FloorMap = self.entity.floor_map
		engine: Engine = floor_map.engine

		item: Entity = inventory.access(self.index)

		#TODO: Check for floorpile stack merging
		new_item: Entity = item.spawn(x, y, floor_map)

		item.useable.current_stack -= self.amount
		new_item.useable.current_stack = self.amount

		if self.display_callback: #adjust the cursor
			self.display_callback(0) #by zero

		msg: str = f"you dropped a partial stack of {new_item.useable.current_stack} {new_item.name}"

		engine.message_log.add_message(msg, color=colors.terminal_light)

		return True


class UsageAction(BaseAction):
	"""Use an item from an entity's inventory, removing it if needed"""
	index: int
	target: Entity
	display_callback: function

	def __init__(self, entity: Entity, index: int, target: Entity, display_callback: function):
		"""override the base __init__"""
		super().__init__(entity)
		self.index = index
		self.target = target
		self.display_callback = display_callback

	def perform(self) -> bool:
		inventory: Inventory = self.entity.inventory
		engine: Engine = self.entity.floor_map.engine

		item: Entity = inventory.access(self.index)
		usable: BaseUseable = item.useable

		if usable.apply(self.target.stats) and usable.is_stack_empty():
			#remove the item from the inventory
			inventory.discard(self.index)

			if self.display_callback:
				self.display_callback(-1)

		msg: str = usable.get_used_msg(item.name)
		if not msg:
			msg = f"you used a(n) {item.name}"

		engine.message_log.add_message(msg, color=colors.terminal_light)

		return True