import os, sys
import json
import cfscrape
import re
from ..excepts import IsStandalone, FetcherNotFound, EmptyChapter, DelayedRelease
from ..Pyscandl import Pyscandl
from ..fetchers import FetcherEnum
from ..fetchers.fetcher import StandaloneFetcher


class Controller:
	__doc__ = """
	Object responsible of the autodl part of the program, all the logic related to it passes here
	"""

	def __init__(self, output:str=".", quiet:bool=False, tiny:bool=False):
		"""
		Initializes this instance of the autodl controller.
		If there is no json database for autodl currently in existence a new one is created at ``pyscandl/modules/autodl/db.json``.

		:param output: location where the outputted scans should be stored
		:type output: str
		:param quiet: should the program not output any information about what it is doing in the console
		:type quiet: bool
		:param tiny: should the name of every downloaded scan be minified and only include the chapter number and the chapter title
		:type tiny: bool
		"""

		try:
			with open(f"{os.path.dirname(sys.modules['pyscandl.modules.autodl'].__file__)}/db.json", "r") as data:
				self.db = json.load(data)
		except FileNotFoundError:
			self.db = {}
		self.output = output
		self.quiet = quiet
		self.tiny = tiny
		self._re_mgdex_scan = re.compile(r"(?:Chapter \d+, )?Chapter (?P<chap>\d+(\.\d+)?)")
		self.scrapper = cfscrape.create_scraper()
		self.missing_chaps = []
		self.downloads = 0

	def save(self):
		"""
		Saves the current state of the database in the ``db.json`` file.
		"""

		with open(f"{os.path.dirname(sys.modules['pyscandl.modules.autodl'].__file__)}/db.json", "w") as data:
			json.dump(self.db, data, indent=4, sort_keys=True)

	def add(self, name:str, link:str, fetcher:str, chapters:list=None, archived=False):
		"""
		Adds a new scan entry to the ``db.json`` file.

		:param name: name of the manga
		:type name: str
		:param link: link to the page of the manga *(same link that is used for the -l arg in other uses of pyscandl)*
		:type link: str
		:param fetcher: name of the associated fetcher
		:type fetcher: str
		:param chapters: list of the already possessed chapters that wont be downloaded again *(Optional)*
		:type chapters: list[int/float/str]
		:param archived: tell if the chapter is considered archived and if it'll be downloaded with autodl
		:type archived: bool

		:raises FetcherNotFound: the specified fetcher doesn't exist
		:raises IsStandalone: the specified fetcher is a standalone fetcher
		"""

		if chapters is None:
			chapters = []

		if fetcher.upper() not in [i.name for i in FetcherEnum]:
			raise FetcherNotFound(fetcher)
		if fetcher.lower() in ["nhentai"]:
			raise IsStandalone(name)
		self.db[name] = {
			"link": link,
			"fetcher": fetcher.upper(),
			"chapters": sorted(chapters, reverse=True),
			"archived": archived
		}

	def edit(self, name:str, link:str=None, fetcher=None, chapters:list=None, archived=None):
		"""
		Edits an already existing entry in the ``db.json`` file.
		The :param name: is mandatory to find the correct entry and every other parameter specified will overwrite the existing values.

		:param name: name of the manga
		:type name: str
		:param link: link to the page of the manga *(same link that is used for the -l arg in other uses of pyscandl)*
		:type link: str
		:param fetcher: name of the associated fetcher
		:type fetcher: str
		:param chapters: list of the already possessed chapters that wont be downloaded again
		:type chapters: list[int/float/str]
		:param archived: tell if the chapter is considered archived and if it'll be downloaded with autodl
		:type archived: bool

		:raises IsStandalone: the specified fetcher is a standalone fetcher
		"""

		if link is not None:
			self.db.get(name)["link"] = link
		if fetcher is not None:
			standalone_check = FetcherEnum.get(fetcher)
			if issubclass(standalone_check, StandaloneFetcher):
				raise IsStandalone(name)
			self.db.get(name)["fetcher"] = fetcher
		if chapters is not None:
			self.db.get(name)["chapters"] = sorted(self.db.get(name)["chapters"] + chapters, reverse=True)
		if archived is not None:
			try:
				self.db.get(name)["archived"] = archived
			except TypeError:
				print(f"\"{name}\" isn't a manga in the database, you may consider adding it with manga add")

	# each website/fetcher can have differently made xml from their rss so we need to treat them separately if need be
	def scan(self, name:str):
		"""
		Scans the asked manga for new and non downloaded chapters and adds them to the controller queue.

		:param name: name of the manga
		:type name: str
		"""

		self.missing_chaps.clear()
		manga = self.db.get(name)

		manga_fetcher = FetcherEnum.get(manga.get("fetcher"))
		manga_chapters = manga_fetcher.scan(link=manga.get("link"))

		self.missing_chaps = list(set(manga_chapters) - set(manga.get("chapters")))

		self.missing_chaps.sort()
		if not self.quiet:
			if self.missing_chaps:
				print(f"new chapter(s) for {name}: {', '.join(map(str, self.missing_chaps))}")
			else:
				print(f"no new chapter for {name}")

	def download(self, name:str, pdf:bool=True, keep:bool=False, image:bool=False):
		"""
		Start the download of the chapters of the asked manga that have their number in the scan results.

		:param name: name of the manga
		:type name: str
		:param pdf: tell if the result should be kept as a pdf
		:type pdf: bool
		:param keep: tell if the result should be kept as a pdf and as a collection of images
		:type keep: bool
		:param image: tell if the result should be kept as a collection of images
		:type image: bool
		"""

		manga = self.db.get(name)
		fetcher = FetcherEnum.get(manga.get("fetcher"))

		# initialize to the first downloadable chapter and download it
		ok = False
		for chapter_id in range(len(self.missing_chaps)):
			try:
				downloader = Pyscandl(fetcher, self.missing_chaps[chapter_id], self.output, link=manga.get("link"), pdf=pdf, image=image, keep=keep, quiet=self.quiet, tiny=self.tiny)

				bad_image = True
				while bad_image:  # protect against bad downloads
					try:
						if keep or image:
							downloader.keep_full_chapter()
						elif pdf:
							downloader.full_chapter()
						if not image:
							downloader.create_pdf()
						bad_image = False
					except IOError:
						print(f"problem during download, retrying {name} chapter {self.missing_chaps[chapter_id]}")
						downloader.go_to_chapter(self.missing_chaps[chapter_id])

				self.db.get(name).get("chapters").append(self.missing_chaps[chapter_id])
				self.downloads += 1

				self.missing_chaps = self.missing_chaps[chapter_id+1:]
				ok = True
				break
			except EmptyChapter:
				if not self.quiet:
					print(f"skipping {name} chapter {self.missing_chaps[chapter_id]}: empty, wont be added in the downloaded list")
			except DelayedRelease as e:
				if not self.quiet:
					print(e)

		# if chapters are left to doawnload proceeds with it
		if ok:
			for chapter_id in range(len(self.missing_chaps)):
				try:
					bad_image = True
					while bad_image:  # protect against bad downloads
						try:
							downloader.go_to_chapter(self.missing_chaps[chapter_id])

							if keep or image:
								downloader.keep_full_chapter()
							else:
								downloader.full_chapter()
							if not image:
								downloader.create_pdf()
							bad_image = False
						except IOError:
							print(f"problem during download, retrying {name} chapter {self.missing_chaps[chapter_id]}")

					self.db.get(name).get("chapters").append(self.missing_chaps[chapter_id])
					self.downloads += 1
				except EmptyChapter:
					if not self.quiet:
						print(f"skipping {name} chapter {self.missing_chaps[chapter_id]}: empty, wont be added in the downloaded list")
				except DelayedRelease as e:
					if not self.quiet:
						print(e)

			downloader.fetcher.quit()
			self.db.get(name).get("chapters").sort(reverse=True)

		# remove the directory if there is no chapter
		try:
			folders = list(os.walk(self.output))[1:]
			for folder in folders:
				if not folder[2]:
					os.rmdir(folder[0])
		except OSError:
			pass

	def list_mangas(self, all=False, only=False):
		"""
		Gives the list of all the names of the mangas in the ``db.json`` file. if the db is empty, returns None

		:param all: get also the archived mangas
		:type all: bool
		:param only: get only the archived mangas
		:type only: bool

		:rtype: list
		"""

		titles = []

		if not self.db:
			return None

		max_len = len(max(self.db.keys(), key=lambda x: len(x)))
		for title in self.db.keys():
			if self.db[title]["archived"] and (all or only):
				titles.append("{title:<{size}}|{}".format("    Archived", title=title, size=max_len + 4))
			elif not self.db[title]["archived"] and not only:
				titles.append("{title:<{size}}|".format(title=title, size=max_len + 4))
		return titles

	def manga_info(self, name):
		"""
		Fet the infos about a specific manga.

		:param name: name of the manga
		:type name: str

		:rtype: dict
		"""

		return self.db.get(name)

	def delete_manga(self, name):
		"""
		Deletes a manga from the ``db.json`` file.

		:param name: name of the manga

		:return: confirms the deletion
		:rtype: bool
		"""
		if name in self.db:
			del self.db[name]
			return True
		else:
			return False

	def rm_chaps(self, name, *rm_chaps):
		"""
		Remove the listed chapters from the asked manga

		:param name: name of he manga
		:type name: str
		:param rm_chaps: list of all the chapters that have to be removed
		:type rm_chaps: str

		:return: confirms the deletion
		:rtype: bool
		"""
		if name in self.db:
			self.db.get(name)["chapters"] = [chap for chap in self.db.get(name)["chapters"] if not chap not in rm_chaps]
			return True
		else:
			return False

	def db_import(self, path:str):
		"""
		Takes an external json file path and put its content as the new database for pyscandl.

		:param path: path to the .json file to import as the database for autodl and manga
		:type path: str
		"""

		with open(path, "r") as data:
			self.db = json.load(data)

	def db_export(self, path:str):
		"""
		Saves a copy of the current database to the file path specified.

		:param path: path to the save location, may be either a file or a folder, if it is a folder the filename will be db.json
		:type path: str

		:raises TypeError: if you specify the file name in the destination, the file extension must be .json
		"""

		if os.path.splitext(path)[1]:  # check if the path includes the filename
			if os.path.splitext(path)[1] != ".json":
				raise TypeError("the file must be a .json file !")

			path, filename = os.path.split(path)

		else:
			filename = "db.json"

		if not os.path.exists(path):
			os.makedirs(path)

		with open(os.path.join(path, filename), "w") as data:
			json.dump(self.db, data, indent=4, sort_keys=True)
