# ###################################################
# Copyright (C) 2008-2017 The Unknown Horizons Team
# team@unknown-horizons.org
# This file is part of Unknown Horizons.
#
# Unknown Horizons is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 2 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the
# Free Software Foundation, Inc.,
# 51 Franklin St, Fifth Floor, Boston, MA  02110-1301  USA
# ###################################################

import logging
from collections import defaultdict

from horizons.ai.aiplayer.behavior import BehaviorManager
from horizons.ai.aiplayer.behavior.profile import BehaviorProfileManager
from horizons.ai.aiplayer.combat.combatmanager import CombatManager
from horizons.ai.aiplayer.combat.unitmanager import UnitManager
from horizons.ai.aiplayer.strategy.strategymanager import StrategyManager
from horizons.ai.generic import GenericAI
from horizons.component.selectablecomponent import SelectableComponent
from horizons.component.stancecomponent import NoneStance
from horizons.ext.enum import Enum
from horizons.messaging import MineEmpty, NewDisaster, SettlementRangeChanged
from horizons.scheduler import Scheduler
from horizons.util.python.callback import Callback
from horizons.util.worldobject import WorldObject
from horizons.world.units.fightingship import FightingShip
from horizons.world.units.weaponholder import MovingWeaponHolder

from .basicbuilder import BasicBuilder
# all subclasses of AbstractBuilding have to be imported here to register the available buildings
from .building import AbstractBuilding
from .building.boatbuilder import AbstractBoatBuilder
from .building.brickyard import AbstractBrickyard
from .building.charcoalburner import AbstractCharcoalBurner
from .building.claydeposit import AbstractClayDeposit
from .building.claypit import AbstractClayPit
from .building.distillery import AbstractDistillery
from .building.doctor import AbstractDoctor
from .building.farm import AbstractFarm, FarmEvaluator
from .building.field import AbstractField
from .building.firestation import AbstractFireStation
from .building.fishdeposit import AbstractFishDeposit
from .building.fisher import AbstractFisher
from .building.irondeposit import AbstractIronDeposit
from .building.ironmine import AbstractIronMine
from .building.lumberjack import AbstractLumberjack
from .building.saltponds import AbstractSaltPonds
from .building.signalfire import AbstractSignalFire
from .building.smeltery import AbstractSmeltery
from .building.stonedeposit import AbstractStoneDeposit
from .building.stonemason import AbstractStonemason
from .building.stonepit import AbstractStonePit
from .building.tobacconist import AbstractTobacconist
from .building.toolmaker import AbstractToolmaker
from .building.tree import AbstractTree
from .building.villagebuilding import AbstractVillageBuilding
from .building.weaver import AbstractWeaver
from .constants import GOAL_RESULT
from .goal.donothing import DoNothingGoal
from .goal.settlementgoal import SettlementGoal
from .internationaltrademanager import InternationalTradeManager
from .landmanager import LandManager
from .mission.domestictrade import DomesticTrade
from .mission.foundsettlement import FoundSettlement
from .mission.internationaltrade import InternationalTrade
from .mission.preparefoundationship import PrepareFoundationShip
from .mission.specialdomestictrade import SpecialDomesticTrade
from .personalitymanager import PersonalityManager
from .settlementfounder import SettlementFounder
from .settlementmanager import SettlementManager
from .specialdomestictrademanager import SpecialDomesticTradeManager
from .unitbuilder import UnitBuilder


class AIPlayer(GenericAI):
	"""This is the AI that builds settlements."""

	shipStates = Enum.get_extended(GenericAI.shipStates, 'on_a_mission',)

	log = logging.getLogger("ai.aiplayer")
	tick_interval = 32
	tick_long_interval = 128

	def __init__(self, session, id, name, color, clientid, difficulty_level, **kwargs):
		super().__init__(session, id, name, color, clientid, difficulty_level, **kwargs)
		self.need_more_ships = False
		self.need_more_combat_ships = True
		self.need_feeder_island = False
		self.personality_manager = PersonalityManager(self)
		self.__init()
		Scheduler().add_new_object(Callback(self.finish_init), self, run_in=0)

	def start(self):
		""" Start the AI tick process. Try to space out their ticks evenly. """
		ai_players = 0
		position = None
		for player in self.world.players:
			if isinstance(player, AIPlayer):
				if player is self:
					position = ai_players
				ai_players += 1
		run_in = self.tick_interval * position // ai_players + 1
		Scheduler().add_new_object(Callback(self.tick), self, run_in=run_in)
		run_in = self.tick_long_interval * position // ai_players + 1
		Scheduler().add_new_object(Callback(self.tick_long), self, run_in=run_in)

	def finish_init(self):
		# initialize the things that couldn't be initialized before because of the loading order
		self.refresh_ships()
		self.start()

	def refresh_ships(self):
		""" called when a new ship is added to the fleet """
		for ship in self.world.ships:
			if ship.owner == self and ship.has_component(SelectableComponent) and ship not in self.ships:
				self.log.info('%s Added %s to the fleet', self, ship)
				self.ships[ship] = self.shipStates.idle
				if isinstance(ship, MovingWeaponHolder):
					ship.stance = NoneStance
				if isinstance(ship, FightingShip):
					self.combat_manager.add_new_unit(ship)
		self.need_more_ships = False
		self.need_more_combat_ships = False

	def __init(self):
		self._enabled = True  # whether this player is enabled (currently disabled at the end of the game)
		self.world = self.session.world
		self.islands = {}
		self.settlement_managers = []
		self._settlement_manager_by_settlement_id = {}
		self.missions = set()
		self.fishers = []
		self.settlement_founder = SettlementFounder(self)
		self.unit_builder = UnitBuilder(self)
		self.unit_manager = UnitManager(self)
		self.combat_manager = CombatManager(self)
		self.strategy_manager = StrategyManager(self)
		self.behavior_manager = BehaviorManager(self)
		self.settlement_expansions = []  # [(coords, settlement)]
		self.goals = [DoNothingGoal(self)]
		self.special_domestic_trade_manager = SpecialDomesticTradeManager(self)
		self.international_trade_manager = InternationalTradeManager(self)
		SettlementRangeChanged.subscribe(self._on_settlement_range_changed)
		NewDisaster.subscribe(self.notify_new_disaster)
		MineEmpty.subscribe(self.notify_mine_empty)

	def get_random_profile(self, token):
		return BehaviorProfileManager.get_random_player_profile(self, token)

	def start_mission(self, mission):
		self.ships[mission.ship] = self.shipStates.on_a_mission
		self.missions.add(mission)
		mission.start()

	def report_success(self, mission, msg):
		if not self._enabled:
			return

		self.missions.remove(mission)
		if mission.ship and mission.ship in self.ships:
			self.ships[mission.ship] = self.shipStates.idle
		if isinstance(mission, FoundSettlement):
			settlement_manager = SettlementManager(self, mission.land_manager)
			self.settlement_managers.append(settlement_manager)
			self._settlement_manager_by_settlement_id[settlement_manager.settlement.worldid] = settlement_manager
			self.add_building(settlement_manager.settlement.warehouse)
			if settlement_manager.feeder_island:
				self.need_feeder_island = False
		elif isinstance(mission, PrepareFoundationShip):
			self.settlement_founder.tick()

	def report_failure(self, mission, msg):
		if not self._enabled:
			return

		self.missions.remove(mission)
		if mission.ship and mission.ship in self.ships:
			self.ships[mission.ship] = self.shipStates.idle
		if isinstance(mission, FoundSettlement):
			del self.islands[mission.land_manager.island.worldid]

	def save(self, db):
		super().save(db)

		# save the player
		db("UPDATE player SET client_id = 'AIPlayer' WHERE rowid = ?", self.worldid)

		current_callback = Callback(self.tick)
		calls = Scheduler().get_classinst_calls(self, current_callback)
		assert len(calls) == 1, "got {0!s} calls for saving {1!s}: {2!s}".format(len(calls), current_callback, calls)
		remaining_ticks = max(list(calls.values())[0], 1)

		current_callback_long = Callback(self.tick_long)
		calls = Scheduler().get_classinst_calls(self, current_callback_long)
		assert len(calls) == 1, "got {0!s} calls for saving {1!s}: {2!s}".format(len(calls), current_callback_long, calls)
		remaining_ticks_long = max(list(calls.values())[0], 1)

		db("INSERT INTO ai_player(rowid, need_more_ships, need_more_combat_ships, need_feeder_island, remaining_ticks, remaining_ticks_long) VALUES(?, ?, ?, ?, ?, ?)",
			self.worldid, self.need_more_ships, self.need_more_combat_ships, self.need_feeder_island, remaining_ticks, remaining_ticks_long)

		# save the ships
		for ship, state in self.ships.items():
			db("INSERT INTO ai_ship(rowid, owner, state) VALUES(?, ?, ?)", ship.worldid, self.worldid, state.index)

		# save the land managers
		for land_manager in self.islands.values():
			land_manager.save(db)

		# save the settlement managers
		for settlement_manager in self.settlement_managers:
			settlement_manager.save(db)

		# save the missions
		for mission in self.missions:
			mission.save(db)

		# save the personality manager
		self.personality_manager.save(db)

		# save the unit manager
		self.unit_manager.save(db)

		# save the combat manager
		self.combat_manager.save(db)

		# save the strategy manager
		self.strategy_manager.save(db)

		# save the behavior manager
		self.behavior_manager.save(db)

	def _load(self, db, worldid):
		super()._load(db, worldid)
		self.personality_manager = PersonalityManager.load(db, self)
		self.__init()

		self.need_more_ships, self.need_more_combat_ships, self.need_feeder_island, remaining_ticks, remaining_ticks_long = \
			db("SELECT need_more_ships, need_more_combat_ships, need_feeder_island, remaining_ticks, remaining_ticks_long FROM ai_player WHERE rowid = ?", worldid)[0]
		Scheduler().add_new_object(Callback(self.tick), self, run_in=remaining_ticks)
		Scheduler().add_new_object(Callback(self.tick_long), self, run_in=remaining_ticks_long)

	def finish_loading(self, db):
		""" This is called separately because most objects are loaded after the player. """

		# load the ships

		for ship_id, state_id in db("SELECT rowid, state FROM ai_ship WHERE owner = ?", self.worldid):
			ship = WorldObject.get_object_by_id(ship_id)
			self.ships[ship] = self.shipStates[state_id]

		# load unit manager
		self.unit_manager = UnitManager.load(db, self)

		# load combat manager
		self.combat_manager = CombatManager.load(db, self)

		# load strategy manager
		self.strategy_manager = StrategyManager.load(db, self)

		# load BehaviorManager
		self.behavior_manager = BehaviorManager.load(db, self)

		# load the land managers
		for (worldid,) in db("SELECT rowid FROM ai_land_manager WHERE owner = ?", self.worldid):
			land_manager = LandManager.load(db, self, worldid)
			self.islands[land_manager.island.worldid] = land_manager

		# load the settlement managers and settlement foundation missions
		for land_manager in self.islands.values():
			db_result = db("SELECT rowid FROM ai_settlement_manager WHERE land_manager = ?", land_manager.worldid)
			if db_result:
				settlement_manager = SettlementManager.load(db, self, db_result[0][0])
				self.settlement_managers.append(settlement_manager)
				self._settlement_manager_by_settlement_id[settlement_manager.settlement.worldid] = settlement_manager

				# load the foundation ship preparing missions
				db_result = db("SELECT rowid FROM ai_mission_prepare_foundation_ship WHERE settlement_manager = ?",
					settlement_manager.worldid)
				for (mission_id,) in db_result:
					self.missions.add(PrepareFoundationShip.load(db, mission_id, self.report_success, self.report_failure))
			else:
				mission_id = db("SELECT rowid FROM ai_mission_found_settlement WHERE land_manager = ?", land_manager.worldid)[0][0]
				self.missions.add(FoundSettlement.load(db, mission_id, self.report_success, self.report_failure))

		for settlement_manager in self.settlement_managers:
			# load the domestic trade missions
			db_result = db("SELECT rowid FROM ai_mission_domestic_trade WHERE source_settlement_manager = ?", settlement_manager.worldid)
			for (mission_id,) in db_result:
				self.missions.add(DomesticTrade.load(db, mission_id, self.report_success, self.report_failure))

			# load the special domestic trade missions
			db_result = db("SELECT rowid FROM ai_mission_special_domestic_trade WHERE source_settlement_manager = ?", settlement_manager.worldid)
			for (mission_id,) in db_result:
				self.missions.add(SpecialDomesticTrade.load(db, mission_id, self.report_success, self.report_failure))

			# load the international trade missions
			db_result = db("SELECT rowid FROM ai_mission_international_trade WHERE settlement_manager = ?", settlement_manager.worldid)
			for (mission_id,) in db_result:
				self.missions.add(InternationalTrade.load(db, mission_id, self.report_success, self.report_failure))

	def tick(self):
		Scheduler().add_new_object(Callback(self.tick), self, run_in=self.tick_interval)
		self.settlement_founder.tick()
		self.handle_enemy_expansions()
		self.handle_settlements()
		self.special_domestic_trade_manager.tick()
		self.international_trade_manager.tick()
		self.unit_manager.tick()
		self.combat_manager.tick()

	def tick_long(self):
		"""
		Same as above but used for reasoning that is not required to be called as often (such as diplomacy, strategy etc.)
		"""
		Scheduler().add_new_object(Callback(self.tick_long), self, run_in=self.tick_long_interval)
		self.strategy_manager.tick()

	def handle_settlements(self):
		goals = []
		for goal in self.goals:
			if goal.can_be_activated:
				goal.update()
				goals.append(goal)
		for settlement_manager in self.settlement_managers:
			settlement_manager.tick(goals)
		goals.sort(reverse=True)

		settlements_blocked = set()  # set([settlement_manager_id, ...])
		for goal in goals:
			if not goal.active:
				continue
			if isinstance(goal, SettlementGoal) and goal.settlement_manager.worldid in settlements_blocked:
				continue  # can't build anything in this settlement
			result = goal.execute()
			if result == GOAL_RESULT.SKIP:
				self.log.info('%s, skipped goal %s', self, goal)
			elif result == GOAL_RESULT.BLOCK_SETTLEMENT_RESOURCE_USAGE:
				self.log.info('%s blocked further settlement resource usage by goal %s', self, goal)
				settlements_blocked.add(goal.settlement_manager.worldid)
				goal.settlement_manager.need_materials = True
			else:
				self.log.info('%s all further goals during this tick blocked by goal %s', self, goal)
				break  # built something; stop because otherwise the AI could look too fast

		self.log.info('%s had %d active goals', self, sum(goal.active for goal in goals))
		for goal in goals:
			if goal.active:
				self.log.info('%s %s', self, goal)

		# refresh taxes and upgrade permissions
		for settlement_manager in self.settlement_managers:
			settlement_manager.refresh_taxes_and_upgrade_permissions()

	def request_ship(self):
		self.log.info('%s received request for more ships', self)
		self.need_more_ships = True

	def request_combat_ship(self):
		self.log.info('%s received request for combat ships', self)
		self.need_more_combat_ships = True

	def add_building(self, building):
		assert self._enabled
		# if the settlement id is not present then this is a new settlement that has to be handled separately
		if building.settlement.worldid in self._settlement_manager_by_settlement_id:
			self._settlement_manager_by_settlement_id[building.settlement.worldid].add_building(building)

	def remove_building(self, building):
		if not self._enabled:
			return

		self._settlement_manager_by_settlement_id[building.settlement.worldid].remove_building(building)

	def remove_unit(self, unit):
		if not self._enabled:
			return

		if unit in self.ships:
			del self.ships[unit]
		self.combat_manager.remove_unit(unit)
		self.unit_manager.remove_unit(unit)

	def count_buildings(self, building_id):
		return sum(settlement_manager.settlement.count_buildings(building_id) for settlement_manager in self.settlement_managers)

	def notify_mine_empty(self, message):
		"""The Mine calls this function to let the player know that the mine is empty."""
		settlement = message.mine.settlement
		if settlement.owner is self:
			self._settlement_manager_by_settlement_id[settlement.worldid].production_builder.handle_mine_empty(message.mine)

	def notify_new_disaster(self, message):
		settlement = message.building.settlement
		if settlement.owner is self:
			Scheduler().add_new_object(Callback(self._settlement_manager_by_settlement_id[settlement.worldid].handle_disaster, message), self, run_in=0)

	def _on_settlement_range_changed(self, message):
		"""Stores the ownership changes in a list for later processing."""
		our_new_coords_list = []
		settlement = message.sender

		for tile in message.changed_tiles:
			coords = (tile.x, tile.y)
			if settlement.owner is self:
				our_new_coords_list.append(coords)
			else:
				self.settlement_expansions.append((coords, settlement))

		if our_new_coords_list and settlement.worldid in self._settlement_manager_by_settlement_id:
			self._settlement_manager_by_settlement_id[settlement.worldid].production_builder.road_connectivity_cache.modify_area(our_new_coords_list)

	def handle_enemy_expansions(self):
		if not self.settlement_expansions:
			return  # no changes in land ownership

		change_lists = defaultdict(list)
		for coords, settlement in self.settlement_expansions:
			if settlement.island.worldid not in self.islands:
				continue  # we don't have a settlement there and have no current plans to create one
			change_lists[settlement.island.worldid].append(coords)
		self.settlement_expansions = []
		if not change_lists:
			return  # no changes in land ownership on islands we care about

		for island_id, changed_coords in change_lists.items():
			affects_us = False
			land_manager = self.islands[island_id]
			for coords in changed_coords:
				if coords in land_manager.production or coords in land_manager.village:
					affects_us = True
					break
			if not affects_us:
				continue  # we weren't using that land anyway

			settlement_manager = None
			for potential_settlement_manager in self.settlement_managers:
				if potential_settlement_manager.settlement.island.worldid == island_id:
					settlement_manager = potential_settlement_manager
					break

			if settlement_manager is None:
				self.handle_enemy_settling_on_our_chosen_island(island_id)
				# we are on the way to found a settlement on that island
			else:
				# we already have a settlement there
				settlement_manager.handle_lost_area(changed_coords)

	def handle_enemy_settling_on_our_chosen_island(self, island_id):
		mission = None
		for a_mission in self.missions:
			if isinstance(a_mission, FoundSettlement) and a_mission.land_manager.island.worldid == island_id:
				mission = a_mission
				break
		assert mission
		mission.cancel()
		self.settlement_founder.tick()

	@classmethod
	def load_abstract_buildings(cls, db):
		AbstractBuilding.load_all(db)

	@classmethod
	def clear_caches(cls):
		BasicBuilder.clear_cache()
		AbstractFarm.clear_cache()

	def __str__(self):
		return 'AI({0!s}/{1!s})'.format(getattr(self, 'name', 'unknown'),
			getattr(self, 'worldid', 'none'))

	def early_end(self):
		"""Called to speed up session destruction."""
		assert self._enabled
		self._enabled = False
		SettlementRangeChanged.unsubscribe(self._on_settlement_range_changed)
		NewDisaster.unsubscribe(self.notify_new_disaster)
		MineEmpty.unsubscribe(self.notify_mine_empty)

	def end(self):
		assert not self._enabled
		self.personality_manager = None
		self.world = None
		self.islands = None
		self.settlement_managers = None
		self._settlement_manager_by_settlement_id = None
		self.missions = None
		self.fishers = None
		self.settlement_founder = None
		self.unit_builder = None
		self.unit_manager = None
		self.behavior_manager = None
		self.combat_manager = None
		self.settlement_expansions = None
		self.goals = None
		self.special_domestic_trade_manager = None
		self.international_trade_manager = None
		self.strategy_manager.end()
		self.strategy_manager = None
		super().end()
