from enum import Enum
import math
import random
from typing import Self

from ..math_util import *
from .. import audio
from .. import config

from . import driver
from .marble import MARBLE, Marble
from . import particles



class WALL(Enum):
	LASER=1; HEDGE=2; METAL=3

class Wall:
	__slots__ = ( "type", "coord", "hidden_sides" )

	def __init__( self:Self, type:WALL, coord:Vector2 ):
		self.type = type
		self.coord = Vector2(coord)
		self.hidden_sides = [ False, False, False, False ] #L, R, B, T

#If a collision, returns tuple of ( ⟨closest position on wall⟩, ⟨restored position⟩ ).  Else return
#	`None`.
def collision_wall_marble( wall:Wall, marble:Marble ) -> None|tuple[Vector2,Vector2]:
	lo = wall.coord
	hi = wall.coord + Vector2(1.0)

	cen:Vector2 = marble.center.xy

	#Coarse test
	if (
		cen.x<=lo.x-marble.radius or cen.x>=hi.x+marble.radius or
		cen.y<=lo.y-marble.radius or cen.y>=hi.y+marble.radius
	): return None
	#print(f"  hit box type={wall.type} lo={lo},hi={hi}")

	#If we're here, we are inside a square `marble.radius` away from the wall's border.

	if   cen.x>=lo.x and cen.x<=hi.x:
		#Intersected above or below
		if cen.y < lo.y: #below
			closest  = Vector2( cen.x, lo.y )
			restored = Vector2( cen.x, lo.y-marble.radius )
		else           : #above
			closest  = Vector2( cen.x, hi.y )
			restored = Vector2( cen.x, hi.y+marble.radius )
		return ( closest, restored )

	elif cen.y>=lo.y and cen.y<=hi.y:
		#Intersected left or right
		if cen.x < lo.x: #left
			closest  = Vector2( lo.x, cen.y )
			restored = Vector2( lo.x-marble.radius, cen.y )
		else           : #right
			closest  = Vector2( hi.x, cen.y )
			restored = Vector2( hi.x+marble.radius, cen.y )
		return ( closest, restored )

	#Maybe intersected a corner
	corner = Vector2(lo)
	if cen.x > hi.x: corner.x=hi.x
	if cen.y > hi.y: corner.y=hi.y
	dx=cen.x-corner.x; dy=cen.y-corner.y
	dist_sq = dx*dx + dy*dy
	if dist_sq < marble.radius*marble.radius:
		#Intersected corner
		normal = Vector2( dx, dy ).normalized()
		restored = corner + marble.radius*normal
		return ( corner, restored )

	return None



class TILE(Enum):
	INFO1=1; INFO2=2; INFO3=3
	ARROWU=4; WARN=5
	R=6; O=7; Y=8; G=9; B=10; P=11

class Tile:
	__slots__ = ( "type", "coord" )

	def __init__( self:Self, type:TILE, coord:Vector2 ):
		self.type = type
		self.coord = Vector2(coord)



class ITEM(Enum):
	GEM=1; LIFE_UP=2; STAR=3; TPLUS60=4

class Item:
	__slots__ = ( "pos", "type", "rot_rad","rot_vel_rad" )

	LIFE_UP_SIZE_FACTOR = 0.3

	def __init__( self:Self, type:ITEM, coord:Vector2 ):
		self.pos = Vector3( coord.x, coord.y, 0.0 )
		self.type = type
		self.rot_rad     = math.tau * random.random()
		self.rot_vel_rad = random.choice(( -1.00, -0.75, 0.75, 1.00 ))

	def update( self:Self, dt:float ):
		self.rot_rad = ( self.rot_rad + self.rot_vel_rad*dt ) % math.tau
		z = 0.3 + 0.02*math.sin( 7.0 * self.rot_rad )
		if self.type == ITEM.STAR:
			z += 0.1
		self.pos.elems.raw[2] = z



class Level:
	def __init__( self:Self, path:str ):
		lines = Level.read_level_lines(path)

		self.name = lines[0]

		self.time_left = float(lines[1])
		self.time_total = self.time_left
		self.lasers_on = False
		self._lasers_time = 0.0

		self.special_tiles:list[Tile] = []
		self.walls:list[Wall] = []
		self.marble_self:Marble = None
		self.marbles_evil:list[Marble] = []
		self.items:list[Item] = []

		ix_max=0; iy_max=0
		iy = 0
		for line in reversed( lines[2:] ):
			ix = 0
			for ch in line:
				coord = Vector2( float(ix), float(iy) )
				if   ch == " ": pass
				#Walls
				elif ch == "!":
					self.walls.append(Wall( WALL.LASER, coord ))
					self.special_tiles.append(Tile( TILE.WARN, coord ))
				elif ch == "%": self.walls.append(Wall( WALL.HEDGE, coord ))
				elif ch == "#": self.walls.append(Wall( WALL.METAL, coord ))
				#Tiles
				elif ch == "r": self.special_tiles.append(Tile( TILE.R     , coord ))
				elif ch == "o": self.special_tiles.append(Tile( TILE.O     , coord ))
				elif ch == "y": self.special_tiles.append(Tile( TILE.Y     , coord ))
				elif ch == "g": self.special_tiles.append(Tile( TILE.G     , coord ))
				elif ch == "b": self.special_tiles.append(Tile( TILE.B     , coord ))
				elif ch == "p": self.special_tiles.append(Tile( TILE.P     , coord ))
				elif ch == "↑": self.special_tiles.append(Tile( TILE.ARROWU, coord ))
				elif ch == "1": self.special_tiles.append(Tile( TILE.INFO1 , coord ))
				elif ch == "2": self.special_tiles.append(Tile( TILE.INFO2 , coord ))
				elif ch == "3": self.special_tiles.append(Tile( TILE.INFO3 , coord ))
				#"People"
				elif ch == "@": self.marble_self=Marble.make_self( coord + Vector2(0.5) )
				elif ch == "G": self.marbles_evil.append(Marble.make_king ( coord + Vector2(0.5) ))
				elif ch == "h": self.marbles_evil.append(Marble.make_hench( coord + Vector2(0.5) ))
				#Items
				elif ch == "L": self.items.append(Item( ITEM.LIFE_UP, coord+Vector2(0.5) ))
				elif ch == "$": self.items.append(Item( ITEM.GEM    , coord+Vector2(0.5) ))
				elif ch == "*": self.items.append(Item( ITEM.STAR   , coord+Vector2(0.5) ))
				elif ch == "T": self.items.append(Item( ITEM.TPLUS60, coord+Vector2(0.5) ))
				#Error
				else: raise RuntimeError(
					f"Unrecognized character '{ch}' at {path}:{len(lines)-int(coord.y)}:{int(coord.x)+1}!"
				)
				ix += 1
				ix_max = max( ix_max, ix )
			iy += 1
			iy_max = max( iy_max, iy )

		self.marbles:list[Marble] = [ self.marble_self ] + self.marbles_evil

		self.has_lasers = False
		self.bound_lo:Vector2 = Vector2( Inf)
		self.bound_hi:Vector2 = Vector2(-Inf)
		for wall in self.walls:
			if wall.type == WALL.LASER:
				self.has_lasers = True
			self.bound_lo = Matrix.min( self.bound_lo, wall.coord              )
			self.bound_hi = Matrix.max( self.bound_hi, wall.coord+Vector2(1.0) )

		self.walls_grid:list[list[Wall|None]] = [ [ None for i in range(ix_max) ] for j in range(iy_max) ]
		for wall in self.walls:
			self.walls_grid[int(wall.coord.y)][int(wall.coord.x)] = wall
		def _occluded( j:int, i:int ):
			neighbor = self.walls_grid[j][i]
			return neighbor and neighbor.type!=WALL.LASER
		nrows, ncols = len(self.walls_grid), len(self.walls_grid[0])
		for     j in range(nrows):
			for i in range(ncols):
				wall = self.walls_grid[j][i]
				if not wall or wall.type==WALL.LASER: continue

				#if i == 0      : wall.hidden_sides[0] = True
				#if i == ncols-1: wall.hidden_sides[1] = True
				if i > 0       and _occluded(j,i-1): wall.hidden_sides[0] = True
				if i < ncols-1 and _occluded(j,i+1): wall.hidden_sides[1] = True

				#if j == 0      : wall.hidden_sides[2] = True
				#if j == nrows-1: wall.hidden_sides[3] = True
				if j > 0       and _occluded(j-1,i): wall.hidden_sides[2] = True
				if j < nrows-1 and _occluded(j+1,i): wall.hidden_sides[3] = True

	@staticmethod
	def read_level_lines( path:str ):
		with open( path, "r", encoding="utf8" ) as lvl_file:
			data = lvl_file.read()
		return data.split("\n")
	@staticmethod
	def load_names():
		names:list[str] = []
		for lvlnum in range( 1, 10+1, 1 ):
			path = f"{config.MAIN_DIR}data/level/lvl{lvlnum}.txt"
			lines = Level.read_level_lines(path)
			names.append(lines[0])
		return names

	def is_complete( self:Self ):
		for item in self.items:
			if item.type == ITEM.GEM: return False
		for marble in self.marbles_evil:
			if marble.type == MARBLE.EVIL_KING:
				return False
		return True

	def complete( self:Self ):
		for item in self.items[:]:
			self.collect_item(item)

		for marble in self.marbles_evil[:]:
			self.kill_marble(marble)
	def collect_item( self:Self, item:Item ):
		if   item.type == ITEM.GEM    : driver.on_collect_gem()
		elif item.type == ITEM.STAR   : driver.on_collect_star()
		elif item.type == ITEM.LIFE_UP: driver.on_collect_life_up()
		elif item.type == ITEM.TPLUS60: driver.on_collect_time(60.0)
		else: assert False
		self.items.remove(item)
	def kill_marble( self:Self, marble:Marble ):
		particles.explode_marble(marble)
		audio.sounds.explode.start()

		self.marbles.remove(marble)
		if marble == self.marble_self:
			self.marble_self = None
			driver.on_exploded_self()
		else:
			driver.on_exploded_evil(marble)
			self.marbles_evil.remove(marble)

	def update( self:Self, dt:float ):
		#Move and Update
		for marble in self.marbles_evil:
			marble.target = self.marble_self

		for marble in self.marbles:
			marble.update(dt)

		for item in self.items:
			item.update(dt)

		#Collision Detection
		for marble in self.marbles:
			self.collide_walls( dt, marble )
		if self.marble_self != None:
			for marble in self.marbles_evil:
				if Marble.collide( self.marble_self, marble ):
					self.marble_self.health -= dt
			self.collide_items(self.marble_self)

		#Death
		for marble in self.marbles[:]:
			if marble.health < 0:
				self.kill_marble(marble)

		#World Update
		if self.has_lasers:
			self._lasers_time += dt
			if self._lasers_time >= 2.0:
				self._lasers_time -= 2.0
				self.lasers_on = not self.lasers_on
				if self.lasers_on: audio.sounds.lasers.start(-1)
				else             : audio.sounds.lasers.stop()
		self.time_left -= dt

	def collide_items( self:Self, marble:Marble ):
		for item in self.items[:]:
			del_x_sq = item.pos.elems.raw[0] - marble.center.elems.raw[0]
			del_x_sq *= del_x_sq
			if del_x_sq >= 0.25: continue

			del_y_sq = item.pos.elems.raw[1] - marble.center.elems.raw[1]
			del_y_sq *= del_y_sq
			if del_y_sq >= 0.25: continue

			dist_xy_sq = del_x_sq + del_y_sq

			#TODO: parametrize distances
			if   item.type == ITEM.GEM:
				if dist_xy_sq < 0.275*0.275: self.collect_item(item)
			elif item.type == ITEM.STAR:
				if dist_xy_sq < 0.425*0.425: self.collect_item(item)
			elif item.type == ITEM.LIFE_UP:
				if dist_xy_sq < 0.1875*0.1875: self.collect_item(item)
			elif item.type == ITEM.TPLUS60:
				if dist_xy_sq < 0.5*0.5: self.collect_item(item)
			else: assert False
	def collide_walls( self:Self, dt:float, marble:Marble ):
		lo = marble.center - Vector3(marble.radius)
		hi = marble.center + Vector3(marble.radius)
		rng_x = ( math.floor(lo.x), math.floor(hi.x)+1 )
		rng_y = ( math.floor(lo.y), math.floor(hi.y)+1 )
		for     iy in range( rng_y[0], rng_y[1], 1 ):
			for ix in range( rng_x[0], rng_x[1], 1 ):
				wall = self.walls_grid[iy][ix]
				if wall == None: continue
				if wall.type == WALL.LASER and not self.lasers_on: continue

				col = collision_wall_marble( wall, marble )
				if col == None: continue
				closest, restored = col

				if wall.type != WALL.LASER:
					marble.center.xy = restored

					if   wall.type == WALL.HEDGE:
						bounce = 5.0
						sound = audio.sounds.wall_hedge
					elif wall.type == WALL.METAL:
						bounce = 10.0
						sound = audio.sounds.wall_metal

					vec = ( closest - restored ).normalized()

					speed_toward = vec | marble.vel.xy
					bounce *= speed_toward

					if speed_toward >= 0.1:
						vol = speed_toward
						if self.marble_self != None:
							#Inverse square falloff for distant sounds
							vol /= max(
								0.5 * ( marble.center - self.marble_self.center ).get_length_sq(),
								1.0
							)
						sound.start( 0, min(vol,1.0) )

					axis = Vector3( vec.y, -vec.x, 0.0 )
					marble.apply_rot_accel( axis*(bounce/dt), dt )
				else:
					marble.health -= dt
