<commit_before>﻿using Newtonsoft.Json;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using System.Net.Http;
using System.Net.Http.Headers;
using System.Threading;
using System.Threading.Tasks;

namespace System.Net.TMDb
{
	public sealed class ServiceClient
	{
		private readonly string BaseUrl;

		#region Constructors

		public ServiceClient(string apiKey)
		{
			this.BaseUrl = String.Concat(@"http://api.themoviedb.org/3/{0}?api_key=", apiKey, "&");

			this.Movies = new MovieContext(this);
			this.Shows = new ShowsContext(this);
			this.Companies = new CompanyContext(this);
			this.Genres = new GenreContext(this);
			this.People = new PeopleContext(this);
			this.Collections = new CollectionContext(this);
			this.Lists = new ListContext(this);
			this.Reviews = new ReviewContext(this);
			this.Settings = new SystemContext(this);
		}

		#endregion

		public IMovieInfo Movies { get; private set; }

		public IShowInfo Shows { get; private set; }

		public ICompanyInfo Companies { get; private set; }

		public IGenreInfo Genres { get; private set; }

		public IPeopleInfo People { get; private set; }

		public ICollectionInfo Collections { get; private set; }

		public IListInfo Lists { get; private set; }

		public IReviewInfo Reviews { get; private set; }

		public ISystemInfo Settings { get; private set; }

		#region Authentication Methods

		/// <summary>
		/// This method is used to generate a valid request token and authenticate user with a TMDb username and password.
		/// </summary>
		public async Task<string> LoginAsync(string username, string password, CancellationToken cancellationToken)
		{
			var response = await GetAsync("authentication/token/new", null, cancellationToken).ConfigureAwait(false);
			var parameters = new Dictionary<string, object>
			{
				{ "request_token", (await Deserialize<AuthenticationResult>(response)).Token },
				{ "username", username },
				{ "language", password }
			};
			response = await GetAsync("authentication/token/validate_with_login", parameters, cancellationToken).ConfigureAwait(false);
			return (await Deserialize<AuthenticationResult>(response)).Token;
		}

		/// <summary>
		/// This method is used to generate a session id for user based authentication, or a guest session if a null token is used.
		/// A session id is required in order to use any of the write methods.
		/// </summary>
		public async Task<string> GetSessionAsync(string token, CancellationToken cancellationToken)
		{
			return await (token == null ? OpenGuestSessionAsync(cancellationToken) : OpenSessionAsync(token, cancellationToken));
		}

		private async Task<string> OpenSessionAsync(string token, CancellationToken cancellationToken)
		{
			var parameters = new Dictionary<string, object> { { "request_token", token } };
			var response = await GetAsync("authentication/session/new", parameters, cancellationToken).ConfigureAwait(false);
			return (await Deserialize<AuthenticationResult>(response)).Session;
		}

		/// <summary>
		/// 
		/// </summary>
		private async Task<string> OpenGuestSessionAsync(CancellationToken cancellationToken)
		{
			var response = await GetAsync("authentication/guest_session/new", null, cancellationToken).ConfigureAwait(false);
			return (await Deserialize<AuthenticationResult>(response)).Guest;
		}

		#endregion

		#region Request Handling Methods

		private Task<HttpResponseMessage> GetAsync(string cmd, IDictionary<string, object> parameters, CancellationToken cancellationToken)
		{
			var client = CreateHttpClient();
			TaskCompletionSource<HttpResponseMessage> tcs = new TaskCompletionSource<HttpResponseMessage>();

			client.GetAsync(CreateRequestUri(cmd, parameters),
				HttpCompletionOption.ResponseHeadersRead, cancellationToken)
				.ContinueWith(t => HandleResponseCompletion(t, tcs));
			return tcs.Task;
		}

		private Task<HttpResponseMessage> SendAsync(string cmd, IDictionary<string, object> parameters, HttpContent content, HttpMethod method, CancellationToken cancellationToken)
		{
			var client = CreateHttpClient();
			TaskCompletionSource<HttpResponseMessage> tcs = new TaskCompletionSource<HttpResponseMessage>();
			var request = new HttpRequestMessage(method, CreateRequestUri(cmd, parameters)) { Content = content };

			client.SendAsync(request, cancellationToken)
				.ContinueWith(t => HandleResponseCompletion(t, tcs));
			return tcs.Task;
		}

		private static async Task<T> Deserialize<T>(HttpResponseMessage response)
		{
			string responseJson = await response.Content.ReadAsStringAsync().ConfigureAwait(false);
#if DEBUG
			System.Diagnostics.Debug.WriteLine(responseJson);
#endif
			return JsonConvert.DeserializeObject<T>(responseJson);
		}

		private static async Task<dynamic> DeserializeDynamic(HttpResponseMessage response)
		{
			string responseJson = await response.Content.ReadAsStringAsync().ConfigureAwait(false);
#if DEBUG
			System.Diagnostics.Debug.WriteLine(responseJson);
#endif
			return JsonConvert.DeserializeObject(responseJson);
		}

		private static void HandleResponseCompletion(Task<HttpResponseMessage> task, TaskCompletionSource<HttpResponseMessage> tcs)
		{
			if (task.IsCanceled) tcs.TrySetCanceled();
			else if (!task.Result.IsSuccessStatusCode)
			{
				if (task.Result.Content != null)
				{
					task.Result.Content.ReadAsStringAsync().ContinueWith(t2 =>
					{
						var status = JsonConvert.DeserializeObject<Status>(t2.Result);
						tcs.TrySetException(new ServiceRequestException((int)task.Result.StatusCode,
							status.Code, status.Message));
					});
				}
				else tcs.TrySetException(new ServiceRequestException(
					(int)task.Result.StatusCode, -1, task.Result.ReasonPhrase));
			}
			else if (task.IsFaulted) tcs.TrySetException(task.Exception);
			else if (task.IsCompleted) tcs.TrySetResult(task.Result);
		}

		private static HttpClient CreateHttpClient()
		{
			var client = new HttpClient(new HttpClientHandler
			{
				AllowAutoRedirect = false,
				PreAuthenticate = true,
				UseDefaultCredentials = true,
				UseCookies = false,
				AutomaticDecompression =  DecompressionMethods.GZip
			});
			client.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json"));
			return client;
		}

		private Uri CreateRequestUri(string cmd, IDictionary<string, object> parameters)
		{
			var sb = new System.Text.StringBuilder();
			sb.AppendFormat(BaseUrl, cmd);

			if (parameters != null)
			{
				sb.Append(parameters.Where(s => s.Value != null)
					.Select(s => String.Concat(s.Key, "=", ConvertParameterValue(s.Value)))
					.Aggregate((s, n) => String.Concat(s, "&", n)));
			}
#if DEBUG
			System.Diagnostics.Debug.WriteLine(sb);
#endif
			return new Uri(sb.ToString(), UriKind.Absolute);
		}

		private static string ConvertParameterValue(object value)
		{
			Type t = value.GetType();
			t = Nullable.GetUnderlyingType(t) ?? t;
			
			if (t == typeof(DateTime)) return ((DateTime)value).ToString("yyyy-MM-dd");
			else if (t == typeof(Decimal)) return ((Decimal)value).ToString(CultureInfo.InvariantCulture);
			else return Uri.EscapeDataString(value.ToString());
		}

		#endregion

		#region Nested Classes

		private sealed class MovieContext : IMovieInfo
		{
			private ServiceClient client;

			internal MovieContext(ServiceClient client)
			{
				this.client = client;
			}

			public async Task<FindResult> FindAsync(string id, string externalSource, CancellationToken cancellationToken)
			{
				string cmd = String.Format("find/{0}", id);
				var parameters = new Dictionary<string, object> { { "external_source", externalSource } };
				var response = await client.GetAsync(cmd, parameters, cancellationToken).ConfigureAwait(false);
				return await Deserialize<FindResult>(response);
			}

			public async Task<Movies> SearchAsync(string query, string language, bool includeAdult, int page, CancellationToken cancellationToken)
			{
				var parameters = new Dictionary<string, object>
				{
					{ "query", query },
					{ "page", page },
					{ "include_adult", includeAdult },
					{ "language", language }
				};
				var response = await client.GetAsync("search/movie", parameters, cancellationToken).ConfigureAwait(false);
				return await Deserialize<Movies>(response);
			}

			public async Task<Movies> DiscoverAsync(string language, bool includeAdult, int? year, DateTime? minimumDate, DateTime? maximumDate, int? voteCount, decimal? voteAverage, string genres, string companies, int page, CancellationToken cancellationToken)
			{
				var parameters = new Dictionary<string, object>
				{
					{ "page", page },
					{ "include_adult", includeAdult },
					{ "language", language },
					{ "year", year },
					{ "release_date.gte", minimumDate },
					{ "release_date.lte", maximumDate },
					{ "vote_count.gte", voteCount },
					{ "vote_average.gte", voteAverage },
					{ "with_genres", genres },
					{ "with_companies", companies },
				};
				var response = await client.GetAsync("discover/movie", parameters, cancellationToken).ConfigureAwait(false);
				return await Deserialize<Movies>(response);
			}

			public async Task<Movie> GetAsync(int id, string language, bool appendAll, CancellationToken cancellationToken)
			{
				string cmd = String.Format("movie/{0}", id);
				var parameters = new Dictionary<string, object> { { "language", language } };
				if (appendAll) parameters.Add("append_to_response", "alternative_titles,images,credits,keywords,releases,videos,translations,external_ids");

				var response = await client.GetAsync(cmd, parameters, cancellationToken).ConfigureAwait(false);
				return await Deserialize<Movie>(response);
			}

			public async Task<Images> GetImagesAsync(int id, string language, CancellationToken cancellationToken)
			{
				string cmd = String.Format("movie/{0}/images", id);
				var parameters = new Dictionary<string, object> { { "language", language } };
				var response = await client.GetAsync(cmd, parameters, cancellationToken).ConfigureAwait(false);
				return await Deserialize<Images>(response);
			}

			public async Task<MediaCredits> GetCreditsAsync(int id, CancellationToken cancellationToken)
			{<commit_msg>Changed instantiation of HttpClient inside ServiceClient<commit_after>﻿using Newtonsoft.Json;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using System.Net.Http;
using System.Net.Http.Headers;
using System.Threading;
using System.Threading.Tasks;

namespace System.Net.TMDb
{
	public sealed class ServiceClient : IDisposable
	{
		private readonly string baseUrl;
		private readonly HttpClient client;
		private bool disposed = false;

		#region Constructors

		public ServiceClient(string apiKey)
		{
			this.baseUrl = String.Concat(@"http://api.themoviedb.org/3/{0}?api_key=", apiKey, "&");
			this.client = new HttpClient(new HttpClientHandler
			{
				AllowAutoRedirect = false,
				PreAuthenticate = true,
				UseDefaultCredentials = true,
				UseCookies = false,
				AutomaticDecompression =  DecompressionMethods.GZip
			});
			this.client.DefaultRequestHeaders.Accept.Add(
				new MediaTypeWithQualityHeaderValue("application/json"));

			this.Movies = new MovieContext(this);
			this.Shows = new ShowsContext(this);
			this.Companies = new CompanyContext(this);
			this.Genres = new GenreContext(this);
			this.People = new PeopleContext(this);
			this.Collections = new CollectionContext(this);
			this.Lists = new ListContext(this);
			this.Reviews = new ReviewContext(this);
			this.Settings = new SystemContext(this);
		}

		#endregion

		public IMovieInfo Movies { get; private set; }

		public IShowInfo Shows { get; private set; }

		public ICompanyInfo Companies { get; private set; }

		public IGenreInfo Genres { get; private set; }

		public IPeopleInfo People { get; private set; }

		public ICollectionInfo Collections { get; private set; }

		public IListInfo Lists { get; private set; }

		public IReviewInfo Reviews { get; private set; }

		public ISystemInfo Settings { get; private set; }

		#region Disposal Implementation
		
		public void Dispose()
		{
			Dispose(true);
			GC.SuppressFinalize(this);
		}
		
		private void Dispose(bool disposing)
		{
			if(!this.disposed)
			{
				if (disposing) // dispose aggregated resources
					this.client.Dispose();
				this.disposed = true; // disposing has been done
			}
		}
		
		#endregion
		
		#region Authentication Methods

		/// <summary>
		/// This method is used to generate a valid request token and authenticate user with a TMDb username and password.
		/// </summary>
		public async Task<string> LoginAsync(string username, string password, CancellationToken cancellationToken)
		{
			var response = await GetAsync("authentication/token/new", null, cancellationToken).ConfigureAwait(false);
			var parameters = new Dictionary<string, object>
			{
				{ "request_token", (await Deserialize<AuthenticationResult>(response)).Token },
				{ "username", username },
				{ "language", password }
			};
			response = await GetAsync("authentication/token/validate_with_login", parameters, cancellationToken).ConfigureAwait(false);
			return (await Deserialize<AuthenticationResult>(response)).Token;
		}

		/// <summary>
		/// This method is used to generate a session id for user based authentication, or a guest session if a null token is used.
		/// A session id is required in order to use any of the write methods.
		/// </summary>
		public async Task<string> GetSessionAsync(string token, CancellationToken cancellationToken)
		{
			return await (token == null ? OpenGuestSessionAsync(cancellationToken) : OpenSessionAsync(token, cancellationToken));
		}

		private async Task<string> OpenSessionAsync(string token, CancellationToken cancellationToken)
		{
			var parameters = new Dictionary<string, object> { { "request_token", token } };
			var response = await GetAsync("authentication/session/new", parameters, cancellationToken).ConfigureAwait(false);
			return (await Deserialize<AuthenticationResult>(response)).Session;
		}

		private async Task<string> OpenGuestSessionAsync(CancellationToken cancellationToken)
		{
			var response = await GetAsync("authentication/guest_session/new", null, cancellationToken).ConfigureAwait(false);
			return (await Deserialize<AuthenticationResult>(response)).Guest;
		}

		#endregion

		#region Request Handling Methods

		private Task<HttpResponseMessage> GetAsync(string cmd, IDictionary<string, object> parameters, CancellationToken cancellationToken)
		{
			TaskCompletionSource<HttpResponseMessage> tcs = new TaskCompletionSource<HttpResponseMessage>();

			this.client.GetAsync(CreateRequestUri(cmd, parameters),
				HttpCompletionOption.ResponseHeadersRead, cancellationToken)
				.ContinueWith(t => HandleResponseCompletion(t, tcs));
			return tcs.Task;
		}

		private Task<HttpResponseMessage> SendAsync(string cmd, IDictionary<string, object> parameters, HttpContent content, HttpMethod method, CancellationToken cancellationToken)
		{
			TaskCompletionSource<HttpResponseMessage> tcs = new TaskCompletionSource<HttpResponseMessage>();
			var request = new HttpRequestMessage(method, CreateRequestUri(cmd, parameters)) { Content = content };

			this.client.SendAsync(request, cancellationToken)
				.ContinueWith(t => HandleResponseCompletion(t, tcs));
			return tcs.Task;
		}

		private static async Task<T> Deserialize<T>(HttpResponseMessage response)
		{
			string responseJson = await response.Content.ReadAsStringAsync().ConfigureAwait(false);
#if DEBUG
			System.Diagnostics.Debug.WriteLine(responseJson);
#endif
			return JsonConvert.DeserializeObject<T>(responseJson);
		}

		private static async Task<dynamic> DeserializeDynamic(HttpResponseMessage response)
		{
			string responseJson = await response.Content.ReadAsStringAsync().ConfigureAwait(false);
#if DEBUG
			System.Diagnostics.Debug.WriteLine(responseJson);
#endif
			return JsonConvert.DeserializeObject(responseJson);
		}

		private static void HandleResponseCompletion(Task<HttpResponseMessage> task, TaskCompletionSource<HttpResponseMessage> tcs)
		{
			if (task.IsCanceled) tcs.TrySetCanceled();
			else if (!task.Result.IsSuccessStatusCode)
			{
				if (task.Result.Content != null)
				{
					task.Result.Content.ReadAsStringAsync().ContinueWith(t2 =>
					{
						var status = JsonConvert.DeserializeObject<Status>(t2.Result);
						tcs.TrySetException(new ServiceRequestException((int)task.Result.StatusCode,
							status.Code, status.Message));
					});
				}
				else tcs.TrySetException(new ServiceRequestException(
					(int)task.Result.StatusCode, -1, task.Result.ReasonPhrase));
			}
			else if (task.IsFaulted) tcs.TrySetException(task.Exception);
			else if (task.IsCompleted) tcs.TrySetResult(task.Result);
		}

		private Uri CreateRequestUri(string cmd, IDictionary<string, object> parameters)
		{
			var sb = new System.Text.StringBuilder();
			sb.AppendFormat(baseUrl, cmd);

			if (parameters != null)
			{
				sb.Append(parameters.Where(s => s.Value != null)
					.Select(s => String.Concat(s.Key, "=", ConvertParameterValue(s.Value)))
					.Aggregate((s, n) => String.Concat(s, "&", n)));
			}
#if DEBUG
			System.Diagnostics.Debug.WriteLine(sb);
#endif
			return new Uri(sb.ToString(), UriKind.Absolute);
		}

		private static string ConvertParameterValue(object value)
		{
			Type t = value.GetType();
			t = Nullable.GetUnderlyingType(t) ?? t;
			
			if (t == typeof(DateTime)) return ((DateTime)value).ToString("yyyy-MM-dd");
			else if (t == typeof(Decimal)) return ((Decimal)value).ToString(CultureInfo.InvariantCulture);
			else return Uri.EscapeDataString(value.ToString());
		}

		#endregion

		#region Nested Classes

		private sealed class MovieContext : IMovieInfo
		{
			private ServiceClient client;

			internal MovieContext(ServiceClient client)
			{
				this.client = client;
			}

			public async Task<FindResult> FindAsync(string id, string externalSource, CancellationToken cancellationToken)
			{
				string cmd = String.Format("find/{0}", id);
				var parameters = new Dictionary<string, object> { { "external_source", externalSource } };
				var response = await client.GetAsync(cmd, parameters, cancellationToken).ConfigureAwait(false);
				return await Deserialize<FindResult>(response);
			}

			public async Task<Movies> SearchAsync(string query, string language, bool includeAdult, int page, CancellationToken cancellationToken)
			{
				var parameters = new Dictionary<string, object>
				{
					{ "query", query },
					{ "page", page },
					{ "include_adult", includeAdult },
					{ "language", language }
				};
				var response = await client.GetAsync("search/movie", parameters, cancellationToken).ConfigureAwait(false);
				return await Deserialize<Movies>(response);
			}

			public async Task<Movies> DiscoverAsync(string language, bool includeAdult, int? year, DateTime? minimumDate, DateTime? maximumDate, int? voteCount, decimal? voteAverage, string genres, string companies, int page, CancellationToken cancellationToken)
			{
				var parameters = new Dictionary<string, object>
				{
					{ "page", page },
					{ "include_adult", includeAdult },
					{ "language", language },
					{ "year", year },
					{ "release_date.gte", minimumDate },
					{ "release_date.lte", maximumDate },
					{ "vote_count.gte", voteCount },
					{ "vote_average.gte", voteAverage },
					{ "with_genres", genres },
					{ "with_companies", companies },
				};
				var response = await client.GetAsync("discover/movie", parameters, cancellationToken).ConfigureAwait(false);
				return await Deserialize<Movies>(response);
			}

			public async Task<Movie> GetAsync(int id, string language, bool appendAll, CancellationToken cancellationToken)
			{
				string cmd = String.Format("movie/{0}", id);
				var parameters = new Dictionary<string, object> { { "language", language } };
                if (appendAll) parameters.Add("append_to_response", "alternative_titles,images,credits,keywords,releases,videos,translations,reviews,external_ids");

				var response = await client.GetAsync(cmd, parameters, cancellationToken).ConfigureAwait(false);
				return await Deserialize<Movie>(response);
			}

			public async Task<Images> GetImagesAsync(int id, string language, CancellationToken cancellationToken)
			{
				string cmd = String.Format("movie/{0}/images", id);
				var parameters = new Dictionary<string, object> { { "language", language } };
				var response = await client.GetAsync(cmd, parameters, cancellationToken).ConfigureAwait(false);
				return await Deserialize<Images>(response);
			}

			public async Task<MediaCredits> GetCreditsAsync(int id, CancellationToken cancellationToken)
			{
{'fraction_non_alphanumeric': 0.1094105681212767, 'fraction_numerical': 0.0007749309827093524, 'mean_word_length': 4.606299212598425, 'pattern_counts': {'":': 0, '<': 97, '<?xml version=': 0, '>': 109, 'https://': 0, 'lorem ipsum': 0, 'www.': 0, 'xml': 0}, 'pii_count': 0, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 0, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'dir': 'git-commits-cleaned', 'id': '2352092', 'n_tokens_mistral': 6735, 'n_tokens_neox': 5720, 'n_words': 1416}