"""
Generate landscapes and check map file combinations. Child class for :class:`.Simulation` and
:class:`.DispersalSimulation`. Contains :class:`.Map` objects for each relevant map file internally.
"""
import logging

import copy

from .system_operations import check_file_exists, create_logger
from .map import Map


class Landscape:
	"""
	Calculates offsets and dimensions of a selection of tif files making up a landscape.
	"""

	def __init__(self):
		"""
		Initialises the default variables for the Landscape object.
		"""
		self.coarse_scale = 1
		self.is_setup_map = False
		self.setup_complete = False
		self.fine_map = Map()
		self.coarse_map = Map()
		self.sample_map = Map()
		# Historical map parameters
		# Rate of habitat change that occurs before historical state
		self.habitat_change_rate = 0
		self.gen_since_historical = 1  # The number of generations ago a habitat was historical
		self.historical_fine_map_file = None
		self.historical_coarse_map_file = None
		self.historical_fine_list = []
		self.historical_coarse_list = []
		self.times_list = []
		self.rates_list = []
		self.logging_level = 10
		self.logger = logging.Logger("pycoalescence.landscape")
		self.landscape_type = False

	def _create_logger(self, file=None, logging_level=None, **kwargs):
		"""
		Creates the logger for use with NECSim simulation. Note you can supply your own logger by over-riding
		self.logger. This function should only be run during self.__init__()

		:param file: file to write output to. If None, outputs to terminal
		:param logging_level: the logging level to use (defaults to INFO)
		:param kwargs: additional keyword arguments to write out to

		:return: None
		"""
		if logging_level is None:
			logging_level = self.logging_level
		self.logger = create_logger(self.logger, file, logging_level, **kwargs)

	def add_historical_map(self, fine_map, coarse_map, time, rate):
		"""
		Adds an extra map to the list of historical maps.

		:param fine_map: the historical fine map file to add
		:param coarse_map: the historical coarse map file to add
		:param time: the time to add (when the map is accurate)
		:param rate: the rate to add (the rate of habitat change at this time)
		"""
		if not self.historical_fine_map_file or self.historical_fine_map_file == "none":
			self.historical_fine_map_file = fine_map
			self.historical_coarse_map_file = coarse_map
			self.gen_since_historical = time
			self.habitat_change_rate = rate
		self.historical_fine_list.append(fine_map)
		self.historical_coarse_list.append(coarse_map)
		self.times_list.append(time)
		self.rates_list.append(rate)

	def sort_historical_maps(self):
		"""
		Sorts the historical maps by time.
		"""
		if len(self.historical_fine_list) > 0 and len(self.historical_coarse_list) > 0:
			self.times_list, self.historical_fine_list, self.historical_coarse_list, self.rates_list = \
				[list(x) for x in zip(*sorted(zip(self.times_list, self.historical_fine_list,
												  self.historical_coarse_list, self.rates_list)))]

	def set_map(self, map_file, x_size=None, y_size=None):
		"""
		Quick function for setting a single map file for both the sample map and fine map, of dimensions x and y.
		Sets the sample file to "null" and coarse file and historical files to "none".

		:param str map_file: path to the map file
		:param int x_size: the x dimension, or None to detect automatically from the ".tif" file
		:param int y_size: the y dimension, or None to detect automatically from the ".tif" file
		"""
		if (x_size is None or y_size is None) and (x_size is not None or y_size is not None):
			raise ValueError("Must specify both a map x and y dimension.")
		self.fine_map.set_dimensions(map_file, x_size=x_size, y_size=y_size)
		self.fine_map.zero_offsets()
		self.sample_map.set_dimensions("null", self.fine_map.x_size, self.fine_map.y_size)
		self.sample_map.zero_offsets()
		self.coarse_map.set_dimensions("none", self.fine_map.x_size, self.fine_map.y_size)
		self.coarse_map.zero_offsets()
		self.is_setup_map = True
		self.coarse_scale = 1.0

	def set_map_files(self, sample_file, fine_file=None, coarse_file=None, historical_fine_file=None,
					  historical_coarse_file=None):
		"""
		Sets the map files (or to null, if none specified). It then calls detect_map_dimensions() to correctly read in
		the specified dimensions.

		If sample_file is "null", dimension values will remain at 0.
		If coarse_file is "null", it will default to the size of fine_file with zero offset.
		If the coarse file is "none", it will not be used.
		If the historical fine or coarse files are "none", they will not be used.

		:param str sample_file: the sample map file. Provide "null" if on samplemask is required
		:param str fine_file: the fine map file. Defaults to "null" if none provided
		:param str coarse_file: the coarse map file. Defaults to "none" if none provided
		:param str historical_fine_file: the historical fine map file. Defaults to "none" if none provided
		:param str historical_coarse_file: the historical coarse map file. Defaults to "none" if none provided

		:rtype: None

		:return: None
		"""
		if fine_file in [None, "null", "none"]:
			raise ValueError("Fine map file cannot be 'none' or 'null' for automatic parameter detection."
							 "Use set_map_parameters() instead.")
		if coarse_file is None:
			coarse_file = "none"
		if historical_fine_file is None:
			historical_fine_file = "none"
		if historical_coarse_file is None:
			historical_coarse_file = "none"
		self.set_map_parameters(sample_file, 0, 0, fine_file, 0, 0, 0, 0, coarse_file, 0, 0, 0, 0, 0,
								historical_fine_file, historical_coarse_file)
		try:
			self.detect_map_dimensions()
		except Exception as e:
			self.is_setup_map = False
			raise e

	def set_map_parameters(self, sample_file, sample_x, sample_y, fine_file, fine_x, fine_y, fine_x_offset,
						   fine_y_offset, coarse_file, coarse_x, coarse_y,
						   coarse_x_offset, coarse_y_offset, coarse_scale, historical_fine_map, historical_coarse_map):
		"""

		Set up the map objects with the required parameters. This is required for csv file usage.

		Note that this function is not recommended for tif file usage, as it is much simpler to call set_map_files() and
		which should automatically calculate map offsets, scaling and dimensions.

		:param sample_file: the sample file to use, which should contain a boolean mask of where to sample
		:param sample_x: the x dimension of the sample file
		:param sample_y: the y dimension of the sample file
		:param fine_file: the fine map file to use (must be equal to or larger than the sample file)
		:param fine_x: the x dimension of the fine map file
		:param fine_y: the y dimension of the fine map file
		:param fine_x_offset: the x offset of the fine map file
		:param fine_y_offset: the y offset of the fine map file
		:param coarse_file: the coarse map file to use (must be equal to or larger than fine map file)
		:param coarse_x: the x dimension of the coarse map file
		:param coarse_y: the y dimension of the coarse map file
		:param coarse_x_offset: the x offset of the coarse map file at the resolution of the fine map
		:param coarse_y_offset: the y offset of the coarse map file at the resoultion of the fine map
		:param coarse_scale: the relative scale of the coarse map compared to the fine map (must match x and y scaling)
		:param historical_fine_map: the historical fine map file to use (must have dimensions equal to fine map)
		:param historical_coarse_map: the historical coarse map file to use (must have dimensions equal to coarse map)
		"""
		if not self.is_setup_map:
			self.sample_map.set_dimensions(sample_file, sample_x, sample_y)
			self.fine_map.set_dimensions(fine_file, fine_x, fine_y, fine_x_offset, fine_y_offset)
			self.coarse_map.set_dimensions(coarse_file, coarse_x, coarse_y, coarse_x_offset, coarse_y_offset)
			self.coarse_scale = coarse_scale
			self.historical_fine_map_file = historical_fine_map
			self.historical_coarse_map_file = historical_coarse_map
			self.is_setup_map = True
		else:
			err = "Map objects are already set up."
			self.logger.warning(err)

	def detect_map_dimensions(self):
		"""
		Detects all the map dimensions for the provided files (where possible) and sets the respective values.
		This is intended to be run after set_map_files()

		:raises TypeError: if a dispersal map or reproduction map is specified, we must have a fine map specified, but
						   not a coarse map.

		:raises IOError: if one of the required maps does not exist

		:raises ValueError: if the dimensions of the dispersal map do not make sense when used with the fine map
							provided

		:return: None
		"""
		self.fine_map.set_dimensions()
		if self.sample_map.file_name == "null":
			self.sample_map.set_dimensions(x_size=self.fine_map.x_size, y_size=self.fine_map.y_size,
										   x_offset=self.fine_map.x_offset, y_offset=self.fine_map.y_offset)
			self.sample_map.x_ul = self.fine_map.x_ul
			self.sample_map.y_ul = self.fine_map.y_ul
		else:
			self.sample_map.set_dimensions()
		x, y = self.fine_map.calculate_offset(self.sample_map)[0:2]
		self.fine_map.x_offset = -x
		self.fine_map.y_offset = -y
		if self.coarse_map.file_name in ["null", "none"]:
			tmpname = copy.deepcopy(self.coarse_map.file_name)
			self.coarse_map = copy.deepcopy(self.fine_map)
			self.coarse_scale = 1
			self.coarse_map.x_offset = 0
			self.coarse_map.y_offset = 0
			self.coarse_map.file_name = tmpname
		else:
			self.coarse_map.set_dimensions()
			self.coarse_map.x_offset = -self.coarse_map.calculate_offset(self.fine_map)[0]
			self.coarse_map.y_offset = -self.coarse_map.calculate_offset(self.fine_map)[1]
			self.coarse_scale = self.fine_map.calculate_scale(self.coarse_map)
		self.check_maps()

	def check_maps(self):
		"""
		Checks that the maps all exist and that the file structure makes sense.

		:raises TypeError: if a dispersal map or reproduction map is specified, we must have a fine map specified, but
			not a coarse map.

		:raises IOError: if one of the required maps does not exist

		:return: None
		"""
		for map_file in [self.coarse_map, self.fine_map, self.sample_map]:
			map_file.check_map()
		for map_file in [self.historical_fine_map_file, self.historical_fine_map_file]:
			check_file_exists(map_file)
		# Now check that the rest of the map naming structures make sense.
		if self.historical_fine_map_file in {"none", None} and len(self.historical_fine_list) == 0:
			if self.historical_coarse_map_file not in {"none", None} or len(self.historical_coarse_list) > 1:
				self.logger.warning(
					"Historical fine file is 'none' but historical coarse file is not none. Check file names.")
				self.logger.warning("Defaulting to historical_coarse_map_file = 'none'")
				self.historical_coarse_map_file = "none"
				self.historical_coarse_list = []
			if len(self.historical_fine_list) > 1:
				self.logger.warning(
					"Set historical fine map file to 'none', but then added other historical maps. Changing.")
				self.historical_fine_list = []
		if self.coarse_map.file_name in {"none", None}:
			if self.historical_coarse_map_file not in {"none", None}:
				self.logger.warning("Coarse file is 'none' but historical coarse file is not none. Check file names.")
				self.logger.warning("Defaulting to historical_coarse_map_file = 'none'")
				self.historical_coarse_map_file = "none"
				self.historical_coarse_list = []
		else:
			if self.coarse_map.file_name != "null" and not self.fine_map.is_within(self.coarse_map):
				raise ValueError("Offsets mean that coarse map does not fully encompass fine map. Check that your"
								 " maps exist at the same spatial coordinates.")
		if self.fine_map.file_name == "none":
			self.logger.warning("Fine map file cannot be 'none', changing to 'null'.")
			self.fine_map.file_name = "null"
		# Check offset of sample mask with fine map
		if self.sample_map.file_name != "null" and not self.sample_map.is_within(self.fine_map):
			raise ValueError(
				"Offsets mean that fine map does not fully encompass sample map. Check that your maps exist"
				" at the same spatial coordinates.")

		if self.landscape_type == "tiled_fine":
			if self.coarse_map.file_name not in {None, "none"}:
				raise TypeError("Cannot use a coarse map with a tiled fine landscape.")
		if self.landscape_type == "tiled_coarse":
			if self.coarse_map.file_name in {None, "none"}:
				raise TypeError("Cannot use a tiled_coarse landscape without a coarse map.")
