# Copyright (C) 2008-2013 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 itertools
import json
import math
import re
from math import sin, cos

import horizons.globals
from fife import fife

from horizons.extscheduler import ExtScheduler
from horizons.util.python import decorators
from horizons.util.shapes import Circle, Point, Rect
from horizons.command.unit import Act
from horizons.component.namedcomponent import NamedComponent
from horizons.messaging import SettingChanged


class Minimap(object):
	"""A basic minimap.

	USAGE:
	Minimap can be drawn via GenericRenderer on an arbitrary position (determined by rect in ctor)
	or
	via Pychan Icon. In this case, the rect parameter only determines the size, the
	Minimap will scroll by default on clicks, overwrite on_click if you don't want that.

	TODO:
	* Remove renderer when used in icon node
	* Clear up distinction of coords where the minimap image or screen is the origin
	* Create a minimap tag for pychan
	** Handle clicks, remove overlay icon
	"""
	COLORS = { "island": (137, 117,  87),
		"cam":    (  1,   1,   1),
		"water" : (198, 188, 165),
		"highlight" : (255, 0, 0), # for events
		}


	WAREHOUSE_IMAGE = "content/gui/icons/minimap/warehouse.png"
	SHIP_NEUTRAL = "content/gui/icons/minimap/ship_neutral.png"
	SHIP_PIRATE = "content/gui/icons/minimap/pirate.png"
	GROUND_UNIT_IMAGE = "content/gui/icons/minimap/groundunit.png"

	SHIP_DOT_UPDATE_INTERVAL = 0.5 # seconds

	RENDER_NAMES = { # alpha-ordering determines the order
	  "background" : "c",
	  "base" : "d", # islands, etc.
	  "warehouse" : "e",
	  "ship" : "f",
	  "cam" : "g",
	  "ship_route" : "h",
	  "highlight" : "l"
	  }

	__minimap_id_counter = itertools.count()
	__ship_route_counter = itertools.count()
	_instances = [] # all active instances

	_dummy_fife_point = fife.Point(0, 0) # use when you quickly need a temporary point

	def __init__(self, position, session, view, targetrenderer, imagemanager, renderer=None, world=None,
	             cam_border=True, use_rotation=True, on_click=None, preview=False, tooltip=None):
		"""
		@param position: a Rect or a Pychan Icon, where we will draw to
		@param world: World object or fake thereof
		@param view: View object for cam control. Can be None to disable this
		@param renderer: renderer to be used if position isn't an icon
		@param targetrenderer: fife target renderer for drawing on icons
		@param imagemanager: fife imagemanager for drawing on icons
		@param cam_border: boolean, whether to draw the cam border
		@param use_rotation: boolean, whether to use rotation (it must also be enabled in the settings)
		@param on_click: function taking 1 argument or None for scrolling
		@param preview: flag, whether to only show the map as preview
		@param tooltip: always show this tooltip when cursor hovers over minimap
		"""
		if isinstance(position, Rect):
			self.location = position
			self.renderer = renderer
		else: # assume icon
			self.location = Rect.init_from_topleft_and_size(0, 0, position.width, position.height)
			self.icon = position
			self.use_overlay_icon(self.icon)
		self.session = session
		self.world = world
		if self.world:
			self._update_world_to_minimap_ratio()
		self.view = view
		self.rotation = 0
		self.fixed_tooltip = tooltip

		if on_click is not None:
			self.on_click = on_click

		self.cam_border = cam_border
		self.use_rotation = use_rotation
		self.preview = preview

		self.location_center = self.location.center

		self._id = str(self.__class__.__minimap_id_counter.next()) # internal identifier, used for allocating resources

		self._image_size_cache = {} # internal detail

		self.imagemanager = imagemanager

		self.minimap_image = _MinimapImage(self, targetrenderer)

		self._rotation_setting = horizons.globals.fife.get_uh_setting("MinimapRotation")
		if self.use_rotation:
			SettingChanged.subscribe(self._on_setting_changed)

	def end(self):
		self.disable()
		self.world = None
		self.session = None
		self.renderer = None
		if self.use_rotation:
			SettingChanged.unsubscribe(self._on_setting_changed)

	def disable(self):
		"""Due to the way the minimap works, there isn't really a show/hide,
		but you can disable it with this and enable again with draw().
		Stops all updates."""
		ExtScheduler().rem_all_classinst_calls(self)
		if self.view is not None:
			self.view.discard_change_listener(self.update_cam)

		if self in self.__class__._instances:
			self.__class__._instances.remove(self)

	def draw(self):
		"""Recalculates and draws the whole minimap of self.session.world or world.
		The world you specified is reused for every operation until the next draw().
		@param recalculate: do a full recalculation
		"""
		if self.world is None and self.session.world is not None:
			self.world = self.session.world # in case minimap has been constructed before the world
			self._update_world_to_minimap_ratio()
		if not self.world.inited:
			return # don't draw while loading

		self.__class__._instances.append(self)

		# update cam when view updates
		if self.view is not None and not self.view.has_change_listener(self.update_cam):
			self.view.add_change_listener(self.update_cam)

		if not hasattr(self, "icon"):
			# add to global generic renderer with id specific to this instance
			self.renderer.removeAll("minimap_image"+self._id)
			self.minimap_image.reset()
			# NOTE: this is for the generic renderer interface, the offrenderer has slightly different methods
			node = fife.RendererNode( fife.Point(self.location.center.x, self.location.center.y) )
			self.renderer.addImage("minimap_image"+self._id, node, self.minimap_image.image, False)

		else:
			# attach image to pychan icon (recommended)
			self.minimap_image.reset()
			self.icon.image = fife.GuiImage(self.minimap_image.image)

		self.update_cam()
		self._recalculate()
		if not self.preview:
			self._timed_update(force=True)
			ExtScheduler().rem_all_classinst_calls(self)
			ExtScheduler().add_new_object(self._timed_update, self,
			                              self.SHIP_DOT_UPDATE_INTERVAL, -1)

	def dump_data(self):
		"""Returns a string representing the minimap data"""
		return self._recalculate(dump_data=True)

	def draw_data(self, data):
		"""Display data from dump_data"""
		# only icon mode for now
		self.minimap_image.reset()
		self.icon.image = fife.GuiImage(self.minimap_image.image)

		self.minimap_image.set_drawing_enabled()
		rt = self.minimap_image.rendertarget
		render_name = self._get_render_name("base")
		draw_point = rt.addPoint
		point = fife.Point()

		# XXX There have been reports about `data` containing Fife debug
		# output (e.g. #2193). As temporary workaround, we try to only
		# parse what looks like valid json in there and ignore the rest.
		found_json = re.findall(r'\[\[.*\]\]', data)[0]

		for x, y, r, g, b in json.loads(found_json):
			point.set(x, y)
			draw_point(render_name, point, r, g, b)


	def _get_render_name(self, key):
		return self.RENDER_NAMES[key] + self._id

	def update_cam(self):
		"""Redraw camera border."""
		if not self.cam_border or self.view is None: # needs view
			return
		if self.world is None or not self.world.inited:
			return # don't draw while loading
		use_rotation = self._get_rotation_setting()
		self.minimap_image.set_drawing_enabled()
		self.minimap_image.rendertarget.removeAll(self._get_render_name("cam"))
		# draw rect for current screen
		displayed_area = self.view.get_displayed_area()
		minimap_corners_as_point = []
		for corner in displayed_area.get_corners():
			# check if the corners are outside of the screen
			corner = list(corner)
			if corner[0] > self.world.max_x:
				corner[0] = self.world.max_x
			if corner[0] < self.world.min_x:
				corner[0] = self.world.min_x
			if corner[1] > self.world.max_y:
				corner[1] = self.world.max_y
			if corner[1] < self.world.min_y:
				corner[1] = self.world.min_y
			corner = tuple(corner)

			coords = self._world_to_minimap( corner, use_rotation )
			minimap_corners_as_point.append( fife.Point(coords[0], coords[1]) )


		for i in xrange(0, 4):
			self.minimap_image.rendertarget.addLine(self._get_render_name("cam"),
			                                        minimap_corners_as_point[i],
			                                        minimap_corners_as_point[ (i+1) % 4],
			                                                         *self.COLORS["cam"])

	@classmethod
	def update(cls, tup):
		for minimap in cls._instances:
			minimap._update(tup)

	def _update(self, tup):
		"""Recalculate and redraw minimap for real world coord tup
		@param tup: (x, y)"""
		if self.world is None or not self.world.inited:
			return # don't draw while loading
		minimap_point = self._world_to_minimap( tup, self._get_rotation_setting() )
		world_to_minimap = self._world_to_minimap_ratio
		# TODO: remove this remnant of the old implementation, perhaps by refactoring recalculate()
		minimap_point = (
		  minimap_point[0] + self.location.left,
		  minimap_point[1] + self.location.top,
		)
		rect = Rect.init_from_topleft_and_size(minimap_point[0], minimap_point[1],
								                           int(round(1/world_to_minimap[0])) + 1,
								                           int(round(1/world_to_minimap[1])) + 1)
		self._recalculate(rect)

	def use_overlay_icon(self, icon):
		"""Configures icon so that clicks get mapped here.
		The current gui requires, that the minimap is drawn behind an icon."""
		self.overlay_icon = icon
		icon.mapEvents({
			icon.name + '/mousePressed' : self._on_click,
			icon.name + '/mouseDragged' : self._on_drag,
			icon.name + '/mouseEntered' : self._mouse_entered,
			icon.name + '/mouseMoved' : self._mouse_moved,
			icon.name + '/mouseExited' : self._mouse_exited,
		})

	def on_click(self, event, drag):
		"""Handler for clicks (pressed and dragged)
		Scrolls screen to the point, where the cursor points to on the minimap.
		Overwrite this method to your convenience.
		"""
		if self.preview:
			return # we don't do anything in this mode
		button = event.getButton()
		map_coords = event.map_coords
		if button == fife.MouseEvent.RIGHT:
			if drag:
				return
			for i in self.session.selected_instances:
				if i.movable:
					Act(i, *map_coords).execute(self.session)
		elif button == fife.MouseEvent.LEFT:
			if self.view is None:
				print "Warning: Can't handle minimap clicks since we have no view object"
			else:
				self.view.center(*map_coords)

	def _on_click(self, event):
		if self.world is not None: # supply world coords if there is a world
			event.map_coords = self._get_event_coords(event)
			if event.map_coords:
				self.on_click(event, drag=False)
		else:
			self.on_click(event, drag=True)

	def _on_drag(self, event):
		if self.world is not None: # supply world coords if there is a world
			event.map_coords = self._get_event_coords(event)
			if event.map_coords:
				self.on_click(event, drag=True)
		else:
			self.on_click(event, drag=True)

	def _get_event_coords(self, event):
		"""Returns position of event as uh map coordinate tuple or None"""
		mouse_position = Point(event.getX(), event.getY())
		if not hasattr(self, "icon"):
			icon_pos = Point(*self.overlay_icon.getAbsolutePos())
			abs_mouse_position = icon_pos + mouse_position
			if not self.location.contains(abs_mouse_position):
				# mouse click was on icon but not actually on minimap
				return
			abs_mouse_position = abs_mouse_position.to_tuple()
		else:
			abs_mouse_position = mouse_position.to_tuple()
		if self._get_rotation_setting():
			abs_mouse_position = self._get_from_rotated_coords(abs_mouse_position)
		return self._minimap_coords_to_world_coords(abs_mouse_position)

	def _mouse_entered(self, event):
		self._show_tooltip(event)

	def _mouse_moved(self, event):
		self._show_tooltip(event)

	def _mouse_exited(self, event):
		if hasattr(self, "icon"): # only supported for icon mode atm
			self.icon.hide_tooltip()

	def _show_tooltip(self, event):
		if not hasattr(self, "icon"):
			# only supported for icon mode atm
			return
		if self.fixed_tooltip is not None:
			self.icon.helptext = self.fixed_tooltip
			self.icon.position_tooltip(event)
			#self.icon.show_tooltip()
		else:
			coords = self._get_event_coords(event)
			if not coords: # no valid/relevant event location
				self.icon.hide_tooltip()
				return

			tile = self.world.get_tile( Point(*coords) )
			if tile is not None and tile.settlement is not None:
				new_helptext = tile.settlement.get_component(NamedComponent).name
				if self.icon.helptext != new_helptext:
					self.icon.helptext = new_helptext
					self.icon.show_tooltip()
				else:
					self.icon.position_tooltip(event)
			else:
				# mouse not over relevant part of the minimap
				self.icon.hide_tooltip()

	def highlight(self, tup, factor=1.0, speed=1.0, finish_callback=None, color=(0, 0, 0)):
		"""Try to get the users attention on a certain point of the minimap.
		@param tup: world coords
		@param factor: float indicating importance of event
		@param speed: animation speed as factor
		@param finish_callback: executed when animation finishes
		@param color: color of anim, (r,g,b), r,g,b of [0,255]
		@return duration of full animation in seconds"""
		tup = self._world_to_minimap( tup, self._get_rotation_setting())

		# grow the circle from MIN_RAD to MAX_RAD and back with STEPS steps, where the
		# interval between steps is INTERVAL seconds
		MIN_RAD = int( 3 * factor) # pixel
		MAX_RAD = int(12 * factor) # pixel
		STEPS = int(20 * factor)
		INTERVAL = (math.pi / 16) * factor

		def high(i=0):
			i += 1
			render_name = self._get_render_name("highlight")+str(tup)
			self.minimap_image.set_drawing_enabled()
			self.minimap_image.rendertarget.removeAll(render_name)
			if i > STEPS:
				if finish_callback:
					finish_callback()
				return
			part = i # grow bigger
			if i > STEPS // 2: # after the first half
				part = STEPS-i  # become smaller

			radius = MIN_RAD + int(( float(part) / (STEPS // 2) ) * (MAX_RAD - MIN_RAD) )

			draw_point = self.minimap_image.rendertarget.addPoint
			for x, y in Circle( Point(*tup), radius=radius ).get_border_coordinates():
				draw_point(render_name, fife.Point(x, y), *color)

			ExtScheduler().add_new_object(lambda : high(i), self, INTERVAL, loops=1)

		high()
		return STEPS*INTERVAL

	def show_unit_path(self, unit):
		"""Show the path a unit is moving along"""
		path = unit.path.path
		if path is None: # show at least the position
			path = [ unit.position.to_tuple() ]

		# the path always contains the full path, the unit might be somewhere in it
		position_of_unit_in_path = 0
		unit_pos = unit.position.to_tuple()
		for i, pos in enumerate(path):
			if pos == unit_pos:
				position_of_unit_in_path = i
				break

		# display units one ahead if possible, it looks nicer if the unit is moving
		if len(path) > 1 and position_of_unit_in_path+1 < len(path):
			position_of_unit_in_path += 1 #
		path = path[position_of_unit_in_path:]

		# draw every step-th coord
		step = 1
		relevant_coords = [ path[0] ]
		for i in xrange(step, len(path), step):
			relevant_coords.append( path[i] )
		relevant_coords.append( path[-1] )

		# get coords, actual drawing
		use_rotation = self._get_rotation_setting()
		self.minimap_image.set_drawing_enabled()
		p = fife.Point(0, 0)
		render_name = self._get_render_name("ship_route") + str(self.__class__.__ship_route_counter.next())
		color = unit.owner.color.to_tuple()
		last_coord = None
		draw_point = self.minimap_image.rendertarget.addPoint
		for i in relevant_coords:
			coord = self._world_to_minimap(i, use_rotation)
			if last_coord is not None and \
			   sum( abs(last_coord[i] - coord[i]) for i in (0, 1) ) < 2: # 2 is min dist in pixels
				continue
			last_coord = coord
			p.x = coord[0]
			p.y = coord[1]
			draw_point(render_name, p, *color)

		def cleanup():
			self.minimap_image.set_drawing_enabled()
			self.minimap_image.rendertarget.removeAll(render_name)

		speed = 1.0 + math.sqrt(5) / 2
		self.highlight(path[-1], factor=0.4, speed=speed, finish_callback=cleanup, color=color)

		return True

	def _recalculate(self, where=None, dump_data=False):
		"""Calculate which pixel of the minimap should display what and draw it
		@param where: Rect of minimap coords. Defaults to self.location
		@param dump_data: Don't draw but return calculated data"""
		self.minimap_image.set_drawing_enabled()

		rt = self.minimap_image.rendertarget
		render_name = self._get_render_name("base")

		if where is None:
			where = self.location
			rt.removeAll(render_name)

		# calculate which area of the real map is mapped to which pixel on the minimap
		pixel_per_coord_x, pixel_per_coord_y = self._world_to_minimap_ratio

		# calculate values here so we don't have to do it in the loop
		pixel_per_coord_x_half_as_int = int(pixel_per_coord_x/2)
		pixel_per_coord_y_half_as_int = int(pixel_per_coord_y/2)

		world_min_x = self.world.min_x
		world_min_y = self.world.min_y
		island_col = self.COLORS["island"]
		water_col = self.COLORS["water"]
		location_left = self.location.left
		location_top = self.location.top
		if dump_data:
			data = []
			draw_point = lambda name, fife_point, r, g, b : data.append( (fife_point.x, fife_point.y, r, g, b) )
		else:
			draw_point = rt.addPoint
		fife_point = fife.Point(0, 0)

		use_rotation = self._get_rotation_setting()
		full_map = self.world.full_map

		# loop through map coordinates, assuming (0, 0) is the origin of the minimap
		# this facilitates calculating the real world coords
		for x in xrange(where.left-self.location.left, where.left+where.width-self.location.left):
			for y in xrange(where.top-self.location.top, where.top+where.height-self.location.top):

				"""
				This code should be here, but since python can't do inlining, we have to inline
				ourselves for performance reasons
				covered_area = Rect.init_from_topleft_and_size(
				  int(x * pixel_per_coord_x)+world_min_x,
				  int(y * pixel_per_coord_y)+world_min_y),
				  int(pixel_per_coord_x), int(pixel_per_coord_y))
				real_map_point = covered_area.center
				"""
				# use center of the rect that the pixel covers
				real_map_x = int(x * pixel_per_coord_x) + world_min_x + pixel_per_coord_x_half_as_int
				real_map_y = int(y * pixel_per_coord_y) + world_min_y + pixel_per_coord_y_half_as_int
				real_map_coords = (real_map_x, real_map_y)

				# check what's at the covered_area
				if real_map_coords in full_map:
					# this pixel is an island
					tile = full_map[real_map_coords]
					settlement = tile.settlement
					if settlement is None:
						# island without settlement
						if tile.id <= 0:
							color = water_col
						else:
							color = island_col
					else:
						# pixel belongs to a player
						color = settlement.owner.color.to_tuple()
				else:
					color = water_col

				if use_rotation:
					# inlined _get_rotated_coords
					rot_x, rot_y = self._rotate( (location_left + x, location_top + y), self._rotations)
					fife_point.set(rot_x - location_left, rot_y - location_top)
				else:
					fife_point.set(x, y)

				draw_point(render_name, fife_point, *color)

		if dump_data:
			return json.dumps( data )


	def _timed_update(self, force=False):
		"""Regular updates for domains we can't or don't want to keep track of."""
		# OPTIMISATION NOTE: there can be pretty many ships, don't rely on the loop being rarely executed
		# update ship icons
		self.minimap_image.set_drawing_enabled()
		render_name = self._get_render_name("ship")
		self.minimap_image.rendertarget.removeAll( render_name )
		use_rotation = self._get_rotation_setting()
		# make use of this dummy points instead of creating a fife.point instances which are consuming a lot of resources
		dummy_point0 = fife.Point(0, 0)
		dummy_point1 = fife.Point(0, 0)
		for ship in self.world.ships:
			if not ship.in_ship_map:
				continue # no fisher ships, etc
			coord = self._world_to_minimap( ship.position.to_tuple(), use_rotation )
			color = ship.owner.color.to_tuple()
			# set correct icon
			if ship.owner is self.session.world.pirate:
				ship_icon_path = self.__class__.SHIP_PIRATE
			else:
				ship_icon_path = self.__class__.SHIP_NEUTRAL
			ship_icon = self.imagemanager.load(ship_icon_path)
			dummy_point1.set(coord[0], coord[1])
			self.minimap_image.rendertarget.addImage(render_name, dummy_point1, ship_icon)
			if ship.owner.regular_player:
				# add the 'flag' over the ship icon, with the color of the owner
				dummy_point0.set(coord[0] - 5, coord[1] - 5)
				dummy_point1.set(coord[0], coord[1] - 5)
				self.minimap_image.rendertarget.addLine(render_name, dummy_point0,
									dummy_point1, color[0], color[1], color[2])
				dummy_point0.set(coord[0] - 6, coord[1] - 6)
				dummy_point1.set(coord[0], coord[1] - 6)
				self.minimap_image.rendertarget.addLine(render_name, dummy_point0,
									dummy_point1, color[0], color[1], color[2])
				dummy_point0.set(coord[0] - 4, coord[1] - 4)
				dummy_point1.set(coord[0], coord[1] - 4)
				self.minimap_image.rendertarget.addLine(render_name, dummy_point0,
									dummy_point1, color[0], color[1], color[2])
				# add black border around the flag
				dummy_point0.set(coord[0] - 6, coord[1] - 7)
				dummy_point1.set(coord[0], coord[1] - 7)
				self.minimap_image.rendertarget.addLine(render_name, dummy_point0, dummy_point1, 0, 0, 0)
				dummy_point0.set(coord[0] - 4, coord[1] - 3)
				dummy_point1.set(coord[0], coord[1] - 4)
				self.minimap_image.rendertarget.addLine(render_name, dummy_point0, dummy_point1, 0, 0, 0)
				dummy_point0.set(coord[0] - 6, coord[1] - 7)
				dummy_point1.set(coord[0] - 4, coord[1] - 3)
				self.minimap_image.rendertarget.addLine(render_name, dummy_point0, dummy_point1, 0, 0, 0)

			# TODO: nicer selected view
			dummy_point0.set(coord[0], coord[1])
			draw_point = self.minimap_image.rendertarget.addPoint
			if ship in self.session.selected_instances:
				draw_point(render_name, dummy_point0, *Minimap.COLORS["water"])
				for x_off, y_off in ((-2,  0),
				                     (+2,  0),
				                     ( 0, -2),
				                     ( 0, +2)):
					dummy_point1.set(coord[0] + x_off, coord[1] + y_off)
					draw_point(render_name, dummy_point1, *color)

		# draw settlement warehouses if something has changed
		settlements = self.world.settlements
		# save only worldids as to not introduce actual coupling
		cur_settlements = set( i.worldid for i in settlements )
		if force or \
		   (not hasattr(self, "_last_settlements") or cur_settlements != self._last_settlements):
			# update necessary
			warehouse_render_name = self._get_render_name("warehouse")
			self.minimap_image.rendertarget.removeAll( warehouse_render_name )
			for settlement in settlements:
				coord = settlement.warehouse.position.center.to_tuple()
				coord = self._world_to_minimap(coord, use_rotation)
				self._update_image( self.__class__.WAREHOUSE_IMAGE,
				                    warehouse_render_name,
				                    coord)
			self._last_settlements = cur_settlements

	def _update_image(self, img_path, name, coord_tuple):
		"""Updates image as part of minimap (e.g. when it has moved)"""
		img = self.imagemanager.load( img_path )

		size_tuple = self._image_size_cache.get(img_path)
		if size_tuple is None:
			ratio = sum(self._world_to_minimap_ratio) / 2.0
			ratio = max(1.0, ratio)
			size_tuple = int(img.getWidth()/ratio), int(img.getHeight()/ratio)
			self._image_size_cache[img_path] = size_tuple
		new_width, new_height = size_tuple
		p = self.__class__._dummy_fife_point
		p.set( *coord_tuple )
		# resizeImage also means draw
		self.minimap_image.rendertarget.resizeImage(name, p, img, new_width, new_height)

	def rotate_right(self):
		# keep track of rotation at any time, but only apply
		# if it's actually used
		self.rotation -= 1
		self.rotation %= 4
		if self._get_rotation_setting():
			self.draw()

	def rotate_left(self):
		# see above
		self.rotation += 1
		self.rotation %= 4
		if self._get_rotation_setting():
			self.draw()

	## CALC UTILITY
	def _world_to_minimap(self, coords, use_rotation):
		"""Complete coord transformation, batteries included.
		The methods below are for more specialized purposes."""
		coords = self._world_coords_to_minimap_coords(coords)

		if use_rotation:
			coords = self._get_rotated_coords(coords)
		# transform from screen coords to minimap coords
		coords = (coords[0] - self.location.left,
		          coords[1] - self.location.top)
		return coords

	def _get_rotation_setting(self):
		if not self.use_rotation:
			return False
		return self._rotation_setting

	def _on_setting_changed(self, message):
		if message.setting_name == "MinimapRotation":
			self._rotation_setting = message.new_value
			self.draw()

	_rotations = { 0 : 0,
				         1 : 3 * math.pi / 2,
				         2 : math.pi,
				         3 : math.pi / 2
				         }
	def _get_rotated_coords(self, tup):
		"""Rotates according to current rotation settings.
		Input coord must be relative to screen origin, not minimap origin"""
		return self._rotate(tup, self._rotations)

	_from_rotations = { 0 : 0,
				              1 : math.pi / 2,
				              2 : math.pi,
				              3 : 3 * math.pi / 2
				              }
	def _get_from_rotated_coords(self, tup):
		return self._rotate(tup, self._from_rotations)

	def _rotate(self, tup, rotations):
		rotation = rotations[ self.rotation ]

		x = tup[0]
		y = tup[1]

		# Rotate around center of minimap.
		x -= self.location_center.x
		y -= self.location_center.y

		new_x = x * cos(rotation) - y * sin(rotation)
		new_y = x * sin(rotation) + y * cos(rotation)

		new_x += self.location_center.x
		new_y += self.location_center.y

		new_x = int(round(new_x))
		new_y = int(round(new_y))

		# Some points may get out of range.
		new_x = max(self.location.left, new_x)
		new_x = min(self.location.right, new_x)
		new_y = max(self.location.top, new_y)
		new_y = min(self.location.bottom, new_y)

		return (new_x, new_y)

	def _update_world_to_minimap_ratio(self):
		world_height = self.world.map_dimensions.height
		world_width = self.world.map_dimensions.width
		minimap_height = self.location.height
		minimap_width = self.location.width
		pixel_per_coord_x = float(world_width) / minimap_width
		pixel_per_coord_y = float(world_height) / minimap_height
		self._world_to_minimap_ratio = (pixel_per_coord_x, pixel_per_coord_y)

	def _world_coords_to_minimap_coords(self, tup):
		"""Calculates which pixel in the minimap contains a coord in the real map.
		@param tup: (x, y) as ints
		@return tuple"""
		pixel_per_coord_x, pixel_per_coord_y = self._world_to_minimap_ratio
		return (
			int(round(float(tup[0] - self.world.min_x)/pixel_per_coord_x))+self.location.left,
			int(round(float(tup[1] - self.world.min_y)/pixel_per_coord_y))+self.location.top
		)

	def _minimap_coords_to_world_coords(self, tup):
		"""Inverse to _world_coords_to_minimap_coords"""
		pixel_per_coord_x, pixel_per_coord_y = self._world_to_minimap_ratio
		return (
			int(round( (tup[0] - self.location.left) * pixel_per_coord_x))+self.world.min_x,
			int(round( (tup[1] - self.location.top)* pixel_per_coord_y))+self.world.min_y
		)

	def get_size(self):
		return (self.location.height, self.location.width)


class _MinimapImage(object):
	"""Encapsulates handling of fife Image.
	Provides:
	- self.rendertarget: instance of fife.RenderTarget
	"""
	def __init__(self, minimap, targetrenderer):
		self.minimap = minimap
		self.targetrenderer = targetrenderer
		size = self.minimap.get_size()
		self.image = self.minimap.imagemanager.loadBlank(size[0], size[1])
		self.rendertarget = targetrenderer.createRenderTarget( self.image )
		self.set_drawing_enabled()

	def reset(self):
		"""Reset image to original image"""
		# reload
		self.rendertarget.removeAll()
		size = self.minimap.get_size()
		self.rendertarget.addQuad( self.minimap._get_render_name("background"),
		                           fife.Point(0, 0),
		                           fife.Point(0, size[1]),
		                           fife.Point(size[0], size[1]),
		                           fife.Point(size[0], 0),
		                           *Minimap.COLORS["water"])

	def set_drawing_enabled(self):
		"""Always call this."""
		targetname = self.rendertarget.getTarget().getName()
		self.targetrenderer.setRenderTarget(targetname, False, 0)


decorators.bind_all(Minimap)