#!/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.

from dataclasses import dataclass
import logging
import os
import re
import sys
import textwrap
from collections import deque
from typing import Any, override, Type

from autolatex2.config.configobj import Config
from autolatex2.make.abstractmaker import TeXMaker
from autolatex2.make.abstractbuilder import Builder
from autolatex2.make.filedescription import FileDescription
from autolatex2.make.make_enums import TeXTools, TeXCompiler, BibCompiler, IndexCompiler, GlossaryCompiler
from autolatex2.make.stamps import StampManager
from autolatex2.tex.biber import BiberErrorParser
from autolatex2.tex.bibtex import BibTeXErrorParser
from autolatex2.tex.citationanalyzer import AuxiliaryCitationAnalyzer
from autolatex2.tex.dependencyanalyzer import DependencyAnalyzer
import autolatex2.tex.utils as texutils
from autolatex2.tex.texlogparser import TeXLogParser, TeXWarnings, DetailedTeXWarning
from autolatex2.tex.utils import FileType
from autolatex2.translator.translatorrepository import TranslatorRepository
from autolatex2.translator.translatorrunner import TranslatorRunner
import autolatex2.utils.extlogging as extlogging
from autolatex2.utils.extlogging import LogLevel
from autolatex2.utils.runner import Runner, ScriptOutput
from autolatex2.utils import extprint
import autolatex2.utils.utilfunctions as genutils
from autolatex2.utils.i18n import T

@dataclass
class BuilderFactory:
	"""
	Class for keeping track of all information about a builder.
	"""
	builder_output : FileType
	builder_type: Type[Builder]
	builder_instance: Builder|None = None

	def builder(self, configuration : Config) -> Builder:
		"""
		Replies the builder.
		:param configuration: the configuration to pass to the builder.
		:type configuration: Config
		:return: the builder
		:rtype: Builder
		"""
		if self.builder_instance is None:
			self.builder_instance = self.builder_type(configuration)
		assert self.builder_instance is not None
		return self.builder_instance


# noinspection DuplicatedCode
class AutoLaTeXMaker(TeXMaker):
	"""
	The maker for the program.
	"""

	__EXTENDED_WARNING_CODE : str = textwrap.dedent("""\
		%*************************************************************
		% CODE ADDED BY AUTOLATEX TO CHANGE THE OUPUT OF THE WARNINGS
		%*************************************************************
		\\makeatletter
		\\newcount\\autolatex@@@lineno
		\\newcount\\autolatex@@@lineno@delta
		\\xdef\\autolatex@@@mainfile@real{::::REALFILENAME::::}
		\\def\\autolatex@@@mainfile{autolatex_autogenerated.tex}
		\\xdef\\autolatex@@@filename@stack{{\\autolatex@@@mainfile}{\\autolatex@@@mainfile}}
		\\global\\let\\autolatex@@@currentfile\\autolatex@@@mainfile
		\\def\\autolatex@@@filename@stack@push#1{%
			\\xdef\\autolatex@@@filename@stack{{#1}\\autolatex@@@filename@stack}%
		}
		\\def\\autolatex@@@filename@stack@pop@split#1#2\\@nil{%
			\\gdef\\autolatex@@@currentfile{#1}%
			\\gdef\\autolatex@@@filename@stack{#2}%
		}
		\\def\\autolatex@@@filename@stack@pop{%
			\\expandafter\\autolatex@@@filename@stack@pop@split\\autolatex@@@filename@stack\\@nil}
		\\def\\autolatex@@@update@filename{%
			\\ifx\\autolatex@@@mainfile\\autolatex@@@currentfile%
				\\edef\\autolatex@@@warning@filename{\\autolatex@@@mainfile@real}%
				\\global\\autolatex@@@lineno@delta=::::AUTOLATEXHEADERSIZE::::\\relax%
			\\else%
				\\edef\\autolatex@@@warning@filename{\\autolatex@@@currentfile}%
				\\global\\autolatex@@@lineno@delta=0\\relax%
			\\fi%
			{\\filename@parse{\\autolatex@@@warning@filename}\\global\\let\\autolatex@@@filename@ext\\filename@ext}%
			\\xdef\\autolatex@@@generic@warning@beginmessage{!!!![BeginWarning]\\autolatex@@@warning@filename:\\ifx\\autolatex@@@filename@ext\\relax.tex\\fi:}%
			\\xdef\\autolatex@@@generic@warning@endmessage{!!!![EndWarning]\\autolatex@@@warning@filename}%
		}
		\\def\\autolatex@@@openfile#1{%
			\\expandafter\\autolatex@@@filename@stack@push{\\autolatex@@@currentfile}%
			\\xdef\\autolatex@@@currentfile{#1}%
			\\autolatex@@@update@filename%
		}
		\\def\\autolatex@@@closefile{%
			\\autolatex@@@filename@stack@pop%
			\\autolatex@@@update@filename%
		}
		\\let\\autolatex@@@InputIfFileExists\\InputIfFileExists
		\\long\\def\\InputIfFileExists#1#2#3{%
			\\autolatex@@@openfile{#1}%
			\\autolatex@@@InputIfFileExists{#1}{#2}{#3}%
			\\autolatex@@@closefile%
		}
		\\let\\autolatex@@@input\\@input
		\\long\\def\\@input#1{%
			\\autolatex@@@openfile{#1}%
			\\autolatex@@@input{#1}%
			\\autolatex@@@closefile%
		}
		\\global\\DeclareRobustCommand{\\GenericWarning}[2]{%
			\\global\\autolatex@@@lineno\\inputlineno\\relax%
			\\global\\advance\\autolatex@@@lineno\\autolatex@@@lineno@delta\\relax%
			\\begingroup
			\\def\\MessageBreak{^^J#1}%
			\\set@display@protect
			\\immediate\\write\\@unused{^^J\\autolatex@@@generic@warning@beginmessage\\the\\autolatex@@@lineno: #2\\on@line.^^J\\autolatex@@@generic@warning@endmessage^^J}%
			\\endgroup
		}
		\\autolatex@@@update@filename
		\\makeatother
		%*************************************************************
		""")

	__COMMAND_DEFINITIONS : dict[int,dict[str,str|list[str]|None]] = {
		TeXTools.pdflatex.value: {
			'cmd': 'pdflatex',
			'flags': ['-halt-on-error', '-interaction', 'batchmode', '-file-line-error'],
			'to_dvi': ['-output-format=dvi'],
			'to_ps': None,
			'to_pdf': ['-output-format=pdf'],
			'synctex': '-synctex=1',
			'jobname': '-jobname',
			'output_dir':  '-output-directory', 
			'ewarnings': __EXTENDED_WARNING_CODE,
			'utf8': [],
		},
		TeXTools.latex.value: {
			'cmd': 'latex',
			'flags': ['-halt-on-error', '-interaction', 'batchmode', '-file-line-error'],
			'to_dvi': ['-output-format=dvi'],
			'to_ps': None,
			'to_pdf': ['-output-format=pdf'],
			'synctex': '-synctex=1',
			'jobname': '-jobname',
			'output_dir':  '-output-directory', 
			'ewarnings': __EXTENDED_WARNING_CODE,
			'utf8': [],
		},
		TeXTools.xelatex.value: {
			'cmd': 'xelatex',
			'flags': ['-halt-on-error', '-interaction', 'batchmode', '-file-line-error'],
			'to_dvi': ['-no-pdf'],
			'to_ps': None,
			'to_pdf': [],
			'synctex': '-synctex=1',
			'jobname': '-jobname',
			'output_dir':  '-output-directory', 
			'ewarnings': __EXTENDED_WARNING_CODE,
			'utf8': [],
		},
		TeXTools.lualatex.value: {
			'cmd': 'luatex',
			'flags': ['-halt-on-error', '-interaction', 'batchmode', '-file-line-error'],
			'to_dvi': ['-output-format=dvi'],
			'to_ps': None,
			'to_pdf': ['-output-format=pdf'],
			'synctex': '-synctex=1',
			'jobname': '-jobname',
			'output_dir':  '-output-directory', 
			'ewarnings': __EXTENDED_WARNING_CODE,
			'utf8': [],
		},
		TeXTools.bibtex.value: {
			'cmd': 'bibtex',
			'flags': [],
			'utf8': [],
		},
		TeXTools.biber.value: {
			'cmd': 'biber',
			'flags': [],
			'utf8': [],
		},
		TeXTools.makeindex.value: {
			'cmd': 'makeindex',
			'flags': [],
			'index_style_flag': '-s',
			'utf8': [],
		},
		TeXTools.texindy.value: {
			'cmd': 'texindy',
			'flags': [],
			'index_style_flag': '',
			'utf8': ['-C', 'utf8'], 
		},
		TeXTools.makeglossaries.value: {
			'cmd': 'makeglossaries',
			'flags': [],
			'glossary_style_flag': '-s',
			'utf8': [],
		},
		TeXTools.dvips.value: {
			'cmd': 'dvips',
			'flags': [],
			'output':'-o', 
			'utf8': [],
		},
	}

	def __init__(self, translator_runner : TranslatorRunner):
		"""
		Construct the make of translators.
		:param translator_runner: The runner of translators.
		:type translator_runner: TranslatorRunner
		"""
		self.__root_files : set[str] = set()
		self.__files : dict[str,FileDescription] = dict()
		self.__stamps : StampManager = StampManager()
		self.__standards_warnings : set[TeXWarnings] = set()
		self.__detailed_warnings : list[DetailedTeXWarning] = list()
		self.translator_runner : TranslatorRunner = translator_runner
		if self.translator_runner is None:
			self.__configuration : Config = Config()
		else:
			self.__configuration : Config = translator_runner.configuration

		# Initialization of the compiler definitions and the command-line options are
		# differed to the "__internal_register_commands" method factory
		self.__instance_compiler_definition : dict[str,str|list[str]|None] | None = None

		# Initialization of the builders
		self.__registered_builders : dict[FileType,BuilderFactory] = AutoLaTeXMaker.build_builder_dict('autolatex2.make.builders')

		# Initialize fields by resetting them
		self.reset()

	@property
	def registered_builders(self) -> dict[FileType,BuilderFactory]:
		"""
		Replies the registered builders.
		:return: the mapping from the output file type to the lambda expression that permits to create a builder for this
		file type.
		:rtype: dict[FileType,BuilderFactory]
		"""
		return self.__registered_builders

	@property
	@override
	def configuration(self) -> Config:
		return self.__configuration

	@property
	@override
	def stamp_manager(self) -> StampManager:
		"""
		Replies the manager of internal stamps.
		:rtype: StampManager
		"""
		return self.__stamps

	@staticmethod
	def create(configuration : Config) -> 'AutoLaTeXMaker':
		"""
		Static factory method for creating an instance of AutoLaTeXMaker with the "standard" building method.
		:param configuration: the configuration to use.
		:param configuration: Config
		:return: the instance of the maker
		:rtype: AutoaTeXMaker
		"""
		# Create the translator repository
		repository = TranslatorRepository(configuration)
		# Create the runner of translators
		runner = TranslatorRunner(repository)
		# Create the general maker
		maker = AutoLaTeXMaker(runner)
		# Set the maker from the configuration
		ddir = configuration.document_directory
		document_file = configuration.document_filename
		if ddir:
			fn = os.path.join(ddir, document_file)
		else:
			fn = document_file
		maker.add_root_file(fn)
		return maker

	def reset_commands(self):
		"""
		Reset the external tool commands and rebuild them from the current configuration.
		"""
		self.__instance_compiler_definition = None

	# noinspection PyTypeChecker
	def __internal_register_commands(self):
		"""
		Build the different commands according to the current configuration. This method should not be called from outside the class.
		It is based on the method factory design pattern.
		"""
		if self.__instance_compiler_definition is None:
			encoding = sys.getdefaultencoding()
			is_utf8_system = encoding.lower() == 'utf-8'

			compiler_num = -1
			compiler = self.configuration.generation.latex_compiler
			if compiler is None:
				compiler_num = TeXTools.pdflatex.value if self.configuration.generation.pdf_mode else TeXTools.latex.value
				compiler = AutoLaTeXMaker.__COMMAND_DEFINITIONS[compiler_num]['cmd']
			elif TeXCompiler[compiler]:
				compiler_definition = TeXCompiler[compiler]
				if compiler_definition is not None:
					compiler_num = TeXCompiler[compiler].value
					

			if compiler_num in AutoLaTeXMaker.__COMMAND_DEFINITIONS:
				self.__instance_compiler_definition = AutoLaTeXMaker.__COMMAND_DEFINITIONS[compiler_num].copy()
			else:
				self.__instance_compiler_definition = None
			if not self.__instance_compiler_definition:
				raise Exception(T("Cannot find a definition of the command line for the LaTeX compiler '%s'") % compiler)

			out_type = 'pdf' if self.configuration.generation.pdf_mode else 'ps'

			# LaTeX
			self.__latex_cli : list[str] = list()
			if self.configuration.generation.latex_cli:
				self.__latex_cli.extend(self.configuration.generation.latex_cli)
			else:
				self.__latex_cli.append(self.__instance_compiler_definition['cmd'])
				self.__latex_cli.extend(self.__instance_compiler_definition['flags'])
				if is_utf8_system and 'utf8' in self.__instance_compiler_definition and self.__instance_compiler_definition['utf8']:
					self.__latex_cli.extend(self.__instance_compiler_definition['utf8'])
				if ('to_%s' % out_type) not in self.__instance_compiler_definition:
					raise Exception(T("No command definition for '%s/%s'") % (compiler, out_type))
				# Support of SyncTeX
				if self.configuration.generation.synctex and self.__instance_compiler_definition['synctex']:
					if isinstance(self.__instance_compiler_definition['synctex'], list):
						self.__latex_cli.extend(self.__instance_compiler_definition['synctex'])
					else:
						self.__latex_cli.append(self.__instance_compiler_definition['synctex'])

				target = self.__instance_compiler_definition['to_%s' % out_type]
				if target:
					if isinstance(target, list):
						self.__latex_cli.extend(target)
					else:
						self.__latex_cli.append(target)
				elif out_type == 'ps':
					if isinstance(self.__instance_compiler_definition['to_dvi'], list):
						self.__latex_cli.extend(self.__instance_compiler_definition['to_dvi'])
					else:
						self.__latex_cli.append(self.__instance_compiler_definition['to_dvi'])
				else:
					raise Exception(T('Invalid maker state: cannot find the command line to compile TeX files.'))

			if self.configuration.generation.latex_flags:
				self.__latex_cli.extend(self.configuration.generation.latex_flags)

			# BibTeX
			self.__bibtex_cli = list()
			if self.configuration.generation.bibtex_cli:
				self.__bibtex_cli.extend(self.configuration.generation.bibtex_cli)
			else:
				cmd = AutoLaTeXMaker.__COMMAND_DEFINITIONS[BibCompiler.bibtex.value]
				if not cmd:
					raise Exception(T("No command definition for 'bibtex'"))
				self.__bibtex_cli.append(cmd['cmd'])
				self.__bibtex_cli.extend(cmd['flags'])
				if is_utf8_system and 'utf8' in cmd and cmd['utf8']:
					self.__bibtex_cli.extend(cmd['utf8'])

			if self.configuration.generation.bibtex_flags:
				self.__bibtex_cli.extend(self.configuration.generation.bibtex_flags)

			# Biber
			self.__biber_cli = list()
			if self.configuration.generation.biber_cli:
				self.__biber_cli.extend(self.configuration.generation.biber_cli)
			else:
				cmd = AutoLaTeXMaker.__COMMAND_DEFINITIONS[BibCompiler.biber.value]
				if not cmd:
					raise Exception(T("No command definition for 'biber'"))
				self.__biber_cli.append(cmd['cmd'])
				self.__biber_cli.extend(cmd['flags'])
				if is_utf8_system and 'utf8' in cmd and cmd['utf8']:
					self.__biber_cli.extend(cmd['utf8'])

			if self.configuration.generation.biber_flags:
				self.__biber_cli.extend(self.configuration.generation.biber_flags)

			# MakeIndex
			self.__makeindex_cli = list()
			if self.configuration.generation.makeindex_cli:
				self.__makeindex_cli.extend(self.configuration.generation.makeindex_cli)
			else:
				cmd = AutoLaTeXMaker.__COMMAND_DEFINITIONS[IndexCompiler.makeindex.value]
				if not cmd:
					raise Exception(T("No command definition for 'makeindex'"))
				self.__makeindex_cli.append(cmd['cmd'])
				self.__makeindex_cli.extend(cmd['flags'])
				if is_utf8_system and 'utf8' in cmd and cmd['utf8']:
					self.__makeindex_cli.extend(cmd['utf8'])

			if self.configuration.generation.makeindex_flags:
				self.__makeindex_cli.extend(self.configuration.generation.makeindex_flags)

			# texindy
			self.__texindy_cli = list()
			if self.configuration.generation.texindy_cli:
				self.__texindy_cli.extend(self.configuration.generation.texindy_cli)
			else:
				cmd = AutoLaTeXMaker.__COMMAND_DEFINITIONS[IndexCompiler.texindy.value]
				if not cmd:
					raise Exception(T("No command definition for 'texindy'"))
				self.__texindy_cli.append(cmd['cmd'])
				self.__texindy_cli.extend(cmd['flags'])
				if is_utf8_system and 'utf8' in cmd and cmd['utf8']:
					self.__texindy_cli.extend(cmd['utf8'])

			if self.configuration.generation.texindy_flags:
				self.__texindy_cli.extend(self.configuration.generation.texindy_flags)

			# MakeGlossaries
			self.__makeglossaries_cli = list()
			if self.configuration.generation.makeglossary_cli:
				self.__makeglossaries_cli.extend(self.configuration.generation.makeglossary_cli)
			else:
				cmd = AutoLaTeXMaker.__COMMAND_DEFINITIONS[GlossaryCompiler.makeglossaries.value]
				if not cmd:
					raise Exception(T("No command definition for 'makeglossaries'"))
				self.__makeglossaries_cli.append(cmd['cmd'])
				self.__makeglossaries_cli.extend(cmd['flags'])
				if is_utf8_system and 'utf8' in cmd and cmd['utf8']:
					self.__makeglossaries_cli.extend(cmd['utf8'])

			if self.configuration.generation.makeglossary_flags:
				self.__makeglossaries_cli.extend(self.configuration.generation.makeglossary_flags)

			# dvips
			self.__dvips_cli = list()
			if self.configuration.generation.dvips_cli:
				self.__dvips_cli.extend(self.configuration.generation.dvips_cli)
			else:
				cmd = AutoLaTeXMaker.__COMMAND_DEFINITIONS[TeXTools.dvips.value]
				if not cmd:
					raise Exception(T("No command definition for 'dvips'"))
				self.__dvips_cli.append(cmd['cmd'])
				self.__dvips_cli.extend(cmd['flags'])
				if is_utf8_system and 'utf8' in cmd and cmd['utf8']:
					self.__dvips_cli.extend(cmd['utf8'])

			if self.configuration.generation.dvips_flags:
				self.__dvips_cli.extend(self.configuration.generation.dvips_flags)

			# Support of extended warnings
			if self.configuration.generation.extended_warnings and 'ewarnings' in self.__instance_compiler_definition and self.__instance_compiler_definition['ewarnings']:
				code = str(self.__instance_compiler_definition['ewarnings']).strip()
				s = str(-(code.count('\n') + 1))
				code = code.replace('::::AUTOLATEXHEADERSIZE::::', s)
				self.__latex_warning_code = code
				self.__is_extended_warning_enable = True
			else:
				self.__latex_warning_code = ''
				self.__is_extended_warning_enable = False

	def reset(self):
		"""
		Reset the maker.
		"""
		self.__root_files = set()
		self.__reset_warnings()
		self.__reset_process_data()

	def __reset_process_data(self):
		"""
		Reset the processing data.
		"""
		self.__files = dict()
		self.stamp_manager.reset()

	def __reset_warnings(self):
		"""
		Reset the lists of warnings.
		"""
		self.__standards_warnings : set[TeXWarnings] = set()
		self.__detailed_warnings : list[DetailedTeXWarning] = list()

	@property
	def compiler_definition(self) -> dict[str,Any]:
		"""
		The definition of the LaTeX compiler that must be used by this maker.
		:rtype: dict[str,Any]
		"""
		self.__internal_register_commands()
		assert self.__instance_compiler_definition is not None
		return self.__instance_compiler_definition

	@property
	def detailed_warnings_enabled(self) -> bool:
		"""
		Replies if the detailed warnings are supported by the TeX compiler.
		:rtype: bool
		"""
		self.__internal_register_commands()
		return self.__is_extended_warning_enable

	@property
	def extended_warnings_code(self) -> str:
		"""
		Replies the TeX code that permits to output the extended warnings.
		:rtype: str
		"""
		self.__internal_register_commands()
		return self.__latex_warning_code

	@property
	def latex_cli(self) -> list[str]:
		"""
		The command-line that is used for running the LaTeX tool.
		:rtype: list[str]
		"""
		self.__internal_register_commands()
		return self.__latex_cli

	@property
	def bibtex_cli(self) -> list[str]:
		"""
		The command-line that is used for running the BibTeX tool.
		:rtype: list[str]
		"""
		self.__internal_register_commands()
		return self.__bibtex_cli

	@property
	def biber_cli(self) -> list[str]:
		"""
		The command-line that is used for running the Biber tool.
		:rtype: list[str]
		"""
		self.__internal_register_commands()
		return self.__biber_cli

	@property
	def makeindex_cli(self) -> list[str]:
		"""
		The command-line that is used for running the makeindex tool.
		:rtype: list[str]
		"""
		self.__internal_register_commands()
		return self.__makeindex_cli

	@property
	def texindy_cli(self) -> list[str]:
		"""
		The command-line that is used for running the texindy tool.
		:rtype: list[str]
		"""
		self.__internal_register_commands()
		return self.__texindy_cli

	@property
	def makeglossaries_cli(self) -> list[str]:
		"""
		The command-line that is used for running the makeglossaries tool.
		:rtype: list[str]
		"""
		self.__internal_register_commands()
		return self.__makeglossaries_cli

	@property
	def dvips_cli(self) -> list[str]:
		"""
		The command-line that is used for running the dvips tool.
		:rtype: list[str]
		"""
		self.__internal_register_commands()
		return self.__dvips_cli

	@property
	def root_files(self) -> set[str]:
		"""
		The root files that are involved within the lastest compilation process.
		:rtype: set[str]
		"""
		return self.__root_files

	def add_root_file(self, filename : str):
		"""
		Add root file.
		:param filename: The name of the root file.
		:type filename: str
		"""
		self.__root_files.add(filename)

	@property
	def files(self) -> dict[str,FileDescription]:
		"""
		The files that are involved within the lastest compilation process.
		:rtype: dict[str,FileDescription]
		"""
		return self.__files

	@property
	def standard_warnings(self) -> set[TeXWarnings]:
		"""
		The standard LaTeX warnings that are discovered during the lastest compilation process.
		:rtype: set[str]
		"""
		return self.__standards_warnings

	@property
	def detailed_warnings(self) -> list[DetailedTeXWarning]:
		"""
		The detailed warnings that are discovered during the lastest compilation process.
		:rtype: list[DetailedTeXWarning]
		"""
		return self.__detailed_warnings

	@override
	def run_latex(self, filename : str, loop : bool = False, extra_run_support : bool = False) -> int:
		"""
		Launch the LaTeX tool and return the number of times the
		tool was launched.
		:param filename: The name TeX file to compile.
		:type filename: str
		:param loop: Indicates if this function may loop on the LaTeX compilation when it is requested by the LaTeX tool. Default value: False.
		:type loop: bool
		:param extra_run_support: Indicates if this function may apply an additional run of LaTeX tool because a tool used in TeX file does
		not provide accurate log message, and needs to have an extra LaTeX run to solves the problem. This is the case of Multibib for example.
		This argument is considered only if the argument "loop" is True; Otherwise, it is ignored.
		Default is False.
		:type extra_run_support: bool
		:return: The number of times the latex tool was run.
		:rtype: int
		"""
		self.__internal_register_commands()
		if filename in self.__files:
			mfn = self.__files[filename].main_filename
			if mfn is not None and mfn != '':
				filename = mfn
		log_file = FileType.log.ensure_extension(filename)
		log_parser = TeXLogParser(log_file=log_file)
		nb_runs = 0
		# This is a do-while implementation
		while True:
			logging.info(T('LATEX: %s') % os.path.basename(filename))
			self.__reset_warnings()
			if os.path.isfile(log_file):
				os.remove(log_file)
			command_output = None
			continue_to_compile = False
			if self.detailed_warnings_enabled:
				with open(filename, "r") as f:
					content = f.readlines()
				autofile = texutils.create_extended_tex_filename(filename)
				with open(autofile, "w") as f:
					code = self.__latex_warning_code.replace('::::REALFILENAME::::', filename)
					f.write(code)
					f.write("\n")
					f.write(''.join(content))
					f.write("\n")
				try:
					cmd = self.__latex_cli.copy()
					assert self.__instance_compiler_definition is not None
					if 'jobname' in self.__instance_compiler_definition and self.__instance_compiler_definition['jobname']:
						cmd.append(str(self.__instance_compiler_definition['jobname']))
						cmd.append(genutils.simple_basename(filename, *FileType.tex_extensions()))
					if 'output_dir' in self.__instance_compiler_definition and self.__instance_compiler_definition['output_dir'] is not None and self.__instance_compiler_definition['output_dir']:
						cmd.append(str(self.__instance_compiler_definition['output_dir']))
						cmd.append(os.path.dirname(filename))
					else:
						logging.warning(T('LATEX: no command-line option provided for changing the output directory'))
					cmd.append(autofile)
					cmd = Runner.normalize_command(*cmd)
					nb_runs += 1
					logging.debug(T('Running: %s') % repr(cmd))
					command_output = self.__run_cmd(*cmd)
					logging.debug(T('Run finished'))
				finally:
					genutils.unlink(autofile)
			else:
				cmd = self.__latex_cli.copy()
				cmd.append(os.path.relpath(filename))
				cmd = Runner.normalize_command(*cmd)
				nb_runs += 1
				logging.debug(T('Running: %s') % repr(cmd))
				command_output = self.__run_cmd(*cmd)
				logging.debug(T('Run finished'))

			if command_output is not None and command_output.return_code != 0:
				logging.debug(T("LATEX: Error when processing %s") % os.path.basename(filename))

				# Parse the log to extract the blocks of messages
				if os.path.isfile(log_file):
					fatal_error, log_blocks = log_parser.extract_failure()
				else:
					raise Exception(T("Log file not found: %s" % log_file))

				# Display the message
				if fatal_error:
					logging.debug(T("LATEX: The first error found in the log file is:"))
					extlogging.multiline_error(fatal_error)
					logging.debug(T("LATEX: End of error log."))
				else:
					logging.error(T("LATEX: Unable to extract the error from the log. Please read the log file."))

				raise Exception(T("LaTeX compiler fail. See log file for details"))

			elif not os.path.isfile(log_file):
				raise Exception(T("Log file not found: %s" % log_file))
			else:
				continue_to_compile = log_parser.extract_warnings(enable_loop=loop,
				                                                  enable_detailed_warnings=self.detailed_warnings_enabled,
				                                                  standards_warnings=self.__standards_warnings,
				                                                  detailed_warnings=self.__detailed_warnings)
				logging.debug(T('Detection of rebuild: %s') % (str(continue_to_compile)))

			# Stoping condition for the do-while loop
			if not continue_to_compile:
				# Special case of Multibib that may not output the "Re-run" warning message when it has missed citations.
				if loop and extra_run_support and (TeXWarnings.undefined_reference in self.standard_warnings or TeXWarnings.undefined_citation in self.standard_warnings):
					# Disable the Multibib support because it is needed to compile only once for solving
					# the missed citations from Multibib
					extra_run_support = False
					continue
				# Stop the do-while loop
				break


		return nb_runs

	# noinspection PyMethodMayBeStatic
	def __run_cmd(self, *cmd) -> ScriptOutput:
		"""
		Run the given command and show up the standard output if it is in debug mode.
		:param cmd: The command to run.
		:type cmd: str array
		:return: An output containing the standard output, the
				 error output, and the exception, the return code.
		:rtype: ScriptOutput
		"""
		if logging.getLogger().isEnabledFor(logging.DEBUG):
			exit_code = 0
			sex = None
			sout = ''
			serr = ''
			try:
				exit_code = Runner.run_command_without_redirect(*cmd)
			except Exception as ex:
				sex = ex
			return ScriptOutput(standard_output=sout, error_output=serr, exception=sex, return_code=exit_code)
		else:
			return Runner.run_command(*cmd)

	# noinspection PyMethodMayBeStatic,PyBroadException
	def __select_aux_file(self, filename : str) -> bool:
		try:
			with open(filename, 'r') as f:
				line = f.readline()
				expr = re.compile(r'\\(?:abx@aux@cite|citation|bibcite)', re.S)
				while line:
					if expr.search(line):
						return True
					line = f.readline()
		except:
			pass
		return False

	def detect_aux_files_with_biliography(self, filename : str, check_aux_content : bool = True) -> list[str]:
		"""
		Explore the document folder and subfolders for finding auxiliary files that contains bibliographical citations.
		:param filename: The name TeX file to compile.
		:type filename: str
		:param check_aux_content: Indicates if this function has to read the auxiliary files to determine if a citation is inside.
		Then, if it is the case, the auxiliary file is passed to the bibliography tool; Otherwise it is ignored. Default is: True.
		:type check_aux_content: bool
		:return: The list of auxiliary files
		:rtype: list[str]
		"""
		if check_aux_content:
			aux_file_list = texutils.find_aux_files(filename, self.__select_aux_file)
		else:
			aux_file_list = texutils.find_aux_files(filename)
		if not aux_file_list:
			aux_file = FileType.aux.ensure_extension(filename)
			aux_file_list.append(aux_file)
		return aux_file_list

	@override
	def run_bibtex(self, filename : str, check_aux_content : bool = True) -> dict[str,Any] | None:
		"""
		Launch the BibTeX tool (BibTeX, Biber, etc.) once time and replies a dictionary that describes any error.
		The returned dictionary has the keys: filename, lineno and message.
		This function also supports the document with zro, one or more bibliography sections, such a those
		introduced by the LaTeX package 'bibunits'.
		:param filename: The name of the auxiliary file or the root TeX file to use as input for the bibliography tool.
		:type filename: str
		:param check_aux_content: Indicates if this function has to read the auxiliary files to determine if a citation is inside.
		Then, if it is the case, the auxiliary file is passed to the bibliography tool; Otherwise it is ignored. Default is: True.
		:type check_aux_content: bool
		:return: the error result, or None if there is no error.
		:rtype: dict[str,Any] | None
		"""
		self.__internal_register_commands()
		self.__reset_warnings()
		if FileType.aux.is_file(filename):
			# The input filename is an auxiliary file, that is the standard type of file for BibTeX.
			aux_file_list = [ filename ]
		else:
			if filename in self.__files:
				mfn = self.__files[filename].main_filename
				if mfn is not None and mfn != '':
					filename = mfn
			aux_file_list = self.detect_aux_files_with_biliography(filename, check_aux_content)
		for aux_file in aux_file_list:
			if self.configuration.generation.is_biber:
				# Remove the file extension because Biber does not support it properly from the CLI
				aux_file = genutils.basename_with_path(aux_file, *FileType.aux.extensions())
				logging.info(T('BIBER: %s') % os.path.basename(aux_file))
				cmd = self.__biber_cli.copy()
			else:
				logging.info(T('BIBTEX: %s') % os.path.basename(aux_file))
				cmd = self.__bibtex_cli.copy()
			cmd.append(os.path.relpath(aux_file))
			cmd = Runner.normalize_command(*cmd)
			if self.configuration.generation.is_biber:
				logging.debug(T('BIBER: Command line is: %s') % ' '.join(cmd))
			else:
				logging.debug(T('BIBTEX: Command line is: %s') % ' '.join(cmd))
			command_output = Runner.run_command(*cmd)
			if command_output.return_code != 0:
				if self.configuration.generation.is_biber:
					logging.debug(T('BIBER: error when processing %s') % os.path.basename(aux_file))
				else:
					logging.debug(T('BIBTEX: error when processing %s') % os.path.basename(aux_file))
				log = command_output.standard_output
				if not log:
					log = command_output.error_output
				if log:
					if self.configuration.generation.is_biber:
						log_parser = BiberErrorParser()
					else:
						log_parser = BibTeXErrorParser()
					current_error = log_parser.parse_log(aux_file, log)
					if current_error:
						return current_error
				current_error = {'filename': aux_file, 'lineno': 0, 'message': command_output.standard_output + "\n" + command_output.error_output}
				return current_error
		return None

	@override
	def run_makeindex(self, filename : str) -> ScriptOutput | None:
		"""
		Launch the MakeIndex tool once time.
		The success status if the run of MakeIndex is replied.
		:param filename: The filename of the index file to compile.
		:type filename: str
		:return: None on success; Otherwise a tuple with the exit code and the standard and error outputs from the Makeindex tool.
		:rtype: tuple[int,str,str] | None
		"""
		self.__internal_register_commands()
		idx_file = FileType.idx.ensure_extension(filename)
		logging.info(T('MAKEINDEX: %s') % os.path.basename(idx_file))
		self.__reset_warnings()
		if self.configuration.generation.is_xindy_index:
			cmd = self.__texindy_cli.copy()
			cmd_def = AutoLaTeXMaker.__COMMAND_DEFINITIONS[IndexCompiler.texindy.value]
		else:
			cmd = self.__makeindex_cli.copy()
			cmd_def = AutoLaTeXMaker.__COMMAND_DEFINITIONS[IndexCompiler.makeindex.value]
		if cmd_def and 'index_style_flag' in cmd_def and cmd_def['index_style_flag']:
			ist_file = self.configuration.generation.makeindex_style_filename
			if ist_file:
				cmd.append(cmd_def['index_style_flag'])
				cmd.append(os.path.relpath(ist_file))
		cmd.append(os.path.relpath(idx_file))
		cmd = Runner.normalize_command(*cmd)
		command_output = Runner.run_command(*cmd)
		if command_output.return_code != 0:
			logging.error(T("%s\n%s") % (command_output.standard_output, command_output.error_output))
			return command_output
		return None

	@override
	def run_makeglossaries(self, filename : str) -> bool:
		"""
		Launch the MakeGlossaries tool once time.
		The success status if the run of MakeGlossaries is replied.
		:param filename: The filename of the TeX file to compile.
		:type filename: str
		:return: True to continue the process. False to stop.
		:rtype: bool
		"""
		self.__internal_register_commands()
		tex_wo_ext = genutils.basename_with_path(filename, *FileType.tex_extensions())
		gls_file = FileType.glo.ensure_extension(filename)
		logging.info(T('MAKEGLOSSARIES: %s') % (os.path.basename(gls_file)))
		self.__reset_warnings()
		cmd = self.__makeglossaries_cli.copy()
		ist_file = self.configuration.generation.makeindex_style_filename
		if ist_file:
			cmd_def = AutoLaTeXMaker.__COMMAND_DEFINITIONS[GlossaryCompiler.makeglossaries.value]
			if not cmd_def:
				raise Exception(T("No command definition for 'makeglossaries'"))
			cmd.append(cmd_def['glossary_style_flag'])
			cmd.append(os.path.relpath(ist_file))
		cmd.append(os.path.relpath(tex_wo_ext))
		cmd = Runner.normalize_command(*cmd)
		command_output = Runner.run_command(*cmd)
		return command_output.return_code == 0

	def run_dvips(self, filename : str) -> dict[str,Any] | None:
		"""
		Launch the tool for converting a DVI file to a Postscript-based file. Replies the description of the current error.
		:param filename: The name dvi file to convert.
		:type filename: str
		:return: None on success; Otherwise a dict with the exit code and the standard and error outputs from the dvips tool.
		:rtype: dict[str,Any] | None
		"""
		self.__internal_register_commands()
		logging.info(T('DVIPS: %s') % os.path.basename(filename))
		if filename in self.__files:
			mfn = self.__files[filename].main_filename
			if mfn is not None and mfn != '':
				filename = mfn
		output = FileType.ps.ensure_extension(filename)
		cmd_def = AutoLaTeXMaker.__COMMAND_DEFINITIONS[TeXTools.dvips.value]
		self.__reset_warnings()
		cmd = self.__dvips_cli.copy()
		cmd.append(cmd_def['output'])
		cmd.append(os.path.relpath(output))
		cmd.append(os.path.relpath(filename))
		cmd = Runner.normalize_command(*cmd)
		command_output = Runner.run_command(*cmd)
		if command_output.return_code != 0:
			logging.debug(T('DVIPS: error when processing %s') % (os.path.basename(filename)))
			current_error = {'filename': filename, 'lineno': 0, 'message': command_output.standard_output + "\n" + command_output.error_output}
			return current_error
		return None

	def __create_file_description(self, output_file : str, output_type : FileType,
								  input_filename : str, main_filename : str | None,
								  use_xindy: bool = False,
								  use_biber: bool = False, use_multibib: bool = False,
								  use_bibunits: bool = False) -> bool:
		"""
		Create an entry into the list of files involved into the execution process.
		:param output_file: The name of the output file.
		:type output_file: str
		:param output_type: The type of the file, 'pdf' or 'ps'.
		:type output_type: str
		:param input_filename: The name of the input file.
		:type input_filename: str
		:param main_filename: The name of the main file associated to this file in the process. If it is None, the
		current file is the main file itself.
		:type main_filename: str | None
		:param use_xindy: Indicates if Xindy must be used for building the index. Default is False.
		:type use_xindy: bool
		:param use_biber: Indicates if Biber must be used for building the bibliography. Default is False.
		:type use_biber: bool
		:param use_multibib: Indicates if Multibib must be used for building the bibliography. Default is False.
		:type use_multibib: bool
		:param use_bibunits: Indicates if Bibunits must be used for building the bibliography. Default is False.
		:type use_bibunits: bool
		:return: True if the list of known dependencies has been changed
		:rtype: bool
		"""
		if output_file not in self.__files:
			desc = FileDescription(output_filename = output_file, file_type = output_type,
								   input_filename = input_filename, main_filename = main_filename)
			self.__files[output_file] = desc
			self.__files[output_file].use_xindy = use_xindy
			self.__files[output_file].use_biber = use_biber
			self.__files[output_file].use_multibib = use_multibib
			self.__files[output_file].use_bibunits = use_bibunits
			return True
		return False

	# noinspection DuplicatedCode
	def __compute_tex_dependencies(self, tex_root_filename : str, root_dir : str, pdf_filename : str) -> bool:
		"""
		Build the dependency tree for the given TeX file. Replies if the dependencies have been changed.
		:param tex_root_filename: The root TeX filename.
		:type tex_root_filename: str
		:param root_dir: The name of the root directory.
		:type root_dir: str
		:param pdf_filename: The name of the PDF file to generate.
		:type pdf_filename: str
		:return: True if the dependency tree has changed.
		:rtype: bool
		"""
		tex_files : deque[str] = deque()
		tex_files.append(tex_root_filename)
		changed = False
		while tex_files:
			tex_file = tex_files.popleft()
			assert tex_file is not None
			if os.path.isfile(tex_file):
				logging.debug(T("Computing dependencies for %s") % tex_file)
				analyzer = DependencyAnalyzer(tex_file, root_dir, tex_root_filename, self.configuration.generation.include_extra_macros)
				analyzer.run()
				chg = self.__create_file_description(tex_file, FileType.tex, tex_file,
													 main_filename=None if tex_file == tex_root_filename else tex_root_filename,
													 use_xindy=analyzer.is_xindy_index,
													 use_biber=analyzer.is_biber,
													 use_bibunits=analyzer.is_bibunits,
													 use_multibib=analyzer.is_multibib)
				# The bibliography and index flags must be propagated to the root tex file in order to be
				# detected outside this function
				if tex_file != tex_root_filename and tex_root_filename in self.__files:
					root_description = self.__files[tex_root_filename]
					if analyzer.is_bibunits:
						root_description.use_bibunits = True
					if analyzer.is_multibib:
						root_description.use_multibib = True
					if analyzer.is_biber:
						root_description.use_biber = True
					if analyzer.is_xindy_index:
						root_description.use_xindy = True
				changed = changed or chg
				all_dep_types = analyzer.get_dependency_types()
				# Treat the pure TeX files
				for dep_type in all_dep_types.intersection(FileType.tex_types()):
						deps = analyzer.get_dependencies_for_type(dep_type)
						for dep in deps:
							self.__create_file_description(dep.file_name, dep_type, dep.file_name,
														   None if dep_type != FileType.tex or dep == tex_root_filename
														   else tex_root_filename,
														   use_xindy=analyzer.is_xindy_index,
														   use_biber=analyzer.is_biber,
														   use_bibunits=analyzer.is_bibunits,
														   use_multibib=analyzer.is_multibib)
							self.__files[tex_file].dependencies.add(dep.file_name)
							changed = True
							if dep_type == FileType.tex:
								tex_files.append(dep.file_name)
				# Treat the bibliography files that are referred from the TeX code
				all_bbl_files = set()
				bibliography_dep_types = all_dep_types.intersection(FileType.bibliography_types())
				for dep_type in bibliography_dep_types:
						deps = analyzer.get_dependencies_for_type(dep_type)
						if dep_type == FileType.bib:
							for description in deps:
								bib_file = description.file_name
								self.__create_file_description(bib_file, FileType.bib, bib_file,
								                               main_filename=tex_root_filename,
								                               use_xindy=analyzer.is_xindy_index,
								                               use_biber=analyzer.is_biber,
								                               use_bibunits=analyzer.is_bibunits,
								                               use_multibib=analyzer.is_multibib)
								dep_bbl_files = description.output_files
								if not dep_bbl_files:
									# The name of the BBL file is from the basename of the AUX file,
									# that is based on the basename of the TeX file.
									bbl_file = FileType.bbl.ensure_extension(tex_root_filename)
									dep_bbl_files = [bbl_file]
								for bbl_file in dep_bbl_files:
									bbl_file = genutils.abs_path(FileType.bbl.ensure_extension(bbl_file),
																 os.path.dirname(bib_file))
									aux_file = FileType.aux.ensure_extension(bbl_file)
									self.__create_file_description(bbl_file, FileType.bbl, aux_file,
																   main_filename=tex_root_filename,
																   use_xindy=analyzer.is_xindy_index,
																   use_biber=analyzer.is_biber,
																   use_bibunits=analyzer.is_bibunits,
																   use_multibib=analyzer.is_multibib)
									self.__files[pdf_filename].dependencies.add(bbl_file)
									self.__files[bbl_file].dependencies.add(bib_file)
									self.__files[bbl_file].dependencies.add(tex_file)
									self.__files[bbl_file].use_xindy = analyzer.is_xindy_index
									self.__files[bbl_file].use_biber = analyzer.is_biber
									self.__files[bbl_file].use_multibib = analyzer.is_multibib
									self.__files[bbl_file].use_bibunits = analyzer.is_bibunits
									all_bbl_files.add(bbl_file)
									changed = True
				for dep_type in bibliography_dep_types:
					deps = analyzer.get_dependencies_for_type(dep_type)
					if dep_type != FileType.bib:
						for description in deps:
							chg = self.__create_file_description(description.filename,
																 description.file_type,
																 tex_root_filename,
																 main_filename=tex_root_filename,
																 use_xindy=analyzer.is_xindy_index,
																 use_biber=analyzer.is_biber,
																 use_bibunits=analyzer.is_bibunits,
																 use_multibib=analyzer.is_multibib)
							changed = changed or chg
							for bbl_file in all_bbl_files:
								self.__files[bbl_file].dependencies.add(description.file_name)
								changed = True

				# Treat the index files that  are referred from the TeX code
				if analyzer.is_makeindex:
					idx_file = FileType.idx.ensure_extension(tex_root_filename)
					self.__create_file_description(idx_file, FileType.idx, tex_root_filename,
												   main_filename=tex_root_filename,
												   use_xindy=analyzer.is_xindy_index,
												   use_biber=analyzer.is_biber,
												   use_bibunits=analyzer.is_bibunits,
												   use_multibib=analyzer.is_multibib)
					self.__files[idx_file].use_xindy = analyzer.is_xindy_index
					self.__files[idx_file].dependencies.add(tex_file)
					ind_file = FileType.ind.ensure_extension(idx_file)
					self.__create_file_description(ind_file, FileType.ind, idx_file,
												   main_filename=tex_root_filename,
												   use_xindy=analyzer.is_xindy_index,
												   use_biber=analyzer.is_biber,
												   use_bibunits=analyzer.is_bibunits,
												   use_multibib=analyzer.is_multibib)
					self.__files[ind_file].use_xindy = analyzer.is_xindy_index
					self.__files[ind_file].dependencies.add(idx_file)
					self.__files[pdf_filename].dependencies.add(ind_file)
					changed = True

				# Treat the glossaries files that  are referred from the TeX code
				if analyzer.is_glossary:
					glo_file = FileType.glo.ensure_extension(tex_root_filename)
					self.__create_file_description( glo_file, FileType.glo, tex_root_filename,
													main_filename=tex_root_filename,
													use_xindy=analyzer.is_xindy_index,
													use_biber=analyzer.is_biber,
													use_bibunits=analyzer.is_bibunits,
													use_multibib=analyzer.is_multibib)
					self.__files[glo_file].dependencies.add(tex_file)
					gls_file = FileType.gls.ensure_extension(glo_file)
					self.__create_file_description(gls_file, FileType.gls, tex_root_filename,
												   main_filename=tex_root_filename,
												   use_xindy=analyzer.is_xindy_index,
												   use_biber=analyzer.is_biber,
												   use_bibunits=analyzer.is_bibunits,
												   use_multibib=analyzer.is_multibib)
					self.__files[gls_file].dependencies.add(glo_file)
					self.__files[pdf_filename].dependencies.add(gls_file)
					changed = True
		return changed

	def __compute_aux_dependencies(self, tex_root_filename : str, root_dir : str, pdf_filename : str) -> bool:
		"""
		Build the dependency tree for the given Aux file. Replies if the dependencies have been changed.
		The references in the auxiliary file is related to specific bibliography systems, e.g., multibib.
		:param tex_root_filename: The TeX root filename.
		:type tex_root_filename: str
		:param root_dir: The name of the root directory.
		:type root_dir: str
		:param pdf_filename: The PDF root filename.
		:type pdf_filename: str
		:rtype: bool
		"""
		onlyfiles = [os.path.join(root_dir, f) for f in os.listdir(root_dir) if f.lower().endswith(FileType.aux.extension()) and os.path.isfile(os.path.join(root_dir, f))]
		changed = False
		for aux_file in onlyfiles:
			analyzer = AuxiliaryCitationAnalyzer(aux_file)
			analyzer.run()
			styles = analyzer.styles
			databases = analyzer.databases
			if styles:
				for style in styles:
					bst_file = os.path.abspath(style + FileType.bst.extension())
					if os.path.isfile(bst_file):
						chg = self.__create_file_description(bst_file, FileType.bst, tex_root_filename,
															 main_filename=tex_root_filename)
						changed = changed or chg
						for db in databases:
							bib_file = os.path.abspath(db)
							if os.path.isfile(bib_file):
								self.__create_file_description(bib_file, FileType.bib, db,
															   main_filename=tex_root_filename)
								bbl_file = os.path.abspath(FileType.bbl.ensure_extension(bib_file))
								self.__create_file_description(bbl_file, FileType.bbl, tex_root_filename,
															   main_filename=tex_root_filename)
								self.__files[bbl_file].dependencies.add(bst_file)
								self.__files[bbl_file].dependencies.add(bib_file)
								self.__files[pdf_filename].dependencies.add(bbl_file)
								changed = True
			if databases:
				for db in databases:
					bib_file = os.path.abspath(db)
					if os.path.isfile(bib_file):
						self.__create_file_description(bib_file, FileType.bib, tex_root_filename,
													   main_filename=tex_root_filename)
						bbl_file = FileType.bbl.ensure_extension(bib_file)
						self.__create_file_description(bbl_file, FileType.bbl, tex_root_filename,
													   main_filename=tex_root_filename)
						self.__files[bbl_file].dependencies.add(bib_file)
						self.__files[pdf_filename].dependencies.add(bbl_file)
						changed = True
		return changed

	def compute_dependencies(self, tex_filename : str, read_aux_file : bool = True) -> tuple[str,dict[str,FileDescription]]:
		"""
		Build the dependency tree for the given TeX file.
		:param tex_filename: The TeX filename.
		:type tex_filename: str
		:param read_aux_file: Indicates if the auxiliary files must be read too. Default is True.
		:type read_aux_file: bool
		:return: The tuple with the root dependency file and the description of a file.
		:rtype: tuple[str,dict[str,FileDescription]]
		"""
		root_dir = os.path.dirname(tex_filename)
		out_type = FileType.pdf if self.configuration.generation.pdf_mode else FileType.ps
		# Add dependency for the final PDF file
		out_file = out_type.ensure_extension(tex_filename)
		self.__create_file_description(out_file, out_type, tex_filename, tex_filename)
		self.__files[out_file].dependencies.add(tex_filename)
		# TeX files
		self.__compute_tex_dependencies(tex_filename, root_dir, out_file)
		if tex_filename in self.__files:
			in_file = self.__files[tex_filename]
			self.__files[out_file].use_xindy = in_file.use_xindy
			self.__files[out_file].use_multibib = in_file.use_multibib
			self.__files[out_file].use_bibunits = in_file.use_bibunits
			self.__files[out_file].use_biber = in_file.use_biber
		# Aux files
		if read_aux_file:
			self.__compute_aux_dependencies(tex_filename, root_dir, out_file)
		return out_file, self.__files


	# noinspection PyMethodMayBeStatic
	@override
	def is_obsolete_timestamp(self, parent_timestamp : float | None, child_timestamp : float | None) -> bool:
		"""
		Detemrine if the two given timestamps correspond to an obsolete parent time stamp compared to the child one.
		A parent time stamp is obsolete when it is undefined or its value is older than the one of the child.
		:param parent_timestamp: The timestamp of the parent file.
		:type parent_timestamp: float | None
		:param child_timestamp: The timestamp of the child file.
		:type child_timestamp: float | None
		:return: True if the parent file is considered as obsolete.
		:rtype: bool
		"""
		if parent_timestamp is None or child_timestamp is None:
			return True
		assert parent_timestamp is not None and child_timestamp is not None
		return parent_timestamp < child_timestamp

	def __build_internal_execution_list_rec(self,
	                                        root_tex_file: str,
	                                        current_filename: str,
	                                        dependencies: dict[str, FileDescription],
											builds : list[FileDescription],
											seen_files : set[str]) -> FileDescription:
		"""
		Build the list of files that needs to be generated in the best order.
		This function is checking if a builder was defined for the file.
		This function does not use the lasted change date of each file to determine if a build is needed.
		The test of recent changes is done by the builders themselves.
		This function must be invoked after a call to compute_dependencies().
		:param root_tex_file: The LaTeX file to compile.
		:type root_tex_file: str
		:param current_filename: The current file to generate.
		:type current_filename: str
		:param dependencies: The tree of the dependencies for the root file.
		:type dependencies: dict[str,FileDescription]
		:param builds: The list of files to be built. This list is filled up by this function.
		:type builds: list[FileDescription]
		:param seen_files: Set of filenames that have been already treated by the function in another recursive call.
		:return: The file description of the current file.
		:rtype: FileDescription
		"""
		assert current_filename in dependencies and dependencies[current_filename]
		current_file = dependencies[current_filename]
		if current_filename not in seen_files:
			# First time the first is found
			seen_files.add(current_filename)
			if current_file.dependencies:
				is_source_type = current_file.file_type.is_source_type()
				is_buildable = False
				for dependency in current_file.dependencies:
					dependency_file = self.__build_internal_execution_list_rec(root_tex_file=root_tex_file,
																			   current_filename=dependency,
																			   dependencies=dependencies,
																			   builds=builds,
																			   seen_files=seen_files)
					# Propagate the timestamp if parent and child are source types
					if is_source_type:
						if dependency_file.file_type.is_source_type() \
								and self.is_obsolete_timestamp(current_file.change, dependency_file.change):
							current_file.change = dependency_file.change
					elif not is_buildable:
						is_buildable = True
				if is_buildable and current_file.file_type in self.registered_builders:
					builds.append(current_file)
		return current_file


	def build_internal_execution_list(self,
									  root_file : str,
									  root_pdf_file : str,
									  dependencies : dict[str,FileDescription],
									  enable_initial_latex_run : bool = True) -> list[FileDescription]:
		"""
		Build the list of files that needs to be generated in the best order. For each file, a builder is defined and must be invoked.
		This function  does not use the lasted change date of each file to determine if a build is needed. It is delegated to
		the builders themselves.
		This function must be invoked after a call to compute_dependencies().
		:param root_file: The LaTeX file to compile.
		:type root_file: str
		:param root_pdf_file: The root PDF file to generate.
		:type root_pdf_file: str
		:param dependencies: The tree of the dependencies for the root file.
		:type dependencies: dict[str,FileDescription]
		:param enable_initial_latex_run: Indicates if the build list could contain the initial (La)TeX call for
		generating the first auxiliary files. Default is True.
		:type enable_initial_latex_run: bool
		:return: the list of files to be built.
		:rtype: list[FileDescription]
		"""
		builds = list()

		if enable_initial_latex_run:
			# Launch one LaTeX compilation to be sure that every auxiliary file that is expected is generated
			main_aux_file = FileType.aux.ensure_extension(root_file)
			description = FileDescription(
					output_filename = main_aux_file,
					file_type = FileType.aux,
					input_filename = root_file,
					main_filename = root_file)
			description.dependencies.add(root_file)
			builds.append(description)

		seen_files = set()
		self.__build_internal_execution_list_rec(root_file, root_pdf_file, dependencies, builds, seen_files)
		return builds

	def run_translators(self, force_generation : bool = False, detect_conflicts : bool = True) -> dict[str,str]:
		"""
		Run the image translators. Replies the list of images that is detected.
		The replied dict associates each source image (keys) to the generated image's filename (values) or None if no file was generated by the call to this function.
		:param force_generation: Indicates if the image generation is forced to be run on all the images (True) or if only the changed source images are considered for the image generation (False). Default is: False.
		:type force_generation: bool
		:param detect_conflicts: Indicates if the conflicts in translator loading is run. Default is True.
		:type detect_conflicts: bool
		:return: the dictionary that maps the source image's filename to the generated image's filename. Only the mapping for
		the images that are generated during this invocation of run_translators() are included in the dictionary.
		The images that are up-to-date are not put in the dictionary.
		:rtype: dict[str,str]
		"""
		self.translator_runner.sync(detect_conflicts = detect_conflicts)
		images = self.translator_runner.get_source_images()
		generated_images = dict()
		for img in images:
			generated_image = self.translator_runner.generate_image(in_file = img, only_more_recent = not force_generation)
			if generated_image:
				generated_images[img] = generated_image
		return generated_images

	# noinspection PyMethodMayBeStatic
	def __need_rebuild(self, root_file : str, file : FileDescription,
					   dependencies : dict[str,FileDescription],
					   builder : Builder) -> bool:
		"""
		Determines if a rebuild is needed for the file according to the behavior of the given builder.
		The builder is invoked for each registered dependencies of the file. If the builder replies that a rebuild
		is needed for at least one of these dependencies, then this function returns True.
		:param root_file: the name of the root TeX file.
		:type root_file: str
		:param file: the description of the file to be build up.
		:type file: FileDescription
		:param dependencies: list of known files in the dependency list.
		:type dependencies: dict[str,FileDescription]
		:param builder: The builder to consider.
		:type builder: Builder
		:return: True if a building is needed according to the behavior of the builder.
		:rtype: bool
		"""
		if builder.need_rebuild_without_dependency(current_file=file,
												   root_tex_file=root_file,
												   maker=self):
			return True
		if builder.consider_dependencies():
			if file.dependencies:
				for dependency in file.dependencies:
					dependency_file = dependencies[dependency] if dependency in dependencies else None
					if dependency_file:
						if builder.need_rebuild_with_dependency(current_file=file,
																dependency_file=dependency_file,
																root_tex_file=root_file,
																maker=self):
							return True
					else:
						return True
		return False

	# noinspection PyMethodMayBeStatic
	def __launch_file_build(self, root_file : str, file : FileDescription, force_change : bool,
							dependencies : dict[str,FileDescription]) -> bool:
		"""
		Launch the builder for the given file. If the builder does not exist, the function returns with True.
		If a builder is defined, the function tests with the builder is a build is needed. If a build is not
		needed, it means that the output file is up-to-date and nothing appends. If a build is needed, the
		builder is invoked for building.
		:param root_file: the name of the root TeX file.
		:type root_file: str
		:param file: the description of the file to be build up.
		:type file: FileDescription
		:param force_change: Indicates if the file needs to be changed or not.
		:type force_change: bool
		:param dependencies: list of known files in the dependency list.
		:type dependencies: dict[str,FileDescription]
		:return: the continuation status of the building process. If True is returned, it means that the building
		process could continue; Otherwise, the building process has to stop because of an abnormal condition from
		the builder.
		:rtype: bool
		"""
		builders = self.registered_builders
		if builders and file.file_type in builders:
			builder_factory = builders[file.file_type]
			if builder_factory:
				builder = builder_factory.builder(self.configuration)
				if builder and (force_change
								or self.__need_rebuild(root_file, file, dependencies, builder)):
					# Build is needed
					continuation =  builder.build(root_file=root_file,
								  input_file=file,
								  maker=self)
					# Reset the last change date of the output file to force the next builders to use
					# the last updated date
					file.reset_change()
					return continuation
			else:
				logging.error(T("A builder is defined for type '%s' without the definition of its internal factory" % file.file_type.name))
				return False
		return True

	# noinspection PyBroadException
	def build(self, force_change : bool = False) -> bool:
		"""
		Launch the building process (latex*, BibTeX, Makeindex, Makeglossaries).
		Caution: this function does not generate the images (See run_translators function).
		Caution: this function may invoke multiple times the latex tool.
		:param force_change: Indicates if the file needs to be changed or not.
		:type force_change: bool
		:return: True to continue process. False to stop the process.
		"""
		self.__reset_process_data()
		for root_file in self.__root_files:
			root_dir = os.path.dirname(root_file)

			# 1. Read building stamps
			logging.debug(T("Reading the saved stamps"))
			self.stamp_manager.read_build_stamps(root_dir)

			# 2. Compute the dependencies of the files
			logging.log(LogLevel.FINE_INFO, T("Building the file dependencies"))
			root_dep_file, dependencies = self.compute_dependencies(root_file)
			extlogging.multiline_debug(T("Dependency List = %s") % repr(dependencies))

			# 3. Construct the build list and launch the required builds
			logging.log(LogLevel.FINE_INFO, T("Building the execution list"))
			builds = self.build_internal_execution_list(root_file, root_dep_file, dependencies)
			if logging.getLogger().isEnabledFor(logging.DEBUG):
				if builds:
					logging.debug(T("Build list:"))
					idx = 1
					for b in builds:
						logging.debug(T("%d) %s") % (idx, b.output_filename))
						idx = idx + 1
				else:
					logging.debug(T("Empty build list"))

			# 4. Build the files
			if builds:
				for file in builds:
					continuation = self.__launch_file_build(root_file=root_file,
															file=file,
															force_change=force_change,
															dependencies=dependencies)
					if not continuation:
						return False

			# 5. Write building stamps
			logging.debug(T("Saving the file stamps"))
			self.stamp_manager.write_build_stamps(root_dir)

			# 6. Output the warnings from the last TeX builds
			main_tex_file = self.__files[root_file].main_filename or root_file
			log_file = FileType.log.ensure_extension(main_tex_file)
			log_parser = TeXLogParser(log_file=log_file)
			detailed_warnings = self.detailed_warnings
			if detailed_warnings:
				if logging.getLogger().isEnabledFor(LogLevel.FINE_INFO):
					for w in detailed_warnings:
						extlogging.multiline_fine_info(textwrap.wrap(
							T("%s:%d: %s") % (w.filename, w.lineno, w.message),
							width=80))
				self.__reset_warnings()

			# 7. Generate the Postscript-based file when requested
			if not self.configuration.generation.pdf_mode:
				dvi_file = FileType.dvi.ensure_extension(root_file)
				dvi_date = genutils.get_file_last_change(dvi_file)
				if dvi_date is not None:
					ps_file = FileType.ps.ensure_extension(root_file)
					ps_date = genutils.get_file_last_change(ps_file)
					if ps_date  is None or dvi_date >= ps_date:
						logging.log(LogLevel.FINE_INFO, T("Converting the DVI file: %s") % dvi_file)
						self.run_dvips(dvi_file)

			# 8. Detect warnings from the log file if not already done
			if not self.standard_warnings:
				log_parser.extract_warnings(enable_loop=False,
				                            enable_detailed_warnings=self.detailed_warnings_enabled,
				                            standards_warnings=self.__standards_warnings,
				                            detailed_warnings=self.__detailed_warnings)

			# 9. Output the last LaTeX warning indicators.
			if logging.getLogger().isEnabledFor(logging.WARNING):
				if TeXWarnings.multiple_definition in self.standard_warnings:
					s = T("LaTeX Warning: There were multiply-defined labels.")
					logging.warning(s)
					if self.detailed_warnings_enabled:
						extprint.eprint("!!" + log_file + ":W1: " + s + "\n")
				if TeXWarnings.undefined_reference in self.standard_warnings:
					s = T("LaTeX Warning: There were undefined references.")
					logging.warning(s)
					if self.detailed_warnings_enabled:
						extprint.eprint("!!" + log_file + ":W2: " + s + "\n")
				if TeXWarnings.undefined_citation in self.standard_warnings:
					s = T("LaTeX Warning: There were undefined citations.")
					logging.warning(s)
					if self.detailed_warnings_enabled:
						extprint.eprint("!!" + log_file + ":W3: " + s + "\n")
				if TeXWarnings.other_warning in self.standard_warnings:
					bn = os.path.basename(log_file)
					logging.warning((T("LaTeX Warning: Please look inside %s for the other the warning messages.") % bn) + "\n")
		return True

	@staticmethod
	def build_builder_dict(package_name : str) -> dict[FileType,BuilderFactory]:
		"""
		Build the dictionary that maps the builder id to AutoLaTeX dynamic builders.
		:param package_name: The name of the package to explore.
		:type package_name: str
		:return: the dict of the factories of builders.
		:rtype: dict[FileType,BuilderFactory]
		"""
		execution_environment : dict[str,Any] = {
			'modules': None,
		}
		exec("import " + package_name + "\nmodules = " + package_name + ".__all__",  None, execution_environment)
		modules = execution_environment['modules']
		ids : dict[FileType,BuilderFactory] = dict()
		for module in modules:
			execution_environment = {
				'type': None,
				'output': None,
			}
			cmd = textwrap.dedent("""\
						from %s.%s import DynamicBuilder
						type = DynamicBuilder
						output = DynamicBuilder.output
				""") % (package_name,  module)
			exec(cmd,  None, execution_environment)
			builder_type : Type[Builder] = execution_environment['type']
			builder_output : FileType = execution_environment['output']
			if builder_output and builder_type:
				ids[builder_output] = BuilderFactory(
					builder_output = builder_output,
					builder_type = builder_type)
		return ids

	@override
	def reset_file_change_for(self, filename : str):
		"""
		Reset the buffered last-changed date for the given filename, if it is known.
		If the filename is not known, this function does nothing.
		:param filename: The name of the file for which the last-change date should be reset.
		:type filename: str
		"""
		if filename in self.__files:
			self.__files[filename].reset_change()


	# noinspection PyBroadException
	def remove_output_file(self):
		"""
		Remove the output file from the TeX process if it exists.
		"""
		for root_file in self.__root_files:
			out_type = FileType.pdf if self.configuration.generation.pdf_mode else FileType.dvi
			out_file = out_type.ensure_extension(root_file)
			if os.path.isfile(out_file):
				try:
					os.unlink(out_file)
				except:
					pass

