#!/usr/bin/env python
#
from pprint import pprint

# ====================================================================
def add_pws(opw=None,upw=None):
    '''Add password options to the poppler commands

    Parameters
    ----------
    opw : str
    	Owner password
    upw : str
    	User password

    Returns
    -------
    opts : list
    	List of arguments 
    '''
    args = []
    if  upw is not None:
        args.extend(['-upw',upw])

    if  opw is not None:
        args.extend(['-opw',opw])

    return args

# --------------------------------------------------------------------
def create_proc(args):
    '''Create a process

    Parameters
    ----------
    args : list
    	List of commmand line arguments

    Returns
    -------
    proc : subprocess.Process
    	Process handle 
    '''
    from os import environ
    from subprocess import Popen, PIPE, TimeoutExpired

    return Popen(args,env=environ.copy(),stdout=PIPE,stderr=PIPE)
    
# --------------------------------------------------------------------
def get_pdf_info(pdfname='export.pdf',upw=None,opw=None,timeout=None):
    '''Get dictionary of PDF information

    Parameters
    ----------
    pdfname : str
    	File name
    opw : str
    	Owner password
    upw : str
    	User password
    timeout : int
    	Timeout in seconds or None
    
    Returns
    -------
    info : dict
        PDF information
    '''
    from subprocess import TimeoutExpired
   
    args = ['pdfinfo', pdfname]
    args.extend(add_pws(opw=opw,upw=upw))

    proc = create_proc(args)

    try:
        out, err = proc.communicate(timeout=timeout)
    except Exception as e:
        proc.kill()
        proc.communicate()
        raise RuntimeError(f'Failed to get PDF info: {e}')

    d = {}
    for field in out.decode("utf8", "ignore").split("\n"):
        sf         = field.split(":")
        key, value = sf[0], ":".join(sf[1:])
        if key != "":
            d[key] = (int(value.strip()) if key == 'Pages' else value.strip())

    if "Pages" not in d:
        raise ValueError

    return d
        
# --------------------------------------------------------------------
def get_images_info(infofile):
    '''Read in image information from file

    Parameters
    ----------
    infofile : str
         file name

    Returns
    -------
    images : list
         List of image information 
    '''
    #from csv import DictReader
    from json import load 

    with open(infofile,'r') as file:
        info = load(file)
        #reader = DictReader(file,fieldnames=['page','name','type','sub type'])
        #return [l for l in reader]

    return info
    

# --------------------------------------------------------------------
def convert_page(pdfname,page,opw=None,upw=None,timeout=None):
    '''Convert a single PDF page to an image
    
    Parameters
    ----------
    pdfname : str
    	File name
    page : int
        Page number 
    opw : str
    	Owner password
    upw : str
    	User password
    timeout : int
    	Timeout in seconds or None
    
    Returns
    -------
    img : bytes
        The bytes of the image 
    '''
    args = ['pdftocairo',
            '-singlefile',
            '-f', str(page),
            '-l', str(page),
            '-png']
    args.extend(add_pws(opw=opw,upw=upw))
    args.append(pdfname)
    args.append('-')

    proc = create_proc(args)

    try:
        out, err = proc.communicate(timeout=timeout)
    except Exception as e:
        proc.kill()
        proc.communicate()
        raise RuntimeError(f'Failed to convert page {page} of {pdfname}: {e}')

    from io import BytesIO
    from PIL import Image
    
    img = out #Image.open(BytesIO(out))

    return img

# --------------------------------------------------------------------
def ignore_entry(entry):
    return entry['category'] in ['<<dummy>>','<<eol>>']

# --------------------------------------------------------------------
def convert_pages(pdfname,infoname,
                  opw=None,upw=None,timeout=None):
    '''Convert pages in a PDF to images

    This creates a list of dictionaries with the image name and data
    (as bytes)

    Parameters
    ----------
    pdfname : str
    	File name of PDF to read images from 
    infoname : str
        File name of CSV to read image info from 
    opw : str
    	Owner password
    upw : str
    	User password
    timeout : int
    	Timeout in seconds or None
    
    Returns
    -------
    imgs : list
        List of images as dicts, info and the bytes of the images 
    '''

    oargs   = {'opw':opw,'upw':upw,'timeout':timeout}
    docinfo = get_pdf_info(pdfname,**oargs)
    info    = get_images_info(infoname)

    if len(info)-1 != docinfo['Pages']:
        raise RuntimeError(f'Number of pages in {pdfname} '
                           f'({docinfo["Pages"]}) not {len(info)-1}')

    for i in info:
        if ignore_entry(i): continue
        i['img'] = convert_page(pdfname,i['number'],**oargs)

    return info

# ====================================================================
def remove_and_get(zipfname,*filenames):
    '''Open zip file, and make a copy of it without the files listed
    in entries.  Return the data of files listed in entry.'''
    from tempfile import mkdtemp
    from zipfile import ZipFile
    from shutil import move, rmtree 
    from os import path

    tempdir = mkdtemp()
    ret     = {}
    try:
        tempname = path.join(tempdir, 'new.zip')
        with ZipFile(zipfname, 'r') as zipread:

            with ZipFile(tempname, 'w') as zipwrite:

                for item in zipread.infolist():
                    data = zipread.read(item.filename)

                    if item.filename not in filenames:
                        zipwrite.writestr(item, data)
                    else:
                        ret[item.filename] = data
                        
        move(tempname, zipfname)
    finally:
        rmtree(tempdir)

    return ret

# --------------------------------------------------------------------
def add_back(zipfname,files):
    '''Open a zip file for appending and add data in files

    Files is a dictionary of filenames mapping to file content,
    as returned by remove_and_get
    '''
    from zipfile import ZipFile
    
    with ZipFile(zipfname, 'a') as zipadd:
        for filename,data in files.items():
            zipadd.writestr(filename,data)

# ====================================================================
# Contants used for tag names
BUILD  = 'VASSAL.build.'
MODULE = BUILD + 'module.'
WIDGET = BUILD + 'widget.'
MAP    = MODULE + 'map.'    
PICKER = MAP + 'boardPicker.'

# ====================================================================
def get_asdict(elem,tag,key,enable=True):
    cont = elem.getElementsByTagName(tag)
    if not enable:
        return cont

    return {e.getAttribute(key): e for e in cont}

# --------------------------------------------------------------------
def get_asone(elem,tag,enable=True):
    cont = elem.getElementsByTagName(tag)
    if enable and len(cont) != 1:
        return None
    return cont
    
# --------------------------------------------------------------------
def get_doc(string):
    from xml.dom.minidom import parseString

    return parseString(string)

# --------------------------------------------------------------------
def get_game(doc):
    return doc.getElementsByTagName('VASSAL.build.GameModule')[0]

# --------------------------------------------------------------------
def get_globalproperties(doc,one=True):
    return get_asone(doc,MODULE+'properties.GlobalProperties',one)

# --------------------------------------------------------------------
def get_globalproperty(doc,asdict=True):
    '''A little confusing - this returns a dictionary'''
    return get_asdict(doc,MODULE+'properties.GlobalProperty','name',asdict)

# --------------------------------------------------------------------
def get_turntrack(doc,asdict=True):
    return get_asdict(doc,MODULE+'turn.TurnTracker','name',asdict)

# --------------------------------------------------------------------
def get_turncounter(doc,asdict=True):
    return get_asdict(doc,MODULE+'turn.CounterTurnLevel','property',asdict)

# --------------------------------------------------------------------
def get_turnlist(doc,asdict=True):
    return get_asdict(doc,MODULE+'turn.ListTurnLevel','property',asdict)

# --------------------------------------------------------------------
def get_turnhotkey(doc,asdict=True):
    return get_asdict(doc,MODULE+'turn.TurnGlobalHotkey','hotkey',asdict)

# --------------------------------------------------------------------
def get_documentation(doc,one=True):
    return get_asone(doc,MODULE+'Documentation',one)

# --------------------------------------------------------------------
def get_helpfiles(doc,key='fileName',asdict=True):
    return get_asdict(doc,MODULE+'documentation.HelpFile',key,asdict)

# --------------------------------------------------------------------
def get_tutorials(doc,key='name',asdict=True):
    return get_asdict(doc,MODULE+'documentation.Tutorial',key,asdict)

# --------------------------------------------------------------------
def get_inventories(doc,key='name',asdict=False):
    return get_asdict(doc,MODULE+'Inventory',key,asdict)

# --------------------------------------------------------------------
def get_maps(doc,asdict=True):
    return get_asdict(doc,MODULE+'Map','mapName',asdict)

# --------------------------------------------------------------------
def get_widgetmap(doc,asdict=True):
    return get_asdict(doc,WIDGET+'WidgetMap','mapName',asdict)

# --------------------------------------------------------------------
def get_chartwindows(doc,asdict=True):
    return get_asdict(doc,MODULE+'ChartWindow','name',asdict)

# --------------------------------------------------------------------
def get_map(doc,name):
    maps = get_maps(doc,True)
    return maps.get(name, None) if maps else None

# --------------------------------------------------------------------
def get_boards(doc,asdict=True):
    return get_asdict(doc,PICKER+'Board','name',asdict)

# --------------------------------------------------------------------
def get_zoned(doc,one=True):
    return get_asone(doc,PICKER+'board.ZonedGrid',one)

# --------------------------------------------------------------------
def get_zone(doc,asdict=True):
    return get_asdict(doc,PICKER+'board.mapgrid.Zone','name',asdict)

# --------------------------------------------------------------------
def get_hexgrid(doc,one=True):
    return get_asone(doc,PICKER+'board.HexGrid',one)

# --------------------------------------------------------------------
def get_squaregrid(doc,one=True):
    return get_asone(doc,PICKER+'board.SquareGrid',one)

# --------------------------------------------------------------------
def get_regiongrid(doc,one=True):
    return get_asone(doc,PICKER+'board.RegionGrid',one)

# --------------------------------------------------------------------
def get_hexnumbering(doc,one=True):
    return get_asone(doc,PICKER+'board.mapgrid.HexGridNumbering',one)
        
# --------------------------------------------------------------------
def get_regions(doc,asdict=True):
    return get_asdict(doc,PICKER+'board.Region', 'name', asdict)

# --------------------------------------------------------------------
def get_squarenumbering(doc,one=True):
    return get_asone(doc,PICKER+'board.mapgrid.SquareGridNumbering', one)

# --------------------------------------------------------------------
def get_pieces(doc,asdict=True):
    return get_asdict(doc,WIDGET+'PieceSlot','entryName',asdict)

# --------------------------------------------------------------------
def get_prototypecontainer(doc,one=True):
    return get_asone(doc,MODULE+'PrototypesContainer',one)

# --------------------------------------------------------------------
def get_prototypes(doc,asdict=True):
    return get_asdict(doc,MODULE+'PrototypeDefinition', 'name',asdict)

# --------------------------------------------------------------------
def get_masskey(map,asdict=True):
    return get_asdict(map,MAP+'MassKeyCommand','name', asdict)

# --------------------------------------------------------------------
def get_masskey(map,asdict=True):
    return get_asdict(map,MAP+'MassKeyCommand','name', asdict)


# --------------------------------------------------------------------
def get_piece_parts(code):
    '''Takes the code part of a pieceslot (the text node) and decodes
    it into traits.

    One can use this to modify the pieces.  One could for example
    remove a trait by calling this function, and remove the element
    corresponding to the trait from the returned list. Then one must
    collect up the definitions and states separately and pass each of
    them to `enc_parts', which gives us two strings that can be passed
    to `add_piece' (or `add_proto') function to get the new text node
    content of the piece slot or prototype definition.  For example

        piece  = get_pieceslots(root,pieces)['foo']
        code   = piece.childNodes[0].nodeValue
        traits = get_piece_parts(code)
        ... # Modify the list
    
        # Separate out definitions and states 
        defs   = [enc_def(e['def'])   for e in traits]
        states = [enc_def(e['state']) for e in traits]

        # Encode definitions and states as a single string, and then form body 
        def    = enc_parts(*defs)
        state  = enc_parts(*states)
        body   = add_piece(def,state)

        # Set new definition 
        piece.childNodes[0].nodeValue = body 
        
    See also dicts_to_piece
    
    Parameters
    ----------
    code : str
        Text node content of <PieceSlot> or <PrototypeDefinition> tags
    
    Returns
    -------
    traits : list of dict
        All the traits in a list.  Each element of the list is a
        dictionary with the keys 'def' and 'status'.

        The key 'def' points to a list of the trait attributes used to
        define the trait type.  The first element of the 'def' list is
        the trait identifier.  The rest are the arguments for that
        trait type.  Note, all fields are encoded as strings. 

        The key 'status' encodes the status of the trait.  The format
        of this depends on the trait.Note, all fields are encoded as
        strings.

    '''
    cmd,iden,typ,sta = code.split('/')
    defs = typ.split(r'	')
    stas = sta.split(r'	')
    defs = [d.rstrip("\\").split(';') for d in defs]
    stas = [s.rstrip("\\").split(';') for s in stas]
    ret  = [{'def': d, 'state': s} for d,s in zip(defs,stas)]
    return ret

# --------------------------------------------------------------------
def dicts_to_piece(traits):
    # Separate out definitions and states 
    defs   = [enc_def(e['def'])   for e in traits]
    states = [enc_def(e['state']) for e in traits]

    # Encode definitions and states as a single string, and then form body 
    df     = enc_parts(*defs)
    state  = enc_parts(*states)
    body   = add_piece(df,state)

    return body
    
    
# ====================================================================
# Key encoding
SHIFT = 65
CTRL = 130
ALT   = 520
CTRL_SHIFT = CTRL+SHIFT
ALT_SHIFT = ALT+SHIFT
NONE = '\ue004'
NONE_MOD = 0
def key(let,mod=CTRL):

    '''Encode a key sequence

    Parameters
    ----------
    let : str
        Key code (Letter)
    mod : int
        Modifier mask
    '''
    return f'{ord(let)},{mod}'
# Colour encoding 
def rgb(r,g,b):
    return ','.join([str(r),str(g),str(b)])

def rgba(r,g,b,a):
    return ','.join([str(r),str(g),str(b),str(a)])


# --------------------------------------------------------------------
def set_node_attr(node,**attr):
    for k,v in attr.items():
        node.setAttribute(k,str(v).lower() if isinstance(v,bool) else str(v))
    
# --------------------------------------------------------------------
def add_node(root,parent,tag,**attr):
    e = root.createElement(tag)
    if parent: parent.appendChild(e)
    set_node_attr(e,**attr)

    return e


# --------------------------------------------------------------------
def add_text(root,parent,text):
    t = root.createTextNode(text)
    parent.appendChild(t)
    return t

# --------------------------------------------------------------------
def add_game(root,doc,
             name,
             version, 
             ModuleOther1    = "",
             ModuleOther2    = "",
             VassalVersion   = "3.6.7",
             description     = "",
             nextPieceSlotId = 0):
    return add_node(root,doc,BUILD+'GameModule',
                    name            = name,
                    version         = version,
                    ModuleOther1    = ModuleOther1,
                    ModuleOther2    = ModuleOther2,
                    VassalVersion   = VassalVersion,
                    description     = description,
                    nextPieceSlotId = nextPieceSlotId)

# --------------------------------------------------------------------
def add_basiccommandencoder(root,doc):
    return add_node(root,doc,MODULE+'BasicCommandEncoder')

# --------------------------------------------------------------------
def add_globalproperties(root,elem,*props,**named):
    gp = add_node(root,elem,MODULE+'properties.GlobalProperties')
    # Add elements where each is a dict with _at least_
    # "{'name':NAME, 'initialValue':VALUE}"
    for p in props:
        add_globalproperty(root,gp,**p)
    # Add elements where each is a named dict with _at least_
    # "{'initialValue':VALUE}"
    for n, p in named:
        add_globalproperty(root,gp,name=n,**p)

    return gp
        

# --------------------------------------------------------------------
def add_globalproperty(root,elem,name,initialValue,
                       isNumeric   = False,
                       min         = "null",
                       max         = "null",
                       wrap        = False,
                       description = ""):
    return add_node(root,elem,MODULE+'properties.GlobalProperty',
                    name         = name,
                    initialValue = initialValue,
                    isNumeric    = isNumeric,
                    min          = min,
                    max          = max,
                    wrap         = wrap,
                    description  = description)

# --------------------------------------------------------------------
def add_turntrack(root, elem, name,
                  buttonText       = 'Turn',
                  hotkey           = '',
                  icon             = '',
                  length           = -1,
                  lengthStyle      = 'Maximum',
                  nexthotkey       = key('T',ALT),
                  plusButtonSize   = 22,
                  prevhotkey       = key('T',ALT_SHIFT),
                  reportFormat     = '<$PlayerId$> Turn updated from $oldTurn$ to $newTurn$',
                  turnButtonHeight = 22,
                  turnFormat       = None,
                  counter          = None,
                  phases           = None):

    levels = (counter if counter is not None else
              phases if phases is not None else None)
    if levels is not None:
        lvl = 1
        lvls = [f'$level{lvl}$']
        sub  = levels
        while True:
            sub = sub.get('counter',sub.get('phases',None))
            if sub is None:
                break
            lvl += 1
            lvls.append(f'$level{lvl}$')
            
        turnFormat = ' '.join(lvls)
        
    if turnFormat is None:
        turnFormat = '$level1$ $level2$ $level3$ $level4$'        
        
    t = add_node(root, elem, MODULE+'turn.TurnTracker',
                 name             = name,
                 buttonText       = buttonText,
                 hotkey           = hotkey,
                 icon             = icon,
                 length           = length,
                 lengthStyle      = lengthStyle,
                 nexthotkey       = nexthotkey,
                 plusButtonSize   = plusButtonSize,
                 prevhotkey       = prevhotkey,
                 reportFormat     = reportFormat,
                 turnButtonHeight = turnButtonHeight,
                 turnFormat       = turnFormat)

    add_turnlevel(root, t, counter=counter, phases=phases)
    
    return t

# --------------------------------------------------------------------
def add_turnlevel(root,parent,counter=None,phases=None):
    f,s = ((add_turncounter,counter) if counter is not None else
           (add_turnlist,phases)     if phases  is not None else (None,None))
    if f is None: return parent

    subcounter = s.pop('counter',None)
    subphases  = s.pop('phases',None)

    sub = f(root,parent,**s)

    return add_turnlevel(root,sub,counter=subcounter,phases=subphases)

# --------------------------------------------------------------------
def add_turncounter(root,elem,property,
                    incr          = 1,
                    loop          = False,
                    loopLimit     = -1,
                    turnFormat    = "$value$"):
    c = add_node(root,elem,MODULE+"turn.CounterTurnLevel",
                 property       = property,
                 incr           = incr,
                 loop           = loop,
                 loopLimit      = loopLimit,
                 turnFormat     = turnFormat)
    return c
                    
# --------------------------------------------------------------------
def add_turnlist(root,elem,property,names,
                 configFirst   = False,
                 configList    = False,
                 turnFormat    = '$value$'):
    c = add_node(root,elem,MODULE+"turn.ListTurnLevel",
                 property       = property,
                 list           = ','.join([str(p) for p in names]),
                 configFirst    = configFirst,
                 configList     = configList,
                 turnFormat     = turnFormat)
    return c
                  
# --------------------------------------------------------------------
def add_turnhotkey(root,elem,
                   hotkey,
                   match        = '{phase=="first"}',
                   reportFormat = '{PlayerName}',
                   name         = ''):
    return add_node(root,elem,MODULE+'turn.TurnGlobalHotkey',
                    hotkey       = hotkey,
                    match        = match,
                    reportFormat = reportFormat,
                    name         = name)

                   
                  
# --------------------------------------------------------------------
def add_translatable(root,elem):
    return add_node(root,elem,MODULE+'properties.GlobalTranslatableMessages')

# --------------------------------------------------------------------
def add_language(root,elem):
    return add_node(root,elem,'VASSAL.i18n.Language')

# --------------------------------------------------------------------
def add_chatter(root,elem):
    return add_node(root,elem,MODULE+'Chatter')

# --------------------------------------------------------------------
def add_keynamer(root,elem):
    return add_node(root,elem,MODULE+'KeyNamer')

# --------------------------------------------------------------------
def add_documentation(root,doc):
    return add_node(root,doc,MODULE+'Documentation')

# --------------------------------------------------------------------
def add_about(root,doc,title,fileName=""):
    return add_node(root,doc,MODULE+'documentation.AboutScreen',
                    fileName = fileName,
                    title = title)
# --------------------------------------------------------------------
def add_pdffile(root,doc,title,pdfFile):
    return add_node(root,doc,MODULE+'documentation.BrowserPDFFile',
                    pdfFile = pdfFile, title=title)
# --------------------------------------------------------------------
def add_helpfile(root,doc,title,fileName,fileType="archive"):
    return add_node(root,doc,MODULE+'documentation.HelpFile',
                    fileName = fileName,
                    fileType = fileType,
                    title    = title)

# --------------------------------------------------------------------
def add_htmlfile(root,doc,title,startingPage='index.html'):
    return add_node(root,doc,MODULE+'documentation.BrowserHelpFile',
                    startingPage=startingPage,
                    title=title)

# --------------------------------------------------------------------
def add_tutorial(root,doc,
                 name            = 'Tutorial',
                 logfile         = 'tutorial.vlog',
                 promptMessage   = 'Load the tutorial?',
                 welcomeMessage  = 'Press "Step forward" (PnDn) to step through the tutorial',
                 launchOnStartup = True):
    return add_node(root,doc,MODULE+'documentation.Tutorial',
                    name            = name,
                    logfile         = logfile,
                    promptMessage   = promptMessage,
                    welcomeMessage  = welcomeMessage,
                    launchOnStartup = launchOnStartup)

# --------------------------------------------------------------------
def add_roster(root,doc,buttonKeyStroke='',
               buttonText='Retire',
               buttonToolTip='Switch sides, become observer, or release faction'):
    return add_node(root,doc,MODULE+'PlayerRoster',
                    buttonKeyStroke = buttonKeyStroke,
                    buttonText      = buttonText,
                    buttonToolTip   = buttonToolTip)

# --------------------------------------------------------------------
def add_entry(root,doc,text):
    n = add_node(root,doc,'entry')
    add_text(root,n,text)
    return n

# --------------------------------------------------------------------
def add_globaloptions(root,doc,
                      autoReport               = "Use Preferences Setting",
                      centerOnMove             = "Use Preferences Setting",
                      chatterHTMLSupport       = "Always",
                      hotKeysOnClosedWindows   = "Never",
                      inventoryForAll          = "Always",
                      nonOwnerUnmaskable       = "Use Preferences Setting",
                      playerIdFormat           = "$playerName$",
                      promptString             = "Opponents can unmask pieces",
                      sendToLocationMoveTrails = "Never"):
    return add_node(root,doc,MODULE+'GlobalOptions',
                      autoReport               = autoReport,
                      centerOnMove             = centerOnMove,
                      chatterHTMLSupport       = chatterHTMLSupport,
                      hotKeysOnClosedWindows   = hotKeysOnClosedWindows,
                      inventoryForAll          = inventoryForAll,
                      nonOwnerUnmaskable       = nonOwnerUnmaskable,
                      playerIdFormat           = playerIdFormat,
                      promptString             = promptString,
                      sendToLocationMoveTrails = sendToLocationMoveTrails)

# --------------------------------------------------------------------
def add_option(root,doc,name,value):
    n = add_node(root,doc,'option',name=name)
    add_text(n,root,value)
    return n

# --------------------------------------------------------------------
# CurrentMap == &quot;Board&quot;
def add_inventory(root,doc,
                  canDisable          = False,
                  centerOnPiece       = True,
                  disabledIcon        = '',
                  drawPieces          = True,
                  foldersOnly         = False,
                  forwardKeystroke    = True,
                  groupBy             = '',
                  hotkey              = key('I',ALT),
                  icon                = '/images/inventory.gif',
                  include             = '{}',
                  launchFunction      = 'functionHide',
                  leafFormat          = '$PieceName$',
                  name                = '',
                  nonLeafFormat       = '$PropertyValue$',
                  pieceZoom           = '0.33',
                  pieceZoom2          = '0.5',
                  pieceZoom3          = '0.6',
                  propertyGate        = '',
                  refreshHotkey       = key('I',ALT_SHIFT),
                  showMenu            = True,
                  sides               = '',
                  sortFormat          = '$PieceName$',
                  sortPieces          = True,
                  sorting             = 'alpha',
                  text                = '',
                  tooltip             = 'Show inventory of all pieces',
                  zoomOn              = False):
    return add_node(root,doc,MODULE+'Inventory',
                    canDisable          = canDisable,
                    centerOnPiece       = centerOnPiece,
                    disabledIcon        = disabledIcon,
                    drawPieces          = drawPieces,
                    foldersOnly         = foldersOnly,
                    forwardKeystroke    = forwardKeystroke,
                    groupBy             = groupBy,
                    hotkey              = hotkey,
                    icon                = icon,
                    include             = include,
                    launchFunction      = launchFunction,
                    leafFormat          = leafFormat,
                    name                = name,
                    nonLeafFormat       = nonLeafFormat,
                    pieceZoom           = pieceZoom,
                    pieceZoom2          = pieceZoom2,
                    pieceZoom3          = pieceZoom3,
                    propertyGate        = propertyGate,
                    refreshHotkey       = refreshHotkey,
                    showMenu            = showMenu,
                    sides               = sides,
                    sortFormat          = sortFormat,
                    sortPieces          = sortPieces,
                    sorting             = sorting,
                    text                = text,
                    tooltip             = tooltip,
                    zoomOn              = zoomOn)
    
                  

# --------------------------------------------------------------------
def add_map(root,doc,
            mapName,
            allowMultiple        = 'false',
            backgroundcolor      = rgb(255,255,255),
            buttonName           = '',
            changeFormat         = '$message$',
            color                = rgb(0,0,0),
            createFormat         = '$pieceName$ created in $location$ *',
            edgeHeight           = '0',
            edgeWidth            = '0',
            hideKey              = '',
            hotkey               = key('M',ALT),
            icon                 = '/images/map.gif',
            launch               = 'false',
            markMoved            = 'Always',
            markUnmovedHotkey    = '',
            markUnmovedIcon      = '/images/unmoved.gif',
            markUnmovedReport    = '',
            markUnmovedText      = '',
            markUnmovedTooltip   = 'Mark all pieces on this map as not moved',
            moveKey              = '',
            moveToFormat         = '$pieceName$ moves $previousLocation$ &amp;rarr; $location$ *',
            moveWithinFormat     = '$pieceName$ moves $previousLocation$ &amp;rarr; $location$ *',
            showKey              = '',
            thickness            = '3',
            cls                  = MODULE+'Map'):
    return add_node(root,doc,cls,
                    allowMultiple        = allowMultiple,
                    backgroundcolor      = backgroundcolor,
                    buttonName           = buttonName,
                    changeFormat         = changeFormat,
                    color                = color,
                    createFormat         = createFormat,
                    edgeHeight           = edgeHeight,
                    edgeWidth            = edgeWidth,
                    hideKey              = hideKey,
                    hotkey               = hotkey,
                    icon                 = icon,
                    launch               = launch,
                    mapName              = mapName,
                    markMoved            = markMoved,
                    markUnmovedHotkey    = markUnmovedHotkey,
                    markUnmovedIcon      = markUnmovedIcon,
                    markUnmovedReport    = markUnmovedReport,
                    markUnmovedText      = markUnmovedText,
                    markUnmovedTooltip   = markUnmovedTooltip,
                    moveKey              = moveKey,
                    moveToFormat         = moveToFormat,
                    moveWithinFormat     = moveWithinFormat,
                    showKey              = showKey,
                    thickness            = thickness)

# --------------------------------------------------------------------
def add_widgetmap(root,doc, mapName,**attr):
    return add_map(root,doc,mapName,cls=WIDGET+'WidgetMap',**attr)

# --------------------------------------------------------------------
def add_boardpicker(root,doc,
                    addColumnText        = 'Add column',
                    addRowText           = 'Add row',
                    boardPrompt          = 'Select board',
                    slotHeight           = 125,
                    slotScale            = 0.2,
                    slotWidth            = 350,
                    title                = 'Choose Boards'):
    return add_node(root,doc,MAP+'BoardPicker',
                    addColumnText        = addColumnText,
                    addRowText           = addRowText,
                    boardPrompt          = boardPrompt,
                    slotHeight           = slotHeight,
                    slotScale            = slotScale,
                    slotWidth            = slotWidth,
                    title                = title) 

# --------------------------------------------------------------------
def add_setup(root,picker,mapName,maxColumns,boardNames):
    '''Add given boards in row-first up to maxColumns per row'''
    s = add_node(root,picker,'setup')
    col = 0
    row = 0
    lst = [f'{mapName}BoardPicker']
    for bn in boardNames:
        lst.extend([bn,str(col),str(row)])
        col += 1
        if col >= maxColumns:
            col = 0
            row += 1
    txt = r'	'.join(lst)
    add_text(root,s,txt)
            
# --------------------------------------------------------------------
def add_board(root,picker,name,
              image      = '',
              reversible = False,
              color      = rgb(255,255,255),
              width      = 0,
              height     = 0):
    return add_node(root,picker,PICKER+'Board',
                    image      = image,
                    name       = name,
                    reversible = reversible,
                    color      = color,
                    width      = width,
                    height     = height)

# --------------------------------------------------------------------
def add_zoned(root,board):
    return add_node(root,board,PICKER+'board.ZonedGrid')

# --------------------------------------------------------------------
def add_zonehighlighter(root,zoned):
    return add_node(root,zoned,PICKER+'board.mapgrid.ZonedGridHighlighter')

# --------------------------------------------------------------------
def add_zone(root,zoned,
             name              = "",
             highlightProperty = "",
             locationFormat    = "$gridLocation$",
             path              = "0,0;976,0;976,976;0,976",
             useHighlight      = False,
             useParentGrid     = False):
    return add_node(root,zoned,PICKER+'board.mapgrid.Zone',
                    name              = name,
                    highlightProperty = highlightProperty,
                    locationFormat    = locationFormat,
                    path              = path,
                    useHighlight      = useHighlight,
                    useParentGrid     = useParentGrid)

# --------------------------------------------------------------------
HEX_WIDTH = 88.50779626676963
HEX_HEIGHT = 102.2
def add_hexgrid(root,zone,
                color        = rgb(0,0,0),
                cornersLegal = False,
                dotsVisible  = False,
                dx           = HEX_WIDTH,
                dy           = HEX_HEIGHT,
                edgesLegal   = False,
                sideways     = False,
                snapTo       = True,
                visible      = True,
                x0           = 0,
                y0           = 32,
                cls          = PICKER+'board.HexGrid'):
    return add_node(root,zone,cls,
                    color        = color,
                    cornersLegal = cornersLegal,
                    dotsVisible  = dotsVisible,
                    dx           = dx,
                    dy           = dy,
                    edgesLegal   = edgesLegal,
                    sideways     = sideways,
                    snapTo       = snapTo,
                    visible      = visible,
                    x0           = x0,
                    y0           = y0)

# --------------------------------------------------------------------
def add_hexnumbering(root,grid,
                     color                = rgb(255,0,0),
                     first                = 'H',
                     fontSize             = 24,
                     hDescend             = False,
                     hDrawOff             = 0,
                     hLeading             = 1,
                     hOff                 = 0,
                     hType                = 'A',
                     locationFormat       = '$gridLocation$',
                     rotateText           = 0,
                     sep                  = '',
                     stagger              = True,
                     vDescend             = False,
                     vDrawOff             = 32,
                     vLeading             = 0,
                     vOff                 = 0,
                     vType                = 'N',
                     visible              = True,
                     cls                  = PICKER+'board.mapgrid.HexGridNumbering'):
    return add_node(root,grid,cls,
                    color                = color,
                    first                = first,
                    fontSize             = fontSize,
                    hDescend             = hDescend,
                    hDrawOff             = hDrawOff,
                    hLeading             = hLeading,
                    hOff                 = hOff,
                    hType                = hType,
                    locationFormat       = locationFormat,
                    rotateText           = rotateText,
                    sep                  = sep,
                    stagger              = stagger,
                    vDescend             = vDescend,
                    vDrawOff             = vDrawOff,
                    vLeading             = vLeading,
                    vOff                 = vOff,
                    vType                = vType,
                    visible              = visible)

# --------------------------------------------------------------------
RECT_WIDTH  = 80
RECT_HEIGHT = 80
def add_squaregrid(root,zone,
                   dx           = RECT_WIDTH,
                   dy           = RECT_HEIGHT,
                   edgesLegal   = False,
                   x0           = 0,
                   y0           = int(0.4*RECT_HEIGHT),
                   **attr):
    return add_hexgrid(root,zone,cls=PICKER+'board.SquareGrid',
                       dx           = dx,
                       dy           = dy,
                       edgesLegal   = edgesLegal,
                       x0           = x0,
                       y0           = y0,
                       **attr)

# --------------------------------------------------------------------
def add_squarenumbering(root,grid,
                        hType                = 'N',
                        **attr):
    return add_hexnumbering(root,grid,
                            cls=PICKER+'board.mapgrid.SquareGridNumbering',
                            hType                = hType,
                            **attr)

# --------------------------------------------------------------------
def add_regiongrid(root,zone,snapto=True,fontsize=9,visible=True):
    # print(f'Make region grid visible: {visible}')
    return add_node(root,zone,PICKER+'board.RegionGrid',
                    fontsize = fontsize,
                    snapto   = snapto,
                    visble   = visible)

# --------------------------------------------------------------------
def get_region_piece(root,grid,name,alsoPiece=True,piece=None,verbose=False):
    if not alsoPiece:
        return None, None

    # Find the parent map element by stepping up the tree 
    from functools import reduce

    map = reduce(lambda nn, _: None if nn is None else nn.parentNode,
                 range(5), grid)
    if map is None or \
       map.tagName not in [MODULE+'Map',WIDGET+'WidgetMap']:
        if map is None:
            print(f'      = Map not found from grid')
        return None, None

    if piece is not None:
        print(f'      Using supplied piece')
        return piece, map

    # Find the piece by searching from the top of the tree
    piece = get_pieces(root,root).get(name,None)
    # if piece is None:
    #     print(list(get_pieces(root,root).keys()))
    if verbose:
        print(f'       Possible piece for region {name}: {piece}')
    return piece, map

# --------------------------------------------------------------------
def check_region_name(grid,name):
    post = ''
    targ = name
    same = 0
    regs = list(get_regions(grid).keys())
    poss = len([e for e in regs if e.startswith(name)])
    if poss == 0:
        return name

    return name + f' {poss}'
    # Loop over keys, sorted
    # regs.sort()
    # for r in regs:
    #     if targ == r:
    #         same += 1
    #         targ =  name + f' {same:02d}'
    #         print(f'        Found region named {r} -> {targ}')

    return targ
    
# --------------------------------------------------------------------
def add_region(root,grid,name,originx,originy,gpid,
               alsoPiece=True,piece=None,verbose=True):
    # First thing is to check if we have a piece
    piece, map = get_region_piece(root,grid,name,alsoPiece,piece,verbose)
    if verbose:
        print(f'       Region {name} -> piece {piece}')
    
            
    nam = check_region_name(grid,name)
    if verbose:
        print(f'       Region {name} -> real name {nam}')
    n   = add_node(root,grid,PICKER+'board.Region',
                   name     = nam,
                   originx  = originx,
                   originy  = originy)
    if piece is None:
        return n, gpid

    if verbose:
        print(f'       Add piece {name} to region')
    _, gpid = add_atstart(root,map,name,piece,
                          location        = nam,
                          useGridLocation = True,
                          owningBoard     = map.getAttribute('mapName'),
                          x               = 0,
                          y               = 0,
                          gpid            = gpid)
    return n, gpid
    

# --------------------------------------------------------------------
def add_stacking(root,map,
                 bottom               = '40,0',
                 disabled             = False,
                 down                 = '37,0',
                 exSepX               = 6,
                 exSepY               = 18,
                 top                  = '38,0',
                 unexSepX             = 8,
                 unexSepY             = 16,
                 up                   = '39,0'):
    return add_node(root,map,MAP+'StackMetrics',
                    bottom               = bottom,
                    disabled             = disabled,
                    down                 = down,
                    exSepX               = exSepX,
                    exSepY               = exSepY,
                    top                  = top,
                    unexSepX             = unexSepX,
                    unexSepY             = unexSepY,
                    up                   = up)

# --------------------------------------------------------------------
def add_camera(root,map,
               buttonText           = '',
               canDisable           = False,
               hotkey               = '',
               icon                 = '/images/camera.gif',
               propertyGate         = '',
               tooltip              = 'Save map as PNG image'):
    return add_node(root,map,MAP+'ImageSaver',
               buttonText           = buttonText,
               canDisable           = canDisable,
               hotkey               = hotkey,
               icon                 = icon,
               propertyGate         = propertyGate,
               tooltip              = tooltip)

# --------------------------------------------------------------------
def add_forwardtochatter(root,map):
    return add_node(root,map,MAP+'ForwardToChatter')

# --------------------------------------------------------------------
def add_menudisplayer(root,map):
    return add_node(root,map,MAP+'MenuDisplayer')

# --------------------------------------------------------------------
def add_mapcenterer(root,map):
    return add_node(root,map,MAP+'MapCenterer')

# --------------------------------------------------------------------
def add_stackexpander(root,map):
    return add_node(root,map,MAP+'StackExpander')

# --------------------------------------------------------------------
def add_piecemover(root,map):
    return add_node(root,map,MAP+'PieceMover')

# --------------------------------------------------------------------
def add_selectionhighlighters(root,map):
    return add_node(root,map,MAP+'SelectionHighlighters')

# --------------------------------------------------------------------
def add_keybufferer(root,map):
    return add_node(root,map,MAP+'KeyBufferer')

# --------------------------------------------------------------------
def add_highlightlastmoved(root,map,color=rgb(255,0,0),enabled=True,thickness=2):
    return add_node(root,map,MAP+'HighlightLastMoved',
                    color=color,enabled=enabled,thickness=thickness)

# --------------------------------------------------------------------
def add_counterviewer(root,map,
                      borderWidth          = 0,
                      centerAll            = False,
                      centerText           = False,
                      combineCounterSummary = False,
                      counterReportFormat  = '',
                      delay                = 700,
                      description          = '',
                      display              = 'from top-most layer only',
                      emptyHexReportForma  = '$LocationName$',
                      enableHTML           = True,
                      extraTextPadding     = 0,
                      fgColor              = rgb(0,0,0),
                      fontSize             = 9,
                      graphicsZoom         = 1.0,
                      hotkey               = key('\n',0),
                      layerList            = '',
                      minDisplayPieces     = 2,
                      propertyFilter       = '',
                      showDeck             = False,
                      showDeckDepth        = 1,
                      showDeckMasked       = False,
                      showMoveSelectde     = False,
                      showNoStack          = False,
                      showNonMovable       = False,
                      showOverlap          = False,
                      showgraph            = True,
                      showgraphsingle      = False,
                      showtext             = False,
                      showtextsingle       = False,
                      stretchWidthSummary  = False,
                      summaryReportFormat  = '$LocationName$',
                      unrotatePieces       = False,
                      version              = 3,
                      verticalOffset       = 0,
                      verticalTopText      = 5,
                      zoomlevel            = 1.0):
    return add_node(root,map,MAP+'CounterDetailViewer',
                    borderWidth          = borderWidth,
                    centerAll            = centerAll,
                    centerText           = centerText,
                    combineCounterSummary = combineCounterSummary,
                    counterReportFormat  = counterReportFormat,
                    delay                = delay,
                    description          = description,
                    display              = display,
                    emptyHexReportForma  = emptyHexReportForma,
                    enableHTML           = enableHTML,
                    extraTextPadding     = extraTextPadding,
                    fgColor              = fgColor,
                    fontSize             = fontSize,
                    graphicsZoom         = graphicsZoom,
                    hotkey               = hotkey,
                    layerList            = layerList,
                    minDisplayPieces     = minDisplayPieces,
                    propertyFilter       = propertyFilter,
                    showDeck             = showDeck,
                    showDeckDepth        = showDeckDepth,
                    showDeckMasked       = showDeckMasked,
                    showMoveSelectde     = showMoveSelectde,
                    showNoStack          = showNoStack,
                    showNonMovable       = showNonMovable,
                    showOverlap          = showOverlap,
                    showgraph            = showgraph,
                    showgraphsingle      = showgraphsingle,
                    showtext             = showtext,
                    showtextsingle       = showtextsingle,
                    stretchWidthSummary  = stretchWidthSummary,
                    summaryReportFormat  = summaryReportFormat,
                    unrotatePieces       = unrotatePieces,
                    version              = version,
                    verticalOffset       = verticalOffset,
                    verticalTopText      = verticalTopText,
                    zoomlevel            = zoomlevel)

# --------------------------------------------------------------------
def add_globalmap(root,map,
                  buttonText           = '',
                  color                = rgb(255,0,0),
                  hotkey               = key('O',CTRL_SHIFT),
                  icon                 = '/images/overview.gif',
                  scale                = 0.2,
                  tooltip              = 'Show/Hide overview window'):
    return add_node(root,map,MAP+'GlobalMap',
                    buttonText           = buttonText,
                    color                = color,
                    hotkey               = hotkey,
                    icon                 = icon,
                    scale                = scale,
                    tooltip              = 'Show/Hide overview window')

# --------------------------------------------------------------------
def add_zoomer(root,map,
               inButtonText         = '',
               inIconName           = '/images/zoomIn.gif',
               inTooltip            = 'Zoom in',
               outButtonText        = '',
               outIconName          = '/images/zoomOut.gif',
               outTooltip           = 'Zoom out',
               pickButtonText       = '',
               pickIconName         = '/images/zoom.png',
               pickTooltip          = 'Select Zoom',
               zoomInKey            = key('=',CTRL_SHIFT),
               zoomLevels           = [0.2,0.25,0.333,0.4,0.5,0.555,0.625,0.75,1.0,1.25,1.6],
               zoomOutKey           = key('-'),
               zoomPickKey          = key('='),
               zoomStart            = 3):
    '''Zoom start is counting from the back (with default zoom levels,
    and zoom start, the default zoom is 1'''
    lvls = ','.join([str(z) for z in zoomLevels])
    return add_node(root,map,MAP+'Zoomer',
                    inButtonText         = inButtonText,
                    inIconName           = inIconName,
                    inTooltip            = inTooltip,
                    outButtonText        = outButtonText,
                    outIconName          = outIconName,
                    outTooltip           = outTooltip,
                    pickButtonText       = pickButtonText,
                    pickIconName         = pickIconName,
                    pickTooltip          = pickTooltip,
                    zoomInKey            = zoomInKey,
                    zoomLevels           = lvls,
                    zoomOutKey           = zoomOutKey,
                    zoomPickKey          = zoomPickKey,
                    zoomStart            = zoomStart)

# --------------------------------------------------------------------
def add_hidepieces(root,map,
                   buttonText           = '',
                   hiddenIcon           = '/images/globe_selected.gif',
                   hotkey               = key('O'),
                   showingIcon          = '/images/globe_unselected.gif',
                   tooltip              = 'Hide all pieces on this map'):
    return add_node(root,map,MAP+'HidePiecesButton',
                    buttonText           = buttonText,
                    hiddenIcon           = hiddenIcon,
                    hotkey               = hotkey,
                    showingIcon          = showingIcon,
                    tooltip              = tooltip)
# --------------------------------------------------------------------
def add_masskey(root,map,
                name,                
                buttonHotkey,
                hotkey,
                buttonText           = '',
                canDisable           = False,
                deckCount            = '-1',
                filter               = '',
                propertyGate         = '',
                reportFormat         = '',
                reportSingle         = False,
                singleMap            = True,
                target               = 'MAP|false|MAP|||||0|0||true|Selected|true|EQUALS',
                tooltip              = '',
                icon                 = ''):
    '''Default targets are selected units'''
    return add_node(root,map,MAP+'MassKeyCommand',
                    name                 = name,                
                    buttonHotkey         = buttonHotkey, # This hot key
                    hotkey               = hotkey,       # Target hot key
                    buttonText           = buttonText,
                    canDisable           = canDisable,
                    deckCount            = deckCount,
                    filter               = filter,
                    propertyGate         = propertyGate,
                    reportFormat         = reportFormat,
                    reportSingle         = reportSingle,
                    singleMap            = singleMap,
                    target               = target,
                    tooltip              = tooltip,
                    icon                 = icon)


# --------------------------------------------------------------------
def add_flare(root,map,
              circleColor          = rgb(255,0,0),
              circleScale          = True,
              circleSize           = 100,
              flareKey             = 'keyAlt',
              flareName            = 'Map Flare',
              flarePulses          = 6,
              flarePulsesPerSec    = 3,
              reportFormat         = ''):
    return add_node(root,map,MAP+'Flare',
                    circleColor          = circleColor,
                    circleScale          = circleScale,
                    circleSize           = circleSize,
                    flareKey             = flareKey,
                    flareName            = flareName,
                    flarePulses          = flarePulses,
                    flarePulsesPerSec    = flarePulsesPerSec,
                    reportFormat         = '')

# --------------------------------------------------------------------
def add_atstart(root,map,name,*pieces,
                location        = '',
                useGridLocation = True,
                owningBoard     = '',
                x               = 0,
                y               = 0,
                gpid            = None):
    '''Pieces are existing PieceSlot elements'''
    a = add_node(root,map,MODULE+'map.SetupStack',
                 name            = name,
                 location        = location,
                 owningBoard     = owningBoard,
                 useGridLocation = useGridLocation,
                 x               = x,
                 y               = y)
    # copy pieces here 
    for p in pieces:
        c = p.cloneNode(True)
        a.appendChild(c)
        if gpid is not None:
            # Update gpid in element
            c.setAttribute('gpid',str(gpid))
            # Update gpid in traits state 
            traits = get_piece_parts(p.childNodes[0].nodeValue)
            for t in traits:
                code = t['def']
                stat = t['state']
                if code[0] == 'piece':
                    stat[3] = gpid
            # Put back the traits 
            c.childNodes[0].nodeValue = dicts_to_piece(traits)
            
            gpid += 1

    return a, gpid

# --------------------------------------------------------------------
def add_pieceslot(root,elem,
                  entryName,
                  gpid,
                  code,
                  height=72,
                  width=72,
                  icon = ''):
    p = add_node(root,elem,WIDGET+'PieceSlot',
                 entryName = entryName,
                 gpid      = gpid,
                 height    = height,
                 width     = width,
                 icon      = icon)
    add_text(root,p,code)
    return p

# --------------------------------------------------------------------
def add_prototypes(root,elem,*defs):
    '''Defs are existing definition elements'''
    p = add_node(root,elem,MODULE+'PrototypesContainer')
    
    for d in defs:
        c = d.cloneNode(True)
        p.appendChild(c)

    return p
        
# --------------------------------------------------------------------
def add_prototype(root,elem,name,code,description=''):
    d = add_node(root,elem,MODULE+'PrototypeDefinition',
                 name        = name,
                 description = description)
    add_text(root,d,code)
    return d
    
# --------------------------------------------------------------------
def add_piecewindow(root,elem,name,
                    defaultWidth = 0,
                    hidden       = False,
                    hotkey       = key('C',ALT),
                    scale        = 1.,
                    text         = '',
                    tooltip      = 'Show/hide piece window',
                    icon         = '/images/counter.gif'):
    return add_node(root,elem,MODULE+'PieceWindow',
                    name = name,
                    defaultWidth = defaultWidth,
                    hidden       = hidden,
                    hotkey       = hotkey,
                    scale        = scale,
                    text         = text,
                    tooltip      = tooltip,
                    icon         = icon)

# --------------------------------------------------------------------
def add_tabs(root,elem,entryName):
    return add_node(root,elem,WIDGET+'TabWidget',entryName=entryName)

# --------------------------------------------------------------------
def add_list(root,elem,
             entryName,
             height  = 0,
             width   = 0,
             scale   = 1.,
             divider = 100):
    return add_node(root,elem,WIDGET+'ListWidget',
                    entryName = entryName,
                    height    = height,
                    width     = width,
                    scale     = scale,
                    divider   = divider)

# --------------------------------------------------------------------
def add_panel(root,elem,
              entryName,
              fixed = False,
              nColumns = 1,
              vert     = False):
    return add_node(root,elem,WIDGET+'PanelWidget',
                    entryName = entryName,
                    fixed     = fixed,
                    nColumns  = nColumns,
                    vert      = vert)

# --------------------------------------------------------------------
def add_mapwidget(root,elem,entryName=''):
    return add_node(root,elem,WIDGET+'MapWidget',entryName=entryName)

# --------------------------------------------------------------------
def add_chartwindow(root,elem,
                    name,
                    hotkey      = key('A',ALT),
                    description = '',
                    text        = '',
                    tooltip     = 'Show/hide Charts',
                    icon        = '/images/chart.gif'):
    return add_node(root,elem,MODULE+'ChartWindow',
                    name        = name,
                    hotkey      = hotkey,
                    description = description,
                    text        = text,
                    tooltip     = tooltip,
                    icon        = icon)

# --------------------------------------------------------------------
def add_chart(root,elem,
              chartName,
              fileName,
              description = ''):
    return add_node(root,elem,WIDGET+'Chart',
                    chartName   = chartName,
                    description = description,
                    fileName    = fileName)

# --------------------------------------------------------------------
def add_dice(root,elem,
             addToTotal           = 0,
             canDisable           = False,
             hotkey               = key('6',ALT),
             icon                 = '/images/die.gif',
             keepCount            = 1,
             keepDice             = False,
             keepOption           = '&gt;',
             lockAdd              = False,
             lockDice             = False,
             lockPlus             = False,
             lockSides            = False,
             nDice                = 1,
             nSides               = 6,
             name                 = '1d6',
             plus                 = 0,
             prompt               = False,
             propertyGate         = '',
             reportFormat         = '** $name$ = $result$ *** &amp;lt;$PlayerName$&amp;gt;',
             reportTotal          = False,
             sortDice             = False,
             text                 = '1d6',
             tooltip              = 'Roll a 1d6'):
    return add_node(root,elem,MODULE+'DiceButton',
                    addToTotal           = addToTotal,
                    canDisable           = canDisable,
                    hotkey               = hotkey,
                    icon                 = icon,
                    keepCount            = keepCount,
                    keepDice             = keepDice,
                    keepOption           = keepOption,
                    lockAdd              = lockAdd,
                    lockDice             = lockDice,
                    lockPlus             = lockPlus,
                    lockSides            = lockSides,
                    nDice                = nDice,
                    nSides               = nSides,
                    name                 = name,
                    plus                 = plus,
                    prompt               = prompt,
                    propertyGate         = propertyGate,
                    reportFormat         = reportFormat,
                    reportTotal          = reportTotal,
                    sortDice             = sortDice,
                    text                 = text,
                    tooltip              = tooltip)

# ====================================================================
def enc_def(args,term=False):
    '''Encode a piece (decorator) definition

    Parameters
    ----------
    args : list
        List of fields
    term : bool
        If true, add terminating ';'

    Returns
    -------
    code : str
        The encoded definition
    '''
    return ';'.join([str(e).lower() if isinstance(e,bool) else str(e)
                     for e in args])+(';' if term else '')

# --------------------------------------------------------------------
def layer_trait(images,
                newNames     = None,
                activateName = 'Activate',
                activateMask = CTRL,
                activateChar = 'A',
                increaseName = 'Increase',
                increaseMask = CTRL,
                increaseChar = '[',
                decreaseName = '',
                decreaseMask = CTRL,
                decreaseChar  = ']',
                resetName    = '',
                resetKey     = '',
                resetLevel   = 1,
                under        = False,
                underXoff    = 0,
                underYoff    = 0,
                loop         = True,
                name         = '',
                description  = '',
                randomKey    = '',
                randomName   = '',
                follow       = False,
                expression   = '',
                first        = 1,
                version      = 1, # 1:new, 0:old
                always       = 'true',
                activateKey  = key('A'),
                increaseKey  = key('['),
                decreaseKey  = key(']'),
                scale        = 1.):
    '''Create a layer trait (VASSAL.counter.Embellishment)

    Parameters
    ----------
    main : dict
        Main image dictionary
    flipped : dict
        Flipped image dictionary

    Returns
    -------
    code : str
        Encoded trait
    stat : str
        Encoded state
    '''
    if newNames is None:
        newNames = ['']*len(images)
    args = ['emb2',                # Code
            activateName, activateMask, activateChar,    # ACTIVATE ; MASK ; KEY
            increaseName, increaseMask, increaseChar,
            decreaseName, decreaseMask, decreaseChar,
            resetName,    resetKey,     resetLevel,
            under,        underXoff,    underYoff,
            ','.join(images),
            ','.join(newNames),
            loop,
            name,
            randomKey, randomName,
            follow,    expression,
            first,
            version,
            always,
            activateKey,
            increaseKey,
            decreaseKey,
            description,
            scale ]
    stat = '1' # CURRENT LEVEL
    return enc_def(args), stat

# --------------------------------------------------------------------
def prototype_trait(name):
    '''Create a prototype trait (VASSAL.counter.UsePrototype)

    Parameters
    ----------
    family : str
        Side in game

    Return
    ------
    code : str
        Encoded trait
    stat : str
        Encoded state (empty string)
    '''
    args = ['prototype', name ]
    stat = '' # State is nothing 
    return enc_def(args), stat

# --------------------------------------------------------------------
def mark_trait(key,value):
    '''Create a marker trait (VASSAL.counter.Marker)

    Parameters
    ----------
    family : str
        Side in game

    Return
    ------
    code : str
        Encoded trait
    stat : str
        Encoded state (empty string)
    '''
    args = ['mark', key ]
    stat = value # State is nothing 
    return enc_def(args), stat

# --------------------------------------------------------------------
def report_trait(*keys,
                 nosuppress = True,
                 description = '',
                 report      = '$location$: $newPieceName$ $menuCommand$ *',
                 cyclekeys   = '',
                 cyclereps   = ''):
    '''Create a report trait (VASSAL.counters.ReportActon)

    Parameters
    ----------
    *keys : tuple of str
    	The keys to report actions for
    
    Returns
    -------
    code : str
        Encoded trait
    stat : str
        Encoded state (empty string)
    '''
    esckeys = ','.join([k.replace(',',r'\,') for k in keys])
    esccycl = ','.join([k.replace(',',r'\,') for k in cyclekeys])
    escreps = ','.join([k.replace(',',r'\,') for k in cyclereps])
    args = ['report',    # CODE
            esckeys,     # ESCAPED KEYS (commas escaped)
            report,      # REPORT
            esccycl,     # CYCLE KEYS (commas escaped)
            escreps,     # CYCKE REPORTS (commas escaped)
            description, # DESCRIPTION
            nosuppress]  # NOSUPPRESS
    stat = '-1'          # CYCLE STATE
    return enc_def(args), stat

# --------------------------------------------------------------------
def moved_trait(image='moved.gif',
                xoff = 36,
                yoff = -38,
                name = 'Mark Moved',
                key  = key('M')):
    '''Create a moved trait (VASSAL.counters.MovementMarkable)

    Returns
    -------
    code : str
        Encoded trait
    stat : str
        Encoded state (empty string)
    '''
    args = ['markmoved',    # CODE
            image,          # IMAGE
            xoff, yoff,     # XOFF ; YOFF
            name,           # MENU
            key,            # KEY, MODIFIER
            '']             # ?
    stat = False          # MOVED?
    return enc_def(args), stat

# --------------------------------------------------------------------
def trail_trait(key             = key('T'),
                name            = 'Movement Trail',
                localVisible    = False,
                globalVisible   = False,
                radius          = 10,
                fillColor       = rgb(255,255,255),
                lineColor       = rgb(0,0,0),
                activeOpacity   = 100,
                inactiveOpacity = 50,
                edgesBuffer     = 20,
                displayBuffer   = 30,
                lineWidth       = 1,
                turnOn          = '',
                turnOff         = '',
                reset           = '',
                description     = ''):
    ''' Create a movement trail trait ( VASSAL.counters.Footprint)
    
    Returns
    -------
    code : str
        Encoded trait
    stat : str
        Encoded state 
    '''
    args = ['footprint',       # CODE
            key,               # ENABLE KEY
            name,              # MENU 
            localVisible,      # LOCAL VISABLE
            globalVisible,     # GLOBAL VISABLE
            radius,            # RADIUS
            fillColor,         # FILL COLOR
            lineColor,         # LINE COLOR 
            activeOpacity,     # ACTIVE OPACITY
            inactiveOpacity,   # INACTIVE OPACITY
            edgesBuffer,       # EDGES BUFFER
            displayBuffer,     # DISPLAY BUFFER
            lineWidth,         # LINE WIDTH 
            turnOn,            # TURN ON KEY
            turnOff,           # TURN OFF KEY
            reset,             # RESET KEY
            description]       # DESC
    stat = [False,             # GLOBAL VIS
            '',                # MAP
            0]                 # POINTS (followed by [; [X,Y]*]
    return enc_def(args), enc_def(stat)

# --------------------------------------------------------------------
def delete_trait(name = 'Delete', key = key('D')):
    '''Create a delete trait (VASSAL.counters.Delete)
    
    Returns
    -------
    code : str
        Encoded trait
    stat : str
        Encoded state (empty string)
    '''
    args = ['delete', name, key, '']
    stat = '' # Nothing
    return enc_def(args), stat

# --------------------------------------------------------------------
def sendto_trait(mapName,
                 boardName,
                 name        = '',
                 key         = key('E'),
                 restoreName = 'Restore',
                 restoreKey  = key('R'),
                 x           = 200,
                 y           = 200,
                 xidx        = 0,
                 yidx        = 0,
                 xoff        = 1,
                 yoff        = 1,
                 description = '',
                 destination = 'L',
                 zone        = '',
                 region      = '',
                 expression  = '',
                 position    = ''):
    '''Create a send to trait (VASSAL.counter.SendToLocation)

    Parameters
    ----------
    side : str
        Sub-map to move to
    
    Returns
    -------
    code : str
        Encoded trait
    stat : str
        Encoded state (empty string)
    '''
    args = ['sendto',              # CODE
            name,           # NAME
            key,              # KEY , MODIFIER
            mapName,            # MAP
            boardName,        # BOARD
            x, y,              # X ; Y
            restoreName,             # BACK
            restoreKey,              # KEY , MODIFIER
            xidx, yidx,            # XIDX ; YIDX
            xoff, yoff,                  # XOFF ; YOFF
            description, # DESC
            destination,                   # DEST
            zone,                    # ZONE
            region,                    # REGION
            expression,                    # EXPRESSION
            position ]                   # GRIDPOS
    stat = ['',                    # BACKMAP 
            '',                    # X
            '']                    # Y
    return enc_def(args), enc_def(stat)

# --------------------------------------------------------------------
def basic_trait(name,
                filename,     # Can be empty
                gpid,         # Can be empty
                cloneKey='',  # Deprecated
                deleteKey=''):# Deprecated
    '''Create a basic unit (VASSAL.counters.BasicPiece)

    Parameters
    ----------
    main : dict
        Information
    gpid : int
        Piece global identifier
    image : bool
        If true, add image

    Returns
    -------
    code : str
        Encoded trait
    stat : str
        Encoded state (empty string)
    '''    
    args = ['piece',    # CODE
            cloneKey,   # CLONEKEY
            deleteKey,  # DELETEKEY
            filename,   # IMAGE  
            name]       # NAME
    stat = ['null',         # MAPID (possibly 'null')
            0,              # X
            0,              # Y
            gpid,           # GLOBAL PIECE ID
            0]              # PROPERTY COUNT (followed by [; KEY; VALUE]+)
    return enc_def(args), enc_def(stat)

# --------------------------------------------------------------------
def enc_parts(*parts):
    '''Encode parts of a full piece definition

    Each trait (VASSAL.counter.Decorator, VASSAL.counter.BasicPiece)
    definition or state is separated by a litteral TAB character.
    Beyond the first TAB separator, additional escape characters
    (BACKSLAH) are added in front of the separator.  This is to that
    VASSAL.utils.SequenceDecoder does not see consequitive TABs as a
    single TAB.

    Parameters
    ----------
    parts : tuple
        The various trait definitions or states

    Returns
    -------
    code : str
        The encoded piece
    '''
    ret = ''
    sep = r'	'
    for i, p in enumerate(parts):
        if i != 0:
            ret += '\\'*(i-1) + sep
        ret += str(p)

    return ret

# --------------------------------------------------------------------
def add_piece(typ, state):
    '''Create and add piece command (VASSAL.command.AddPiece)

    Each part is separated by a SLASH ('/').  The first part is the
    command type (PLUS '+') to perform.  Second is the IDENTIFIER,
    possibly 'null'.  Then comes the fully encoded type of the piece,
    followed by the state of the piece (also fully encoded).

    Parameters
    ----------
    typ : str
        Encoded piece definition
    state : str
        Encoded state definition

    Returns
    -------
    cmd : str
        The add command encoded
    '''
    args = ['+',      # Type
            'null',   # IDENTIFER  
            typ,      # DEFINITION
            state]    # STATE 
    return '/'.join(args)

# --------------------------------------------------------------------
def add_proto(typ,state):
    '''Create an add prototype command.  This is the same as 'add_piece'

    Parameters
    ----------
    typ : str
        Encoded piece definition
    state : str
        Encoded state definition

    Returns
    -------
    cmd : str
        The add command encoded
    '''    
    return add_piece(typ,state)
                      

# --------------------------------------------------------------------
def add_proto_code(decs):
    typs = [d[0] for d in decs]
    stts = [d[1] for d in decs]
    typ  = enc_parts(*typs)
    stt  = enc_parts(*stts)

    return add_proto(typ,stt)

# ====================================================================
#
# Below specific to the export app
#
# ====================================================================
def get_bb(buffer):
    '''Get bounding box of image

    Parameters
    ----------
    buffer : bytes
         The image bytes

    Returns
    -------
    ulx, uly, lrx, lry : tuple
         The coordinates of the bounding box 
    '''
    from PIL import Image
    from io import BytesIO

    with Image.open(BytesIO(buffer)) as img:
        bb  = img.getbbox()

    return bb

# ====================================================================
def piece_body(family,main,flipped,gpid):
    '''Create an full add piece command from the side, image, and ID
    information.  This adds (in reverse order)

    - BasicPiece
    - Side prototype
    - Flip layer (if 'flipped' is not none)

    Parameters
    ----------
    family : str
        Side in the game
    main : dict
        Main image information
    flipped : dict
        Flipped image information, possibly None
    gpid : int
        Global piece identifier

    Returns
    -------
    cmd : str
        The command to add the piece
    '''
    # Create the layer, prototype, and basic piece traits
    decs = []
    if flipped is not None:
        imgs = [ main['filename'], flipped['filename'] ]
        decs.extend([ layer_trait(images=imgs,
                                  newNames=['','Reduced +'],
                                  activateName = '',
                                  decreaseName = '',
                                  increaseName = 'Flip',
                                  increaseKey  = key('F'),
                                  decreaseKey  = '',
                                  name='Step'),
                      report_trait(key('F')) ])
    for m in main.get('mains',[]):
        decs.append(prototype_trait(m + ' prototype'))
    ech = main.get('echelon',None)
    cmd = main.get('command',None)
    if ech is not None: decs.append(prototype_trait(ech + ' prototype'))
    if cmd is not None: decs.append(prototype_trait(cmd + ' prototype'))

    decs.extend([ prototype_trait(family+' prototype'),
                  basic_trait(main['name'], main['filename'],gpid)
                 ])

    typs = [d[0] for d in decs]
    stts = [d[1] for d in decs]
    typ  = enc_parts(*typs)
    stt  = enc_parts(*stts)

    return add_piece(typ,stt)

# --------------------------------------------------------------------
def proto_body(family):
    '''Create an full add prototype com from the side information.
    This adds (in reverse order)

    - An empty BasicPiece
    - An eliminate (or sendto) trait
    - A delete trait
    - A mark moved trait
    - A move trail trait 

    Parameters
    ----------
    family : str
        Side in the game

    Returns
    -------
    cmd : str
        The command to add the piece
    '''
    # Dummy image 
    decs = [ report_trait(key('E'),key('R'),key('M')),
             moved_trait(),
             trail_trait(),
             delete_trait(),
             sendto_trait('DeadMap',family+' pool','Eliminate',key('E'),
                          'Restore',key('R'),description="Eliminate unit"),
             mark_trait('Faction',family),
             basic_trait('','','') ]
    typs = [d[0] for d in decs]
    stts = [d[1] for d in decs]
    typ  = enc_parts(*typs)
    stt  = enc_parts(*stts)

    return add_proto(typ,stt)


# --------------------------------------------------------------------
def add_splash(root,doc,categories,title='',verbose=False):
    '''Add an about page

    Parametersg
    ----------
    root : xml.dom.minidom.Document
        Root of the document
    doc : xml.dom.minidom.Element
        The documentation element
    categories : dict
        Catalog of images
    title : str
        Title of the game 
    '''
    fronts    = categories.get('front',{}).get('all',[])
    front     = list(fronts.values())[0] if len(fronts) > 0 else None
    if front is None:
        return

    if verbose:
        print(f'Adding about page with {title}')

    add_about(root,doc, f'About {title}', fileName = front['filename'])

# --------------------------------------------------------------------
def add_rules(root,doc,zipfile,rules,verbose=False):
    '''Add rules to the module

    Parameters
    ----------
    root : xml.dom.minidom.Document
        Root of the document
    doc : xml.dom.minidom.Element
        The documentation element
    zipfile : zipfile.ZipFile
        The module archive 
    rules : str
        File name of rules (PDF)
    '''
    if rules is None or rules == '':
        return

    if verbose:
        print('Adding rules PDF to module')
        
    zipfile.write(rules,'rules.pdf')

    add_pdffile(root,doc,title='Show rules',pdfFile='rules.pdf')

# --------------------------------------------------------------------
def add_notes(root,doc,zipfile,title,version,sides,pdffile,infofile,rules,
              verbose=False):
    '''Add rules to the module

    Parameters
    ----------
    root : xml.dom.minidom.Document
        Root of the document
    doc : xml.dom.minidom.Element
        The documentation element
    zipfile : zipfile.ZipFile    
        The module archive
    sides : list
        Sides in the game
    pdffile : str
        The images PDF
    infofile : str
        The images information    
    rules : str
        File name of rules (PDF)
    '''
    factions = "".join([f'<li>{s}</li>' for s in sides])
    notes = f'''
    <html>
    <body>
    <h1>Draft VASSAL module of {title} (Version {version})</h1>
    <p>
     This is a draft VASSAL module of {title} generated from
    </p>
    <ul>
     <li>images in {pdffile}, and</li>
     <li>information in {infofile}</li>
    </ul>
    <p>
     both generated by LaTeX using the <b>wargame</b> [1] package, and
     processed by the Python script <code>wgexport.py</code> from that
     package.
    </p>
    <p>
     The module is <i>not</i> complete and the developer should edit
     the module as needed in the VASSAL module editor.
    </p>
    <p>
     [1] <code>https://gitlab.com/wargames_tex/wargame_tex</code>
    </p>
    <h2>Content of the module</h2>
    <p>
     The <code>wgexport.py</code> Python script has identified the
     factions of the game to be
    </p>
    <ul>
    '''+factions+f'''
    </ul>
    <p>
     and generated prototype pieces for these (along with possible
     non-faction pieces.  The script also generated a map ("dead map")
     that contains boards for each faction and is meant to store
     eliminated units ("&lt;faction&gt; pool").  These pools have a
     generic shape and colour, and the developer should take care to
     correct these as needed.
    </p>
    <p>
     Boards of the game present in the {pdffile} and {infofile} files
     have been generated.  Each board has a single zone in it that
     encompasses the full board, and a hex grid is added to that
     zone. The module developer should adjust the zone and embedded
     hex grid according to the actual map, using the VASSAL module
     editor.  The hex grid (as well as coordinates) is drawn by
     default so as to make this obvious to the developer.  The hex
     sizes should be correct, and mainly the offset needs to be
     adjusted.
    </p>
    <p>
     Images marked as OOBs have also been added as maps to the
     module. These also have a single zone with a rectangular grid
     imposed in it.  This grid, and the grid coordinates are drawn by
     default, so it is easier to see and to remember to adjust it. The
     module developer should adjust this, possibly adding zones, so
     that it matches up with the graphics.  The developer can add
     units to the OOB via 'At-start' stacks in the VASSAL editor.
    </p>
    <p>
     All pieces identified by {infofile} and present in {pdffile} have
     been generated.  Double sided pieces (identified by images which
     also has a suffix-"flipped" image) are created as such.
    </p>
    <p>
     Charts identified by the {infofile} and {pdffile} have been added
     to the module.  The are added as separate (pop-up) frames, but
     the module developer can change this in the VASSAL module editor.
    </p>
    <p>
     If the {infofile} and {pdffile} identified a front page, then that
     has been added to module as the splash screen.
    </p>
    <p>
     If a PDF rules file ({rules}) was passed to the Python script,
     then that has been included in the module and is accessible from
     the "Help" menu.
    </p>
    <h2>After finalising the module</h2>
    <p>
     After the developer has finalised the module (added more pieces,
     fixed board zones a and grids, add in more commands), then this
     Help menu entry should be deleted.
    </p>
    </body>
    </html>
    '''
    if version.lower() == 'draft':
        if verbose:
            print(f' Adding draft notes {version}')
        zipfile.writestr('help/notes.html',notes)
        add_helpfile(root,doc,title='Draft Module notes',
                     fileName='help/notes.html')

    keys = f'''
    <html>
    <body>
    <h1>{title} (Version {version}) Key bindings</h1>
    
    <table>
     <tr>
      <th>Key   </th>
      <th>Where </th>
      <th>Effect</th>
     </tr>
     <tr>
      <td>Alt-A</td>
      <td>-</td>
      <td>Show the charts panel</td>
     </tr>
     <tr>
      <td>Alt-B</td>
      <td>-</td>
      <td>Show the OOBs</td>
     </tr>
     <tr>
      <td>Alt-C</td>
      <td>-</td>
      <td>Show the counters panel</td>
     </tr>
     <tr>
      <td>Alt-E</td>
      <td>-</td>
      <td>Show the eliminated units</td>
     </tr>
     <tr>
      <td>Alt-I</td>
      <td>-</td>
      <td>Show/refresh inventory window</td>
     </tr>
     <tr>
      <td>Alt-M</td>
      <td>-</td>
      <td>Show map</td>
     </tr>
     <tr>
      <td>Alt-T</td>
      <td>-</td>
      <td>Increase turn track</td>
     </tr>
     <tr>
      <td>Alt-Shift-T</td>
      <td>-</td>
      <td>Decrease turn track</td>
     </tr>
     <tr>
      <td>Alt-6</td>
      <td>-</td>
      <td>Roll the dice</td>
     </tr>
     <tr>
      <td>Ctrl-D</td>
      <td>Board,Counter</td>
      <td>Delete selected counters</td>
     </tr>
     <tr>
      <td>Ctrl-E</td>
      <td>Board,Counter</td>
      <td>Eliminate selected counters</td>
     </tr>
     <tr>
      <td>Ctrl-F</td>
      <td>Board,Counter</td>
      <td>Flip selected counters</td>
     </tr>
     <tr>
      <td>Ctrl-M</td>
      <td>Board,Counter</td>
      <td>Toggle "moved" markers on selected counters</td>
     </tr>
     <tr>
      <td>Ctrl-O</td>
      <td>Board</td>
      <td>Hide/show counters</td>
     </tr>
     <tr>
      <td>Ctrl-R</td>
      <td>Board,Counter</td>
      <td>Restore unit (from dead pool)</td>
     </tr>
     <tr>
      <td>Ctrl-T</td>
      <td>Board,Counter</td>
      <td>Toggle move trail</td>
     </tr>
     <tr>
      <td>Ctrl-+</td>
      <td>Board</td>
      <td>Zoom in</td>
     </tr>
     <tr>
      <td>Ctrl--</td>
      <td>Board</td>
      <td>Zoom out</td>
     </tr>
     <tr>
      <td>Ctrl-=</td>
      <td>Board</td>
      <td>Select zoom</td>
     </tr>
     <tr>
      <td>Ctrl-Shift-O</td>
      <td>Board</td>
      <td>Show overview map</td>
     </tr>
     <tr>
      <td>&larr;,&rarr;,&uarr;&darr;</td>
      <td>Board</td>
      <td>Scroll board left, right, up, down (slowly)</td>
     </tr>
     <tr>
      <td>PnUp,PnDn</td>
      <td>Board</td>
      <td>Scroll board up/down (fast)</td>
     </tr>
     <tr>
      <td>Ctrl-PnUp,Ctrl-PnDn</td>
      <td>Board</td>
      <td>Scroll board left/right (fast)</td>
     </tr>
     <tr>
      <td>Mouse-scroll up/down</td>
      <td>Board</td>
      <td>Scroll board up//down</td>
     </tr>
     <tr>
      <td>Shift-Mouse-scroll up/down</td>
      <td>Board</td>
      <td>Scroll board right/leftown</td>
     </tr>
     <tr>
      <td>Ctrl-Mouse-scroll up/down</td>
      <td>Board</td>
      <td>Zoom board out/in</td>
     </tr>
     <tr>
      <td>Mouse-2</td>
      <td>Board</td>
      <td>Centre on mouse</td>
     </tr>
    </table>
    </body>
    </html>    
    '''
    if verbose:
        print(f'Add key bindings help file')
    zipfile.writestr('help/keys.html',keys)
    add_helpfile(root,doc,title='Key bindings',
                 fileName='help/keys.html')
    
    return notes

# --------------------------------------------------------------------
def add_tut(root,doc,zipfile,tutorial,verbose=False):
    if tutorial is None:
        return None

    if verbose:
        print(f'Adding tutorial {tutorial}')
    zipfile.write(tutorial,'tutorial.vlog')
    
    return add_tutorial(root,doc,name='Tutorial',logfile='tutorial.vlog')

# --------------------------------------------------------------------
def add_basics_to_map(root,map,**attr):
    '''Add a map to the document

    Parameters
    ----------
    root : xml.dom.minidom.Document
        Root of the document
    parent : xml.dom.minidom.Element
        The parent element
    button : str
        Button text
    name : str
        Name of the map
    stacking : bool
        Whether to enable stacking

    Returns
    -------
    map : xml.dom.minidom.Element
        The map element
    bpicker : xml.dom.minidom.Element
        The board picker in map 
    '''
    stacking = add_stacking(root,map)
    add_camera               (root,map)
    add_forwardtochatter     (root,map)
    add_menudisplayer        (root,map)
    add_mapcenterer          (root,map)
    add_stackexpander        (root,map)
    add_piecemover           (root,map)
    add_keybufferer          (root,map)
    add_selectionhighlighters(root,map) # TBD
    add_highlightlastmoved   (root,map)
    add_zoomer               (root,map)

    return stacking

# --------------------------------------------------------------------
def add_elim(root,game,sides,categories,verbose=False):
    '''Add an eliminated map

    Parameters
    ----------
    root : xml.dom.minidom.Document
        Root of the document
    game : xml.dom.minidom.Element
        The parent element
    sides : list of str
        List of sides in the game
    '''
    if verbose:
        print(' Adding eliminated map and boards')

    if sides is None or len(sides) == 0:
        return
        
    map = add_map(root,game,'DeadMap',
                  buttonName    = '', #'Eliminated units',
                  icon          = '/images/playerAway.gif',
                  markMoved     = 'Never',
                  launch        = True,
                  allowMultiple = True,
                  hotkey=key('E',ALT))
    bpicker = add_boardpicker(root,map,
                              slotHeight = 400,
                              slotWidth  = 400)
    add_setup(root,bpicker,'DeadMap',2,[s+' pool' for s in sides])
    add_masskey(root,map,'Restore',
                key('R'),
                key('R'),
                icon         = '/images/Undo16.gif',
                tooltip      = "Restore selected units")
    
    for i,s in enumerate(sides):
        if verbose:
            print(f'  Adding {s} pool')

        color        = [0,0,0,64]
        color[i % 3] = 255

        w     = 400
        h     = 400
        c     = ','.join([str(c) for c in color])
        dimg  = categories.get('pool',{}).get('all',{}).get(s,None)
        img   = ''

        if dimg:
            bb  = get_bb(dimg['img'])
            w   = bb[2] - bb[0]
            h   = bb[3] - bb[1]
            c   = ''
            img = dimg['filename']
            if verbose:
                print(f'   Using image provided by the user: {img}')

        add_board(root,bpicker,f'{s} pool',
                  image = img,
                  color = c,
                  width = w,
                  height = h)

    add_basics_to_map(root,map)

# --------------------------------------------------------------------
def add_hex_grid(root,parent,color=rgb(255,0,0),visible=True,grid=True):
    '''Add a hex grid, either to a zone or to the multizone container'''

    hexes = add_hexgrid(root,parent,color=color,visible=visible,
                        edgesLegal=True)
    if not grid:
        return hexes

    add_hexnumbering(root,hexes,
                     color   =  color,
                     hType   = 'A',
                     hOff    = -1,
                     vType   = 'N',
                     vOff    = -1,
                     visible = visible)

    return hexes

# --------------------------------------------------------------------
def get_picture_info(picture,name,width,height,verbose):
    '''
    Returns
    -------
    hex_width, hex_height : float, float
        Scale hex width
    scx, scy : float, float, float, float
        Scale to image and picture (x,y)
    rot90 : bool
        True if rotated +/-90 degrees
    tran : callable
        Translation function
    '''
    if picture is None:
        print(f'WARNING: No Tikz picture information.'
              f"Are you sure you used the `[zoned]' option for the "
              f"tikzpicture environment of {name}?")
        f = lambda x,y: (x,y)
        return HEX_WIDTH,HEX_HEIGHT,1,1,False,f
    
    # Get picture bounding box
    tll = picture['lower left']
    tur = picture['upper right']
    # Get picture transformation
    pa  = picture['xx']
    pb  = picture['xy']
    pc  = picture['yx']
    pd  = picture['yy']
    # Get picture offset (always 0,0?)
    pdx = picture['dx']
    pdy = picture['dy']
    # Define picture global transformation
    pr  = lambda x,y: (pa * x + pc * y, pb * x + pd * y)
    # Globally transform (rotate) picture bounding box 
    pll  = pr(*tll)
    pur  = pr(*tur)
    # Calculate widht, height, and scaling factors 
    pw   = pur[0] - pll[0]
    ph   = pur[1] - pll[1]
    scw  = width / pw
    sch  = height / ph
    # Extract picture scales and rotation
    # Courtesy of
    # https://math.stackexchange.com/questions/13150/extracting-rotation-scale-values-from-2d-transformation-matrix
    from math import sqrt, atan2, degrees, isclose
    psx   = sqrt(pa**2 + pb**2) # * (-1 if pa < 0 else 1)
    psy   = sqrt(pc**2 + pd**2) # * (-1 if pd < 0 else 1)
    prt   = degrees(atan2(pc,pd))
    if not any([isclose(abs(prt),a) for a in [0,90,180,270]]):
        raise RuntimeException('Rotations of Tikz pictures other than '
                               '0 or +/-90,+/- 180, or +/-270 not supported. '
                               'found {prt}')
    rot90      = isclose(abs(prt),90)
    if any([isclose(prt,a) for a in [90,270,180,-180]]):
        print(f'WARNING: rotations by {prt} not fully supported')
        
    hex_width  = psx * HEX_WIDTH
    hex_height = psy * HEX_HEIGHT
    if verbose:
        print(f'   Picture transformations: {pa},{pb},{pc},{pd}\n'
              f'     Scale (x,y):        {psx},{psy}\n'
              f'     Rotation (degrees): {prt}')

    # When translating the Tikz coordinates, it is important to note
    # that the Tikz y-axis point upwards, while the picture y-axis
    # point downwards.  This means that the upper right corner is at
    # (width,0) and the lower left corner is at (0,height).
    def tranx(x,off=-pll[0]):
        # print(f'x: {x} + {off} -> {x+off} -> {int(scw*(x+off))}')
        return int(scw * (x + off)+.5)
    def trany(y,off=-pur[1]):
        # print(f'y: {y} + {off} -> {y+off} -> {-int(sch*(y+off))}')
        return -int(sch * (y + off)+.5)
    tran  = lambda x,y : (tranx(x), trany(y))
        
    return hex_width, hex_height, scw * psx, sch * psy, rot90, tran

# --------------------------------------------------------------------
def get_hexparams(i,llx,ury,hex_width,hex_height,scx,scy,rot90,
                  labels,coords,targs,nargs):
    targs['dx'] = hex_width
    targs['dy'] = hex_height
    if labels is not None:
        labmap = {
            'auto': {
                'hLeading': 1, 'vLeading': 1, 'hType': 'N', 'vType': 'N' },
            'auto=numbers' : {
                'hLeading': 1, 'vLeading': 1, 'hType': 'N', 'vType': 'N' },
            'auto=alpha column': {
                'hLeading': 0, 'vLeading': 0, 'hType': 'A', 'vType': 'N',
                'hOff': -1 },
            'auto=alpha 2 column': {# Not supported
                'hLeading': 1, 'vLeading': 1, 'hType': 'A', 'vType': 'N' },
            'auto=inv y x plus 1': {
                'hLeading': 1, 'vLeading': 1, 'hType': 'N', 'vType': 'N',
                'hOff': 0, 'vDescend': True }, 
            'auto=x and y plus 1': {
                'hLeading': 1, 'vLeading': 1, 'hType': 'N', 'vType': 'N',
                'hOff': 0, 'vOff': 0  }
        }
        for l in labels.split(','): 
            nargs.update(labmap.get(l,{}))
            
    # Field/Direction | Normal | Rotated |
    # ----------------|--------|---------|
    # hOff            | column | column  |
    # vOff            | row    | row     |
    # hDescend        | column | row     | True, incr to left 
    # vDescend        | row    | column  | True, incr upward
    # First='H'       | column | column  |
    
    # In wargame offset=0 means that the first coordinates are 0,0.
    # But offset 0,0 in VASSAL means start at 1,1
    horiz         = 'column'
    vert          = 'row'
    nargs['vOff'] = nargs.get('vOff',0)-coords[vert] ['offset']-1
    nargs['hOff'] = nargs.get('hOff',0)-coords[horiz]['offset']-1

    # In VASSAL, vDesend=False means rows increase downward,
    # while in wargame, factor=-1 means rows increase downward
    horiz               = 'row'    if rot90 else 'column'
    vert                = 'column' if rot90 else 'row'
    nargs['hDescend']   = coords[horiz]['factor'] !=  1
    nargs['vDescend']   = coords[vert] ['factor'] != -1 # flipped y
    targs['edgesLegal'] = True

    # Check for inversed columns 
    inv_col = coords['column']['factor'] == -1
    ell     = hex_width if inv_col else 0

    # Some special stuff in case of 90 degree rotation
    if rot90:
        targs.update({'sideways': True})
        nargs['hOff']     -= 1
        nargs['hDescend'] = not nargs['hDescend']
        if inv_col:
            nargs['hOff'] += 3

    margin = i.get('board frame',{}).get('margin',0)        
    mx     = scx * margin
    my     = scy * margin
    if inv_col:  mx = 0 # Bug in wargame?
        
    # Grid offset is defined relative to the full image,
    # not the zone:
    #
    #   X,Y offset: The horizontal and vertical position of
    #   the center of the first hex of the grid.
    #
    # That is, (x,y)=(0,0) is in the top-left corner of the
    # image, while in TikZ it is in the bottom left corner.
    # This means we shift by the boudning box offsets plus
    # some stuff to align with the zone.
    #
    # In case of inversed columns, we need to add some extra stuff.
    x0, y0 = llx-hex_width/3 + ell + mx, ury + my

    # Rotated?
    if rot90:
        x0, y0 = ury+2*hex_width/3 + ell + mx, llx + my
        
    # Below should be double-checked.
    
    if not rot90:
        if coords.get('column',{}).get('top short','') == 'isodd' \
           and coords.get('column',{}).get('offset',0) == -1:
            y0 -= hex_width/2
        elif coords.get('column',{}).get('top short','') == 'iseven' \
             and coords.get('column',{}).get('offset',-1) == 0:
            y0 -= hex_width/2

    targs.update({'x0': int(x0),'y0': int(y0)})

# --------------------------------------------------------------------
def get_rectparams(i,llx,ury,width,height,rot90,targs,nargs):
    targs['dx']       = width
    targs['dy']       = height
    targs['x0']       = int(llx - width/2)
    targs['y0']       = int(ury + height/2)
    targs['color']    = rgb(0,255,0)
    nargs['color']    = rgb(0,255,0)
    nargs['vDescend'] = True
    nargs['vOff']     = -3
    nargs.update({'sep':',','vLeading':0,'hLeading':0})
    
# --------------------------------------------------------------------
def add_zones(root,zoned,name,board,width,height,gpid,
              labels=None,coords=None,picinfo=None,
              verbose=False,visible=True):
    grids = []
    picture = None
    if verbose:
        print(f'  Adding zones to {name} zoned={"zoned" in board}')

    for k, v in board.items():
        if k == 'labels': labels = v
        if k == 'coords': coords = v
        if k == 'zoned':  picture = v
        if 'zone' not in k or k == 'zoned':
            continue

        gridtype = (add_hexgrid
                    if 'hex' in k.lower()
                    else add_squaregrid)
        gridnum  = (add_hexnumbering
                    if 'hex' in k.lower()
                    else add_squarenumbering)
        grids.append([k,v,gridtype,gridnum])

    if len(grids) < 1: return gpid
    if verbose:
        print(f'    Got zones: {[k[0] for k in grids]}')
    # Either get picture information from argument, or deduce from the
    # found 'zoned' key.
    if picinfo is None:
        picinfo = get_picture_info(picture,name,width,height,verbose)
        
    hex_width, hex_height, scx, scy, rot90, tran = picinfo 
        
    
    for g in grids:
        n, i, typ, num = g
        if verbose:
            print(f'   Adding zone {n}')
        if 'scope' in n:
            llx,lly = tran(*i['global lower left'])
            urx,ury = tran(*i['global upper right'])
            path    = [[llx,ury],[urx,ury],[urx,lly],[llx,lly]]
            nm      = n.replace('zone scope ','')
        elif 'path' in n:
            path    = [tran(*ii) for ii in i['path']]
            llx     = min([px for px,py in path])
            ury     = max([py for px,py in path])
            nm      = n.replace('zone path ','')

        # Checkf if we have "point" type elements in this object and
        # add them to dict.
        points = [ v for k,v in i.items()
                   if (k.startswith('point') and
                       isinstance(v,dict) and \
                       v.get('type','') == 'point')]

        pathstr = ';'.join([f'{s[0]},{s[1]}' for s in path])
        if verbose:
            print(f'    Zone path ({llx},{ury}): {pathstr}')
        zone    = add_zone(root,zoned,
                           name=nm,
                           locationFormat = ("$name$"
                                             if 'pool' in n.lower() else
                                             "$gridLocation$"),
                           useParentGrid=False,
                           path=pathstr)

        # Do not add grids to pools 
        if 'pool' in n.lower():
            continue

        targs  = {'color':rgb(255,0,0),'visible':visible}
        nargs  = {'color':rgb(255,0,0),'visible':visible}
        if 'hex' in n.lower():
            get_hexparams(i,llx,ury,hex_width,hex_height,scx,scy,rot90,
                          labels,coords,targs,nargs)
        else:
            width = hex_width / HEX_WIDTH * RECT_WIDTH
            height = hex_height / HEX_HEIGHT * RECT_HEIGHT
            get_rectparams(i,llx,ury,width,height,rot90,targs,nargs)
            
        # print(targs,nargs)
        if 'turn' in n.lower(): nargs['sep'] = 'T'
        if 'oob' in n.lower():  nargs['sep'] = 'O'

        if verbose:
            print(f'    Adding grid')

        if len(points) <= 0:
            grid = typ(root,zone,**targs)
            num(root,grid,**nargs)
        else:
            if verbose:
                print(f'     Use a region grid')
            grid = add_regiongrid(root,zone,snapto=True,visible=visible)
            for p in points:
                pn = p["name"]
                pc = p["coords"]
                if verbose:
                    print(f'      Add region {pn} {p}')

                n, gpid = add_region(root,grid,pn,*tran(*pc),gpid,
                                     alsoPiece=True,verbose=verbose)
                

        # Once we've dealt with this grid, we should see if we have
        # any embedded zones we should deal with.
        add_zones(root,zoned,name,i,width,height,gpid,
                  labels=labels,coords=coords,picinfo=picinfo,
                  verbose=verbose)
        
        # Adding a global grid
        #targs['color'] = rgb(0,255,0)
        #nargs['color'] = rgb(0,255,0)
        #grid = typ(root,zoned,**targs)
        #num(root,grid,**nargs)

    return gpid

    
# --------------------------------------------------------------------
def add_inv(root,game,sides,**attr):
    '''Add inventory to module'''
    filt = '{' + '||'.join([f'Faction=="{s}"' for s in sides])+'}'
    grp  = 'Faction,Command,Echelon,Type'
    inv = add_inventory(root,game,
                        include             = filt,
                        groupBy             = grp,
                        sortFormat          = '$PieceName$',
                        tooltip             = 'Show inventory of all pieces',
                        zoomOn              = True,
                        **attr)
    return inv
                        
                        
# --------------------------------------------------------------------
def add_map_board(root,game,name,board,gpid,verbose=False,visible=False):
    '''A add a single board to the module
    '''

    map = add_map(root,game, name)
    bpicker = add_boardpicker(root,map)
    
    add_counterviewer      (root,map)
    add_hidepieces         (root,map)
    add_globalmap          (root,map)
    add_basics_to_map      (root,map)
    add_masskey            (root,map,'Flip',     key('F'),key('F'),
                            icon = '/images/Undo16.gif',
                            tooltip='Flip selected units')
    add_masskey            (root,map,'Eliminate',key('E'),key('E'),
                            icon = '/images/playerAway.gif',
                            tooltip='Eliminate selected units')

    # Get the image so we may make a rectangle
    if verbose:
        print(f' Adding board {board["name"]} -> {board["filename"]}')
    ulx,uly,lrx,lry = get_bb(board['img'])
    eboard = add_board(root,bpicker,name,image=board['filename'])
    zoned  = add_zoned(root,eboard)
    #hexes = add_hexgrid(root,zoned)
    add_zonehighlighter(root,zoned)

    if not 'zones' in board:
        full = add_zone(root,zoned,
                        name='full',
                        useParentGrid=False,
                        path=(f'{ulx},{uly};' +
                              f'{lrx},{uly};' +
                              f'{lrx},{lry};' +
                              f'{ulx},{lry}'))

        # We do not have board info, so add a grid to the whole thing
        add_hex_grid(root,full,rgb(255,0,0),True,True,visible=visible)
        return

    # If we get here, we have board info!
    w = abs(ulx-lrx)
    h = abs(uly-lry)
    return add_zones(root,zoned,name,board['zones'],w,h,gpid,
                     verbose=verbose,visible=visible)
    
# --------------------------------------------------------------------
def add_boards(root,game,categories,sides,gpid,verbose=False,visible=False):
    '''Add all boards to the module, including eliminated map

    Parameters
    ----------
    root : xml.dom.minidom.Document
        Root of the document
    game : xml.dom.minidom.Element
        The parent element
    categories : dict
        Catalogue of images 
    sides : list of  str
        List of sides in the game
        Name of the map
    '''
    if verbose:
        print('Adding maps and boards')

    # Loop over defined boards
    for bn, b in categories.get('board',{}).get('all',{}).items():
        gpid = add_map_board(root,game,bn,b,gpid,verbose,visible)
        
    add_elim(root,game,sides,categories,verbose)

    return gpid

# --------------------------------------------------------------------
def add_oob(root,widget,name,oob,gpid,hotkey='',verbose=False,visible=False):
    '''A add a single board to the module
    '''
    # Short hand 
    cr = lambda parent,tag,**attr : create_elem(root,parent,vn(tag),**attr)

    if verbose:
        print(f' Adding oob {name}')

    map     = add_widgetmap(root,widget, name,markMoved='Never',hotkey='')
    bpicker = add_boardpicker(root,map)

    add_counterviewer      (root,map)
    add_basics_to_map      (root,map)
    

    # Get the image so we may make a rectangle
    if verbose:
        print(f'  Adding OOB {oob["name"]} -> {oob["filename"]}')
        
    ulx,uly,lrx,lry = get_bb(oob['img'])
    board = add_board(root,bpicker,name,image=oob['filename'])
    zoned = add_zoned(root,board)
    add_zonehighlighter(root,zoned)

    if not 'zones' in oob:
        zone = add_zone(root,zoned,
                        name='full',
                        useParentGrid=False,
                        path=(f'{ulx},{uly};' +
                              f'{lrx},{uly};' +
                              f'{lrx},{lry};' +
                              f'{ulx},{lry}'))
        # We do not have board info, so add a grid to the whole thing
        grid = add_squaregrid(root,zone,visible=visible)
        add_squarenumbering(root,grid,visible=visible)
        return

    # If we get here, we have board info!
    w = abs(ulx-lrx)
    h = abs(uly-lry)
    return add_zones(root,zoned,name,oob['zones'],w,h,gpid,
                     verbose=verbose,visible=visible)
    
# --------------------------------------------------------------------
def add_oobs(root,game,categories,gpid,verbose=False,visible=False):
    '''Add all OObs to the module

    Parameters
    ----------
    root : xml.dom.minidom.Document
        Root of the document
    game : xml.dom.minidom.Element
        The parent element
    categories : dict
        Catalogue of images 
    '''
    # Short hand
    oobc = categories.get('oob',{}).get('all',{}).items()
    if len(oobc) < 1:
        return gpid
    
    oobs = add_chartwindow(root,game, 'OOBs',
                           hotkey      = key('B',ALT),
                           description = 'OOBs',
                           text        = '',
                           icon        = '/images/inventory.gif',
                           tooltip     = 'Show/hide OOBs')
    tabs = add_tabs(root,oobs,'OOBs')

    if verbose:
        print('Adding OOBs')

    # Loop over defined boards
    for on, o in oobc:
        widget = add_mapwidget(root,tabs, entryName=on)
        gpid = add_oob(root,widget,on,o,gpid,
                       verbose = verbose,
                       hotkey  = key('B',ALT),
                       visible = visible)

    return gpid

# --------------------------------------------------------------------
def add_markproto(root,protos,key,value):
    decs = [ mark_trait(key,value.capitalize()),
             basic_trait('','','') ]
    typs = [d[0] for d in decs]
    stts = [d[1] for d in decs]
    typ  = enc_parts(*typs)
    stt  = enc_parts(*stts)
    body = add_proto(typ,stt)
    add_prototype(root,protos,value+' prototype',body)
                  
# --------------------------------------------------------------------
def add_counters(root,game,categories,
                 unittypes,echelons,commands,verbose=False):
    '''Add all counters to the module.

    This will add all counters to the module, as well as prototypes
    for each sub-category.

    Parameters
    ----------
    root : xml.dom.minidom.Document
        Root of the document
    game : xml.dom.minidom.Element
        The game element
    categories : dict
        Catalogue of images 
    '''
    # Short hand 
    # Create prototypes container before adding pieces 
    protos = add_prototypes(root,game)
    for utype in unittypes:  add_markproto(root,protos,'Type',utype)
    for ech   in echelons:   add_markproto(root,protos,'Echelon',ech)
    for cmd   in commands:   add_markproto(root,protos,'Command',cmd)

    # Stating global piece ID
    gpid  = 20

    # A window to hold the pieces 
    piecew = add_piecewindow(root,game,'Counters',
                             hotkey = key('C',ALT))
    # Tab container for each sub-category
    piecet = add_tabs(root,piecew,entryName='Counters')

    if verbose:
        print('Adding counters and prototypes')
        
    # Loop over sub-categories 
    for subn, subc in categories.get('counter',{}).items():
        # Friendly message
        if verbose:
            print(f' Adding {subn} counters and prototype')

        # Create a panel for this sub-category 
        piecep = add_panel(root,piecet,
                           entryName = subn,
                           fixed     = False)
        # Add a list to the panel
        piecel = add_list(root,piecep,
                          entryName=f'{subn} counters')

        if verbose:
            print(f'  Prototype: {subn} prototype')
            # Create prototype for this sub-category 
        proto  = add_prototype(root,protos,
                               name        = f'{subn} prototype',
                               code        = proto_body(subn),
                               description = f'Prototype for {subn} counters')

        # Loop over counters in this sub-category 
        for cn, c in subc.items():
            # Flipped counters are not added directly 
            if cn.endswith('flipped'): continue

            if verbose:
                print(f'  counter: {cn}')
                
            # Get image bounding box 
            bb = get_bb(c['img'])

            # Get flipped image, if any 
            cf = subc.get(cn + ' flipped', None)

            # Create a slot for this piece 
            sl = add_pieceslot(root,piecel,
                               entryName = cn,
                               gpid      = gpid,
                               code      = piece_body(subn,c,cf,gpid),
                               height    = bb[3]-bb[1],
                               width     = bb[2]-bb[0])

            # Store the global piece identifier  - do we need this?
            c['gpid'] = gpid

            # Increment global piece identifier 
            gpid += 1

    # Return next free piece identifier 
    return gpid

# --------------------------------------------------------------------
def add_charts(root,game,categories,verbose=False):
    '''Add all boards to the module

    This will select all 'chart' elements from the catalogue and add
    them as individual (popup) panels to the module.
    
    Parameters
    ----------
    root : xml.dom.minidom.Document
        Root of the document
    game : xml.dom.minidom.Element
        The game element
    categories : dict
        Catalogue of images

    '''
    chartc = categories.get('chart',{}).get('all',{}).items()
    if len(chartc) < 1:
        return
    
    # Short hand 
    cr = lambda parent,tag,**attr : create_elem(root,parent,vn(tag),**attr)
    cw = lambda parent,tag,**attr : create_elem(root,parent,vw(tag),**attr)

    if verbose:
        print('Adding charts')

    charts = add_chartwindow(root,game, 'Charts',
                             hotkey      = key('A',ALT),
                             description = 'Charts',
                             tooltip     = 'Show/hide Charts')
    tabs = add_tabs(root,charts,'Charts')
    
    
    # Loop over defined charts
    for cn, c in chartc:
        if verbose:
            print(f' Chart {cn} ({c["filename"]})')
        add_chart(root,tabs,
                  chartName = cn,
                  description = cn,
                  fileName = c['filename'])

# --------------------------------------------------------------------
def add_diceroll(root,game,categories,verbose=False):
    '''Add a dice button (1d6)
    
    Parameters
    ----------
    root : xml.dom.minidom.Document
        Root of the document
    game : xml.dom.minidom.Element
        The game element
    categories : dict
        Catalogue of images
    '''
    if verbose:
        print('Adding dice')

    add_dice(root,game,
             hotkey      = key('6',ALT),
             nDice       = 1,
             nSides      = 6,
             text        = '1d6',
             tooltip     = 'Roll a 1d6',
             name        = '1d6')

    
# ====================================================================
def create_build(info,
                 categories,
                 unittypes,
                 echelons,
                 commands,
                 zipfile,
                 title,
                 version,
                 desc     = '',
                 rules    = None,
                 tutorial = None,
                 pdffile  = '',
                 infofile = '',
                 verbose  = False,
                 visible  = False):
    '''Create the buildFile.xml document

    Parameters
    ----------
    info : dict
         Information dicationary
    categories : dict
         Catalogue of images
    unittypes : set
    	Set of used unit types
    zipfile : zipfile.ZipFile
         Module archive
    title : str
         Title of module
    version : str
         Version number of module
    desc : str
         Short description of the game
    rules : str
         Filename of PDF rules document (if any)

    Returns
    -------
    xml : str
        The encoded document
    '''
    from xml.dom.minidom import Document

    if verbose:
        print(f'Creating buildFile.xml: {title},{version}')

    # Create document 
    root = Document()

    # Add game element 
    game = add_game(root,root,
                    name = title,
                    version = version,
                    description = desc)

    # Extract the sides of the game from the counter sub-categories
    # Should be a little more clever about the case here
    dsides   = {k.lower() : k for k in categories.get('counter',{}).keys()}
    sides    = None
    for skip in ['all','common', 'marker', 'markers']:
        if skip in dsides: del dsides[skip]
        sides = list(dsides.values())
    if verbose:
        print('Identified factions: ',sides)
        
    # Use the standard command encoder
    add_basiccommandencoder(root,game)

    # Add document parts
    doc = add_documentation(root,game)
    add_splash(root, doc, categories, title,verbose)
    add_rules(root, doc, zipfile, rules, verbose)
    add_tut(root, doc, zipfile, tutorial, verbose)
    notes = add_notes(root,doc,zipfile,title,version,
                      sides,pdffile,infofile,rules,verbose)


    # Create roster or player sides
    roster   = add_roster(root,game)
    for s in sides:
        add_entry(root,roster,s)

    # Add global options - mainly defer to user settings
    add_globaloptions(root,game,
                      autoReport         = 'Use Preferences Setting',
                      centerOnMove       = 'Use Preferences Setting',
                      nonOwnerUnmaskable = 'Use Preferences Setting',
                      playerIdFormat     = '$playerName$')
    # Add global properties
    gp = add_globalproperties(root,game)
    # Add a turn tracker
    tt = add_turntrack(root,game,'Turn',counter={
        'property': 'Turn',
        'phases': {
            'property': 'Phase',
            'names': sides }})
    add_globalproperty(root,gp,'TurnTracker.defaultDocked',True)
    
    # Add all pieces to the module (including prototypes)
    gpid = add_counters(root,game,categories,
                        unittypes,echelons,commands,verbose)    
    # Add an inventory
    add_inv(root,game,sides)    
    # Add all boards to the module (including eliminated map)
    gpid = add_boards(root,game,categories,sides,gpid,
                      verbose=verbose,visible=visible)
    # Add all OOBs to the module 
    gpid = add_oobs(root,game,categories,gpid,
                    verbose=verbose,visible=visible)
    # Add charts to the module 
    add_charts(root,game,categories,verbose=verbose)
    # Add a dice rool
    add_diceroll(root,game,categories,verbose=verbose)

    # Set next available piece identifier on the game 
    game.setAttribute('nextPieceSlotId',str(gpid+1))

    # Return the document 
    return root, notes

# --------------------------------------------------------------------
def create_data(title,version,desc='',verbose=False):
    '''Create the module data XML

    Parameters
    ----------
    title : str
        Title of the game
    version : str
        Version number of the module
    desc : str
        Short description
    
    Returns
    -------
    xml : str
        The encoded document
    '''
    from xml.dom.minidom import Document
    from time import time

    if verbose:
        print('Create module data') 
        
    # The document
    root = Document()

    # Top of the document
    data = root.createElement('data')
    data.setAttribute("version","1")
    root.appendChild(data)

    # Version number of module 
    vers = root.createElement('version')
    vers.appendChild(root.createTextNode(str(version)))
    data.appendChild(vers)

    # Name of the game 
    name = root.createElement('name')
    name.appendChild(root.createTextNode(title))
    data.appendChild(name)

    # VASSAL version (should get this in some other way)
    vvers = root.createElement('VassalVersion')
    vvers.appendChild(root.createTextNode("3.6.7"))
    data.appendChild(vvers)

    # Description, if any 
    desc = root.createElement('description')
    data.appendChild(desc)

    # Automatically set the save data 
    save = root.createElement('dateSaved')
    save.appendChild(root.createTextNode(f'{int(time())}'))
    data.appendChild(save)

    return root


# ====================================================================
def create_vmod(vmodname,
                info,
                title,
                version,
                desc     = '',
                rules    = None,
                tutorial = None,
                pdfname  = '',
                infoname = '',
                patch    = None,
                verbose  = False,
                visible  = False):
    '''Create a VASSAL module from the information passed

    Parameters
    ----------
    vmodname : str
        Name of the module
    info : list
        List of images and image data
    title : str
        Name of the game
    version : str
        Version of the module
    desc : str
        Short description of the game
    rules : str
        Rules PDF (optional)

    Returns
    -------
    '''
    from zipfile import ZipFile, ZIP_DEFLATED
    from pprint import pprint

    categories = {}
    unittypes  = []
    echelons   = []
    commands   = []
    with ZipFile(vmodname,'w',compression=ZIP_DEFLATED) as vmod:
        # Create images in ZIP file 
        for i in info:
            if ignore_entry(i): continue

            # Store the name of the image file in catalogue
            i['filename'] = i['name'].replace(' ','_') + '.png'
            imgfn = 'images/'+i['filename']
            if imgfn not in vmod.namelist():
                vmod.writestr(imgfn,i['img'])

            # Categorize - assume counter 
            typ = i.get('category','counter')
            sub = i.get('subcategory','all')
            if sub == '':
                i['subcategory'] = 'all'
                sub              = 'all'

            # Add to catalogue 
            if typ not in categories:
                categories[typ] = {}
            cat = categories[typ]

            if sub not in cat:
                cat[sub] = {}
            tgt = cat[sub]

            tgt[i['name']] = i

            natoapp6c = i.get('natoapp6c',None)
            if natoapp6c is not None:
                from re import sub
                mains   = natoapp6c.get('main',None)
                echelon = natoapp6c.get('echelon',None)
                command = natoapp6c.get('command',None)

                if mains is not None:
                    mains = sub(r'\[[^]]+\]','',mains)\
                        .replace('{','').replace('}','').split(',')
                    unittypes.extend(mains)
                    i['mains'] = mains
                    
                if echelon is not None:
                    echelons.append(natoapp6c['echelon'])
                    i['echelon'] = echelon

                if command is not None:
                    commands.append(command)
                    i['command'] = command

            # Friendly message
            # if verbose:
            #    print(f'{i["name"]} -> {i["type"]}/{i["sub type"]} {typ}/{sub}')

        unittypes = set(unittypes)
        echelons  = set(echelons)
        commands  = set(commands)
        
        # Now create the `buildFile.xml` file
        build, notes = create_build(info,
                                    categories,
                                    unittypes,
                                    echelons,
                                    commands,
                                    vmod,
                                    title,
                                    version,
                                    desc,
                                    rules,
                                    tutorial,
                                    pdfname,
                                    infoname,
                                    verbose,
                                    visible)
        # Now create the `moduledata` file 
        data  = create_data(title, version, desc,verbose)

    
        if patch:
            from importlib.util import spec_from_file_location, \
                module_from_spec
            from pathlib import Path
            from sys import modules

            p = Path(patch)
            if verbose:
                pn = str(p.stem)
                print(f'Will patch module with {pn}.patch function')

            spec = spec_from_file_location(p.stem, p.absolute())
            module = module_from_spec(spec)
            spec.loader.exec_module(module)
            modules[p.stem] = module


            # Patch must accept xml.dom.document,xml.dom.document,ZipFile
            module.patch(build,data,vmod,verbose)
            
        buildstr = build.toprettyxml(indent=' ',
                                     encoding="UTF-8",
                                     standalone=False)
        datastr = data.toprettyxml(indent=' ',
                                   encoding="UTF-8",
                                   standalone=False)
                    
        vmod.writestr('buildFile.xml',buildstr)
        vmod.writestr('moduledata',datastr)

    return notes

# ====================================================================
def export(vmodname     = 'Draft.vmod',
           pdfname      = 'export.pdf',
           infoname     = 'export.json',
           title        = 'Test',
           version      = 'Draft',
           desc         = '',
           rules        = None,
           tutorial     = None,
           patch        = None,
           verbose      = False,
           visible      = False):
    '''Do the whole thing

    - Read in information form the 'infoname' file (a CSV file
      generated by LaTeX)

    - Convert each page in the 'pdfname' file (a PDF generated by
      LaTex) into images

    - Create the VASSAL module 

    '''
    if verbose:
        print(f'Module name : {vmodname}\n'
              f'PDF name    : {pdfname}\n'
              f'Info name   : {infoname}\n'
              f'Title       : {title}\n'
              f'Version     : {version}\n'
              f'Description : {desc}\n'
              f'Rules       : {rules}\n'
              f'Tutorial    : {tutorial}')
    info = convert_pages(pdfname=pdfname,infoname=infoname)
    return create_vmod(vmodname = vmodname,
                       info     = info,
                       title    = title,
                       version  = version,
                       desc     = desc,
                       rules    = rules,
                       tutorial = tutorial,
                       pdfname  = pdfname,
                       infoname = infoname,
                       patch    = patch,
                       verbose  = verbose,
                       visible  = visible)
        
# ====================================================================
if __name__ == '__main__':
    from argparse import ArgumentParser, FileType

    ap = ArgumentParser(description='Create draft VASSAL module')
    ap.add_argument('pdffile',
                    help='The PDF file to read images from',
                    type=FileType('r'),
                    default='export.pdf',
                    nargs='?')
    ap.add_argument('infofile',
                    help='The JSON file to read image information from',
                    type=FileType('r'),
                    default='export.json',
                    nargs='?')
    ap.add_argument('-p','--patch',
                    help='A python script to patch generated module',
                    type=FileType('r'))
    ap.add_argument('-o','--output',
                    help='Output file to write module to',
                    type=FileType('w'),
                    default='Draft.vmod')
    ap.add_argument('-t','--title',
                    help='Module title', default='Draft',
                    type=str)
    ap.add_argument('-v','--version',
                    help='Module version',
                    type=str,
                    default='draft')
    ap.add_argument('-r','--rules',
                    help='Rules PDF file',
                    type=FileType('r'))
    ap.add_argument('-T','--tutorial',
                    help='Tutorial (v)log file',
                    type=FileType('r'))
    ap.add_argument('-d','--description',
                    help='Short description of module',
                    type=str,
                    default='draft of module')
    ap.add_argument('-V','--verbose',
                    help='Be verbose',
                    action='store_true')
    ap.add_argument('-G','--visible-grids',
                    action='store_true',
                    help='Make grids visible in the module')
    ap.add_argument('-n','--notes',
                    help='Show notes',
                    action='store_true')


    args = ap.parse_args()
    
    vmodname  = args.output.name
    rulesname = args.rules.name    if args.rules    is not None else None
    tutname   = args.tutorial.name if args.tutorial is not None else None
    args.output.close()

    patchname = args.patch.name if args.patch is not None else None
    if args.patch is not None:
        args.patch.close()

    if args.version.lower() == 'draft':
        args.visible_grids = True

    try:
        notes = export(vmodname = vmodname,
                       pdfname  = args.pdffile.name,
                       infoname = args.infofile.name,
                       title    = args.title,
                       version  = args.version,
                       desc     = args.description,
                       rules    = rulesname,
                       tutorial = tutname,
                       patch    = patchname,
                       verbose  = args.verbose,
                       visible  = args.visible_grids)

        if args.notes:
            print(f'Module {vmodname} created. '
                  'Here are the notes from the module')
            print(notes)

    except Exception as e:
        from sys import stderr 
        print(f'Failed to build {vmodname}: e',file=stderr)
        from os import unlink
        try:
            unlink(vmodname)
        except:
            pass

        raise e
