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

import shutil
from datetime import datetime
import logging
import gzip
import platform
import os
import glob
import re
import subprocess
import sys
from typing import override

from setuptools import setup, find_packages, Command
from setuptools.command.build_py import build_py
from setuptools.command.install import install
from setuptools.command.sdist import sdist as sourcedist

# Read the program information
CURRENT_DIR = os.path.normpath(os.path.dirname(__file__))
with open(os.path.join(CURRENT_DIR, 'src', 'autolatex2', 'VERSION'), 'r', encoding='utf-8') as fh:
	line = fh.read()
	m = re.match('^([^ ]+)\\s+(.*?)\\s*$', line)
	if m:
		PROGRAM_NAME = m.group(1)
		PROGRAM_VERSION = m.group(2)
	else:
		raise Exception("Cannot read VERSION file")

now = datetime.now()
COPYRIGHT_YEAR = str(now.year)
PUB_YEAR = int(now.year)
PUB_MONTH = int(now.month)
PUB_DAY = int(now.day)
PUB_DATE_SLASH = f'{PUB_YEAR:04d}/{PUB_MONTH:02d}/{PUB_DAY:02d}'
PUB_DATE_DASH = f'{PUB_YEAR:04d}-{PUB_MONTH:02d}-{PUB_DAY:02d}'


def is_unix():
	return platform.system() in ('Linux', 'Darwin', 'FreeBSD', 'OpenBSD', 'NetBSD')


def has_pandoc():
	if shutil.which('pandoc'):
		return True
	return False


class PostBuildCommand(build_py):
	"""
	Custom build process that update the date in the STY files, ensure the version number in the root VERSION
	file corresponds to those from src/, generate the manual pages.
	"""

	def __init__(self, *args, **kwargs):
		super().__init__(*args, **kwargs)
		self.isunix = None
		self.unixman = None
		self.pandoc = None
		self.install_layout = 'deb'

	@override
	def initialize_options(self):
		super().initialize_options()
		self.isunix = is_unix()
		self.unixman = self.isunix
		self.pandoc = has_pandoc()

	@override
	def finalize_options(self):
		super().finalize_options()
		if self.isunix is None:
			self.isunix = is_unix()
		if self.unixman is None:
			self.unixman = self.isunix
		if self.pandoc is None:
			self.pandoc = has_pandoc()

	@staticmethod
	def replace_variables_in_file(input_file : str, **variables : str) -> str:
		"""
		Replace the variables in the input file. A variable is represented by "{{ variable_name }}" in
		the input file.
		:param input_file: path of the input file.
		:param variables: mapping from variable names to their values.
		:return: the path to the result file.
		"""
		with open(input_file, 'r') as f:
			content = f.read()
		pattern = r'\{\{\s*(\w+)\s*\}\}'
		def replacer(match):
			key = match.group(1)
			if key in variables:
				return str(variables[key])
			else:
				return match.group(0)
		content = re.sub(pattern, replacer, content)
		filtered_file = os.path.join(CURRENT_DIR, 'filtered_markdown.md')
		with open(filtered_file, 'w') as f:
			f.write(content)
		return filtered_file

	@staticmethod
	def replace_standard_variables_in_file(input_file : str) -> str:
		"""
		Replace the variables in the input file with the standard variables.
		:param input_file: path of the input file.
		:return: the path to the result file.
		"""
		return PostBuildCommand.replace_variables_in_file(input_file,
		                                                  python_version='3.12',
		                                                  program_name=f'{PROGRAM_NAME}',
		                                                  readable_name='AutoLaTeX',
		                                                  program_version=f'{PROGRAM_VERSION}',
														  pub_date=f'{PUB_DATE_DASH}',
		                                                  copyright_year=f'{COPYRIGHT_YEAR}')

	@override
	def run(self):
		print("Updating the LaTeX sty file")
		PostBuildCommand.update_sty_file()
		print("Updating the LaTeX Beamer sty file")
		PostBuildCommand.update_beamer_sty_file()
		print("Updating the VERSION file")
		self.update_version_file()
		super().run()
		print("Building final Markdown documentation")
		PostBuildCommand.md2md()
		if self.pandoc:
			print("Refreshing README")
			PostBuildCommand.md2readme()
		else:
			print("WARN: Skipping README updating because 'pandoc' cannot be found")
		if self.pandoc:
			print("Building ROFF man page")
			PostBuildCommand.md2man()
		else:
			print("WARN: Skipping ROFF man page creation because 'pandoc' cannot be found")
		if self.pandoc:
			print("Building PDF documentation")
			PostBuildCommand.md2pdf()
		else:
			print("WARN: Skipping PDF creation because 'pandoc' cannot be found")
		if self.isunix:
			PostBuildCommand.create_development_launcher()


	@staticmethod
	def md2md(in_md : str = None, out_md : str = None):
		if not in_md:
			in_md = os.path.join(CURRENT_DIR, 'docs', 'autolatex.md')
		if not out_md:
			out_md = os.path.join(CURRENT_DIR, 'build', 'doc', 'autolatex-base', 'autolatex.md')
		os.makedirs(os.path.dirname(out_md), exist_ok=True)
		in_filtered = PostBuildCommand.replace_standard_variables_in_file(in_md)
		try:
			shutil.copyfile(in_filtered, out_md)
		finally:
			os.unlink(in_filtered)


	@staticmethod
	def md2man(in_md : str = None, out_man : str = None, out_gz : str = None):
		program = shutil.which('pandoc')
		if program:
			if not in_md:
				in_md = os.path.join(CURRENT_DIR, 'docs', 'autolatex.md')
			if not out_man:
				out_man = os.path.join(CURRENT_DIR, 'build', 'man', 'man1', 'autolatex.1')
			if not out_gz:
				out_gz = out_man + '.gz' #usr/share/man/man1
			print("\tcreating %s and %s" % (out_man, out_gz))
			os.makedirs(os.path.dirname(out_man), exist_ok=True)
			in_filtered = PostBuildCommand.replace_standard_variables_in_file(in_md)
			try:
				rc = subprocess.call([program, in_filtered,
									  '-s',
									  '-t', 'man',
									  '-o', out_man,
									  '--variable', f'title={PROGRAM_NAME}',
									  '--variable', 'section=1',
									  '--variable', 'header=AutoLaTeX',
									  '--variable', f'footer={PROGRAM_VERSION}'])
			finally:
				os.unlink(in_filtered)
			if rc == 0:
				with open(out_man, 'rb') as f_in:
					with gzip.open(out_gz, 'wb') as f_out:
						shutil.copyfileobj(f_in, f_out)
			else:
				sys.exit(rc)
		else:
			print("WARNING: pandoc cannot be find in PATH. Skipping the generation of the ROFF man page")

	@staticmethod
	def md2pdf(in_md : str = None, out_pdf : str = None):
		program0 = shutil.which('pandoc')
		if program0:
			program1 = shutil.which('libreoffice')
			if program1:
				if not in_md:
					in_md = os.path.join(CURRENT_DIR, 'docs', 'autolatex.md')
				if not out_pdf:
					out_pdf = os.path.join(CURRENT_DIR, 'build', 'doc', 'autolatex-base', 'autolatex.pdf')
				out_pdf_basename, out_pdf_ext = os.path.splitext(out_pdf)
				out_odt = out_pdf_basename + '.odt'
				os.makedirs(os.path.dirname(out_odt), exist_ok=True)
				print("\tgenerating ODT in %s" % out_odt)
				in_filtered = PostBuildCommand.replace_standard_variables_in_file(in_md)
				try:
					rc = subprocess.call([program0, in_filtered,
										  '-s',
										  '-t', 'odt',
										  '-o', out_odt])
				finally:
					os.unlink(in_filtered)
				if rc == 0:
					try:
						rc = subprocess.call([program1,
											  '--headless',
											  '--nologo',
											  '--convert-to', 'pdf',
											  '--outdir', os.path.dirname(out_pdf),
											  out_odt])
						if rc != 0:
							sys.exit(rc)
					finally:
						os.unlink(out_odt)
				else:
					sys.exit(rc)
			else:
				print("WARNING: libreoffice cannot be find in PATH. Skipping the generation of the PDF documentation")
		else:
			print("WARNING: pandoc cannot be find in PATH. Skipping the generation of the ODT/PDF documentation")

	@staticmethod
	def md2readme(in_md : str = None, out_readme : str = None):
		program = shutil.which('pandoc')
		if program:
			if not in_md:
				in_md = os.path.join(CURRENT_DIR, 'docs', 'autolatex.md')
			if not out_readme:
				out_readme = os.path.join(CURRENT_DIR, 'README')
			in_filtered = PostBuildCommand.replace_standard_variables_in_file(in_md)
			try:
				rc = subprocess.call([program, in_filtered,
					'-s',
					'-t', 'plain',
					'-o', out_readme])
				if rc != 0:
					sys.exit(rc)
			finally:
				os.unlink(in_filtered)
		else:
			print("WARNING: pandoc cannot be find in PATH. Skipping the refreshing of README")

	# noinspection DuplicatedCode
	@staticmethod
	def update_sty_file(in_sty: str = None, out_sty: str = None):
		if not in_sty:
			in_sty = os.path.join(CURRENT_DIR, 'src', 'autolatex2', 'tex', 'autolatex.sty')
		if not out_sty:
			out_sty = os.path.join(CURRENT_DIR, 'src', 'autolatex2', 'tex', 'autolatex.sty')
		print("\treading %s" % in_sty)
		with open(in_sty, 'rt') as f_in:
			content = f_in.read()
		content = re.sub('autolatex@package@ver\\{[^}]+}',
						 'autolatex@package@ver{%s}' % PUB_DATE_SLASH,
						 content, re.DOTALL)
		content = re.sub('autolatexversion\\{[^}]+}',
						 'autolatexversion{%s}' % str(PROGRAM_VERSION),
						 content, re.DOTALL)
		print("\twriting %s" % out_sty)
		with open(out_sty, 'wt') as f_out:
			f_out.write(content)

	# noinspection DuplicatedCode
	@staticmethod
	def update_beamer_sty_file(in_sty: str = None, out_sty: str = None):
		if not in_sty:
			in_sty = os.path.join(CURRENT_DIR, 'src', 'autolatex2', 'tex', 'autolatex-beamer.sty')
		if not out_sty:
			out_sty = os.path.join(CURRENT_DIR, 'src', 'autolatex2', 'tex', 'autolatex-beamer.sty')
		print("\treading %s" % in_sty)
		with open(in_sty, 'rt') as f_in:
			content = f_in.read()
		content = re.sub('autolatexbeamer@package@ver\\{[^}]+}',
						 'autolatexbeamer@package@ver{%s}' % PUB_DATE_SLASH,
						 content, re.DOTALL)
		print("\twriting %s" % out_sty)
		with open(out_sty, 'wt') as f_out:
			f_out.write(content)

	def update_version_file(self, in_version: str = None, out_version: str = None):
		if not in_version:
			in_version = os.path.join(CURRENT_DIR, 'src', 'autolatex2', 'VERSION')
		if not out_version:
			out_version = os.path.join(CURRENT_DIR, 'VERSION')
		print("\tcopying %s to %s" % (in_version, out_version))
		self.copy_file(in_version, out_version, level=self.verbose)

	@staticmethod
	def create_development_launcher():
		bash_code = "#!/usr/bin/env bash\nDIR=`dirname \"$0\"`\nPYTHONPATH=\"$DIR/src\" exec python3 -B -m autolatex2.cli.autolatex \"$@\""
		bash_script = os.path.join(CURRENT_DIR, 'autolatex.sh')
		with open(bash_script, 'wt') as f_out:
			f_out.write(bash_code)

class PostInstallCommand(install):
	"""
	Custom installation process that install the manual pages.
	"""

	def __init__(self, *args, **kwargs):
		super().__init__(*args, **kwargs)
		self.unixman = True
		self.is_local_layout = True
		# The following attributes correspond to CLI options
		self.install_layout = 'deb'
		self.verbose = 1

	@override
	def initialize_options(self):
		super().initialize_options()
		self.unixman = is_unix()

	@override
	def finalize_options(self):
		super().finalize_options()
		layout = self.install_layout
		if layout is not None:
			self.is_local_layout = str(layout) != 'deb'
		else:
			self.is_local_layout = True
		if self.unixman is None:
			self.unixman = is_unix()

	@override
	def run(self):
		super().run()
		if self.unixman:
			print("Installing Unix manual page")
			self.install_man()
		print("Installing regular documentation")
		self.install_docs()

	def compute_install_path(self):
		if self.root:
			pfx = self.prefix[1:] if self.prefix and str(self.prefix).startswith(os.path.sep) else self.prefix
			path = os.path.join(self.root, str(pfx))
		else:
			path = self.prefix
		if self.is_local_layout:
			path = os.path.join(str(path), 'local')
		return path

	def install_man(self):
		path = self.compute_install_path()

		man_path = os.path.join(str(path), 'share', 'man', 'man1')
		man_path = os.path.normpath(man_path)

		src_mangz_file = os.path.join(CURRENT_DIR, 'build', 'man', 'man1', 'autolatex.1.gz')
		mangz_file = os.path.join(man_path, 'autolatex.1.gz')
		if os.path.isfile(src_mangz_file):
			os.makedirs(os.path.dirname(mangz_file), exist_ok=True)
			self.copy_file(src_mangz_file, mangz_file, level=self.verbose)

	def install_docs(self):
		path = self.compute_install_path()

		doc_md_path = os.path.join(str(path), 'share', 'doc', 'autolatex-base')
		doc_md_path = os.path.normpath(doc_md_path)

		src_doc_md_file = os.path.join(CURRENT_DIR, 'build', 'doc', 'autolatex-base', 'autolatex.md')
		doc_md_file = os.path.join(doc_md_path, 'autolatex.md')
		if os.path.isfile(src_doc_md_file):
			os.makedirs(os.path.dirname(doc_md_file), exist_ok=True)
			self.copy_file(src_doc_md_file, doc_md_file, level=self.verbose)

		src_doc_pdf_file = os.path.join(CURRENT_DIR, 'build', 'doc', 'autolatex-base', 'autolatex.pdf')
		doc_pdf_file = os.path.join(doc_md_path, 'autolatex.pdf')
		if os.path.isfile(src_doc_pdf_file):
			os.makedirs(os.path.dirname(doc_pdf_file), exist_ok=True)
			self.copy_file(src_doc_pdf_file, doc_pdf_file, level=self.verbose)


class CustomSourceDistributionCommand(sourcedist):
	"""
	Custom source distribution building that renames the root directory inside the archive in order to be
	compliant with the CTAN standards.
	"""

	DELETION_CANDIDATES = [
		os.path.join('src', 'autolatex.egg-info', 'dependency_links.txt')
	]

	def __init__(self, *args, **kwargs):
		super().__init__(*args, **kwargs)

	def make_distribution(self):
		"""
		Override the distribution creation to use a custom base directory name.
		"""
		with self._remove_os_link():
			# Set the desired name for the root directory inside the archive
			base_dir = PROGRAM_NAME
			full_base_dir = self.distribution.get_fullname()
			full_base_name = os.path.join(self.dist_dir, full_base_dir)

			logging.warning("CTAN standards: use base directory name '%s'" % base_dir)

			self.make_release_tree(base_dir, self.filelist.files)

			archive_files = []
			# Ensure tar format is processed last (to avoid accidental overwrites)
			if 'tar' in self.formats:
				self.formats.append(self.formats.pop(self.formats.index('tar')))

			# CTAN standard: Remove empty files from the source archive
			for deletion_candidate in CustomSourceDistributionCommand.DELETION_CANDIDATES:
				if os.path.isfile(deletion_candidate) and os.path.getsize(deletion_candidate) <= 1: # 1 byte is assumed to be empty
					logging.warning("CTAN standards: deleting empty %s" % deletion_candidate)
					full_path = deletion_candidate
					if not os.path.isabs(full_path):
						full_path = os.path.join(os.getcwd(), base_dir, full_path)
					#logging.warning("CTAN standards: %s" % full_path)
					os.unlink(full_path)

			for fmt in self.formats:
				file = self.make_archive(
					full_base_name, fmt, base_dir=base_dir, owner=self.owner, group=self.group
				)
				archive_files.append(file)
				self.distribution.dist_files.append(('sdist', '', file))

			self.archive_files = archive_files

			if not self.keep_temp:
				shutil.rmtree(base_dir, ignore_errors=True)


class CustomCleanCommand(Command):
	"""
	Custom clean command to tidy up the project root.
	"""

	user_options = [
		('all', 'a', 'Remove all temporary files, including Python cache files and extra folders'),
	]

	def initialize_options(self):
		self.all = False   # default value

	def finalize_options(self):
		pass

	def run(self):
		# The base build directories setuptools usually creates
		dirs_to_clean = [
			'build',
			'dist',
			'**/*.egg-info',
			'**/__pycache__',
		]

		# Custom files and directories to clean
		extra_paths = [
			'autolatex.sh',
			'filtered_markdown.md',
			'bin',
			'autolatex_*.dsc',
			'autolatex_*.tar.gz',
			'autolatex_*.buildinfo',
			'autolatex_*.changes',
			'autolatex*.deb'
		]

		if self.all:
			all_folders = dirs_to_clean + extra_paths
		else:
			all_folders = dirs_to_clean

		# Remove directories
		for pattern in all_folders:
			# Check if pattern contains a wildcard
			rec = '**' in pattern
			if rec or '*' in pattern:
				#print(f'\tcleaning: {pattern}')
				for path in glob.glob(pattern, recursive=rec):
					self._remove_path(path)
			else:
				self._remove_path(pattern)

	# noinspection PyMethodMayBeStatic
	def _remove_path(self, path):
		"""
		Safely remove a file or directory.
		"""
		try:
			#print(f"Removing: {path}")
			if os.path.isfile(path) or os.path.islink(path):
				os.unlink(path)
				print(f"Removed file: {path}")
			elif os.path.isdir(path):
				shutil.rmtree(path, ignore_errors=True)
				print(f"Removed directory: {path}")
		except Exception as e:
			print(f"Error removing {path}: {e}")


# Setup
setup(
	cmdclass ={
		'build_py': PostBuildCommand,
		'install': PostInstallCommand,
		'sdist': CustomSourceDistributionCommand,
		'clean': CustomCleanCommand,
	},
	name=PROGRAM_NAME,
	version=PROGRAM_VERSION,
	author="Stéphane Galland",
	author_email="galland@arakhne.org",
	description="AutoLaTeX is a tool for managing LaTeX documents",
	url="https://www.arakhne.org/autolatex",
	license='LGPL',
	project_urls={
		"Git": "https://github.com/gallandarakhneorg/autolatex2",
		"Bug Tracker": "https://github.com/gallandarakhneorg/autolatex2/issues",
	},
	classifiers=[
		"Programming Language :: Python :: 3",
		"License :: OSI Approved :: LGPL License",
		"Operating System :: OS Independent",
	],
	python_requires=">=3.12",
	install_requires=[
		"packaging",
		"sortedcontainers",
		"pyyaml",
	],
	package_dir={"":"src"},
	packages=find_packages(where='src'),
	entry_points=dict(
		console_scripts=[
			'autolatex=autolatex2.cli.autolatex:main'
		]
	),
	include_package_data = True,
	package_data={
		"": ["VERSION", "*.ist", "*.cfg", "*.transdef2", "*.sty"],
	},
)
