from Globals import Globals
from Kobo import Kobo, KoboException

import colorama

import os

class Commands:
	# It wasn't possible to format the main help message to my liking, so using a custom one.
	# This was the most annoying:
	#
	# commands:
	#   command <-- absolutely unneeded text
	#     get     List unread books
	#     list    Get book
	#
	# See https://stackoverflow.com/questions/13423540/ and https://stackoverflow.com/questions/11070268/
	@staticmethod
	def ShowUsage():
		usage = \
"""Kobo book downloader and DRM remover

Usage:
  kobo-book-downloader [--help] command ...

Commands:
  get      Download book
  info     Show the location of the configuration file
  list     List your books
  pick     Download books using interactive selection
  wishlist List your wish listed books

Optional arguments:
  -h, --help    Show this help message and exit

Examples:
  kobo-book-downloader get /dir/book.epub 01234567-89ab-cdef-0123-456789abcdef   Download book
  kobo-book-downloader get /dir/ 01234567-89ab-cdef-0123-456789abcdef            Download book and name the file automatically
  kobo-book-downloader get /dir/ --all                                           Download all your books
  kobo-book-downloader info                                                      Show the location of the program's configuration file
  kobo-book-downloader list                                                      List your unread books
  kobo-book-downloader list --all                                                List all your books
  kobo-book-downloader list --help                                               Get additional help for the list command (it works for get and pick too)
  kobo-book-downloader pick /dir/                                                Interactively select unread books to download
  kobo-book-downloader pick /dir/ --all                                          Interactively select books to download
  kobo-book-downloader wishlist                                                  List your wish listed books"""

		print( usage )

	@staticmethod
	def __GetBookAuthor( book: dict ) -> str:
		contributors = book.get( "ContributorRoles" )

		authors = []
		for contributor in contributors:
			role = contributor.get( "Role" )
			if role == "Author":
				authors.append( contributor[ "Name" ] )

		# Unfortunately the role field is not filled out in the data returned by the "library_sync" endpoint, so we only
		# use the first author and hope for the best. Otherwise we would get non-main authors too. For example Christopher
		# Buckley beside Joseph Heller for the -- terrible -- novel Catch-22.
		if len( authors ) == 0 and len( contributors ) > 0:
			authors.append( contributors[ 0 ][ "Name" ] )

		return " & ".join( authors )

	@staticmethod
	def __SanitizeFileName( fileName: str ) -> str:
		result = ""
		for c in fileName:
			if c.isalnum() or " ,;.!(){}[]#$'-+@_".find( c ) >= 0:
				result += c

		result = result.strip( " ." )
		result = result[ :100 ] # Limit the length -- mostly because of Windows. It would be better to do it on the full path using MAX_PATH.
		return result

	@staticmethod
	def __MakeFileNameForBook( book: dict ) -> str:
		fileName = ""

		author = Commands.__GetBookAuthor( book )
		if len( author ) > 0:
			fileName = author + " - "

		fileName += book[ "Title" ]
		fileName = Commands.__SanitizeFileName( fileName )
		fileName += ".epub"

		return fileName

	@staticmethod
	def __IsBookArchived( newEntitlement: dict ) -> bool:
		bookEntitlement = newEntitlement.get( "BookEntitlement" )
		if bookEntitlement is None:
			return False

		isRemoved = bookEntitlement.get( "IsRemoved" )
		if isRemoved is None:
			return False

		return isRemoved

	@staticmethod
	def __GetBook( revisionId: str, outputPath: str ) -> None:
		if os.path.isdir( outputPath ):
			book = Globals.Kobo.GetBookInfo( revisionId )
			fileName = Commands.__MakeFileNameForBook( book )
			outputPath = os.path.join( outputPath, fileName )
		else:
			parentPath = os.path.dirname( outputPath )
			if not os.path.isdir( parentPath ):
				raise KoboException( "The parent directory ('%s') of the output file must exist." % parentPath )

		print( "Downloading book to '%s'." % outputPath )
		Globals.Kobo.Download( revisionId, Kobo.DisplayProfile, outputPath )

	@staticmethod
	def __GetAllBooks( outputPath: str ) -> None:
		if not os.path.isdir( outputPath ):
			raise KoboException( "The output path must be a directory when downloading all books." )

		bookList = Globals.Kobo.GetMyBookList()

		for entitlement in bookList:
			newEntitlement = entitlement.get( "NewEntitlement" )
			if newEntitlement is None:
				continue

			bookMetadata = newEntitlement[ "BookMetadata" ]
			fileName = Commands.__MakeFileNameForBook( bookMetadata )
			outputFilePath = os.path.join( outputPath, fileName )

			# Skip archived books.
			if Commands.__IsBookArchived( newEntitlement ):
				title = bookMetadata[ "Title" ]
				author = Commands.__GetBookAuthor( bookMetadata )
				if len( author ) > 0:
					title += " by " + author

				print( colorama.Fore.LIGHTYELLOW_EX + ( "Skipping archived book %s." % title ) + colorama.Fore.RESET )
				continue

			print( "Downloading book to '%s'." % outputFilePath )
			Globals.Kobo.Download( bookMetadata[ "RevisionId" ], Kobo.DisplayProfile, outputFilePath )

	@staticmethod
	def GetBookOrBooks( revisionId: str, outputPath: str, getAll: bool ) -> None:
		revisionIdIsSet = ( revisionId is not None ) and len( revisionId ) > 0

		if getAll:
			if revisionIdIsSet:
				raise KoboException( "Got unexpected book identifier parameter ('%s')." % revisionId )

			Commands.__GetAllBooks( outputPath )
		else:
			if not revisionIdIsSet:
				raise KoboException( "Missing book identifier parameter. Did you mean to use the --all parameter?" )

			Commands.__GetBook( revisionId, outputPath )

	@staticmethod
	def __IsBookRead( newEntitlement: dict ) -> bool:
		readingState = newEntitlement.get( "ReadingState" )
		if readingState is None:
			return False

		statusInfo = readingState.get( "StatusInfo" )
		if statusInfo is None:
			return False

		status = statusInfo.get( "Status" )
		return status == "Finished"

	@staticmethod
	def __GetBookList( listAll: bool ) -> list:
		bookList = Globals.Kobo.GetMyBookList()
		rows = []

		for entitlement in bookList:
			newEntitlement = entitlement.get( "NewEntitlement" )
			if newEntitlement is None:
				continue

			bookEntitlement = newEntitlement.get( "BookEntitlement" )
			if bookEntitlement is not None:
				# Skip saved previews.
				if bookEntitlement.get( "Accessibility" ) == "Preview":
					continue

				# Skip refunded books.
				if bookEntitlement.get( "IsLocked" ):
					continue

			if ( not listAll ) and Commands.__IsBookRead( newEntitlement ):
				continue

			bookMetadata = newEntitlement[ "BookMetadata" ]
			book = [ bookMetadata[ "RevisionId" ],
				bookMetadata[ "Title" ],
				Commands.__GetBookAuthor( bookMetadata ),
				Commands.__IsBookArchived( newEntitlement ) ]
			rows.append( book )

		rows = sorted( rows, key = lambda columns: columns[ 1 ].lower() )
		return rows

	@staticmethod
	def ListBooks( listAll: bool ) -> None:
		colorama.init()

		rows = Commands.__GetBookList( listAll )
		for columns in rows:
			revisionId = colorama.Style.DIM + columns[ 0 ] + colorama.Style.RESET_ALL
			title = colorama.Style.BRIGHT + columns[ 1 ] + colorama.Style.RESET_ALL

			author = columns[ 2 ]
			if len( author ) > 0:
				title += " by " + author

			archived = columns[ 3 ]
			if archived:
				title += colorama.Fore.LIGHTYELLOW_EX + " (archived)" + colorama.Fore.RESET

			print( "%s \t %s" % ( revisionId, title ) )

	@staticmethod
	def __ListBooksToPickFrom( rows: list ) -> None:
		longestIndex = len( "%d" % len( rows ) )

		for index, columns in enumerate( rows ):
			alignedIndexText = str( index + 1 ).rjust( longestIndex, ' ' )

			title = colorama.Style.BRIGHT + columns[ 1 ] + colorama.Style.RESET_ALL

			author = columns[ 2 ]
			if len( author ) > 0:
				title += " by " + author

			archived = columns[ 3 ]
			if archived:
				title += colorama.Fore.LIGHTYELLOW_EX + " (archived)" + colorama.Fore.RESET

			print( "%s. %s" % ( alignedIndexText, title ) )

	@staticmethod
	def __GetPickedBookRows( rows: list ) -> list:
		print( """\nEnter the number of the book(s) to download. Use comma or space to list multiple. Enter "all" to download all of them.""" )
		indexText = input( "Books: " )

		if indexText == "all":
			return rows

		indexList = indexText.replace( " ", "," ).split( "," )
		rowsToDownload = []

		for indexText in indexList:
			try:
				index = int( indexText.strip() ) - 1
				if index >= 0 and index < len( rows ):
					rowsToDownload.append( rows[ index ] )
			except Exception:
				pass

		return rowsToDownload

	@staticmethod
	def __DownloadPickedBooks( outputPath: str, rows: list ) -> None:
		for columns in rows:
			revisionId = columns[ 0 ]
			title = columns[ 1 ]
			author = columns[ 2 ]
			archived = columns[ 3 ]

			if archived:
				if len( author ) > 0:
					title += " by " + author

				print( colorama.Fore.LIGHTYELLOW_EX + ( "Skipping archived book %s." % title ) + colorama.Fore.RESET )
			else:
				Commands.GetBookOrBooks( revisionId, outputPath, False )

	@staticmethod
	def PickBooks( outputPath: str, listAll: bool ) -> None:
		colorama.init()

		rows = Commands.__GetBookList( listAll )
		Commands.__ListBooksToPickFrom( rows )
		rowsToDownload = Commands.__GetPickedBookRows( rows )
		Commands.__DownloadPickedBooks( outputPath, rowsToDownload )

	@staticmethod
	def ListWishListedBooks() -> None:
		colorama.init()

		rows = []

		wishList = Globals.Kobo.GetMyWishList()
		for wishListEntry in wishList:
			productMetadata = wishListEntry.get( "ProductMetadata" )
			if productMetadata is None:
				continue

			book = productMetadata.get( "Book" )
			if book is None:
				continue

			title = colorama.Style.BRIGHT + book[ "Title" ] + colorama.Style.RESET_ALL
			author = Commands.__GetBookAuthor( book )
			isbn = book.get( "ISBN", "" )

			row = title
			if len( author ) > 0:
				row += " by " + author
			if len( isbn ) > 0:
				row += " (ISBN: %s)" % isbn

			rows.append( row )

		rows = sorted( rows, key = lambda row: row.lower() )
		print( "\n".join( rows ) )

	@staticmethod
	def Info():
		print( "The configuration file is located at:\n%s" % Globals.Settings.SettingsFilePath )