# セーブデータ読み込み後の状態チェック
require 'rarg'

require 'darkhall/savedata'
require 'darkhall/switch'
require 'darkhall/party'
require 'darkhall/game_config'
require 'darkhall/rule'

module DarkHall
	class GameStateError < Error
		attr_accessor :target
	end

	class GameState
		OVERWRITTEN_KEYS = %w(
			format_version
			starting_game_version_code
		
			playing_time
			week
			game_config
			switch
			rule
			latest_floor_id

			parties
			current_party_id
			members
			bar_member_ids
			reviving_member_ids
			revived_member_ids

			room_troops
			room_treasure_boxes
			found_area
			unlocked_door_data

			merchant_products
			shop_stocks
			
			got_spell_ids

			found_bgms
		)
		
		
		MERGED_KEYS = %w(
			player_profiles
			player_records
			found_model_switches
		)

		(OVERWRITTEN_KEYS + MERGED_KEYS).each do |key|
			attr_accessor key
		end
		
		# ロスト・削除されていないメンバー全員
		def existing_members
			members.compact.find_all{|x| x.existing?}
		end
		
		def bar_members
			@bar_member_ids.map{|x| @members[x]}
		end
		
		def change_to_new_party
			@parties << Party.new
			@current_party_id = @parties.size - 1
			
			return current_party
		end
		
		def current_party
			@parties[@current_party_id]
		end
		
		def dungeon_parties
			@parties.find_all{|x| x.floor}.freeze
		end
		
		def dungeon_members
			re = []
			dungeon_parties.each do |party|
				re.concat party.members
			end
		end
		
		alias party current_party
		alias all_parties parties

		def move_party_to_first(id)
			old_party = current_party
			selected = GS.parties.slice!(id)
			GS.parties.unshift(selected)
			@current_party_id = @parties.index(old_party)
		end
		
		def found_model?(type, id)
			@found_model_switches[type.to_s][id.to_s]
		end
		
		def found_item?(id)
			found_model?('item', id)
		end
		
		def found_enemy?(id)
			found_model?('enemy', id)
		end
		
		def found_trick?(id)
			found_model?('trick', id)
		end
		
		def found_bgm?(key)
			@found_bgms[key.to_s]

		end

	
		def clear
			@format_version = 'DarkHall/1.0'
			@starting_game_version_code = nil
		
			@playing_time = 0
			@week = 1
			@latest_floor_id = '1'
			@game_config = GameConfig.new
			@switch = Switch.new
			@rule = NormalRule.new
			
			@parties = [Party.new]
			@current_party_id = 0
			@members = []
			@bar_member_ids = []
			@reviving_member_ids = []
			@revived_member_ids = []

			@room_troops = {}
			@room_treasure_boxes = {}	
			@found_area = {}
			@unlocked_door_data = {}

			@merchant_products = []
			@shop_stocks = {
				'Lamellar' => 1,
				'RingOfBlackCrystal' => 1,
			}
			@got_spell_ids = []
			
			@player_profiles = {}
			PlayerProfileType::LIST.each do |x|
				@player_profiles[x] = 0
			end
			@player_records = {}
			@found_model_switches = {'item' => {}, 'trick' => {}, 'enemy' => {}}
			@found_bgms = {}
			
			setup
		end
		
		# 旧仕様からのデータコンバートなど
		def setup
			@members.compact.each{|x| x.setup_on_load}
			
			# パーティー仕様変更
			if @party then
				GS.parties << @party
				@current_party_id = GS.parties.size - 1
				@party = nil
			end
			
			if @dungeon_parties then
				GS.parties += @dungeon_parties
			end
			
			# 空パーティーを削除
			old_party = GS.current_party
			old_party_size = @parties.size
			@parties.delete_if{|x| x.member_ids.empty?}
			if @parties.size < old_party_size then
				if (id = @parties.index(old_party)) then
					@current_party_id = id
				else
					change_to_new_party
				end
			end

			# Array => Hash
			names = [:@room_troops, :@room_treasure_boxes, :@found_area, :@unlocked_door_data]
			names.each do |name|
				table = instance_variable_get(name)
				unless table.respond_to?(:each_pair) then
					new = {}
					table.each_with_index do |content, id|
						next unless content
						new[id] = content
					end
					
					instance_variable_set(name, new)
				end
			end
			
			# 旧仕様からのコンバート
			@found_area.each_value do |table|
				if table.kind_of?(Array) then
					new = {}
					table.each_index do |x|
						table.each_index do |y|
							if table[x][y] == 1 then
								new["#{x},#{y}"] = true
							end
						end
					end
					@found_area = new
				end
			end
			
			@unlocked_door_data.each_value do |table|
				if table.kind_of?(Array) then
					new = {}
					table.each do |lock_id|
						new[lock_id] = true
					end
					@unlocked_door_data = new
				end
			end
			
			@got_spell_ids ||= []
			@found_bgms ||= {}
			@player_records[@latest_floor_id] ||= {}
			
			# バグで経験値がnilになってしまったとき対策
			@members.each do |member|
				if member.exp.nil? then
					if member.level_max? then
						member.exp = EXP_TABLE[member.level]
					else
						member.exp = EXP_TABLE[member.level + 1]
					end
				end
			end
			
			
			setup_by_floors
		end
		
		def setup_by_floors
			FLOORS.each_key do |id|
				@room_troops[id] ||= {}
				@room_treasure_boxes[id] ||= {}
				@found_area[id] ||= {}

				@unlocked_door_data[id] ||= {}
			end
		end
		
		def ready
			clear
			@starting_game_version_code = VERSION_CODE
			Game.set_room_events
			Game.set_merchant_products
		end

		
		def check_oneself
			gs = self
			context = GameStateCheckContext.new
			context.run do
				check(gs.starting_game_version_code, Proc.new{|x| x.kind_of?(String) or x.nil?})
				check(gs.playing_time, Integer, Proc.new{|x| x >= 0})
				check(gs.week, Integer, Proc.new{|x| x >= 0})
				check(gs.game_config, GameConfig)
				check(gs.switch, Switch)
				check(gs.rule, Rule)
				check(gs.latest_floor_id, String)
				
				check(gs.current_party_id, Integer)
				check(gs.parties, Array)
				check(gs.current_party, Party)
				
				gs.parties.each do |party|
					check(party, Party)
					check(party.members, Array, :each => [Member])
					if party.floor_id then
						check(party.floor_id, String)
					end
				end
				
				check(gs.members, Array)
				
				gs.members.compact.each do |member|
					check(member, Member)
					check(member.level, Integer, 1..LEVEL_MAX)
					check(member.exp, Integer, 0..99999999)
					check(member.hp, Integer)
					check(member.hp_max_base, Integer)
					check(member.mp, Hash)
					check(member.spell_learning, Hash)
					DB.spells.each do |spell|
						check(member.mp[spell.id], Integer)
						check(member.spell_learning[spell.id], Integer, 0..4)
						check(member.get_spell_mastery(spell.id), Float, Proc.new{|x| x >= 0.0})
					end
					check(member.profiles, Hash)
					check(member.inherited_member_ids, Array)
					check(member.regenerated, Integer)
					check(member.senility, Integer)
					if member.life_end_week then
						check(member.life_end_week, Integer)
					end
					if (rem = member.remaining_week_before_revived) then
						check(rem, 0..3)
					end
					check(member.last_select_ids, Hash, :each_key => [Symbol])
				end
				
				check(gs.bar_member_ids, Array)
				check(gs.reviving_member_ids, Array)
				check(gs.revived_member_ids, Array)
				
				ids = gs.bar_member_ids + gs.reviving_member_ids + gs.revived_member_ids
				ids.each do |id|
					check(gs.members[id], Member)
				end
				
				check(gs.room_troops, Hash)
				check(gs.room_treasure_boxes, Hash)
				check(gs.found_area, Hash)
				check(gs.unlocked_door_data, Hash)
				
				FLOORS.each_pair do |floor_id, floor|
					check(gs.room_troops[floor_id], Hash, :each_key => [Integer])
					gs.room_troops[floor_id].values.compact.each do |troop|
						check(troop, Troop)
					end
					check(gs.room_treasure_boxes[floor_id], Hash, :each_key => [Integer])
					gs.room_treasure_boxes[floor_id].values.compact.each do |box|
						check(box, TreasureBox)
					end
					check(gs.found_area[floor_id], Hash, :each_key => [String], :each_value => [TrueClass])
					check(gs.unlocked_door_data[floor_id], Hash, :each_key => [String], :each_value => [TrueClass])
				end
				
				check(gs.merchant_products, Array, :each => [Item])
				check(gs.shop_stocks, Hash, :each_key => [String], :each_value => [Integer, 0..999])

				check(gs.got_spell_ids, Array, :each => [String])

				check(gs.found_bgms, Hash, :each_key => [String], :each_value => [TrueClass])
				
				check(gs.player_profiles, Hash, :each_key => [String], :each_value => [Integer])
				check(gs.player_records, Hash, :each_key => [String], :each_value => [Hash])
				check(gs.found_model_switches, Hash, :each_value => [Hash])

			end
		end
		
		def load_from_hash(objects)
			clear
			objects.each_pair do |key, obj|
				if key.in?(MERGED_KEYS) then
					cur = instance_variable_get("@#{key}")
					instance_variable_set("@#{key}", cur.merge(obj))
				else
					instance_variable_set("@#{key}", obj)
				end
			end
			
		
			self.setup
			
		end
		
		
		def to_hash
			re = {}
			(OVERWRITTEN_KEYS + MERGED_KEYS).each do |key|
				re[key] = instance_variable_get("@#{key}")
			end
			
			re
		end

	end

	class GameStateCheckContext
		def initialize
			@target_stack = []
		end
		
		def run(&proc)
			instance_eval(&proc)
		end
		
		def check(target, *expects)
			@target_stack << target
			yield if block_given?
			be(*expects)
			@target_stack.pop
		end
		
		def assert
			unless yield then
				raise GameStateError, "assertion should return true, but false"
			end
		end
		
		def be(*expects)
			target = @target_stack.last
			expects.each do |exp|
				case exp
				when Proc
					unless exp.call(target) then
						err = GameStateError.new("assertion failed")
						err.target = target
						raise err
					end
				when Range
					case target
					when Array
						unless exp.include?(target.size) then
							err = GameStateError.new("size should be in #{exp}, but #{target.size}")
							err.target = target
							raise err
						end
					else
						unless exp.include?(target) then
							err = GameStateError.new("should be in #{exp}, but #{target.inspect}")
							err.target = target
							raise err
						end
					end
					
				when Class
					unless target.kind_of?(exp) then
						err = GameStateError.new("should be #{exp}, but #{target.class} (#{target.inspect})")
						err.target = target
						raise err
					end
				when Hash
					re = RArg.parse(exp) do
						default_arg :each, nil
						default_arg :each_key, nil
						default_arg :each_value, nil
					end
					
					if re[:each] then
						target.each do |item|
							check(item, *re[:each])
						end
					end

					if re[:each_key] then
						target.each_key do |key|
							check(key, *re[:each_key])
						end
					end
					
					if re[:each_value] then
						target.each_value do |key|
							check(key, *re[:each_value])
						end
					end

				end
			end
		end
		
		
	end
	
	GS = GameState.new
end

