#! /usr/bin/env python

##############################################
# Generate a fancy title page.               #
#                                            #
# Author: Scott Pakin <scott.clsl@pakin.org> #
##############################################

import random
import re
import subprocess
import sys


def read_logos():
    '''Read a list of logo symbols from symbols.tex.  Return a regular
    expression that matches them.'''
    # Prepare to match tables and symbols.
    table_re = re.compile(r'^\\begin\{(?:long)?symtable}.*'
                          r'(Brand Icons|Academic Profile Icons)\}')
    logo_re = re.compile(r'\\K(\S+)')

    # Read a list of logos from symbols.tex.
    logo_icons = []
    with open('symbols.tex') as r:
        in_logo_table = False
        for ln in r:
            if in_logo_table:
                # We're in a table of logos.
                if '\\end{longsymtable}' in ln or '\\end{symtable}' in ln:
                    # We reached the end of the table.
                    in_logo_table = False
                    continue
                for m in logo_re.finditer(ln):
                    # Store all brand-name symbols encountered.
                    logo_icons.append(m[1])
            elif table_re.match(ln) is not None:
                # We weren't in a table of logos but are now.
                in_logo_table = True

    # Convert the list to a regular expression and return it.
    esc_syms = [re.escape(b) for b in logo_icons]
    return re.compile('^(%s)$' % '|'.join(esc_syms))


def read_glyphs(fname):
    'Return a set of glyphs read from an index file.'
    # Define a list of symbols to ignore.
    ignore = [
        r'\pkgname',        # Not a symbol
        r'\trysym',         # Not a symbol
        r'$\neswarrow$',    # Not defined in the document's preamble
        r'$\nwsearrow$',    # Not defined in the document's preamble
        r'{\ndttstile}',    # I don't know why this fails.
        r'\blackacc\actuarial',    # Not defined in the document's preamble
        r'$\dashint$',      # Not defined in the document's preamble
        r'$\ddashint$',     # Not defined in the document's preamble
        r'$\independent$',  # Not defined in the document's preamble
        r'\irony',          # Not defined in the document's preamble
        r'$\topbot$',       # Not defined in the document's preamble
        r'$\dotcup$',       # Not defined in the document's preamble
        r'$\rqm$',          # Not defined in the document's preamble
        r'$\threesim$',     # Not defined in the document's preamble
        r'\AA',             # Font-encoding error (not sure why)
        r'\aa',             # Font-encoding error (not sure why)
        r'{\OGONk}',        # Font-encoding error (not sure why)
        r'{\underparenthesis}',    # Not defined in the document's preamble
        r'{\overparenthesis}',     # Not defined in the document's preamble
        r'$\stst$',         # Not defined in the document's preamble
        r'\DEDEwholeof',    # Not defined in the document's preamble
        r'\DEDEpartof',     # Not defined in the document's preamble
        r'$\suchthat$',     # Not defined in the document's preamble
        r'\definitedescription',   # Not defined in the document's preamble
        r'$\revddots$',     # Not defined in the document's preamble
        r'$\barcirc$',      # Not defined in the document's preamble
        r'$\bbar$',         # Not defined in the document's preamble
        r'$\dbar$',         # Not defined in the document's preamble
        r'\ismodeledby',    # Not defined in the document's preamble
        r'\hksqrt',         # Not defined in the document's preamble
        r'\asterism',       # Not defined in the document's preamble
        r'\suchthat',       # Not defined in the document's preamble
        r'\twemoji',        # Color; we want only black-and-white here
        r'\worldflag',      # Color; we want only black-and-white here
        r'\RHAT',           # Color; we want only black-and-white here
        r'\euflag',         # Color; we want only black-and-white here
        r'\spverb+(+',      # Single parenthesis confuses us
    ]
    smiley_re = re.compile(r'^\\d?([A-Z].*ey|Ninja)$')
    tree_re = re.compile(r'^\\(Autumn|Summer|Worst|Spring|Winter)[Tt]ree$')
    logos_re = read_logos()

    # Extract a list of symbols from the index file.
    glyphs = set()
    glyph_re = re.compile(r'^\\indexentry\{.*\(([^)]*\\[^)]+)\)\|hyperpage')
    with open(fname) as r:
        for ln in r:
            m = glyph_re.match(ln)
            if m is not None:
                g = m[1].strip()
                if any([bad in g for bad in ignore]):
                    continue
                if g in [
                        '}',   # Confusion from "\textknit{(}"
                        '$',   # Confusion from "($($)"
                        '}}',  # Confusion from "\ensuremath{...\char`(}}"
                ]:
                    continue
                if g.startswith(r'\bc'):
                    continue   # Color; we want only black-and-white here
                if smiley_re.match(g) is not None:
                    continue   # Color; we want only black-and-white here
                if tree_re.match(g) is not None:
                    continue   # Color; we want only black-and-white here
                if logos_re.match(g) is not None:
                    continue   # Don't give free advertising to organizations
                glyphs.add(g)
    return glyphs


def write_latex_file(paper_size, glyphs, terminate_after=False):
    'Write a .tex file suitable for inclusion in symbols.tex.'
    tex_name = f'title-{paper_size}.tex'
    with open(tex_name, 'w') as w:
        # Output header boilerplate.
        w.write(r'''
% Show a glyph with some trailing stretchable space.  If the glyph is
% too tall or too wide, shrink it to fit.  If the glyph is too deep,
% discard it.
\newlength{\symdim}
\newsavebox{\symbox}
\newcommand*{\maybeshow}[1]{%
  \savebox{\symbox}{#1}%
  \settoheight{\symdim}{\usebox{\symbox}}%
  \ifdim\symdim>10pt\relax
    \savebox{\symbox}{\resizebox{!}{10pt}{\usebox{\symbox}}}%
  \fi
  \settowidth{\symdim}{\usebox{\symbox}}%
  \ifdim\symdim>10pt\relax
    \savebox{\symbox}{\resizebox{10pt}{!}{\usebox{\symbox}}}%
  \fi
  \settodepth{\symdim}{\usebox{\symbox}}%
  \ifdim\symdim<2pt\relax
    \usebox{\symbox}%
    \hskip 1.5pt plus 1pt\relax
  \fi
}

% Define the title block.
\makeatletter
\let\todaysdate=\@date
\makeatother
\newsavebox{\titlebox}
\begin{lrbox}{\titlebox}
  \usefont{T1}{phv}{bx}{n}%
  \begin{tabular}{@{}c@{}}
    \\[20pt]
    \fontsize{28}{30}\selectfont The Comprehensive \\[14pt]
    \fontsize{28}{30}\selectfont \LaTeX\ Symbol List \\[3cm]
    \fontsize{14}{18}\selectfont
        Scott Pakin, \textit{scott-ctan@pakin.org} \\[1cm]
    \fontsize{14}{18}\selectfont \todaysdate
  \end{tabular}
\end{lrbox}
\renewcommand*{\windowpagestuff}{%
  \centering\usebox{\titlebox}%
}

% Typeset the title page.
''')

        # Use the cutwin package to draw the title page.
        top = 12 if paper_size == 'a4' else 10
        margin = '2cm' if paper_size == 'a4' else '1in'
        w.write('\\begin{cutout}{%d}{%s}{%s}{20}\n' % (top, margin, margin))
        w.write(r'  \cutfuzz\parindent=0pt\parfillskip=0pt' + '\n')
        for g in glyphs:
            w.write(r'  \maybeshow{%s}%%' % g)
            w.write('\n')
        w.write('\\end{cutout}\n')
        if terminate_after:
            w.write('\n')
            w.write('\\end{titlepage}\n')
            w.write('\\end{document}\n')


def build_latex_file(paper_size):
    '''Run pdflatex on the title page as incorporated into the symbol
    list.  Return the page count.'''
    # Run pdflatex.
    subprocess.run(['pdflatex',
                    '-jobname',
                    'symbols-' + paper_size,
                    r'\PassOptionsToClass{%spaper}{article}' % paper_size +
                    r'\def\titlefile{title-%s}\input symbols' % paper_size],
                   check=True)

    # Query the log file for the page count.
    symbols_base = f'symbols-{paper_size}'
    rerun = False
    with open(f'{symbols_base}.log') as r:
        for ln in r:
            # Check if we need to re-run pdflatex.
            if ln.startswith('LaTeX Warning: Temporary extra page added'
                             ' at the end. Rerun to get it removed.'):
                rerun = True
                break

            # Return the number of pages.
            if ln.startswith(f'Output written on {symbols_base}.pdf'):
                fields = ln.split()
                return int(fields[4][1:])

    # Try again if we got stuck with an extra page.
    if rerun:
        return build_latex_file(paper_size)
    raise RuntimeError(f'unexpected contents of symbols-{paper_size}.log')


def binary_search_num_glyphs(paper_size, glyphs, lb, ub):
    '''Return the maximum number of glyphs that can fit on a single page.
    The invariant is that lb glyphs fit on the page and ub glyphs do not.'''
    # Handle the base case.
    if lb == ub - 1:
        sys.stderr.write(f'INFO: Binary search found {lb} symbols is'
                         f' optimal for {paper_size} paper.\n')
        return lb

    # Build with the midpoint of lb and ub number of glyphs.
    mb = (lb + ub)//2
    write_latex_file(paper_size, glyphs[:mb], terminate_after=True)
    npages = build_latex_file(paper_size)
    sys.stderr.write(f'INFO: Binary search found that {mb} symbols produce' +
                     (' 1 page' if npages == 1 else f' {npages} pages') +
                     ' of output.\n')

    # Narrow the range of glyphs and recursively try again.
    if npages == 1:
        return binary_search_num_glyphs(paper_size, glyphs, mb, ub)
    else:
        return binary_search_num_glyphs(paper_size, glyphs, lb, mb)


###########################################################################

# Parse the command line.
try:
    idx_name = sys.argv[1]
    paper_size = sys.argv[2]
except IndexError:
    raise SystemExit('Usage: %s <file.idx> "a4"|"letter"' % sys.argv[0])

# Acquire a list of glyphs and randomize their order.
glyphs = list(read_glyphs(idx_name))
random.shuffle(glyphs)

# Determine the maximum number of glyphs that can fit on a page.
nglyphs = binary_search_num_glyphs(paper_size, glyphs, 1000, 3000)

# Perform a final build to leave LaTeX's auxiliary files as we found them.
write_latex_file(paper_size, glyphs[:nglyphs])
build_latex_file(paper_size)