# -*- coding: utf-8 -*-
# plugin.video.alkoflix
# resources/lib/modules/utils.py

import re
import time
import random
import hashlib
import unicodedata
import _strptime  # Contourne un bogue d'importation Python.
from queue import SimpleQueue
from html import unescape
from importlib import import_module
from datetime import datetime, timedelta, date
from modules.kodi_utils import local_string as ls, get_setting, logger
# from modules.kodi_utils import logger

# Correspondance entre les jours en anglais et les chaînes localisées de Kodi.
days_translate = {'Monday': 32971, 'Tuesday': 32972, 'Wednesday': 32973, 'Thursday': 32974, 'Friday': 32975, 'Saturday': 32976, 'Sunday': 32977}

def get_max_threads(value=None, default=80, minimum=1, maximum=100):
	"""Retourne une limite de threads sécurisée pour éviter de saturer Kodi."""
	try:
		threads = int(value if value is not None else get_setting('alkoflix.max_threads', str(default)))
	except Exception:
		threads = default
	return max(minimum, min(maximum, threads))

class TaskPool:
	"""
	Petit gestionnaire de tâches parallèles utilisé pour exécuter une liste
	d'éléments dans plusieurs threads, sans dépasser la limite configurée.
	"""
	@staticmethod
	def process(_threads):
		for index, i in enumerate(_threads, 1):
			try: i.start()
			except Exception as e: logger('thread error', f"{index}: {e}")
			else: yield i

	def __init__(self, maxsize=None):
		# Limite le nombre de threads afin d'éviter de saturer Kodi ou le système.
		self.maxsize = get_max_threads(maxsize)
		self._queue = SimpleQueue()

	def _thread_target(self, queue, target):
		# Chaque thread dépile une tâche et exécute la fonction cible avec ses arguments.
		while not queue.empty():
			try: target(*queue.get())
			except Exception as e: logger('queue error', f"{e}")

	def tasks(self, _target, _list, _thread):
		maxsize = min(len(_list), self.maxsize)
		[self._queue.put(tag) for tag in _list]
		threads = [_thread(target=self._thread_target, args=(self._queue, _target)) for i in range(maxsize)]
		return list(self.process(threads))

	def tasks_enumerate(self, _target, _list, _thread):
		maxsize = min(len(_list), self.maxsize)
		[self._queue.put((p, tag)) for p, tag in enumerate(_list, 1)]
		threads = [_thread(target=self._thread_target, args=(self._queue, _target)) for i in range(maxsize)]
		return list(self.process(threads))

def manual_function_import(location, function_name):
	"""Importe dynamiquement une fonction à partir de son module."""
	return getattr(import_module(location), function_name)

def make_thread_list(_target, _list, _thread):
	"""Crée et démarre un thread pour chaque élément de la liste."""
	for item in _list:
		threaded_object = _thread(target=_target, args=(item,))
		threaded_object.start()
		yield threaded_object

def make_thread_list_enumerate(_target, _list, _thread):
	"""Crée et démarre un thread pour chaque élément avec son index."""
	for item_position, item in enumerate(_list):
		threaded_object = _thread(target=_target, args=(item_position, item))
		threaded_object.start()
		yield threaded_object

def chunks(item_list, limit):
	"""
	Découpe une liste en blocs successifs de taille maximale ``limit``.
	"""
	for i in range(0, len(item_list), limit): yield item_list[i:i + limit]

def string_to_float(string, default_return):
	"""
	Retire tous les caractères non numériques d'une chaîne et retourne un nombre flottant.
	Retourne ``default_return`` converti en flottant en cas de valeur invalide.
	"""
	try: return float(''.join(c for c in string if (c.isdigit() or c =='.')))
	except ValueError: return float(default_return)

def string_alphanum_to_num(string):
	"""
	Retire tous les caractères non numériques d'une chaîne.
	Retourne la chaîne originale en cas de valeur invalide.
	"""
	try: return ''.join(c for c in string if c.isdigit())
	except ValueError: return string

def jsondate_to_datetime(jsondate_object, resformat, remove_time=False):
	"""Convertit une date reçue en texte/JSON vers un objet ``datetime`` ou ``date``."""
	if remove_time: datetime_object = datetime_workaround(jsondate_object, resformat).date()
	else: datetime_object = datetime_workaround(jsondate_object, resformat)
	return datetime_object

def get_datetime(string=False, dt=False):
	"""Retourne la date courante sous forme d'objet, de texte ou de ``datetime`` complet."""
	d = datetime.now()
	if dt: return d
	if string: return d.strftime('%Y-%m-%d')
	return datetime.date(d)

def adjust_premiered_date(orig_date, adjust_hours):
	"""Ajuste une date de première diffusion selon un décalage horaire donné."""
	if not orig_date: return None, None
	# Heure de référence utilisée pour éviter les changements de date trop tôt dans la journée.
	orig_date += ' 20:00:00'
	datetime_object = jsondate_to_datetime(orig_date, '%Y-%m-%d %H:%M:%S')
	adjusted_datetime = datetime_object + timedelta(hours=adjust_hours)
	adjusted_string = adjusted_datetime.strftime('%Y-%m-%d')
	return adjusted_datetime.date(), adjusted_string

def make_day(today, date, date_format, use_words=True):
	"""Formate une date en utilisant, au besoin, les libellés Hier/Aujourd'hui/Demain."""
	day_diff = (date - today).days
	try: day = date.strftime(date_format)
	except ValueError: day = date.strftime('%Y-%m-%d')
	if use_words:
		if day_diff == -1: day = ls(32848).upper()
		elif day_diff == 0: day = ls(32849).upper()
		elif day_diff == 1: day = ls(32850).upper()
		elif 1 < day_diff < 7: day = ls(days_translate[date.strftime('%A')])
	return day

def subtract_dates(date1, date2):
	"""Retourne l'écart en jours entre deux dates."""
	return (date1 - date2).days
	return day

def datetime_workaround(data, str_format):
	"""Convertit une date avec solution de secours pour certains environnements Python/Kodi."""
	try: datetime_object = datetime.strptime(data, str_format)
	except: datetime_object = datetime(*(time.strptime(data, str_format)[0:6]))
	return datetime_object

def date_difference(current_date, compare_date, difference_tolerance, allow_postive_difference=False):
	"""Vérifie si deux dates respectent une tolérance d'écart en jours."""
	try:
		difference = subtract_dates(current_date, compare_date)
		if not allow_postive_difference and difference > 0: return False
		else: difference = abs(difference)
		if difference > difference_tolerance: return False
		return True
	except: return True

def calculate_age(born, str_format, died=None):
	''' born et died sont des chaînes de caractères, par exemple : '1972-05-28'. '''
	born = datetime_workaround(born, str_format)
	if not died: today = date.today()
	else: today = datetime_workaround(died, str_format)
	return today.year - born.year - ((today.month, today.day) < (born.month, born.day))

def batch_replace(s, replace_info):
	"""Applique une série de remplacements texte dans l'ordre fourni."""
	for r in replace_info:
		s = str(s).replace(r[0], r[1])
	return s

def clean_file_name(s, use_encoding=False, use_blanks=True):
	"""Nettoie une chaîne pour produire un nom de fichier plus sûr."""
	try:
		# Entités HTML et caractères encodés fréquemment rencontrés dans les titres.
		hex_entities = [['&#x26;', '&'], ['&#x27;', '\''], ['&#xC6;', 'AE'], ['&#xC7;', 'C'],
					['&#xF4;', 'o'], ['&#xE9;', 'e'], ['&#xEB;', 'e'], ['&#xED;', 'i'],
					['&#xEE;', 'i'], ['&#xA2;', 'c'], ['&#xE2;', 'a'], ['&#xEF;', 'i'],
					['&#xE1;', 'a'], ['&#xE8;', 'e'], ['%2E', '.'], ['&frac12;', '%BD'],
					['&#xBD;', '%BD'], ['&#xB3;', '%B3'], ['&#xB0;', '%B0'], ['&amp;', '&'],
					['&#xB7;', '.'], ['&#xE4;', 'A'], ['\xe2\x80\x99', '']]
		# Remplacements utilisés lorsqu'un encodage URL est souhaité.
		special_encoded = [['"', '%22'], ['*', '%2A'], ['/', '%2F'], [':', ','], ['<', '%3C'],
							['>', '%3E'], ['?', '%3F'], ['\\', '%5C'], ['|', '%7C']]

		# Remplacements utilisés lorsqu'on préfère neutraliser les caractères problématiques.
		special_blanks = [['"', ' '], ['/', ' '], [':', ''], ['<', ' '],
							['>', ' '], ['?', ' '], ['\\', ' '], ['|', ' '], ['%BD;', ' '],
							['%B3;', ' '], ['%B0;', ' '], ["'", ""], [' - ', ' '], ['.', ' '],
							['!', ''], [';', ''], [',', '']]
		s = batch_replace(s, hex_entities)
		if use_encoding: s = batch_replace(s, special_encoded)
		if use_blanks: s = batch_replace(s, special_blanks)
		s = s.strip()
	except: pass
	return s

def clean_title(title):
	"""Nettoie un titre pour faciliter les comparaisons et les recherches."""
	try:
		if not title: return
		title = title.lower()
		title = re.sub(r'&#(\d+);', '', title)
		title = re.sub(r'(&#[0-9]+)([^;^0-9]+)', '\\1;\\2', title)
		title = re.sub(r'(&#[0-9]+)([^;^0-9]+)', '\\1;\\2', title)
		title = title.replace('&quot;', '\"').replace('&amp;', '&')
		# Retire les blocs entre crochets/parenthèses, la ponctuation et les espaces.
		title = re.sub(r'\n|([\[({].+?[})\]])|([:;–\-"\',!_.?~$@])|\s', '', title)
	except: pass
	return title

def byteify(data, ignore_dicts=False):
	"""Convertit récursivement des chaînes unicode en UTF-8 pour l'ancien code Python 2."""
	try:
		if isinstance(data, unicode): return data.encode('utf-8')
		if isinstance(data, list): return [byteify(item, ignore_dicts=True) for item in data]
		if isinstance(data, dict) and not ignore_dicts:
			return dict([(byteify(key, ignore_dicts=True), byteify(value, ignore_dicts=True)) for key, value in data.iteritems()])
	except: pass
	return data

def normalize(txt):
	"""Supprime les caractères non ASCII d'une chaîne."""
	txt = re.sub(r'[^\x00-\x7f]',r'', txt)
	return txt

def safe_string(obj):
	"""Retourne une version texte sûre, même en présence de caractères problématiques."""
	try:
		try: return str(obj)
		except UnicodeEncodeError: return obj.encode('utf-8', 'ignore').decode('ascii', 'ignore')
		except: return ""
	except: return obj

def remove_accents(obj):
	"""Retire les accents d'une chaîne en conservant autant que possible les caractères de base."""
	try:
		try: obj = u'%s' % obj
		except: pass
		obj = ''.join(c for c in unicodedata.normalize('NFD', obj) if unicodedata.category(c) != 'Mn')
	except: pass
	return obj

def regex_from_to(text, from_string, to_string, excluding=True):
	"""Retourne le contenu trouvé entre deux marqueurs avec une expression régulière."""
	if excluding: r = re.search(r"(?i)" + from_string + r"([\S\s]+?)" + to_string, text).group(1)
	else: r = re.search(r"(?i)(" + from_string + r"[\S\s]+?" + to_string + ")", text).group(1)
	return r

def regex_get_all(text, start_with, end_with):
	"""Retourne toutes les portions de texte comprises entre deux marqueurs."""
	r = re.findall(r"(?i)(" + start_with + r"[\S\s]+?" + end_with + ")", text)
	return r

def replace_html_codes(txt):
	"""Décode les entités HTML courantes présentes dans une chaîne."""
	txt = re.sub(r"(&#[0-9]+)([^;^0-9]+)", "\\1;\\2", txt)
	txt = unescape(txt)
	txt = txt.replace("&quot;", "\"")
	txt = txt.replace("&amp;", "&")
	return txt

def gen_file_hash(file):
	"""Génère le hash MD5 d'un fichier."""
	try:
		md5_hash = hashlib.md5()
		with open(file, 'rb') as afile:
			buf = afile.read()
			md5_hash.update(buf)
			return md5_hash.hexdigest()
	except: pass

def sec2time(sec, n_msec=3):
	''' Convertit des secondes au format 'D days, HH:MM:SS.FFF'. '''
	if hasattr(sec,'__len__'): return [sec2time(s) for s in sec]
	m, s = divmod(sec, 60)
	h, m = divmod(m, 60)
	d, h = divmod(h, 24)
	if n_msec > 0: pattern = '%%02d:%%02d:%%0%d.%df' % (n_msec+3, n_msec)
	else: pattern = '%02d:%02d:%02d'
	if d == 0: return pattern % (h, m, s)
	return ('%d days, ' + pattern) % (d, h, m, s)

def released_key(item):
	"""Retourne une date de sortie/diffusion utilisable comme clé de tri."""
	if 'released' in item: return item['released'] or '2050-01-01'
	if 'first_aired' in item: return item['first_aired'] or '2050-01-01'
	return '2050-01-01'

def title_key(title, ignore_articles):
	"""Retourne un titre adapté au tri, avec option pour ignorer les articles anglais."""
	if not ignore_articles: return title
	try:
		if title is None: title = ''
		articles = ['the', 'a', 'an']
		match = re.match(r'^((\w+)\s+)', title.lower())
		if match and match.group(2) in articles: offset = len(match.group(1))
		else: offset = 0
		return title[offset:]
	except: return title

def sort_for_article(_list, _key, ignore_articles):
	"""Trie une liste en ignorant optionnellement les articles anglais en début de titre."""
	if not ignore_articles: _list.sort(key=lambda k: k[_key])
	else: _list.sort(key=lambda k: re.sub(r'(^the |^a |^an )', '', k[_key].lower()))
	return _list

def sort_list(sort_key, sort_direction, list_data, ignore_articles):
	"""Trie une liste de médias selon la clé et la direction demandées."""
	try:
		reverse = sort_direction != 'asc'
		if sort_key == 'rank': return sorted(list_data, key=lambda x: x['rank'], reverse=reverse)
		elif sort_key == 'added': return sorted(list_data, key=lambda x: x['listed_at'], reverse=reverse)
#		elif sort_key == 'title': return sorted(list_data, key=lambda x: title_key(x[x['type']].get('title'), ignore_articles), reverse=reverse)
		elif sort_key == 'title': return sorted(list_data, key=lambda x: title_key(x['movie' if x['type'] == 'movie' else 'show'].get('title'), ignore_articles), reverse=reverse)
		elif sort_key == 'released': return sorted(list_data, key=lambda x: released_key(x[x['type']]), reverse=reverse)
		elif sort_key == 'runtime': return sorted(list_data, key=lambda x: x[x['type']].get('runtime', 0), reverse=reverse)
		elif sort_key == 'popularity': return sorted(list_data, key=lambda x: x[x['type']].get('votes', 0), reverse=reverse)
		elif sort_key == 'percentage': return sorted(list_data, key=lambda x: x[x['type']].get('rating', 0), reverse=reverse)
		elif sort_key == 'votes': return sorted(list_data, key=lambda x: x[x['type']].get('votes', 0), reverse=reverse)
		elif sort_key == 'random': return sorted(list_data, key=lambda k: random.random())
		else: return list_data
	except: return list_data

def paginate_list(item_list, page, letter, limit=20):
	"""Découpe une liste en pages et peut démarrer à partir d'une lettre donnée."""
	def _get_start_index(letter):
		# Recherche le premier élément correspondant à la lettre, en tenant compte des articles anglais.
		if letter == 't':
			try:
				beginswith_tuple = ('s', 'the s', 'a s', 'an s')
				indexes = [i for i, v in enumerate(title_list) if v.startswith(beginswith_tuple)]
				start_index = indexes[-1:][0] + 1
			except: start_index = None
		else:
			beginswith_tuple = (letter, 'the %s' % letter, 'a %s' % letter, 'an %s' % letter)
			try: start_index = next(i for i, v in enumerate(title_list) if v.startswith(beginswith_tuple))
			except: start_index = None
		return start_index
	if not item_list:
		return [], 1
	if letter != 'None':
		from itertools import chain, zip_longest
		title_list = [i.get('title', '').lower() for i in item_list]
		start_list = [chr(i) for i in range(97, 123)]
		try: letter_index = start_list.index(letter)
		except ValueError: letter_index = 0
		# Réorganise l'alphabet pour trouver une lettre disponible autour de celle demandée.
		base_list = [element for element in list(chain.from_iterable([val for val in zip_longest(start_list[letter_index:], start_list[:letter_index][::-1])])) if element != None]
		start_index = None
		for i in base_list:
			start_index = _get_start_index(i)
			if start_index is not None: break
		if start_index is not None: item_list = item_list[start_index:]
		if not item_list:
			return [], 1
	pages = list(chunks(item_list, limit))
	total_pages = len(pages) or 1
	try: page = int(page)
	except: page = 1
	if page < 1: page = 1
	if page > len(pages):
		return [], total_pages
	return pages[page - 1], total_pages

def make_title_slug(name):
	"""Transforme un titre en slug simple utilisable dans une URL ou une clé interne."""
	name = name.strip()
	name = name.lower()
	name = re.sub('[^a-z0-9_]', '-', name)
	name = re.sub('--+', '-', name)
	return name
