local UNIVERSE_DEFS = dofile("../defs/universe_defs.lua")
Universe = class("Universe")

function Universe:new(universe_type, seed, num_players, chunk_cols, chunk_rows, chunk_size, cell_pixels, start_level, max_level, rules)
	local uni = instance(self)
	uni.index = universe_type
	uni.def = assert(UNIVERSE_DEFS[universe_type], universe_type)
	uni.rules = rules
	uni.seed = seed
	uni.num_players = num_players
	uni.start_level = start_level
	uni.max_level = max_level
	uni.chunk_cols = chunk_cols
	uni.chunk_rows = chunk_rows
	uni.cell_pixels = cell_pixels	
	uni.chunk_size = chunk_size
	uni.half_chunk_size = chunk_size*0.5
	uni.cols = uni.chunk_cols*chunk_size
	uni.rows = uni.chunk_rows*chunk_size
	uni.width = uni.cols*cell_pixels
	uni.height = uni.rows*cell_pixels

	uni.locations_to_generate = {}
	uni.areas_to_decorate = {}
	uni.areas_to_populate = {}
		
	uni.area_lookup_size = chunk_size*4
	uni.area_lookup_cols = uni.cols/uni.area_lookup_size
	uni.area_lookup_rows = uni.rows/uni.area_lookup_size
	return uni
end

function Universe:init_tables()
	--self.items = {}
	--self.heroes = {}
	--self.maps = {}
	--self.dungeons = {}
	--self.bosses = {}
	--self.loots = {}

	self.teams = {}
	self.locations = {}
	self.areas = {}
	self.area_lookup = {}

	--[[
	for i=1,self.area_lookup_cols do
		self.area_lookup[i] = {}
		for j=1,self.area_lookup_rows do
			self.area_lookup[i][j] = {}
		end
	end
	]]

	self.chunks = {}
	self.active_chunk_map = {}
end

function Universe:create_chunks()
	local chunk_size = self.chunk_size
	local cell_pixels = self.cell_pixels
	for col=1,self.chunk_cols do
		local col_chunks = {}
		self.chunks[col] = col_chunks
		for row=1, self.chunk_rows do
			local chunk_seed = math.randomMax()
			local rng = RandomGenerator:new(chunk_seed)
			col_chunks[row] = MapChunk:new(col, row, chunk_size, chunk_size, cell_pixels, cell_pixels, rng)
		end
	end
end

local function dist_to_center(a,b,ref)
	local cx, cy = ref.width*0.5, ref.height*0.5
	local da = math.distance(a.x, a.y, cx,cy)
	local db = math.distance(b.x, b.y, cx,cy)
	return da < db
end

local CHUNK_SIZE = 64
function Universe:generate(world)
	self.world = world
	local seed = self.seed
	local num_players = self.num_players
	local start_level = self.start_level
	local max_level = self.max_level
	local cell_pixels = self.cell_pixels
	local cell_pixels = self.cell_pixels

	assert(seed, "no seed")
	assert(num_players, "no num_players")
	assert(start_level, "no start_level")

	self:init_tables()
	math.initRandom(seed)
	self:create_chunks()

	local player_team = Team:new("player")
	local allies_team = Team:new("allies")
	local neutral_team = Team:new("neutral")
	local neutral_agro_team = Team:new("neutral_agro")
	local monster_team = Team:new("monster")

	self:add_team(player_team)
	self:add_team(allies_team)
	self:add_team(neutral_team)
	self:add_team(neutral_agro_team)
	self:add_team(monster_team)

	allies_team:set_loot_team(neutral_team)

	allies_team:add_trading_team(allies_team)
	allies_team:add_trading_team(player_team)

	neutral_team:add_trading_team(player_team)
	neutral_team:add_trading_team(allies_team)
	neutral_team:set_agro_team(neutral_agro_team)	

	player_team:add_trading_team(allies_team)
	player_team:add_trading_team(player_team)
	
	monster_team:add_trading_team(monster_team)

	player_team:add_enemy_team(monster_team)
	player_team:add_enemy_team(neutral_team)
	player_team:add_enemy_team(neutral_agro_team)

	neutral_agro_team:add_enemy_team(player_team)
	neutral_agro_team:add_enemy_team(monster_team)
	neutral_agro_team:add_enemy_team(allies_team)
	
	allies_team:add_enemy_team(monster_team)
	allies_team:add_enemy_team(neutral_agro_team)

	monster_team:add_enemy_team(player_team)
	monster_team:add_enemy_team(allies_team)
	monster_team:add_enemy_team(neutral_agro_team)

	self:set_player_team(player_team)

	local sx,sy = self.width*0.5, self.height*0.5
	local first_city = Entity:new("location", self.width*0.5, self.height*0.5)
	first_city:set_level(start_level)
	first_city:set_team(allies_team)
	first_city:set_name("Hope")
	first_city:set_area_generator("city", sx,sy)
	self:add_location(first_city)
	self:set_spawn_point(first_city)
	world:add_ent(first_city)	

	local def = self.def

	local scol,srow = self:position_to_col_row(sx,sy)
	local max_radius = math.distance(scol, srow, self.cols, self.rows)
	local center_zone_radius = def.center_zone_factor*max_radius
	local edge_zone_radius = def.edge_zone_factor*max_radius
	local outer_zone_radius = max_radius-center_zone_radius-edge_zone_radius
	local repeats = {}
	
	local all_good = true
	local level_adjust = {}

	while all_good do
		all_good = false
		for index, loc in ipairs(def.locations) do
			repeats[index] = (repeats[index] or 0) +1
			local max_repeats = (loc.max_repeats or 1)*(def.max_repeat_factor or 1)
			if repeats[index] <= max_repeats then
				local treshold = loc.giveup_threshold or 20
				local fail_safe = 0
				local num = math.random(loc.min_amount, loc.max_amount)
				--print(index, num)
				if num > 0 then
					local margin_cell_range = loc.margin or math.ceil(self.cols*loc.margin_factor)
					local margin_pixel_range = margin_cell_range*self.cell_pixels
					local success_gen = 0					
								
					while success_gen < num do
						local col = math.random(margin_cell_range,self.cols-margin_cell_range)
						local row = math.random(margin_cell_range,self.rows-margin_cell_range)
						local x,y = self:col_row_to_center_position(col, row)
						if not self:find_first_location_within_range(x,y, margin_pixel_range) then
							local from_mid = math.distance(scol,srow, col, row)
							local percent = math.clamp(math.max(from_mid-center_zone_radius)/outer_zone_radius,0,1)
							percent = percent*percent
							local level = math.max(start_level,math.floor(start_level + (max_level-start_level)*percent))
							
							if loc.monster then
								for i=0,10 do
									if not level_adjust[level] then
										level_adjust[level] = true
										break
									else
										level = math.min(max_level, level +1)
									end
								end
							end
							

							local min_loc_level = start_level + (max_level-start_level)*loc.min_level_factor
							local max_loc_level = start_level + (max_level-start_level)*loc.max_level_factor
							if level >= min_loc_level and level <= max_loc_level then
								local location = Entity:new("location", x, y)
								local location_level = level
								location:set_level(location_level)
								
								if loc.monster then
									location:set_team(monster_team)
								elseif loc.neutral then
									location:set_team(neutral_team)
								elseif loc.allied then
									location:set_team(allies_team)
								else
									assert(false, "location lacks team settings")
								end
								
								location:set_area_generator(loc.area_generator, margin_pixel_range, margin_cell_range)
								location:set_name(self.rules:generate_instance("name", loc.name_category, location_level))
								self:add_location(location)
								world:add_ent(location)					
								success_gen = success_gen + 1
								fail_safe = 0
								all_good = true
							end
							--print("added", loc.area_generator)
						else
							--print("fail", loc.area_generator)
						end
						fail_safe = fail_safe + 1

						if fail_safe >= treshold then
							--give up and move on threshold
							--print("gave up")
							--print("WHILE GENERATION ERROR LOGIC!!!!")
							break
						end
					end
				end
			end
		end
	end	

	shellsort(self.locations_to_generate, dist_to_center, nil, self)
	self.locations_to_generate_start_num = #self.locations_to_generate
end

function Universe:generate_location(location, yield)
	if not location.area_generated then
		location.area_generated = true
		local area_gen = assert(location.area_generator, "no area gen")
		local level = location.level
		local team = location.team

		local area_generator = self.rules:generate_instance("area_generator", area_gen, level)
		area_generator:generate_using_rules(self.rules, team, location, yield)

		app.checkYield(yield)

		local location_col, location_row = self:position_to_col_row(location.x, location.y)

		area_generator:layout_areas(self, location_col, location_row, yield)
		app.checkYield(yield)
		
		for index, area in ipairs(area_generator:get_areas()) do		
			--print("AREA!", index, area.index, area.placed)
			if area.placed then							
				self:add_area(area)
				app.checkYield(yield)
			end
		end

		
		app.checkYield(yield)
	end	
end

--[[
function Universe:get_area_lookup_at_col_row(col,row)
	local c,r = self:col_row_to_area_lookup_col_row(col, row)
	if self:is_inside_area_lookup(c,r) then
		return self.area_lookup[c][r]
	end
end
]]

function Universe:is_area_intersecting_any_shape(area, yield)
	local area_col,area_row = area.col, area.row	
	for index, shape in ipairs(area.shapes) do
		local shape_col = area_col + shape.col
		local shape_row = area_row + shape.row
		for col, cells in pairs(shape.mask) do
			for row, v in pairs(cells) do
				if v then
					local rcol = shape_col + col
					local rrow = shape_row + row
					if self:is_tile_layedout(rcol, rrow) then
						return true
					end
				end
			end
		end
		app.checkYield(yield)
	end

	return false
	
	--local areas = self:get_area_lookup_at_col_row(area.col, area.row)
	--if areas then
	--	print(#areas)
	--	for _, blocking_area in ipairs(areas) do
	--		if blocking_area.placed then			
	--			--print(area.col, area.row, blocking_area.col, blocking_area.row)
	--			if not blocking_area.def.allow_overlap then
	--				if blocking_area:is_area_intersecting_any_shape(area) then
	--					return true
	--				end
	--			end
	--		end
	--	end
	--else
	--	assert(false, "area failed to get", area.col, area.row)
	--end
	--return false
end

function Universe:add_ent(ent)
	local chunk = self:position_to_chunk(ent.x, ent.y)
	if chunk:is_active() then
		chunk:add_ent(ent)
		self.world:add_ent(ent)
	else
		chunk:add_ent_to_add(ent)
	end
end


function Universe:get_relative_col_row(chunk, col, row)
	return col-chunk.col_offset, row-chunk.row_offset
end

function Universe:change_ent_team(ent, to_team)
	self.world:ent_leaving_team(ent, ent.team)
	ent.team = nil
	ent:set_team(to_team)
	self.world:ent_joined_team(ent, to_team)
end

function Universe:add_team(team)
	table.insert(self.teams, team)
end

function Universe:add_location(location)
	table.insert(self.locations, location)	
	table.insert(self.locations_to_generate, location)
	--print("CITY ADDED", city.x, city.y, city.name)
end

function Universe:get_chunk_at_col_row(col, row)
	local chunk_col, chunk_row = self:col_row_to_chunk_col_row(col, row)
	local chunk = self:get_chunk(chunk_col, chunk_row)
	return chunk
end

function Universe:find_first_location_within_range(x,y,range)
	local range2 = range^2
	for index, location in pairs(self.locations) do
		local dist2 = math.distance2(location.x, location.y, x, y)
		if dist2 < range2 then
			return location
		end
	end
	return false
end

function Universe:set_spawn_point(spawn_point)
	self.spawn_point = spawn_point
end

function Universe:set_player_team(team)
	self.player_team = team
end

function Universe:get_spawn_point()
	return self.spawn_point
end

function Universe:get_player_team()
	return self.player_team
end

function Universe:col_row_to_center_position(col, row)
	return (col-0.5)*self.cell_pixels, (row-0.5)*self.cell_pixels
end

function Universe:position_to_col_row(x, y)
	return math.ceil(x/self.cell_pixels), math.ceil(y/self.cell_pixels)	
end

function Universe:position_to_chunk(x,y)
	local col,row = self:position_to_col_row(x,y)
	local ccol, crow = self:col_row_to_chunk_col_row(col, row)
	return self:get_chunk(ccol, crow)
end

function Universe:col_row_to_chunk_col_row(col, row)
	return self:clampChunkCol(math.ceil(col/self.chunk_size)), self:clampChunkRow(math.ceil(row/self.chunk_size))
end

function Universe:col_row_to_area_lookup_col_row(col, row)
	return math.clamp(math.ceil(col/self.area_lookup_size),1, self.area_lookup_cols), math.clamp(math.ceil(row/self.area_lookup_size),1,self.area_lookup_rows)
end


function Universe:activate_chunk_at(chunk, time_stamp)
	assert(chunk, "no chunk")
	self.last_activated_at = time_stamp
	chunk.activated_at = time_stamp
	if not chunk.active then
		chunk:activate()
		self.active_chunk_map[chunk] = true
		return true
	end
	return false
end

function Universe:fetch_to_add(chunk)
	if chunk:has_ents_to_add() then
		return chunk.ents_to_add
	end	
end

function Universe:set_tile(col, row, tile)
	local chunk = self:get_chunk_at_col_row(col, row)
	if not chunk:is_generated() then
		chunk:generate()
	end	
	chunk:set_tile(col, row, tile)
end

function Universe:get_tile_at_position(x,y)
	local col,row = self:position_to_col_row(x,y)
	local chunk = self:get_chunk_at_col_row(col,row)
	return chunk:get_tile(col,row)	
end

function Universe:get_tile_at_col_row(col,row)
	local chunk = self:get_chunk_at_col_row(col,row)
	return chunk:get_tile(col,row)	
end

local TILES = TILES
function Universe:get_tile_def_at_col_row(col,row)
	local chunk = self:get_chunk_at_col_row(col,row)
	return TILES[chunk:get_tile(col,row)]
end

function Universe:get_light_at(col,row)
	col = math.clamp(col,1,self.cols)
	row = math.clamp(row,1,self.rows)
	local chunk = self:get_chunk_at_col_row(col,row)
	return chunk:get_light_at(col,row)
end

function Universe:adjust_light(center_col,center_row,color,radius,factor)
	local cols = self.cols
	local rows = self.rows
	for col=center_col-radius,center_col+radius do
		for row=center_row-radius,center_row+radius do
			local dist = math.distance(center_col,center_row,col,row)
			if dist <= radius then
				if col >= 1 and col <= cols and row >= 1 and row >= 1 and row <= rows then
					local chunk = self:get_chunk_at_col_row(col,row)
					local amt = (1-(dist/radius)^2)*factor
					chunk:adjust_light(col,row,color,amt)
				end
			end
		end
	end
end

function Universe:is_line_of_sight_blocked(x1,y1, x2, y2)
	local scol, srow = self:position_to_col_row(x1,y1)	
	local ecol, erow = self:position_to_col_row(x2,y2)
	local col = scol
	local row = srow
	local delta = 0
	local angle = math.angleBetweenPoints(x1,y1, x2, y2)
	local dist = math.distance(x1,y1, x2, y2)
	repeat
		delta = math.min(dist,delta + self.cell_pixels)
		local tcol, trow = self:position_to_col_row(x1+ math.cos(angle)*delta,y1 + math.sin(angle)*delta)
		local dx = tcol-col
		local dy = trow-row
		local ratio = dx/dy
		if math.abs(ratio) > 1 then
			local dxs = math.sign(dx)
			col = col + dxs
		else
			local dys = math.sign(dy)
			row = row + dys
		end
		if self:is_tile_blocking_at_col_row(col, row) then
			return true
		end
		--self:set_tile(col, row, "enchanting_floor")		
	until(col == ecol and row == erow)
	return false
end

function Universe:is_tile_generated(col, row)
	local chunk = self:get_chunk_at_col_row(col,row)
	if chunk:is_tile_used(col, row) then
		return true
	end
	return false
end

function Universe:is_tile_layedout(col, row)
	local chunk = self:get_chunk_at_col_row(col,row)
	if not chunk:is_generated() then
		chunk:generate()
	end	
	if chunk:is_tile_layedout(col, row) then
		return true
	end
	return false
end

function Universe:set_tile_layedout(col, row, v)
	local chunk = self:get_chunk_at_col_row(col,row)
	if not chunk:is_generated() then
		chunk:generate()
	end	
	if chunk:set_tile_layedout(col, row, v) then
		return true
	end
	return false
end

function Universe:is_tile_occupied(col, row)
	local chunk = self:get_chunk_at_col_row(col,row)
	if chunk:get_ent_at_tile(col, row) then
		return true
	end
	return false
end

function Universe:is_tile_blocking_at_col_row(col,row)
	local chunk = self:get_chunk_at_col_row(col,row)
	return TILES[chunk:get_tile(col,row)].blocking
end

function Universe:activate_chunk(chunk)
	if not chunk:is_generated() then
		chunk:generate()
	end
	if not chunk:is_decorated() then
		for index, area in ipairs(chunk:get_areas()) do
			self:add_area_to_decorate(area)
		end
		chunk:set_decorated(true)
	end
	if not chunk:is_populated() then
		for index, area in ipairs(chunk:get_areas()) do
			self:add_area_to_populate(area)
		end
		chunk:set_populated(true)
	end
end

function Universe:add_area_to_decorate(area)
	table.insert(self.areas_to_decorate, area)
end

function Universe:add_area_to_populate(area)
	table.insert(self.areas_to_populate, area)
end

local function area_prio(a,b)
	return a.def.layout_prio > b.def.layout_prio
end

local function area_decorate_prio(a,b)
	return a.def.decorate_prio > b.def.decorate_prio
end

function Universe:generate_routine()
	--self:generate_locations()
	if self.routine then
		if self.routine:step() or self.routine:getError() then
			self.routine = nil
			self.locations_to_generate = nil
		end
	elseif next(self.locations_to_generate) then
		self.routine,self.yield = SimpleRoutine:new(self.generate_locations, APP.yieldStep)
		self.routine:step(self,self.yield)
	end
end

function Universe:prio_location(loc)
	for index, loc_to_gen in pairs(self.locations_to_generate) do
		if loc_to_gen == loc then
			table.remove(self.locations_to_generate, index)
		end
	end
	table.insert(self.locations_to_generate, 1, loc)
end

function Universe:generate_locations(yield)
	--print(yield)
	if self.locations_to_generate then
		local next_loc = self.locations_to_generate[1]
		while next_loc do
			table.remove(self.locations_to_generate,1)
			self:generate_location(next_loc, yield)
			next_loc = self.locations_to_generate[1]
			app.checkYield(yield)
			--print("remining:", #self.locations_to_generate)
		end
	end
	self.locations_to_generate = nil
end

function Universe:is_generating()
	return self.locations_to_generate
end

function Universe:get_generating_progress()
	if self.locations_to_generate then
		return 1-(#self.locations_to_generate/self.locations_to_generate_start_num)
	end
end

function Universe:update_generation()
	if self.locations_to_generate then
		self:generate_routine()
	end
end

function Universe:check_areas()
	
	--debug.tag_dt("dec")
	if next(self.areas_to_decorate) then
		shellsort(self.areas_to_decorate, area_decorate_prio)
		for index, area in ipairs(self.areas_to_decorate) do
			area:apply_tiles_universe(self)
			--print("decor", area.index)
		end
		table.clear(self.areas_to_decorate)
	end
	--debug.tag_dt("pop")
	if next(self.areas_to_populate) then
		shellsort(self.areas_to_populate, area_prio)
		for index, area in ipairs(self.areas_to_populate) do
			area:apply_characters_universe(self)
		end
		table.clear(self.areas_to_populate)
	end
	--debug.tag_dt(nil)
end

--[[
function Universe:is_inside_area_lookup(acol, arow)
	if acol >= 1 and acol <= self.area_lookup_cols then
		if arow >= 1 and arow <= self.area_lookup_rows then
			return true
		end
	end
	return false
end
]]

function Universe:add_area(area)
	local chunk = self:get_chunk_at_col_row(area.col, area.row)
	chunk:add_area(area)
	if chunk:is_decorated() then
		self:add_area_to_decorate(area)
	end
	if chunk:is_populated() then
		self:add_area_to_populate(area)
	end
	table.insert(self.areas, area)

	--local c,r = self:col_row_to_area_lookup_col_row(area.col, area.row)
	--for ox=-1, 1 do
	--	for oy=-1,1 do
	--		local cx,cy = c+ox,r+oy
	--		if self:is_inside_area_lookup(cx, cy) then			
	--			table.insert(self.area_lookup[cx][cy], area)
	--		end
	--	end
	--end

	local area_col,area_row = area.col, area.row	
	for index, shape in ipairs(area.shapes) do
		local shape_col = area_col + shape.col
		local shape_row = area_row + shape.row
		for col, cells in pairs(shape.mask) do
			for row, v in pairs(cells) do
				local rcol = shape_col + col
				local rrow = shape_row + row				
				if self:is_in_col_row_bounds(rcol, rrow) then
					self:set_tile_layedout(rcol, rrow, true)		
				end		
			end
		end
	end

end

function Universe:is_in_col_row_bounds(col, row)
	return col >= 1 and col <= self.cols and row >= 1 and row <= self.rows
end

function Universe:get_areas()
	return self.areas
end

function Universe:get_active_chunk_map()
	return self.active_chunk_map
end

function Universe:deactivate_chunk(chunk)
	assert(chunk, "no chunk")
	if self.active_chunk_map[chunk] then		 
		chunk:deactivate()
		self.active_chunk_map[chunk] = nil	
	end
end

function Universe:get_chunk(col, row)
	assert(col, col)
	assert(row, row)
	assert(self.chunks[col], col)
	assert(self.chunks[col][row], col, row)
	return self.chunks[col][row]
end

function Universe:get_map(col, row)
	local chunk = self.chunks[col][row]	
	return chunk.map
end

function Universe:colToPixels(col)
	return (col-1) * self.chunk_size*self.cell_pixels
end
function Universe:rowToPixels(row)
	return (row-1) * self.chunk_size*self.cell_pixels
end

function Universe:clampChunkCol(col)
	return math.clamp(col, 1, self.chunk_cols)
end
function Universe:clampChunkRow(row)
	return math.clamp(row, 1, self.chunk_rows)
end
function Universe:xToChunkCol(x, camera)
	return math.round((x + camera.cx)/self.cell_pixels/self.chunk_size)
end
function Universe:yToChunkRow(y, camera)
	return math.round((y + camera.cy)/self.cell_pixels/self.chunk_size)
end
function Universe:xToChunkColCeil(x, camera)
	return math.ceil((x + camera.cx)/self.cell_pixels/self.chunk_size)
end
function Universe:yToChunkRowCeil(y, camera)
	return math.ceil((y + camera.cy)/self.cell_pixels/self.chunk_size)
end

function Universe:get_chunk_range_rows(camera)
	local startRow = self:clampChunkRow(self:yToChunkRow(camera.top, camera))
	local endRow = self:clampChunkRow(self:yToChunkRowCeil(camera.bottom, camera))
	return startRow, endRow
end
function Universe:get_chunk_range_cols(camera)
	local startCol = self:clampChunkCol(self:xToChunkCol(camera.left, camera))
	local endCol = self:clampChunkCol(self:xToChunkColCeil(camera.right, camera))
	return startCol, endCol
end