#!/usr/bin/env python3
# -*- coding: utf-8 -*-

# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
# IMPORTS
# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

from __future__ import annotations;
import sys;
import os;
import re;
import random;
import subprocess;
import numpy as np;

sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)));
os.chdir(os.path.dirname(os.path.abspath(__file__)));

## USER IMPORTS
{imports}

# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
# GLOBAL VARIABLES AND CONSTANTS
# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

____phpytex_params____ = dict(
    lines = dict(
        content = [ ],
        anon    = [ ],
        hide    = [ ],
        bib     = dict(),
    ),
    rootdir        = '{root}',
    output         = '{output}',
    name           = '{name}',
    compile_latex  = {compile_latex},
    pdflatex       = [
        # '-interaction=scrollmode',
        # '-interaction=batchmode',
        '-interaction=errorstopmode',
        # '-interaction=nonstopmode',
    ],
    insert_bib     = {insert_bib},
    bib_options    = [
    ],
    bib_backend    = '{backend_bib}',
    seed           = {seed},
    ctr_emptylines = 0,
    length         = 0,
    length_max     = {length_max},
    length_error   = False,
    indentsymb     = '{indentsymb}',
    censorsymb     = '{censorsymb}',
);
__FNAME__  = '<undefined>';
__ROOT__   = '.';
__DIR__    = '.';
__ANON__   = False;
__HIDE__   = False;
__IGNORE__ = False;
__MARGIN__ = '';

# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
# MAIN PROCESS
# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

def main():
    global ____phpytex_params____;
    global __ANON__;
    global __HIDE__;
    global __FNAME__;

    rootdir = ____phpytex_params____['rootdir'];
    os.chdir(rootdir);

    ## set rng seed
    ____reseed();

    ## transpile (python -> latex):
    {mainfct}();

    ## compile (latex -> pdf):
    if ____phpytex_params____['compile_latex']:
        ____compilelatex();

    ## rewrite contents of latex file:
    contents = ____phpytex_params____['lines']['content'];
    anon_indexes = ____phpytex_params____['lines']['anon'];
    hide_indexes = ____phpytex_params____['lines']['hide'];
    bib = ____phpytex_params____['lines']['bib'];
    bibcontents = ____get_bib_data();
    ____cleardocument();
    feed_buffer = [];
    # NOTE: usage of `all_lines_empty` ensure that final line is non-empty
    for index, line in enumerate(contents):
        tab = re.sub(r'^(\s*)(.*)$', r'\1', line);
        anon = (index in anon_indexes);
        hide = (index in hide_indexes);
        if hide:
            continue;
        sources = [
            (fname, src) for fname, (src, indexes) in bib.items()
                if src in bibcontents.keys()
                and index in indexes
        ];
        # check if insert bib contents necessary, and do this if possible:
        if len(sources) > 0:
            fname, src = sources[0];
            # NOTE: bibcontents[src] is guaranteed to not end in an empty line:
            # Print lines, even if contents are empty.
            __FNAME__ = fname;
            __ANON__ = anon;
            __HIDE__ = hide;
            # ____printfilestamp(depth=0, start=True, anon=anon, hide=hide);
            with open(____phpytex_params____['output'], 'a') as fp:
                for _line in bibcontents[src]:
                    print(tab + _line, file=fp);
            # ____printfilestamp(depth=0, start=False, anon=anon, hide=hide);
            feed_buffer = [];
            continue;
        ## or print original line if non-empty:
        if re.match('\S', line):
            with open(____phpytex_params____['output'], 'a') as fp:
                for _line in feed_buffer + [ line ]:
                    print(_line, file=fp);
            feed_buffer = [];
            continue;
        ## otherwise add to buffer and continue
        feed_buffer = feed_buffer + [ line ];
    return;

# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
# SECONDARY METHODS
# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

## reseed-funktion:
def ____reseed():
    global ____phpytex_params____;
    seed = ____phpytex_params____['seed'];
    if isinstance(seed, int):
        random.seed(seed);
        np.random.seed(seed);
    return True;

## record + print-to-latex:
def ____cleardocument():
    with open(____phpytex_params____['output'], 'w') as fp:
        fp.write('');
    return;

def ____forceprint(s: str, anon: bool = False, hide: bool = False, align: bool = False):
    global ____phpytex_params____;

    n = len(____phpytex_params____['lines']['content']);
    first = True;
    with open(____phpytex_params____['output'], 'a') as fp:
        for line in re.split(r'\r?\n', s):
            if align and not first:
                line = __MARGIN__ + line;
            first = False;
            print(line, file=fp);
            ____phpytex_params____['lines']['content'].append(line);
            if anon:
                ____phpytex_params____['lines']['anon'].append(n);
            if hide:
                ____phpytex_params____['lines']['hide'].append(n);
            n += 1;
    return;

def ____print(s: str, anon: bool = False, hide: bool = False, align: bool = False):
    global ____phpytex_params____;
    global __ANON__;
    global __HIDE__;
    global __IGNORE__;

    if __IGNORE__:
        return;
    if ____phpytex_params____['length_error']:
        return;

    ## reduces blocks of empty lines to single empty lines:
    ____phpytex_params____['ctr_emptylines'] = (____phpytex_params____['ctr_emptylines'] + 1) if re.match(r'^\s*$', s) else 0;
    if ____phpytex_params____['ctr_emptylines'] > 1:
        return;

    anon = (anon or __ANON__);
    hide = (hide or __HIDE__);
    ____phpytex_params____['length'] += 1;
    ____phpytex_params____['length_error'] = ____phpytex_params____['length_error'] or ( ____phpytex_params____['length'] > ____phpytex_params____['length_max'] );
    ____forceprint(s, anon=anon, hide=hide, align=align);
    return;

def ____printfilestamp(depth: int, start: bool, anon: bool, hide: bool):
    global ____phpytex_params____;
    global __ANON__;
    global __HIDE__;
    global __FNAME__;

    anon = (anon or __ANON__);
    hide = (hide or __HIDE__);
    params = dict(
        tab  = ____phpytex_params____['indentsymb'] * depth,
        path = ____phpytex_params____['censorsymb'] if anon else __FNAME__,
    );
    if start:
        ____print('{{tab}}%% ********************************************************************************'.format(**params), anon=anon, hide=hide);
        ____print('{{tab}}%% FILE: {{path}}'.format(**params), anon=anon, hide=hide);
        ____print('{{tab}}%% ********************************************************************************'.format(**params), anon=anon, hide=hide);
        ____print('', anon=anon, hide=hide);
    else:
        ____print('{{tab}}%% ********** END OF FILE: {{path}} **********'.format(**params), anon=anon, hide=hide);
    return;

## handling of bib:
def ____insertbib(
    fname:      str,
    textindent: str  = '',
    anon:       bool = False,
    mode:       str = 'basic',
    options:    str = '',
):
    global ____phpytex_params____;

    insert = True;

    if mode == 'biblatex':
        insert = False;
        if options == '':
            line = f'{{textindent}}\\printbibliography'
        else:
            line = f'{{textindent}}\\printbibliography[{{options}}]'
    else:
        # NOTE: older compilers require .bib to be stripped!
        # file = os.path.splitext(fname)[0]
        # NOTE: \bibliography file argument cannot contain ", ', or  spaces!
        line = f'{{textindent}}\\bibliography{{{{{{fname}}}}}}'

    n = len(____phpytex_params____['lines']['content']);
    with open(____phpytex_params____['output'], 'a') as fp:
        print(line, file=fp);

    ____phpytex_params____['lines']['content'].append(line);

    ## TODO: This needs to be generalised to allow the possibility of compiling multiple .bib files
    bibname = ____phpytex_params____['name'];
    src = f'{{bibname}}.bbl';

    if not ( fname in ____phpytex_params____['lines']['bib'] ):
        ____phpytex_params____['lines']['bib'][fname] = (src, []);
    if anon:
        ____phpytex_params____['lines']['anon'].append(n);
    elif ____phpytex_params____['insert_bib'] and insert:
        ____phpytex_params____['lines']['bib'][fname][1].append(n);
    return;

def ____get_bib_data() -> dict:
    global ____phpytex_params____;
    bib = ____phpytex_params____['lines']['bib'];
    bibcontents = dict();
    for fname, (src, indexes) in bib.items():
        if src in bibcontents:
            continue;
        try:
            lines = [];
            _lines = [];
            with open(src, 'r') as fp:
                _lines = fp.readlines();
            for line in _lines:
                line = re.sub(r'[\s\r\n]+$', '', line);
                if re.match(r'^\s*\%.*', line):
                    continue;
                lines.append(line);
            # Ensure that final line is non-empty
            while len(lines) > 0 and re.match(r'^\s*$', lines[-1]):
                lines = lines[:-1];
            bibcontents[src] = lines;
        except:
            print('[\033[93;1mWARNING\033[0m] Bibliography file, \033[1m{{file}}\033[0m, could not be found or read.'.format(file=src));
    return bibcontents;

## handling of pdflatex:
def ____compilelatex():
    global ____phpytex_params____;

    name = ____phpytex_params____['name'];
    bib = ____phpytex_params____['lines']['bib'];
    pdflatex_options = ____phpytex_params____['pdflatex'];
    bib_options = ____phpytex_params____['bib_options'];
    bib_be = ____phpytex_params____['bib_backend'];

    print('[\033[94;1mINFO\033[0m] RUN PDFLATEX');
    pipeCall('pdflatex', *pdflatex_options, name);#, errormsg='PDFLATEX encountered a problem.');

    print(f'[\033[94;1mINFO\033[0m] RUN {{bib_be.upper()}}');
    sources = [];
    for fname, (src, indexes) in bib.items():
        if src in sources:
            continue;
        bibname = os.path.splitext(src)[0];
        pipeCall(f'{{bib_be.lower()}}', *bib_options, bibname);#, errormsg=f'{{bib_be.upper()}} encountered a problem.');
        sources.append(src);
    pass;

## NOTE: subprocess.run is like subprocess.Popen but waits for result
def pipeCall(*args: str, cwd = None, errormsg: str = '', fnameOut = None):
    cwd = cwd if isinstance(cwd, str) else os.getcwd();
    if not isinstance(fnameOut, str):
        result = subprocess.run(list(args), cwd=cwd)
    else:
        with open(fnameOut, 'w') as fp:
            result = subprocess.run(list(args), cwd=cwd, stdout=fp);
    if result.returncode == 0:
        return;
    raise Exception(errormsg or 'Shell command < \033[94;1m{{}}\033[0m > failed.'.format(' '.join(args)));

# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
# TRANSPILED METHODS
# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
