#!/usr/bin/env python3
# -*- coding: utf-8 -*-
#
# Copyright (C) 1998-2026 Stephane Galland <galland@arakhne.org>
#
# This program is free library; you can redistribute it and/or modify
# it under the terms of the GNU Lesser General Public License as
# published by the Free Software Foundation; either version 3 of the
# License, or any later version.
#
# This library is distributed in the hope that it will be useful, but
# WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
# Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public
# License along with this library; see the file COPYING.  If not,
# write to the Free Software Foundation, Inc., 59 Temple Place - Suite
# 330, Boston, MA 02111-1307, USA.

"""
Tools that is extracting the dependencies of the TeX file.
"""

import os
import re
import json
from typing import override, Any, Callable, Sized

from autolatex2.tex.texobservers import Observer
from autolatex2.tex.texparsers import Parser
from autolatex2.tex.texparsers import TeXParser
from autolatex2.tex.utils import FileType, TeXMacroParameter
import autolatex2.utils.utilfunctions as genutils
import autolatex2.tex.extra_macros as extramacros

FULL_EXPAND_REGISTRY : dict[str, Callable[[Any, str, list[TeXMacroParameter]], None]] = dict()

EXTRA_FULL_EXPAND_REGISTRY : dict[str, Callable[[Any, str, list[TeXMacroParameter]], None]] = dict()

PREFIX_EXPAND_REGISTRY : dict[str, Callable[[Any, str, list[TeXMacroParameter]], None]] = dict()

EXTRA_PREFIX_EXPAND_REGISTRY : dict[str, Callable[[Any, str, list[TeXMacroParameter]], None]] = dict()

# noinspection DuplicatedCode
def expand_function(start_symbol : bool, extra_macro : bool = False):
	"""
	Decorator to register functions with __expand__ prefix.
	:param start_symbol: Marks the function as associated to the prefix of a LaTeX macro name.
	:type start_symbol: bool
	:param extra_macro: Marks the function as part of th supporting features for the extra macros. Default is False.
	:type extra_macro: bool
	:return: the decorator.
	"""
	def decorator(func: Callable) -> Callable:
		# Store the function and its metadata
		# Remove "_expand__" prefix
		if not func.__name__.startswith('_expand__'):
			raise NameError('Function name must start with \'_expand__\'')
		func_name = str(func.__name__)[9:]
		if start_symbol:
			if extra_macro:
				EXTRA_PREFIX_EXPAND_REGISTRY[func_name] = func
			else:
				PREFIX_EXPAND_REGISTRY[func_name] = func
		else:
			if extra_macro:
				EXTRA_FULL_EXPAND_REGISTRY[func_name] = func
			else:
				FULL_EXPAND_REGISTRY[func_name] = func
		return func
	return decorator


class DependencyDescription:
	"""
	Description of a dependency with all the interesting information.
	:param filename: The filename.
	:type filename: str
	:param file_type: The type of the dependency.
	:type file_type: FileType
	:param scope: The name of the scope that may be associated to the filename dependency.
	:type scope: str|None
	:param output_file: The name of the file that may be considered as an output. The type of file depends on file_type.
	For example, for a bib file, the output file may be a specific bbl file.
	:type output_file: str|None
	"""
	def __init__(self, filename : str, file_type : FileType, scope : str|None, output_file : str|None):
		self.__filename : str = filename
		self.__type : FileType = file_type
		self.__has_change : bool = False
		self.__change : float | None = None
		self.__scopes : set[str] = set()
		self.__output_files : list[str] = list()
		self.add_scope(scope)
		self.add_output_file(output_file)

	def __str__(self):
		return '->' + self.file_name

	def __repr__(self):
		json_content = {
			'file_name': self.file_name,
			'file_type': self.file_type,
			'scopes': self.scopes,
			'change': self.change,
			'output_files': self.output_files
		}
		return json.dumps(json_content,  indent = 4)

	@property
	def file_name(self) -> str:
		"""
		Replies the filename.
		:rtype: str
		"""
		return self.__filename

	@property
	def file_type(self) -> FileType:
		"""
		Replies the type of file.
		:rtype: FileType
		"""
		return self.__type

	@property
	def scopes(self) -> set[str]:
		"""
		Replies the scopes in which this file was defined as a dependency.
		:rtype: set[str]
		"""
		return self.__scopes

	def add_scope(self, scope : str|None):
		"""
		Add the given scope.
		:param scope: The name of the scope.
		:type scope: str|None
		"""
		if scope:
			self.__scopes.add(scope)

	def add_output_file(self, output_file : str|None):
		"""
		Add the given output file.
		:param output_file: The name of the output file if relevant.
		:type output_file: str|None
		"""
		if output_file:
			self.__output_files.append(output_file)

	@property
	def change(self) -> float | None:
		"""
		Replies the time of the last change for the file.
		:rtype: float
		"""
		if not self.__has_change:
			self.__has_change = True
			self.__change = genutils.get_file_last_change(self.file_name)
		return self.__change

	@property
	def output_files(self) -> list[str]:
		"""
		Replies the linked output file. For example, for a Bibtex file, it may be a specific BBL file.
		:rtype: list[str]
		"""
		return self.__output_files



class _TypeDependencyRepositoryDescriptionIterator:
	def __init__(self, parent_iterator):
		self.__parent_iterator = parent_iterator

	def __next__(self) -> DependencyDescription:
		name, description = self.__parent_iterator.__next__()
		return description




class TypeDependencyRepository(Sized):
	"""
	Repository of dependency descriptions of a specific type.
	"""
	def __init__(self, database : dict[str,DependencyDescription] = None):
		if database is None:
			self.__database : dict[str,DependencyDescription] = dict()
		else:
			self.__database: dict[str, DependencyDescription] = database
		self.__buffer_scopes : set[str] | None = None
		self.__buffer_new_database : dict[str,dict[str, DependencyDescription]] | None = None

	def __str__(self):
		return str(self.__database.keys())

	def __repr__(self):
		json_content = list()
		for dep in self:
			json_content.append(repr(dep))
		return json.dumps(json_content,  indent = 4)

	def __len__(self) -> int:
		return len(self.__database)

	def __contains__(self, item : str) -> bool:
		return item in self.__database

	def __iter__(self):
		return _TypeDependencyRepositoryDescriptionIterator(self.__database.items().__iter__())

	def __reset_buffers(self):
		self.__buffer_scopes = None
		self.__buffer_new_database = None

	def update(self, file_type : FileType, filename : str, scope : str|None = None, output_file : str|None = None) -> DependencyDescription:
		"""
		Add a dependency for the given file.
		:param file_type: The type of the dependency.
		:type file_type: FileType
		:param filename: The filename.
		:type filename: str
		:param scope: The name of the scope that may be associated to the filename dependency.
		:type scope: str|None
		:param output_file: The name of the file that may be considered as an output. The type of file depends on file_type.
		For example, for a bib file, the output file may be a specific bbl file.
		:type output_file: str|None
		:return: The dependency description.
		:rtype: DependencyDescription
		"""
		self.__reset_buffers()
		if filename not in self.__database:
			desc = DependencyDescription(filename, file_type, scope=scope, output_file=output_file)
			self.__database[filename] = desc
		else:
			desc = self.__database[filename]
			desc.add_scope(scope)
			desc.add_output_file(output_file)
		return desc

	def scope_to(self, scope : str) -> 'TypeDependencyRepository':
		"""
		Create a scopy of this repository with only the dependencies of the given scope.
		:param scope: The name of the scope.
		:return: the scoped repository.
		"""
		if self.__buffer_new_database is None:
			self.__buffer_new_database = dict()
		assert self.__buffer_new_database is not None
		if scope not in self.__buffer_new_database:
			self.__buffer_new_database[scope] = dict()
			for name, dep in self.__database.items():
				if scope in dep.scopes:
					self.__buffer_new_database[scope][name] = dep
		return TypeDependencyRepository(database=self.__buffer_new_database[scope])

	def get_scopes(self) -> set[str]:
		"""
		Replies the scopes that are defined in the repository.
		:return: the scopes.
		:rtype: set[str]
		"""
		if self.__buffer_scopes is None:
			self.__buffer_scopes = set()
			assert self.__buffer_scopes is not None
			for name, dep in self.__database.items():
				if dep.scopes:
					self.__buffer_scopes.update(dep.scopes)
		return self.__buffer_scopes



class _DependencyRepository(Sized):
	"""
	Repository of dependency descriptions.
	"""
	def __init__(self):
		self.__database : dict[FileType,TypeDependencyRepository] = dict()
		self.__buffer_bibliography_databases : set[str] | None = None

	def __str__(self):
		return str(self.__database.keys())

	def __repr__(self):
		return json.dumps(self.__database,  indent = 4)

	def __len__(self) -> int:
		return len(self.__database)

	def __reset_buffers(self):
		self.__buffer_bibliography_databases = None

	def update(self, file_type : FileType, filename : str, scope : str|None = None, output_file : str|None = None) -> DependencyDescription:
		"""
		Add a dependency for the given file, associated to the given type.
		:param file_type: The type of the dependency.
		:type file_type: FileType
		:param filename: The filename.
		:type filename: str
		:param scope: The name of the scope that may be associated to the filename dependency.
		:type scope: str|None
		:param output_file: The name of the file that may be considered as an output. The type of file depends on file_type.
		For example, for a bib file, the output file may be a specific bbl file.
		:type output_file: str|None
		:return: The dependency description.
		:rtype: DependencyDescription
		"""
		self.__reset_buffers()
		if file_type not in self.__database:
			self.__database[file_type] = TypeDependencyRepository()
		return self.__database[file_type].update(file_type, filename, scope=scope, output_file=output_file)

	@property
	def types(self) -> set[FileType]:
		"""
		Replies the types of files that have been stored in this repository.
		:return: the set of file types.
		:rtype: set[FileType]
		"""
		return set(self.__database.keys())

	def get_bibliography_scopes(self) -> set[str]:
		"""
		Replies the names of the bibliography scopes that have been detected.
		The scopes of the bibliographies are usually defined by the LaTeX multibib package.
		:return: the names of the bibliography database.
		:rtype: set[str]
		"""
		if self.__buffer_bibliography_databases is None:
			self.__buffer_bibliography_databases = set()
			assert self.__buffer_bibliography_databases is not None
			for btype in FileType.bibliography_types():
				if btype in self.__database:
					content = self.__database[btype]
					if content:
						self.__buffer_bibliography_databases.update(content.get_scopes())
		return self.__buffer_bibliography_databases

	def get_dependencies_for_type(self, dependency_type : FileType, scope : str|None = None) -> TypeDependencyRepository:
		"""
		Replies the dependencies of the given type.
		:param dependency_type: The type of the dependency (tex, bib, ...)
		:type dependency_type: FileType
		:param scope: The scope of the dependency to restrict to.
		:type scope: str|None
		:return: the set of dependencies. The replied dictionary maps the dependency filenames to their detailed descriptions
		:rtype: TypeDependencyRepository
		"""
		if dependency_type not in self.__database:
			self.__database[dependency_type] = TypeDependencyRepository()
		if scope:
			return self.__database[dependency_type].scope_to(scope)
		return self.__database[dependency_type]



class DependencyAnalyzer(Observer):
	"""
	Observer on TeX parsing for extracting the dependencies of the TeX file.
	"""

	__MACROS : dict[str,str] = {
		# TeX
		'input'						: '!{}',
		'include'					: '!{}',
		'usepackage'				: '![]!{}',
		'RequirePackage'			: '![]!{}',
		'documentclass'				: '![]!{}',
		# Index
		'makeindex'					: '',
		'printindex'				: '',
		# Glossaries
		'makeglossaries'			: '',
		'printglossaries'			: '',
		'newglossaryentry'			: '![]!{}',
		# BibTeX
		'addbibresource'			: '![]!{}',
		'begin'						: '![]!{}![]',
		'end'						: '!{}',
		'putbib'					: '![]',
		'defaultbibliography'		: '!{}',
		'defaultbibliographystyle'	: '!{}',
	}

	def __init__(self, filename : str, root_directory : str, main_filename : str, include_extra_macros : bool):
		"""
		Constructor.
		:param filename: The name of the file to parse.
		:type filename: str
		:param root_directory: The name of the root directory.
		:type root_directory: str
		:param main_filename: The name of the main TeX file that is used as the TeX entry point. This filename
		 may differ from those provided as the filename argument for this function.
		:type main_filename: str
		:param include_extra_macros: Indicates if the extra macros must be included in the dependency analysis.
		:type include_extra_macros: bool
		"""
		self.__include_extra_macros = include_extra_macros
		if self.__include_extra_macros:
			self.__full_expand_registry = FULL_EXPAND_REGISTRY | EXTRA_FULL_EXPAND_REGISTRY
			self.__prefix_expand_registry = PREFIX_EXPAND_REGISTRY | EXTRA_FULL_EXPAND_REGISTRY
		else:
			self.__full_expand_registry = FULL_EXPAND_REGISTRY
			self.__prefix_expand_registry = PREFIX_EXPAND_REGISTRY
		self.__is_multibib : bool = False
		self.__bibunit_index : int = 0
		self.__in_bibunit : bool = False
		self.__is_bibunits : bool = False
		self.__is_biblatex : bool = False
		self.__is_biber : bool = False
		self.__is_index : bool = False
		self.__is_xindy : bool = False
		self.__is_glossary : bool = False
		self.__dependency_repository : _DependencyRepository = _DependencyRepository()
		self.__filename : str = filename
		self.__basename : str = os.path.basename(os.path.splitext(filename)[0])
		self.__root_directory : str = root_directory
		self.__main_filename : str = main_filename
		self.__explicit_bibliography : bool = False
		self.__explicit_bibliography_style : bool = False
		self.__default_bibliography : dict[str,list[TeXMacroParameter]] = dict()
		self.__default_bibliography_style : dict[str,list[TeXMacroParameter]] = dict()

	@property
	def root_directory(self) -> str:
		"""
		Replies the root directory of the document.
		:return: The root directory of the document.
		:rtype: str
		"""
		return self.__root_directory

	@root_directory.setter
	def root_directory(self, d : str):
		"""
		Set the root directory of the document.
		:param d: The root directory of the document.
		:type d: str
		"""
		self.__root_directory = d

	@property
	def main_filename(self) -> str:
		"""
		Replies the filename of the main TeX document that serves as the entry point for the TeX tools.
		:return: The name of the main TeX document file.
		:rtype: str
		"""
		return self.__main_filename

	@main_filename.setter
	def main_filename(self, f : str):
		"""
		Set the filename of the main TeX document that serves as the entry point for the TeX tools.
		:param f: The name of the main TeX document file.
		:type f: str
		"""
		self.__main_filename = f

	@property
	def basename(self) -> str:
		"""
		Replies the basename of the document.
		:return: The basename  of the document.
		:rtype: str
		"""
		return self.__basename

	@basename.setter
	def basename(self, n : str):
		"""
		Set the basename of the document.
		:param n: The basename of the document.
		:type n: str
		"""
		self.__basename = n

	@property
	def filename(self) -> str:
		"""
		Replies the filename of the parsed file.
		:return: The filename of the parsed file.
		:rtype: str
		"""
		return self.__filename

	@filename.setter
	def filename(self, n : str):
		"""
		Set the filename of the parsed document.
		:param n: The filename of the parsed document.
		:type n: str
		"""
		self.__filename = n

	@property
	def is_multibib(self) -> bool:
		"""
		Replies the multibib support is enable
		:return: True if the multibib support is enabled.
		:rtype: bool
		"""
		return self.__is_multibib

	@is_multibib.setter
	def is_multibib(self, enable : bool):
		"""
		Set if the multibib support is enable
		:param enable: True if the multibib support is enabled.
		:type enable: bool
		"""
		self.__is_multibib = enable

	@property
	def is_bibunits(self) -> bool:
		"""
		Replies the Bibunits support is enable
		:return: True if the Bibunits support is enabled.
		:rtype: bool
		"""
		return self.__is_bibunits

	@is_bibunits.setter
	def is_bibunits(self, enable : bool):
		"""
		Set if the Bibunits support is enable
		:param enable: True if the Bibunits support is enabled.
		:type enable: bool
		"""
		self.__is_bibunits = enable

	@property
	def is_biblatex(self) -> bool:
		"""
		Replies the biblatex support is enable
		:return: True if the biblatex support is enabled.
		:rtype: bool
		"""
		return self.__is_biblatex

	@is_biblatex.setter
	def is_biblatex(self, enable : bool):
		"""
		Set if the biblatex support is enable
		:param enable: True if the biblatex support is enabled.
		:type enable: bool
		"""
		self.__is_biblatex = enable

	@property
	def is_biber(self) -> bool:
		"""
		Replies the biber support is enable
		:return: True if the biber support is enabled.
		:rtype: bool
		"""
		return self.__is_biber

	@is_biber.setter
	def is_biber(self, enable : bool):
		"""
		Set if the biber support is enable
		:param enable: True if the biber support is enabled.
		:type enable: bool
		"""
		self.__is_biber = enable

	@property
	def is_makeindex(self) -> bool:
		"""
		Replies the makeindex support is enable
		:return: True if the makeindex support is enabled.
		:rtype: bool
		"""
		return self.__is_index

	@is_makeindex.setter
	def is_makeindex(self, enable : bool):
		"""
		Set if the makeindex support is enable
		:param enable: True if the makeindex support is enabled.
		:type enable: bool
		"""
		self.__is_index = enable

	@property
	def is_xindy_index(self) -> bool:
		"""
		Replies if the support for xindy support is enable.
		This flag is considered only if is_makeindex is enabled.
		:return: True if the xindy support is enabled.
		:rtype: bool
		"""
		return self.__is_xindy

	@is_xindy_index.setter
	def is_xindy_index(self, enable : bool):
		"""
		Set if the support for xindy support is enable.
		This flag is considered only if is_makeindex is enabled.
		:param enable: True if the xindy support is enabled.
		:type enable: bool
		"""
		self.__is_xindy = enable

	@property
	def is_glossary(self) -> bool:
		"""
		Replies the glossary support is enable
		:return: True if the glossary support is enabled.
		:rtype: bool
		"""
		return self.__is_glossary

	@is_glossary.setter
	def is_glossary(self, enable : bool):
		"""
		Set if the glossary support is enable
		:param enable: True if the glossary support is enabled.
		:type enable: bool
		"""
		self.__is_glossary = enable

	def get_dependency_types(self) -> set[FileType]:
		"""
		Replies the dependency types.
		:return: the set of dependency types.
		:rtype: set[FileType]
		"""
		return self.__dependency_repository.types

	def get_dependencies_for_type(self, dependency_type : FileType, scope : str|None = None) -> TypeDependencyRepository:
		"""
		Replies the dependencies of the given type.
		:param dependency_type: The type of the dependency (tex, bib, ...)
		:type dependency_type: FileType
		:param scope: The scope of the dependency to restrict to.
		:type scope: str|None
		:return: the set of dependencies. The set of dependency names is the more used form. The dictionary of dictionary is usually used for bibliography dependencies.
		:rtype: TypeDependencyRepository
		"""
		return self.__dependency_repository.get_dependencies_for_type(dependency_type, scope)

	def get_bibliography_scopes(self) -> set[str]:
		"""
		Replies the names of the bibliography scopes that have been detected.
		:return: the names of the bibliography database.
		:rtype: set[str]
		"""
		return self.__dependency_repository.get_bibliography_scopes()

	def __extract_bibdb(self, macro_name_prefix : str, name : str) -> str:
		l = len(macro_name_prefix)
		if self.is_multibib:
			return name[l:] if len(name) > l else self.basename
		else:
			return self.basename

	def __parse_bib_references(self, bib_db : str, bbl_file : str, *files : TeXMacroParameter):
		"""
		Add a dependency to a bibliography database.
		:param bib_db: the name of the database.
		:type bib_db: str
		:param bbl_file: the name of the BBL file.
		:type bbl_file: str
		:param files: the bibliography files.
		:type files: TeXMacroParameter
		:type files: str
		"""
		# Special case: the bibunit
		if self.__in_bibunit:
			bbl_file = genutils.basename2(bbl_file, '.bbl') + str(self.__bibunit_index)
		if not os.path.isabs(bbl_file):
			bbl_file = genutils.ensure_filename_extension(bbl_file, '.bbl')
			bbl_file = os.path.normpath(os.path.join(self.root_directory, bbl_file))
		for param in files:
			value = param.text
			if value:
				for svalue in re.split(r'\s*,\s*', value):
					if svalue:
						self.__explicit_bibliography = True
						if svalue.endswith('.bib'):
							bib_file = svalue
						else:
							bib_file = svalue + '.bib'
						if not os.path.isabs(bib_file):
							bib_file = os.path.normpath(os.path.join(self.root_directory, bib_file))
						if os.path.isfile(bib_file):
							self.__dependency_repository.update(FileType.bib, bib_file, scope=bib_db, output_file=bbl_file)

	# noinspection PyCallingNonCallable
	@override
	def expand(self, parser : Parser, raw_text : str, name : str, *parameters : TeXMacroParameter) -> str:
		"""
		Expand the given macro on the given parameters.
		:param parser: reference to the parser.
		:type parser: Parser
		:param raw_text: The raw text that is the source of the expansion.
		:type raw_text: str
		:param name: Name of the macro.
		:type name: str
		:param parameters: Descriptions of the values passed to the TeX macro.
		:type parameters: TeXMacroParameter
		:return: the result of expansion of the macro, or None to not replace the macro by something (the macro is used as-is)
		:rtype: str
		"""
		if name.startswith('\\'):
			callback_name = name[1:]
			if callback_name in self.__full_expand_registry:
				func = self.__full_expand_registry[callback_name]
				func(self, name, list(parameters))
			else:
				largest_size = 0
				largest_func = None
				for prefix, func in self.__prefix_expand_registry.items():
					if callback_name.startswith(prefix):
						l = len(prefix)
						if l > largest_size:
							largest_size = l
							largest_func = func
				if largest_func is not None:
					largest_func(self, name, list(parameters))
		return ''

	# noinspection PyUnusedLocal
	@expand_function(start_symbol=False)
	def _expand__documentclass(self, name : str, parameters : list[TeXMacroParameter]):
		assert len(parameters) > 1
		cls = parameters[1].text
		if cls.endswith('.cls'):
			cls_file = cls
		else:
			cls_file = cls + '.cls'
		if not os.path.isabs(cls_file):
			cls_file = os.path.normpath(os.path.join(self.root_directory, cls_file))
		if os.path.isfile(cls_file):
			self.__dependency_repository.update(FileType.cls, cls_file)

	@expand_function(start_symbol=False)
	def _expand__input(self, name : str, parameters : list[TeXMacroParameter]):
		self._expand__include(name, parameters)

	# noinspection PyUnusedLocal
	@expand_function(start_symbol=False)
	def _expand__include(self, name : str, parameters : list[TeXMacroParameter]):
		for param in parameters:
			value = param.text
			if value:
				if FileType.is_tex_document(value):
					tex_file = value
				else:
					tex_file = value + FileType.tex_extensions()[0]
				if not os.path.isabs(tex_file):
					tex_file = os.path.normpath(os.path.join(self.root_directory, tex_file))
				if os.path.isfile(tex_file):
					self.__dependency_repository.update(FileType.tex, tex_file)

	@expand_function(start_symbol=False)
	def _expand__newglossaryentry(self, name : str, parameters : list[TeXMacroParameter]):
		self._expand__makeglossaries(name, parameters)

	@expand_function(start_symbol=False)
	def _expand__printglossaries(self, name : str, parameters : list[TeXMacroParameter]):
		self._expand__makeglossaries(name, parameters)

	# noinspection PyUnusedLocal
	@expand_function(start_symbol=False)
	def _expand__makeglossaries(self, name : str, parameters : list[TeXMacroParameter]):
		self.is_glossary = True

	@expand_function(start_symbol=False)
	def _expand__printindex(self, name : str, parameters : list[TeXMacroParameter]):
		self._expand__makeindex(name, parameters)

	# noinspection PyUnusedLocal
	@expand_function(start_symbol=False)
	def _expand__makeindex(self, name : str, parameters : list[TeXMacroParameter]):
		self.is_makeindex = True

	# noinspection PyPep8Naming
	@expand_function(start_symbol=False)
	def _expand__RequirePackage(self, name : str, parameters : list[TeXMacroParameter]):
		self._expand__usepackage(name, parameters)

	@expand_function(start_symbol = True)
	def _expand__bibliography(self, name : str, parameters : list[TeXMacroParameter]):
		assert len(parameters) > 0
		bibdb = self.__extract_bibdb('\\bibliography', name)
		self.__parse_bib_references(bibdb, bibdb, *parameters)

	@expand_function(start_symbol = True)
	def _expand__bibliographystyle(self, name : str, parameters : list[TeXMacroParameter]):
		assert len(parameters) > 0
		bibdb = self.__extract_bibdb('\\bibliographystyle', name)
		self.__analyse_bst_specification(bibdb, parameters)

	# noinspection PyUnusedLocal
	@expand_function(start_symbol = False)
	def _expand__addbibresource(self, name : str, parameters : list[TeXMacroParameter]):
		assert len(parameters) > 0
		bibdb = self.basename
		self.__parse_bib_references(bibdb, *parameters)

	# noinspection PyUnusedLocal
	@expand_function(start_symbol = False)
	def _expand__putbib(self, name : str, parameters : list[TeXMacroParameter]):
		bibdb = self.basename
		# By default, Bibunits uses the 'bu' prefix for its auxiliary files
		bbl_file = 'bu'
		if len(parameters) > 0 and parameters[0].text:
			self.__parse_bib_references(bibdb, bbl_file, *parameters)
		elif '\\defaultbibliography' in self.__default_bibliography:
			self.__parse_bib_references(bibdb, bbl_file,*self.__default_bibliography['\\defaultbibliography'])
		else:
			self.__parse_bib_references(bibdb, bbl_file, TeXMacroParameter(text=self.basename))

	# noinspection PyUnusedLocal
	@expand_function(start_symbol = False, extra_macro=True)
	def _expand__bibliographyslide(self, name : str, parameters : list[TeXMacroParameter]):
		# The name of the auxiliary file that contains the bibliography entry is based on: \jobname.\index.aux
		self.__parse_bib_references(self.basename, self.__build_bibunit_auxiliary_basename_prefix(), TeXMacroParameter(text='biblio'))

	# noinspection PyUnusedLocal
	@expand_function(start_symbol = False)
	def _expand__begin(self, name : str, parameters : list[TeXMacroParameter]):
		assert len(parameters) > 1
		tex_name = parameters[1].text
		if tex_name == 'bibunit':
			self.__in_bibunit = True
			self.__bibunit_index = self.__bibunit_index + 1
			assert len(parameters) > 2
			if parameters[2].text:
				self.__analyse_bst_specification(self.basename, [ parameters[2] ])
		elif self.__include_extra_macros and tex_name == 'bibliographysection':
			self.__in_bibunit = True
			self.__bibunit_index = self.__bibunit_index + 1
			self.__parse_bib_references(self.basename, self.__build_bibunit_auxiliary_basename_prefix(), TeXMacroParameter(text='biblio'))

	# noinspection PyUnusedLocal
	@expand_function(start_symbol = False)
	def _expand__end(self, name : str, parameters : list[TeXMacroParameter]):
		assert len(parameters) > 0
		tex_name = parameters[0].text
		if tex_name == 'bibunit' or (self.__include_extra_macros and tex_name == 'bibliographysection'):
			self.__in_bibunit = False

	# noinspection DuplicatedCode,PyUnusedLocal
	@expand_function(start_symbol=False)
	def _expand__usepackage(self, name : str, parameters : list[TeXMacroParameter]):
		assert len(parameters) > 1
		sty = parameters[1].text
		if sty.endswith('.sty'):
			sty_file = sty
		else:
			sty_file = sty + ".sty"
		if sty_file == 'multibib.sty':
			self.is_multibib = True
		elif sty_file == 'bibunits.sty':
			self.is_bibunits = True
		elif sty_file == 'biblatex.sty':
			self.is_biblatex = True
			# Parse the biblatex parameters
			if parameters[0].text:
				params = re.split(r'\s*,\s*', (parameters[0].text or '').strip())
				for p in params:
					r = re.match(r'^([^=]+)\s*=\s*(.*?)\s*$', p, re.DOTALL)
					if r:
						k = r.group(1)
						v = r.group(2) or ''
					else:
						k = p
						v = ''
					if k == 'backend':
						self.is_biber = (v == 'biber')
					elif k == 'style':
						if v.endswith('.bbx'):
							bbx_file = v
						else:
							bbx_file = v + ".bbx"
						if not os.path.isabs(bbx_file):
							bbx_file = os.path.normpath(os.path.join(self.root_directory, bbx_file))
						if os.path.isfile(bbx_file):
							self.__dependency_repository.update(FileType.bbx, bbx_file)
						if v.endswith('.cbx'):
							cbx_file = v
						else:
							cbx_file = v + ".cbx"
						if not os.path.isabs(cbx_file):
							cbx_file = os.path.normpath(os.path.join(self.root_directory, cbx_file))
						if os.path.isfile(cbx_file):
							self.__dependency_repository.update(FileType.cbx, cbx_file)
					elif k == 'bibstyle':
						if v.endswith('.bbx'):
							bbx_file = v
						else:
							bbx_file = v + ".bbx"
						if not os.path.isabs(bbx_file):
							bbx_file = os.path.normpath(os.path.join(self.root_directory, bbx_file))
						if os.path.isfile(bbx_file):
							self.__dependency_repository.update(FileType.bbx, bbx_file)
					elif k == 'citestyle':
						if v.endswith('.cbx'):
							cbx_file = v
						else:
							cbx_file = v + '.cbx'
						if not os.path.isabs(cbx_file):
							cbx_file = os.path.normpath(os.path.join(self.root_directory, cbx_file))
						if os.path.isfile(cbx_file):
							self.__dependency_repository.update(FileType.cbx, cbx_file)
		elif sty_file == 'indextools.sty':
			if parameters[0] and parameters[0].text and 'xindy' in parameters[0].text:
				self.is_xindy_index = True
		elif sty_file == 'glossaries.sty':
			self.is_glossary = True
		else:
			if not os.path.isabs(sty_file):
				sty_file = os.path.normpath(os.path.join(self.root_directory, sty_file))
			if os.path.isfile(sty_file):
				self.__dependency_repository.update(FileType.sty, sty_file)

	# noinspection DuplicatedCode,PyUnusedLocal
	@expand_function(start_symbol=False)
	def _expand__defaultbibliography(self, name : str, parameters : list[TeXMacroParameter]):
		self.__default_bibliography[name] = parameters

	# noinspection DuplicatedCode,PyUnusedLocal
	@expand_function(start_symbol=False)
	def _expand__defaultbibliographystyle(self, name : str, parameters : list[TeXMacroParameter]):
		self.__default_bibliography_style[name] = parameters

	def __analyse_bst_specification(self, bib_db : str, parameters: list[TeXMacroParameter]):
		for param in parameters:
			value = param.text
			if value:
				for svalue in re.split('\\s*,\\s*', value.strip()):
					if svalue:
						self.__explicit_bibliography_style = True
						if svalue.endswith('.bst'):
							bst_file = svalue
						else:
							bst_file = svalue + '.bst'
						if not os.path.isabs(bst_file):
							bst_file = os.path.normpath(os.path.join(self.root_directory, bst_file))
						if os.path.isfile(bst_file):
							self.__dependency_repository.update(FileType.bst, bst_file, scope=bib_db)

	def __build_bibunit_auxiliary_basename_prefix(self) -> str:
		"""
		Compute the prefix text that will serve as the starting value for the filename of Bibunits auxiliary files
		according to the API and extra macros provided by S. Galland.
		:return: The prefix.
		:rtype: str
		"""
		if self.__include_extra_macros:
			return genutils.basename(self.main_filename, *FileType.tex_extensions()) + '.'
		return 'bu'

	@override
	def find_macro(self, parser : Parser, name : str, special : bool, math : bool) -> str | None:
		"""
		Invoked each time a macro definition is not found in the parser data.
		:param parser: reference to the parser.
		:type parser: Parser
		:param name: Name of the macro.
		:type name: str
		:param special: Indicates if the macro is a special macro or not.
		:type special: bool
		:param math: Indicates if the math mode is active.
		:type math: bool
		:return: the definition of the macro, i.e., the macro prototype. See the class documentation for an explanation about the format of the macro prototype.
		:rtype: str
		"""
		if not special:
			if name.startswith('bibliographystyle'):
				return '!{}'
			elif name.startswith('bibliography'):
				return '!{}'
		return None

	# noinspection DuplicatedCode
	def run(self):
		"""
		Detect the dependencies.
		"""
		with open(self.filename) as f:
			content = f.read()

		parser = TeXParser()
		parser.observer = self
		parser.filename = self.filename

		for k, v in self.__MACROS.items():
			parser.add_text_mode_macro(k, v)
			parser.add_math_mode_macro(k, v)

		if self.__include_extra_macros:
			for k, v in extramacros.ALL_EXTRA_MACROS.items():
				parser.add_text_mode_macro(k, v)
				parser.add_math_mode_macro(k, v)

		parser.parse(content)

		if not self.__explicit_bibliography and self.__default_bibliography:
			for name, parameters in self.__default_bibliography.items():
				self._expand__bibliography(name, parameters)

		if not self.__explicit_bibliography_style and self.__default_bibliography_style:
			for name, parameters in self.__default_bibliography_style.items():
				self._expand__bibliographystyle(name, parameters)


	@override
	def text(self, parser: Parser, text: str):
		pass

	@override
	def comment(self, parser: Parser, raw: str, comment: str) -> str | None:
		return None

	@override
	def open_block(self, parser: Parser, text: str) -> str | None:
		return None

	@override
	def close_block(self, parser: Parser, text: str) -> str | None:
		return None

	@override
	def open_math(self, parser: Parser, inline: bool) -> str | None:
		return None

	@override
	def close_math(self, parser: Parser, inline: bool) -> str | None:
		return None
