﻿//-----------------------------------------------------------------------
// <copyright file="Tile.cs" company="Mapbox">
//     Copyright (c) 2016 Mapbox. All rights reserved.
// </copyright>
//-----------------------------------------------------------------------

namespace Mapbox.Map
{
	using System;
	using Mapbox.Platform;
	using System.Linq;
	using System.Collections.Generic;
	using System.Collections.ObjectModel;
	using Mapbox.Unity.Utilities;


	/// <summary>
	///    A Map tile, a square with vector or raster data representing a geographic
	///    bounding box. More info <see href="https://en.wikipedia.org/wiki/Tiled_web_map">
	///    here </see>.
	/// </summary>
	public abstract class Tile : IAsyncRequest
	{


		private CanonicalTileId _id;
		private List<Exception> _exceptions;
		private State _state = State.New;
		private IAsyncRequest _request;
		private Action _callback;

		/// <summary> Tile state. </summary>
		public enum State
		{
			/// <summary> New tile, not yet initialized. </summary>
			New,
			/// <summary> Loading data. </summary>
			Loading,
			/// <summary> Data loaded and parsed. </summary>
			Loaded,
			/// <summary> Data loading cancelled. </summary>
			Canceled,
			/// <summary> Data has been loaded before and got updated. </summary>
			Updated
		}

		/// <summary> Gets the <see cref="T:Mapbox.Map.CanonicalTileId"/> identifier. </summary>
		/// <value> The canonical tile identifier. </value>
		public CanonicalTileId Id
		{
			get { return _id; }
			set { _id = value; }
		}


		/// <summary>Flag to indicate if the request was successful</summary>
		public bool HasError
		{
			get
			{
				return _exceptions == null ? false : _exceptions.Count > 0;
			}
		}


		/// <summary> Exceptions that might have occured during creation of the tile. </summary>
		public ReadOnlyCollection<Exception> Exceptions
		{
			get { return null == _exceptions ? null : _exceptions.AsReadOnly(); }
		}


		/// <summary> Messages of exceptions otherwise empty string. </summary>
		public string ExceptionsAsString
		{
			get
			{
				if (null == _exceptions || _exceptions.Count == 0) { return string.Empty; }
				return string.Join(Environment.NewLine, _exceptions.Select(e => e.Message).ToArray());
			}
		}


		/// <summary>
		/// Sets the error message.
		/// </summary>
		/// <param name="errorMessage"></param>
		internal void AddException(Exception ex)
		{
			if (null == _exceptions) { _exceptions = new List<Exception>(); }
			_exceptions.Add(ex);
		}


		/// <summary>
		///     Gets the current state. When fully loaded, you must
		///     check if the data actually arrived and if the tile
		///     is accusing any error.
		/// </summary>
		/// <value> The tile state. </value>
		public State CurrentState
		{
			get
			{
				return _state;
			}
		}


		public HttpRequestType RequestType { get { return _request.RequestType; } }


		public bool IsCompleted
		{
			get
			{
				return _state == State.Loaded;
			}
		}

		/// <summary>
		///     Initializes the <see cref="T:Mapbox.Map.Tile"/> object. It will
		///     start a network request and fire the callback when completed.
		/// </summary>
		/// <param name="param"> Initialization parameters. </param>
		/// <param name="callback"> The completion callback. </param>
		public void Initialize(Parameters param, Action callback)
		{
			Cancel();

			_state = State.Loading;
			_id = param.Id;
			_callback = callback;
			_request = param.Fs.Request(MakeTileResource(param.TilesetId).GetUrl(), HandleTileResponse, tileId: _id, tilesetId: param.TilesetId);
		}

		internal void Initialize(IFileSource fileSource, CanonicalTileId canonicalTileId, string tilesetId, Action p)
		{
			Cancel();

			_state = State.Loading;
			_id = canonicalTileId;
			_callback = p;
			_request = fileSource.Request(MakeTileResource(tilesetId).GetUrl(), HandleTileResponse, tileId: _id, tilesetId: tilesetId);
		}

		/// <summary>
		///     Returns a <see cref="T:System.String"/> that represents the current
		///     <see cref="T:Mapbox.Map.Tile"/>.
		/// </summary>
		/// <returns>
		///     A <see cref="T:System.String"/> that represents the current
		///     <see cref="T:Mapbox.Map.Tile"/>.
		/// </returns>
		public override string ToString()
		{
			return Id.ToString();
		}


		/// <summary>
		///     Cancels the request for the <see cref="T:Mapbox.Map.Tile"/> object.
		///     It will stop a network request and set the tile's state to Canceled.
		/// </summary>
		/// <example>
		/// <code>
		/// // Do not request tiles that we are already requesting
		///	// but at the same time exclude the ones we don't need
		///	// anymore, cancelling the network request.
		///	tiles.RemoveWhere((T tile) =>
		///	{
		///		if (cover.Remove(tile.Id))
		///		{
		///			return false;
		///		}
		///		else
		///		{
		///			tile.Cancel();
		///			NotifyNext(tile);
		///			return true;
		/// 	}
		///	});
		/// </code>
		/// </example>
		public void Cancel()
		{
			if (_request != null)
			{
				_request.Cancel();
				_request = null;
			}

			_state = State.Canceled;
		}


		// Get the tile resource (raster/vector/etc).
		internal abstract TileResource MakeTileResource(string tilesetId);


		// Decode the tile.
		internal abstract bool ParseTileData(byte[] data);


		// TODO: Currently the tile decoding is done on the main thread. We must implement
		// a Worker class to abstract this, so on platforms that support threads (like Unity
		// on the desktop, Android, etc) we can use worker threads and when building for
		// the browser, we keep it single-threaded.
		List<string> ids = new List<string>();
		private void HandleTileResponse(Response response)
		{

			if (response.HasError)
			{
				if (!ids.Contains(_id.ToString()))
					ids.Add(_id.ToString());
				else
					return;

				response.Exceptions.ToList().ForEach(e => AddException(e));
			}
			else
			{
				// only try to parse if request was successful

				// current implementation doesn't need to check if parsing is successful:
				// * Mapbox.Map.VectorTile.ParseTileData() already adds any exception to the list
				// * Mapbox.Map.RasterTile.ParseTileData() doesn't do any parsing
				ParseTileData(response.Data);
			}

			// Cancelled is not the same as loaded!
			if (_state != State.Canceled)
			{
				if (response.IsUpdate)
				{
					_state = State.Updated;
				}
				else
				{
					_state = State.Loaded;
				}
			}
			_callback();
		}


		/// <summary>
		///    Parameters for initializing a Tile object.
		/// </summary>
		/// <example>
		/// <code>
		/// var parameters = new Tile.Parameters();
		/// parameters.Fs = MapboxAccess.Instance;
		/// parameters.Id = new CanonicalTileId(_zoom, _tileCoorindateX, _tileCoordinateY);
		/// parameters.TilesetId = "mapbox.mapbox-streets-v7";
		/// </code>
		/// </example>
		public struct Parameters
		{
			/// <summary> The tile id. </summary>
			public CanonicalTileId Id;

			/// <summary>
			///     The tileset ID, usually in the format "user.mapid". Exceptionally,
			///     <see cref="T:Mapbox.Map.RasterTile"/> will take the full style URL
			///     from where the tile is composited from, like mapbox://styles/mapbox/streets-v9.
			/// </summary>
			public string TilesetId;

			/// <summary> The data source abstraction. </summary>
			public IFileSource Fs;
		}


	}
}
