# ダンジョンを表すクラス
require 'msgpack'

module DarkHall


	module Dungeon
		module Area
			WALL = 0
			ROOM = 1
			ROUTE = 2
			DOOR = 3
			UP_STAIR = 10
			DOWN_STAIR = 11
			EXIT_DOOR = 12
		end
		
		
		class Cell
			attr_accessor :section_id, :region_id, :enemy_appearance, :treasure_type
		end
		
		
		class Section
			attr_accessor :area, :largeness, :enemy_appearance, :treasure_type, :region_id
			
			def initialize(area = nil)
				@area = area # デバッグ表示と、敵が出現する場所かどうかのチェック用
				@largeness = 0
				@enemy_appearance = EA::MEDIUM
				@treasure_type = TRT::ITEM
				@region_id = nil
			end
			
			def enemy_appearance_percentage
				EA_TABLE[@enemy_appearance || EA::NOTHING]
			end
		end
		


		class Floor
			attr_accessor :name, :bgm
			attr_accessor :area_data, :cells, :sections, :region_treasure_levels, :enemy_appearance_table
			attr_accessor :width, :height
			attr_accessor :found_area
			attr_accessor :tereport_points
			attr_accessor :special_detect_caption, :special_action_data
			
			attr_accessor :lock_data, :lock_types
			
			attr_reader :one_way_door_data, :region_candidate_ids
			
			def self.load_form_model(form_model)
				self.new.load_form_model(form_model)
			end
			
			def initialize
				@name = "????"
				@bgm = :Dungeon1
				@width = 60
				@height = 60
				@region_treasure_levels = Hash.new(1)
				
				initialize_form
				
				
				@sections = []
				
				@tereport_points = []
				@target_region_id = nil
				
				@special_detect_caption = nil
				@special_action_data = []
				
				@lock_types = {}
				
				@enemy_appearance_table = {}
				@region_candidate_ids = []
			end
			
			def initialize_form
				@area_data = Array.new(@width * 2 + 1)
				@area_data.each_index{|i| @area_data[i] = Array.new(@height * 2 + 1)}
				@area_data.each{|x| x.fill(Area::WALL)}
				@lock_data = Array.new(@width * 2 + 1)
				@lock_data.each_index{|i| @lock_data[i] = Array.new(@height * 2 + 1)}
				@stair_data = Array.new(@width * 2 + 1)
				@stair_data.each_index{|i| @stair_data[i] = Array.new(@height * 2 + 1)}
				@one_way_door_data = Array.new(@width * 2 + 1)
				@one_way_door_data.each_index{|i| @one_way_door_data[i] = Array.new(@height * 2 + 1)}

				@cells = Array.new(@width)
				@cells.each_index{|i| @cells[i] = Array.new(@height)}
				@cells.each_index do |x|
					@cells[x].each_index do |y|
						@cells[x][y] = Cell.new
					end
				end

			end
			
			def [](half_x, half_y)
				@area_data[half_x][half_y]
			end
			
			def []=(half_x, half_y, element)
				@area_data[half_x][half_y] = element
			end
			
			def flooring(x, y)
				return self[x * 2 + 1, y * 2 + 1]
			end
			
			def set_flooring(x, y, area)
				self[x * 2 + 1, y * 2 + 1] = area
			end
			
			
			def wall(x, y, direction)
				self[x_to_half(x, direction), y_to_half(y, direction)]
			end
			
			def set_wall(x, y, direction, area = Area::WALL)
				self[x_to_half(x, direction), y_to_half(y, direction)] = area
		
				return self
			end
			
			# 開いている方向の数
			def open_dir_number(x, y)
				re = 0
				re += 1 unless wall(x, y, DIR_N) == Area::WALL
				re += 1 unless wall(x, y, DIR_E) == Area::WALL
				re += 1 unless wall(x, y, DIR_S) == Area::WALL
				re += 1 unless wall(x, y, DIR_W) == Area::WALL
				
				re
			end
			
			def set_door(x, y, direction)
				set_wall(x, y, direction, Area::DOOR)
			end
			
			def set_one_way_door(x, y, dir)
				set_wall(x, y, dir, Area::DOOR)
				@one_way_door_data[x_to_half(x, dir)][y_to_half(y, dir)] = dir
			end
			
			
			# 扉がその方向に通行可能かどうか
			def passable_door?(x, y, dir)
				d = @one_way_door_data[x_to_half(x, dir)][y_to_half(y, dir)]
				
				return(d.nil? or d == dir) 
			end


			
			def set_locked_door(x, y, direction, lock_id, lock_type)
				set_door(x, y, direction)
				@lock_types[lock_id] = lock_type
				@lock_data[x_to_half(x, direction)][y_to_half(y, direction)] = lock_id
				return self
			end

			def get_lock_id(x, y, direction)
				@lock_data[x_to_half(x, direction)][y_to_half(y, direction)]
			end

			
			def set_stair(x, y, direction, to_floor_id, to_x, to_y, to_direction)
				set_door(x, y, direction)
				@stair_data[x_to_half(x, direction)][y_to_half(y, direction)] = [to_floor_id, to_x, to_y, to_direction]
				
				return self
			end

			def get_stair_data(x, y, direction)
				@stair_data[x_to_half(x, direction)][y_to_half(y, direction)]
			end


			
			
			def in_region(id)
				@target_region_id = id
				@region_candidate_ids << id
				yield
				@target_region_id = nil
			end
			
			def get_flooring_data
				data = []
				@width.each_index do |x|
					data[x] = []
					@height.each_index do |y|
						data[x][y] = @area_data[x * 2 + 1][y * 2 + 1]
					end
				end
				
				return data
			end
			
			
			
			def set_room(left, top, width, height, treasure_type = TRT::ITEM, enemy_appearance = EA::MEDIUM, region_id = @target_region_id)
				set_flooring_area(Area::ROOM, left, top, width, height, treasure_type, enemy_appearance, region_id)
				return self
			end
			
			def pset_room(left, top, right, bottom, treasure_type = TRT::ITEM, enemy_appearance = EA::MEDIUM, region_id = @target_region_id)
				set_flooring_area(Area::ROOM, left, top, right - left + 1, bottom - top + 1, treasure_type, enemy_appearance, region_id)
				return self
			end

			
			def set_route(left, top, width, height, region_id = @target_region_id)
				set_flooring_area(Area::ROUTE, left, top, width, height, nil, EA::NOTHING, region_id)
				return self
			end
			
			def pset_route(x1, y1, x2 = nil, y2 = nil, region_id = @target_region_id)
				
				# 座標が一つしか指定されなかった場合、前回の終点座標を今回の始点とする
				unless x2 then
					x2 = x1
					x1 = @last_x2
				end
				
				unless y2 then
					y2 = y1
					y1 = @last_y2
				end

				@last_x2 = x2
				@last_y2 = y2
				
				
				if x1 < x2 then
					left = x1; right = x2
				else
					left = x2; right = x1
				end
				
				if y1 < y2 then
					top = y1; bottom = y2
				else
					top = y2; bottom = y1
				end
				

				set_flooring_area(Area::ROUTE, left, top, right - left + 1, bottom - top + 1, nil, EA::NOTHING, region_id)
				return self
			end
			
			def set_tereport_point(left, top)
				@tereport_points << [left, top]
			end
			
			def set_wall_area(left, top, width, height)
				(left...(left + width)).each do |x|
					half_top = top * 2
					half_height = height * 2 + 1
					@area_data[x * 2].fill(Area::WALL, half_top, half_height)
					@area_data[x * 2 + 1].fill(Area::WALL, half_top, half_height)
					@area_data[x * 2 + 2].fill(Area::WALL, half_top, half_height)
					
				end
				return self
			end
			
			def pset_wall_area(left, top, right, bottom)
				set_wall_area(left, top, right - left + 1, bottom - top + 1)
			end

			# 床箇所のみを指定タイプのエリアで埋める
			def set_flooring_area(area_type, left, top, width, height, treasure_type = TRT::ITEM, enemy_appearance = EA::NOTHING, region_id = nil)
				(left...(left + width)).each do |x|
					half_top = top * 2 + 1
					half_height = height * 2 - 1
					@area_data[x * 2 + 1].fill(area_type, half_top, half_height)
					
					# リージョン＆敵出現率の設定
					@cells[x][top...(top + height)].each do |cell|
						cell.region_id = region_id
						cell.treasure_type = treasure_type
						cell.enemy_appearance = enemy_appearance
					end
					
					# 最後のループならスキップ
					next if x == left + width - 1
					
					@area_data[x * 2 + 2].fill(area_type, half_top, half_height)
					
				end
				return self
			end
			
			def pset_flooring_area(area_type, left, top, right, bottom, treasure_type = TRT::ITEM, enemy_appearance = EA::NOTHING, region_id = nil)
				set_flooring_area(area_type, left, top, right - left + 1, bottom - top + 1, treasure_type, enemy_appearance, region_id)
			end

			
			
			# 部屋・通路の区画分けの更新
			def update_section_data
				@sections.clear
				
				# ひとまず全部屋と全通路のsectionを-1に
				@cells.each_index do |x|
					@cells[x].each_index do |y|
						#if self.flooring(x, y) == Area::ROOM || self.flooring(x, y) == Area::ROUTE then
						#	@cells[x][y].section_id = -1
						#else
							@cells[x][y].section_id = nil
						#end
					end
				end
				
				# 番号分け
				id = 0
				@cells.each_index do |x|
					@cells[x].each_index do |y|
						if @cells[x][y].section_id.nil? and (flooring(x, y) == Area::ROOM or flooring(x, y) == Area::ROUTE) then
							@sections[id] = Section.new(flooring(x, y))
							section_fill_sequence(x, y, id)
							@sections[id].region_id = @cells[x][y].region_id
							@sections[id].enemy_appearance = @cells[x][y].enemy_appearance
							@sections[id].treasure_type = @cells[x][y].treasure_type
							
							id += 1
						end
					end
				end
				
			end
			
			
			
			def section_fill_sequence(x, y, id)
				@cells[x][y].section_id = id
				@sections[id].largeness += 1
				
				area_type = flooring(x, y)
				
				# 四方をそれぞれチェック
				# その方向が空いていて、今の地点と同じ種類のエリアで
				# なおかつSectionIDが違うなら、その方向に塗りつぶしを進める
				if regular_position?(x, y - 1) and (wall(x, y, DIR_N) == area_type) &&
				(@cells[x][y - 1].section_id.nil? or @cells[x][y - 1].section_id != id) then
					section_fill_sequence(x, y - 1, id)
				end
				
				if regular_position?(x + 1, y) && (wall(x, y, DIR_E) == area_type) &&
				(@cells[x + 1][y].section_id.nil? or @cells[x + 1][y].section_id != id) then
					section_fill_sequence(x + 1, y, id)
				end
				
				if regular_position?(x, y + 1) && (wall(x, y, DIR_S) == area_type) &&
				(@cells[x][y + 1].section_id.nil? or @cells[x][y + 1].section_id != id) then
					section_fill_sequence(x, y + 1, id)
				end
				
				if regular_position?(x - 1, y) && (wall(x, y, DIR_W) == area_type) &&
				(@cells[x - 1][y].section_id.nil? or @cells[x - 1][y].section_id != id) then
					section_fill_sequence(x - 1, y, id)
				end

			end
			
			def regular_position?(x, y)
				return((0...@width).include?(x) && (0...@height).include?(y))
			end
			
			
			
			# パーティーの視界に入る部分のエリア情報を得る
			def get_viewable_area_data(party)
				data = []
				(VIEWABLE_FRONT_RANGE * 2 + 1).times do |i|
					data[i] = {}
				end
				
				
				data.each_index do |half_range|
					((VIEWABLE_SIDE_RANGE * 2 * -1)..(VIEWABLE_SIDE_RANGE * 2)).each do |half_pos|
						data[half_range][half_pos] = get_area_data(party, half_range, half_pos)
					end
				end
				
				return data
			end
			
			def get_area_data(party, half_range, half_pos)
				half_x = party.x * 2 + 1
				half_y = party.y * 2 + 1
				case party.direction
				when DIR_N
					@area_data[half_x + half_pos][half_y - half_range]
				when DIR_E
					@area_data[half_x + half_range][half_y + half_pos]
				when DIR_S
					@area_data[half_x - half_pos][half_y + half_range]
				when DIR_W
					@area_data[half_x - half_range][half_y - half_pos]
				end
				
			end
			
			# マップ埋め
			def find(x, y)
				found_area = GS.found_area[GS.party.floor_id]
				key = "#{x},#{y}"
				

				case flooring(x, y)
				when Area::ROOM
					# 部屋に踏み込んだ場合は全部埋める
					section_id = @cells[x][y].section_id
					@cells.each_index do |cx|
						@cells[cx].each_index do |cy|
							if @cells[cx][cy].section_id == section_id
								found_area["#{cx},#{cy}"] = true
							end
						end
					end
				else
					found_area[key] = true
				end
			end
			
			def appearing_enemy(rate, *enemy_ids)
				enemy_ids.map!{|x| x.to_s}
				raise "no target region" unless @target_region_id
				@enemy_appearance_table[@target_region_id] ||= []
				@enemy_appearance_table[@target_region_id] += ([enemy_ids] * rate)
			end
			
			def treasure_level(level)
				raise "no target region" unless @target_region_id
				@region_treasure_levels[@target_region_id] = level
			end
			
			def pick_enemy_list(region)
				if @enemy_appearance_table[region] then
					return random_pick(@enemy_appearance_table[region])
				else
					return nil
				end
			end
			
			def enemy_appear_on?(region)
				@enemy_appearance_table.include?(region)
			end
			
			
			
			
			
			def on_enter_region(party, old_region)
			end
			
			def on_walk(party)
			end
			
			def on_move(party)
			end
			
			def on_detect(party)
			end
			
			def on_special_action(party, action_id)
			end
			
			def on_door(party)
				true
			end
			
			
			
			
			
			
			
			def load_form_model(model)
				@width, @height = model['Size']
				initialize_form
				@area_data = model['Area-Data']
				@tereport_points = model['Tereport-Points']
				@sections = model['Sections'].map do |ea, trt|
					if ea or trt then
						re = Section.new
						if ea and not ea.empty? then
							re.enemy_appearance = ea.to_sym
						else
							re.enemy_appearance = EA::NOTHING
						end
						
						re.treasure_type = trt if trt
						
						re
					else
						nil
					end
				end
				@cells = model['Cells'].map do |row|
					row.map do |x|
						c = Cell.new
						c.region_id = (x[1] ? x[1].to_sym : nil)
						c.section_id = x[0]
						if c.section_id then
							sect = @sections[c.section_id]
							sect.region_id = c.region_id
							c.enemy_appearance = sect.enemy_appearance
							c.treasure_type = sect.treasure_type
						end
						
						c
					end
				end

			end
			
			# MessagePack向けの構造に変換
			def to_form_model
				re = {}
				re['Size'] = [@width, @height]
				re['Area-Data'] = @area_data
				re['Sections'] = @sections.map{|x| (x ? [x.enemy_appearance.to_s, (x.treasure_type ? x.treasure_type : nil)] : nil)}
				re['Tereport-Points'] = @tereport_points
				re['Cells'] = @cells.map{|row| row.map{|c| [c.section_id, (c.region_id ? c.region_id.to_s : nil)]}}
				
				re

			end

			


			private
			
			def show_special_action(id, caption, shortcut)
				@special_action_data << [id, caption, shortcut]
			end
			
			def show_special_detect(caption)
				@special_detect_caption = caption
			end
			
			def reload_dungeon
				Dungeon.load_floors
			end
			
			def save(scene = "")
				Game.save(FQ_BIG_CHANGE, scene)
			end
			
			def shoot(floor_id, x, y, dir = nil)
				GS.party.x = x
				GS.party.y = y
				GS.party.direction = dir if dir
				old_floor = GS.party.floor
				GS.party.floor_id = floor_id if floor_id
				save('シュート作動')
		
				Game.working(_('シュート！')){
					SE.trap_switch
					Game.wait(15)
					
					SE.open_box
					DUNGEON_WINDOW.hide
					$phase.hide_sub_windows

					to_bgm = GS.party.floor.bgm
					unless old_floor.bgm == to_bgm then
						BGM.fade_out
						BGM.load(:Camp, to_bgm, :Battle, :NPC1, :NPC2)
					end

					Game.wait(30)

							

					
					$phase.show_sub_windows

					DUNGEON_WINDOW.update
					DUNGEON_WINDOW.show
					SE.hit
					Game.shake_windows($windows)

					BGM.play(to_bgm)

				}
			end
			
			def fountain
				message(_("%{party}は泉を見つけた\n泉には白色の水が満ちている").evaluate(:party => GS.party.name))
				if Game.ask(_('泉の水を飲む？')) then
					index = Game.member_select(_('だれが？'), :alive_member_only => true)
					if index then
						drinker = GS.party.members[index]
						
						drinker.recover_hp(999)
						case rand(100)
						when 80...86
							drinker.add_state(PoisonState.new(1))
							msg = _("%{drinker}のHPが全回復した\nしかし、同時に%{drinker}は毒に侵されてしまった")
						when 86...92
							drinker.add_state(CurseState.new(3))
							msg = _("%{drinker}のHPが全回復した\nしかし、同時に%{drinker}は呪われてしまった")
						when 92...96
							drinker.life_damage(4 + rand(5))
							msg = _("%{drinker}のHPが全回復した\nしかし、同時に%{drinker}の最大HPが減少してしまった")
						when 96...100
							drinker.lose_exp_rate(0.3)
							msg = _("%{drinker}のHPが全回復した\nしかしなぜか、頭から記憶が抜け落ちてしまったような気がする……")
						else
							msg = _('%{drinker}のHPが全回復した')
						end
						
						Game.save(FQ_BIG_CHANGE, "#{drinker.name}が泉の水を飲んだ")
						
						drinker.update_last_parameters
						PARTY_WINDOW.update
						message(msg.evaluate(:drinker => drinker.name))
		
					end
				end

			end
			
			def message(msg)
				Game.message(msg)
			end
			
			def dungeon_message(msg)
				Game.dungeon_message(msg)
			end
			
			def x_to_half(x, dir = nil)
				case dir
				when DIR_N, DIR_S, nil
					x * 2 + 1
				when DIR_E
					x * 2 + 1 + 1
				when DIR_W
					x * 2 + 1 - 1
				end
			end

			def y_to_half(y, dir = nil)
				case dir
				when DIR_E, DIR_W, nil
					y * 2 + 1
				when DIR_N
					y * 2 + 1 - 1
				when DIR_S
					y * 2 + 1 + 1
				end
			end




			
			
		end
		
	end
	include Dungeon
	


	FLOORS = {}
	
	module Dungeon
		def self.load_floors
			FLOORS.clear
			
			dir_path = Pathname.new('./data/dungeon')
			children = dir_path.children
			
			# 単一フォームファイル読み込み
			path = dir_path + 'dungeon_form.mpac'
			if path.exist? then
				open(path, 'rb'){|f|
					
					MessagePack.unpack(f.read).each_pair do |floor_id, data|
						floor_id = floor_id.to_s # 旧仕様ではInteger
						FLOORS[floor_id] = Floor.new
						FLOORS[floor_id].load_form_model(data)
					end
				}
			end			
			
			
			rb_pattern = /^(\d+)f\.rb$/
			dat_pattern = /^(\d+)f\.dat$/
			random_rb_pattern = /^(random)\.rb$/
			

			
			dir_path.children.each do |path|
				case path.basename.to_s.downcase
				when rb_pattern, random_rb_pattern
					src = path.read
				when dat_pattern
					src = Util.decrypt(path.read)
				else
					next
				end
				
				floor_id = $1
				FLOORS[floor_id] ||= Floor.new
				FLOORS[floor_id].instance_eval(src, path.basename.to_s)
				FLOORS[floor_id].update_section_data
				LOGGER.puts "ダンジョンマップファイル #{path.to_s} ロード完了"
			end
			
			
			GS.setup_by_floors

		end # def self.load_floors
		
		def save_floors
			data = {}
			FLOORS.each_pair do |floor_id, floor|
				next if floor_id == 'random'
				data[floor_id] = floor.to_form_model if floor
			end
			
			open('data/dungeon/dungeon_form.mpac', 'wb'){|f|
				f.write(data.to_msgpack)
			}
		end
		
	end # module Dungeon
	
	
	
end

	
	
	
	
