#!/usr/bin/env python

# ====================================================================
def ensure_list(what):
    if not what:
        return None

    if isinstance(what,str):
        return [what]

    return what

# --------------------------------------------------------------------
def clean(base, suffixes):
    from pathlib import Path

    if not isinstance(base,Path):
        base = Path(base)
        
    for suffix in suffixes:
        tgt = base.with_suffix(suffix)
        tgt.unlink(missing_ok=True)

# ====================================================================
possible_commands = ['air',
                     'land',
                     'equipment',
                     'installation',
                     'sea surface',
                     'sub surface',
                     'space',
                     'activity',
                     'dismounted']
possible_factions = ['friendly',
                     'hostile',
                     'neutral',
                     'unknown']
echelon_mapping = {'':       'team',
                   '*':      'squad',
                   '**':     'section',
                   '***':    'platoon',
                   '|':      'company',
                   '||':     'battalion',
                   '|||':    'regiment',
                   'x':      'brigade',
                   'xx':     'division',
                   'xxx':    'corps',
                   'xxxx':   'army',
                   'xxxxx':  'army group',
                   'xxxxxx': 'theater'}
possible_echelons = list(echelon_mapping.values())+list(echelon_mapping.keys())
        
# ====================================================================
def make_picture(command,
                 faction,
                 echelon,
                 main,
                 top=None,
                 bottom=None,
                 left=None,
                 right=None,
                 below=None,
                 decoy=False,
                 line_width='1pt',
                 scale=.45,
                 more=None,
                 **kwargs):
    '''Create latex code for the NATO App6 symbol - a single tikzpicture

    Parameters
    ----------
    command : str
        Command (base symbol)
    faction : str
        Faction (base symbol)
    echelon : str or None
        Echelon (top of base symbol
    main : str or list of str
        Main symbols
    top : str or list of str
        Top modifiers
    bottom : str or list of str
        Bottom modifiers
    left : str or list of str
        Left modifiers
    right : str or list of str
        Right modifiers
    below : str or list of str
        Below base frame
    decoy : bool
        True if it should be a decoy
    line_width : str
        Line width, including unit
    scale : float
        Scaling of image

    Return
    ------
    out : str
        LaTeX code to write to file 
    '''
    ind = '               '
    def add_line(out,key,what,ind=ind):
        '''Adds a line with a key'''
        lwhat = ensure_list(what)
        if not lwhat:
            return out

        swhat = ",".join(lwhat)

        return out+f'{ind}    {key}={{{swhat}}},%'+'\n'

        
    out = fr'''\begin{{tikzpicture}}%
                 \natoapp[%
                   scale={scale},%
                   scale line widths,%
                   line width={line_width},%
                   command={command},%
                   faction={faction},%'''+'\n'

    out = add_line(out,'echelon',echelon)
    out = add_line(out,'main',   main)
    out = add_line(out,'top',    top)
    out = add_line(out,'bottom', bottom)
    out = add_line(out,'left',   left)
    out = add_line(out,'right',  right)
    out = add_line(out,'below',  below)
    if more:
        out += f'{ind}    {more}%'+'\n'
        
    out += fr'''    ];%
                \end{{tikzpicture}}%
             '''
    lout = out.split('\n')
    lout = [l[len(ind):] if l.startswith(ind) else l for l in lout]
    return '\n'.join(lout)

# --------------------------------------------------------------------
def make_latex(db):
    '''Create latex code for the all NATO App6 symbols -
    a complete document

    Parameters
    ----------
    db : list of dict
        Configurations of each symbol

    Return
    ------
    out : str
        LaTeX code to write to file 
    '''

    ind = '              '
    out = r'''\documentclass[tikz,11pt]{standalone}
              \usepackage{wargame}
              \begin{document}%'''+'\n'

    for entry in db:
        out += make_picture(**entry)

    out += r'\end{document}'

    lout = out.split('\n')
    lout = [l[len(ind):] if l.startswith(ind) else l for l in lout]
    return '\n'.join(lout)

    
# --------------------------------------------------------------------
def make_name(command,
              faction,
              echelon,
              main,
              top=None,
              bottom=None,
              left=None,
              right=None,
              below=None,
              decoy=False,
              output=None,
              **kwargs):
    '''Create the file name
    
    Parameters
    ----------
    command : str
        Command (base symbol)
    faction : str
        Faction (base symbol)
    echelon : str or None
        Echelon (top of base symbol
    main : str or list of str
        Main symbols
    top : str or list of str
        Top modifiers
    bottom : str or list of str
        Bottom modifiers
    left : str or list of str
        Left modifiers
    right : str or list of str
        Right modifiers
    decoy : bool
        True if it should be a decoy

    Return
    ------
    out : str
        Output file name
    '''
    if output:
        return output.format(command=command,
                             faction=faction,
                             echelon=echelon)\
                     .replace(' ','-')\
                     .replace('=','-')
        
    def clean_sub(what):
        from re import sub

        res = what
        while True:
            old = res
            res = sub(r'\[[^]]+\]','',res)
            res = sub(r'\{[^}]+\}','',res)
            if res == old:
                break

        return res
            
    def add_component(out, what,sep='-'):
         
        lwhat = ensure_list(what)
        
        if not lwhat:
            return out

        return out+'_'+sep.join([clean_sub(l) for l in lwhat])

    def add_front(out,what,sep='_'):
        if not what:
            return out

        if len(out) > 0: out += sep
        return out + what
    
    out = ''
    out = add_front(out,faction)
    out = add_front(out,command.replace(' ','-'))
    out = add_front(out,echelon.replace(' ','-'))
    out = add_component(out,main)
    out = add_component(out,top)
    out = add_component(out,bottom)
    out = add_component(out,left)
    out = add_component(out,right)
    out = add_component(out,below)

    return out.replace(' ','-').replace('=','-')


# ====================================================================
default_latex_flags = ['-interaction=nonstopmode',
		       '-file-line-error',
		       '-synctex','15',
		       '-shell-escape']
default_latex_command = 'pdflatex'

# --------------------------------------------------------------------
def create_process(args,verbose=False):
    '''Create a subprocess with arguments

    Returns
    -------
    proc : Popen
        Process with stdout and stderr as pipes (fifos)
    '''
    from os import environ
    from subprocess import Popen, PIPE

    if verbose:
        print(f'Will run: "{" ".join(args)}"')
        
    return Popen(args,env=environ.copy(),stdout=PIPE,stderr=PIPE)

# --------------------------------------------------------------------
default_timeout = None
default_keep    = False
# --------------------------------------------------------------------
def run_latex(file,
              content,
              latex_cmd    = default_latex_command,
              latex_flags  = default_latex_flags,
              timeout      = default_timeout,
              keep         = default_keep,
              verbose      = False):
    '''Run latex on generated content

    Parameters
    ----------
    filename : str
        The file to write
    content : str
        Content to write to source file
    latex_cmd : str
        LaTeX command to run e.g., 'pdflatex'
    latex_flags : list of str
        Flags to pass to the LaTeX command line
    timeout : int
        Process timeout
    keep : bool
        If true, keep intermediate files

    Returns
    -------
    None
    '''
    from pathlib import Path

    fpath  = Path(file.name).with_suffix('')
    csuf   = ['.aux','.log','.out','.synctex','.synctex.gz','.tex']
        
    file.write(content)
    file.close()
        
    lflags = ensure_list(latex_flags)

    args = [latex_cmd] + (lflags if lflags else + []) + \
        [fpath.with_suffix('.tex').name]

    proc = create_process(args,verbose=verbose)

    try:
        out, err = proc.communicate(timeout=timeout)

        sout = out.decode()
        if 'Error' in sout or \
           'Emergency stop' in sout or \
           not Path(fpath).with_suffix('.pdf').is_file():
            raise RuntimeError(sout)
        
    except Exception as e:
        proc.kill()
        proc.communicate()
        if not keep:
            clean(base=fpath,suffixes=csuf+['.pdf'])

        raise RuntimeError(f'Failed on "{" ".join(args)}" with:\n'+
                           str(e) + '\n--- Start LaTeX code -------\n'+
                           content+ '\n--- End LaTeX code ---------')

    if not keep:
        clean(base=fpath,suffixes=csuf)

# --------------------------------------------------------------------
possible_formats   = ['png','svg','jpg','tiff']
default_format     = possible_formats[0]
default_resolution = 150

# --------------------------------------------------------------------
def run_cairo(filename,
              npages,
              format     = default_format,
              resolution = default_resolution,
              timeout    = default_timeout,
              keep       = default_keep,
              verbose    = False):
    '''Run 'pdftocairo' on generated PDF

    Parameters
    ----------
    filename : str
        The file to write
    format : str
        Image format to write
    resolution : int
        Pixels Per Inch (PPI)
    timeout : int
        Process timeout
    keep : bool
        If true, keep intermediate files

    Returns
    -------
    None
    '''
    from pathlib import Path

    fpath = Path(filename)
    args  = ['pdftocairo']
    if format != 'svg':
        args.extend(['-transp'])

    args.extend(['-r',str(resolution),f'-{format}'])

    inpdf = fpath.with_suffix('.pdf').name
    
    def runit(args,pdf=inpdf,outimg=None):
        args.append(inpdf)
        if outimg: args.append(outimg)
        
        proc = create_process(args,verbose)

        try:
            out, err = proc.communicate(timeout=timeout)
              
        except Exception as e:
            proc.kill()
            proc.communicate()
            if not keep:
                clean(base=fpath,suffixes=[f'.{format}','.pdf'])

            raise RuntimeError(f'Failed on "{" ".join(args)}" with:\n'+e)

    if format != 'svg':
        runit(args)
    else:
        from math import ceil, log10

        base    = fpath.stem
        wn      = ceil(log10(npages))
        for no in range(1,npages+1):
            no_args = args.copy()
            no_args.extend(['-f',str(no),'-l',str(no)])

            runit(no_args,outimg=base+f'-{no:0{wn}d}.{format}')
        
    if not keep:
        clean(base=fpath,suffixes=['.pdf'])

# --------------------------------------------------------------------
def make_images(db,
                base_name,
                format   = default_format,
                verbose = False):
    from math import ceil, log10
    from pathlib import Path

    results = []
    ndb     = len(db)
    wn      = ceil(log10(ndb))
    for no,entry in enumerate(db):
        output = entry['output']+'.'+format
        inpath = Path(f'{base_name}-{no+1:0{wn}d}.{format}')

        inpath.rename(output)
        results.append(output)
        if verbose:
            print(f'{no+1:0{wn}d}/{ndb}: {output}')

    return results
    
# --------------------------------------------------------------------
default_xml = False

# --------------------------------------------------------------------
def write_xmls(db,
               format  = default_format,
               verbose = False):
    '''Build an XML snippet of a prototype'''

    ndb     = len(db)
    wn      = ceil(log10(ndb))
    for no,entry in enumerate(db):
        if verbose:
            print(f'{no+1:0{wn}d}/{ndb} ...{entry["output"]}.xml')

        write_xml(**entry,format=format)

# --------------------------------------------------------------------
def write_xml(command = None,
              faction = None,
              echelon = None,
              main    = None,
              top     = None,
              bottom  = None,
              left    = None,
              right   = None,
              below   = None,
              decoy   = False,
              output  = None,
              format  = default_format,
              **kwargs):
    '''Build an XML snippet of a prototype'''
    from xml.dom.minidom import Document
    from wgexport import LayerTrait, Prototype, MarkTrait, \
        BasicTrait, BuildFile

    doc = BuildFile()
    hasEchelon = echelon and len(echelon) > 0
    
    yoff   = -8 if main else -10 if echelon else -8
    traits = [
        LayerTrait(
            images   = [f'{output}.{format}'],
            newNames = [''],
            activateName = '',
            activateMask = '',
            activateChar = '',
            increaseName = '',
            increaseMask = '',
            increaseChar = '',
            decreaseName = '',
            decreaseMask = '',
            decreaseChar = '',
            resetName    = '',
            resetKey     = '',
            resetLevel   = 1,
            under        = False,
            underXoff    = 0,
            underYoff    = yoff,
            loop         = False,
            name         = ('NATOAPP6_command_faction'
                            +('_main' if main else '')
                            +('_echelon' if hasEchelon else '')),
            description  = 'NATO App6 symbology',
            randomKey    = '',
            randomName   = '',
            follow       = False,
            expression   = '',
            first        = 1,
            version      = 1,
            always       = True,
            activateKey  = '',
            increaseKey  = '',
            decreaseKey  = '',
            scale        = 1.)]

    if command:
        traits.append(MarkTrait(name  = 'NATOAPP6_command',
                                value = command))
    if main:
        traits.append(MarkTrait(name  = 'NATOAPP6_type',
                                value = make_name(command = '',
                                                  faction = '',
                                                  echelon = '',
                                                  main    = main,
                                                  top     = top,
                                                  bottom  = bottom,
                                                  left    = left,
                                                  right   = right,
                                                  below   = below,
                                                  decoy   = decoy)))
    if echelon:
        traits.append(MarkTrait(name = 'NATOAPP6_echelon',
                                value = echelon))

    traits.append(BasicTrait())


    proto = Prototype(doc,
                      name        = output,
                      traits      = traits,
                      description = 'NATO App6 symbol')

    doc._node.insertBefore(
        doc._root.createComment('Created via LaTeX wargame package, '
                                'licensed under the CreativeCommons '
                                'Share-Alike, '
                                '© 2024 Christian Holm Christensen'),
        proto._node)
    xml_out = doc.encode()

    with open(output+'.xml','wb') as out:
        out.write(xml_out)


# ====================================================================
def create_images(db,
                  latex_command = default_latex_command,
                  latex_flags   = default_latex_flags,
                  timeout       = default_timeout,
                  resolution    = default_resolution,
                  format        = default_format,
                  keep          = default_keep,
                  verbose       = False,
                  xml           = default_xml):
    from  tempfile import NamedTemporaryFile
    from pathlib import Path
    
    result = []

    
    with NamedTemporaryFile(mode='w',suffix='.tex',dir='.',
                            delete=not keep,
                            delete_on_close=False) as texfile:
        if verbose:
            print(f'Temporary file: {texfile.name}')
            
        base_name  = Path(texfile.name).stem
        latex_code = make_latex(db)
        try:
            run_latex(texfile,
                      latex_code,
                      latex_cmd   = latex_command,
                      latex_flags = latex_flags,
                      timeout     = timeout,
                      keep        = keep,
                      verbose     = verbose)
                  
            run_cairo(base_name,
                      npages     = len(db),
                      format     = format,
                      resolution = resolution,
                      timeout    = timeout,
                      keep       = keep,
                      verbose    = verbose)

            result = make_images(db,
                                 base_name,
                                 format   = format,
                                 verbose  = verbose)

            if xml:
                write_xmls(db)
            
        except Exception as e:
            import traceback
            print(traceback.format_exc())        
            print(e)
            return None

    return result
        
        
# ====================================================================
#
# Main program
#
if __name__ == '__main__':
    from argparse import ArgumentParser, FileType
    from pprint import pprint
    
    ap = ArgumentParser(description='Create an image of NATO App6 symbol')
    ap.add_argument('-c','--command',type=str,choices=possible_commands,
                    help='Select command',default=possible_commands[1])
    ap.add_argument('-f','--faction',type=str,choices=possible_factions,
                    help='Select faction',default=possible_factions[0])
    ap.add_argument('-e','--echelon',type=str,choices=possible_echelons,
                    help='Select echelon')
    ap.add_argument('-m','--main',type=str,nargs='*',
                    help='Select main symbol(s)')
    ap.add_argument('-t','--top',type=str,nargs='*',
                    help='Select top modifier(s)')
    ap.add_argument('-b','--bottom',type=str,nargs='*',
                    help='Select bottom modifier(s)')
    ap.add_argument('-l','--left',type=str,nargs='*',
                    help='Select left modifier(s)')
    ap.add_argument('-r','--right',type=str,nargs='*',
                    help='Select left modifier(s)')
    ap.add_argument('-B','--below',type=str,nargs='*',
                    help='Select below (mobility) modifier(s)')
    ap.add_argument('-d','--decoy',action='store_true',
                    help='Mark as decoy')
    ap.add_argument('-w','--line-width',default='1pt',type=str,
                    help='Line width, including unit')
    ap.add_argument('-s','--scale',default=0.45,type=float,
                    help='Scaling of symbol (width in centimetre)')
    ap.add_argument('-I','--format',default=default_format,
                    choices=possible_formats,type=str,
                    help='Output image format')
    ap.add_argument('-L','--latex-command',default=default_latex_command,
                    help='Set the LaTeX command to use',type=str)
    ap.add_argument('-F','--latex-flags',default=default_latex_flags,
                    help='Set the LaTeX command to use',type=str,nargs='*')
    ap.add_argument('-T','--timeout',default=default_timeout,type=int,
                    help='Timeout of sub-processes')
    ap.add_argument('-R','--resolution',default=default_resolution,type=int,
                    help='Resolution out output image (ppi)')
    ap.add_argument('-k','--keep',action='store_true',
                    help='Leave intermediate files')
    ap.add_argument('-o','--output',type=str,
                    help='Set output file name, default is autogenerated')
    ap.add_argument('-j','--json',type=str,nargs='*',
                    help='Read symbols to create from JSON file')
    ap.add_argument('-v','--verbose',action='store_true')
    ap.add_argument('-X','--xml',action='store_true',
                    help='Also output XML snippets')

    args      = ap.parse_args()
    vargs     = vars(args)
    oargs     = {}
    to_remove = ['latex_command',
                 'latex_flags',
                 'resolution',
                 'format',
                 'timeout',
                 'keep',
                 'json',
                 'verbose',
                 'xml']
    for key in to_remove:
        if key not in vargs:
            continue

        oargs[key] = vargs[key]
        del vargs[key]
        
    commons = ['infantry',
               'armoured',
               'armoured+infantry',
               'reconnaissance',
               'armoured+reconnaissance',
               '[fill=pgfstrokecolor]artillery',
               'armoured+[fill=pgfstrokecolor]artillery',
               'text=SF']
    

    db     = None
    main   = vargs['main']
    injson = oargs.pop('json',None)
    
    #pprint(vargs)
    #pprint(oargs)
    
    if injson:
        from json import load

        db = []
        for j in injson:
            print(f'=== JSON input: {j}')
            with open(j,'r') as jin:
                db.extend(load(jin))
                
    elif main and len(main) == 1 and main[0] == 'common':
        db = [{'main': w.split('+')} for w in commons]
    else:
        db = [vargs]

    if not db:
        raise RuntimeError('Noting to do')

    from math import log10, ceil
    
    def ensure(d, key, default):
        if key not in d and default:
            d[key] = default[key]

    results = []
    ndb     = len(db)
    wn      = ceil(log10(ndb))
    for no,entry in enumerate(db):
        print(f'{no:{wn}d} of {ndb} ...',end='')
        if not isinstance(entry,dict):
            print(' skipped')
            continue

        for key in vargs.keys():
            ensure(entry,key,vargs)
        entry['output'] = make_name(**entry)
        print(entry['output'])


    results = create_images(db,**oargs)
#
#
#