#!/usr/bin/env python

import argparse
import os
import re
import sys
import shlex
from pprint import pprint

def trim(s):
    """
    All repetition of any kind of space is replaced by a single space
    and remove trailing space at beginning or end.
    """
    # regex to replace all space groups by a single space
    # use split() to remove trailing space at beginning/end
    return re.sub(r'\s+', ' ', s).strip()


def quotesForStrings(valueStr):
    """
    Return the input string with quotes if it cannot be cast into another builtin type.
    """
    v = valueStr
    try:
        int(valueStr)
    except ValueError:
        try:
            float(valueStr)
        except ValueError:
            if "'" in valueStr:
                v = f"'''{valueStr}'''"
            else:
                v = f"'{valueStr}'"
    return v

def convertToLabel(name):
    camelCaseToLabel = re.sub('()([A-Z][a-z]*?)', r'\1 \2', name)
    snakeToLabel = ' '.join(word.capitalize() for word in camelCaseToLabel.split('_'))
    snakeToLabel = ' '.join(word.capitalize() for word in snakeToLabel.split(' '))
    return snakeToLabel

def is_int(s):
    try:
        int(s)
        return True
    except ValueError:
        return False

def is_float(s):
    try:
        float(s)
        return True
    except ValueError:
        return False


parser = argparse.ArgumentParser(description='Create a new Node Type')
parser.add_argument('node', metavar='NODE_NAME', type=str,
                    help='New node name')
parser.add_argument('bin', metavar='CMDLINE', type=str,
                    default=None,
                    help='Input executable')
parser.add_argument('--output', metavar='DIR', type=str,
                    default=os.path.dirname(__file__),
                    help='Output plugin folder')
parser.add_argument('--parser', metavar='PARSER', type=str,
                    default='boost',
                    help='Select the parser adapted for your command line: {boost,cmdLineLib,basic}.')
parser.add_argument("--force", help="Allows to overwrite the output plugin file.",
                    action="store_true")

args = parser.parse_args()

inputCmdLineDoc = None
soft = "{nodeType}"
if args.bin:
    soft = args.bin
    import subprocess
    proc = subprocess.Popen(args=shlex.split(args.bin) + ['--help'], stdout=subprocess.PIPE, stderr=subprocess.PIPE)
    stdout, stderr = proc.communicate()
    inputCmdLineDoc = stdout if stdout else stderr
elif sys.stdin.isatty():
    inputCmdLineDoc = ''.join([line for line in sys.stdin])

if not inputCmdLineDoc:
    print('No input documentation.')
    print(f'Usage: YOUR_COMMAND --help | {os.path.splitext(__file__)[0]}')
    sys.exit(-1)


fileStr = '''import sys
from meshroom.core import desc


class __COMMANDNAME__(desc.CommandLineNode):
    commandLine = '__SOFT__ {allParams}'
'''.replace('__COMMANDNAME__', args.node).replace('__SOFT__', soft)


print(inputCmdLineDoc)

args_re = None
if args.parser == 'boost':
    args_re = re.compile(
        r'^\s+'  # space(s)
        r'(?:-(?P<argShortName>\w+)\|?)?'  # potential argument short name
        r'\s*\[?'  # potential '['
        r'\s*--(?P<argLongName>\w+)'  # argument long name
        r'(?:\s*\])?' # potential ']'
        r'(?:\s+(?P<arg>\w+)?)?'  # potential arg
        r'(?:\s+\(\=(?P<defaultValue>.+)\))?'  # potential default value
        r'\s+(?P<descriptionFirst>.*?)\n'  # end of the line
        r'(?P<descriptionNext>(?:\s+[^-\s].+?\n)*)'  # next documentation lines
        , re.MULTILINE)
elif args.parser == 'cmdLineLib':
    args_re = re.compile(
        '^'
        r'\[' # '['
        r'-(?P<argShortName>\w+)'  # argument short name
        r'\|'
        r'--(?P<argLongName>\w+)'  # argument long name
        r'(?:\s+(?P<arg>\w+)?)?'  # potential arg
        r'\]' # ']'
        r'()' # no default value
        r'(?P<descriptionFirst>.*?)?\n' # end of the line
        r'(?P<descriptionNext>(?:[^\[\w].+?\n)*)' # next documentation lines
        , re.MULTILINE)
elif args.parser == 'basic':
    args_re = re.compile(r'()--(?P<argLongName>\w+)()()()()')
else:
    print(f'Error: Unknown input parser "{args.parser}"')
    sys.exit(-1)

choiceValues1_re = re.compile(r'\* (?P<value>\w+):')
choiceValues2_re = re.compile(r'\((?P<value>.+?)\)')
choiceValues3_re = re.compile(r'\{(?P<value>.+?)\}')

cmdLineArgs = args_re.findall(inputCmdLineDoc.decode('utf-8'))

print('='*80)
pprint(cmdLineArgs)

outputNodeStr = ''
inputNodeStr = ''

for cmdLineArg in cmdLineArgs:
    shortName = cmdLineArg[0]
    longName = cmdLineArg[1]
    if longName == 'help':
        continue  # skip help argument

    arg = cmdLineArg[2]
    value = cmdLineArg[3]
    descLines = cmdLineArg[4:]
    description = ''.join(descLines).strip()
    if description.endswith(':'):
        # If documentation is multiple lines and the last line ends with ':',
        # we remove this last line as it is probably the title of the next group of options
        description = '\n'.join(description.split('\n')[:-1])
    description = trim(description)

    values = choiceValues1_re.findall(description)
    if not values:
        possibleLists = choiceValues2_re.findall(description) + choiceValues3_re.findall(description)
        for possibleList in possibleLists:
            candidate = possibleList.split(',')
            if len(candidate) > 1:
                values = [trim(v) for v in candidate]

    cmdLineArgLower = ' '.join([shortName, longName, arg, value, description]).lower()
    namesLower = ' '.join([shortName, longName]).lower()
    isBool = (arg == '' and value == '')
    isFile = 'path' in cmdLineArgLower or 'folder' in cmdLineArgLower or 'file' in cmdLineArgLower
    isChoice = bool(values)
    isOutput = 'output' in cmdLineArgLower or 'out' in namesLower
    isInt = is_int(value)
    isFloat = is_float(value)

    argStr = None
    if isBool:
        argStr = """
        desc.BoolParam(
            name='{name}',
            label='{label}',
            description='''{description}''',
            value={value},
            ),""".format(
                name=longName,
                label=convertToLabel(longName),
                description=description,
                value=quotesForStrings(value),
                arg=arg,
                )
    elif isFile:
        argStr = """
        desc.File(
            name='{name}',
            label='{label}',
            description='''{description}''',
            value={value},
            ),""".format(
                name=longName,
                label=convertToLabel(longName),
                description=description,
                value=quotesForStrings(value),
                arg=arg,
                )
    elif isChoice:
        argStr = """
        desc.ChoiceParam(
            name='{name}',
            label='{label}',
            description='''{description}''',
            value={value},
            values={values},
            exclusive={exclusive},
            ),""".format(
                name=longName,
                label=convertToLabel(longName),
                description=description,
                value=quotesForStrings(value),
                values=values,
                exclusive=True,
                )
    elif isInt:
        argStr = """
        desc.IntParam(
            name='{name}',
            label='{label}',
            description='''{description}''',
            value={value},
            range={range},
            ),""".format(
                name=longName,
                label=convertToLabel(longName),
                description=description,
                value=value,
                range='(-sys.maxsize, sys.maxsize, 1)',
                )
    elif isFloat:
        argStr = """
        desc.FloatParam(
            name='{name}',
            label='{label}',
            description='''{description}''',
            value={value},
            range={range},
            ),""".format(
                name=longName,
                label=convertToLabel(longName),
                description=description,
                value=value,
                range='''(-float('inf'), float('inf'), 0.01)''',
                )
    else:
        argStr = """
        desc.StringParam(
            name='{name}',
            label='{label}',
            description='''{description}''',
            value={value},
            ),""".format(
                name=longName,
                label=convertToLabel(longName),
                description=description,
                value=quotesForStrings(value),
                range=range,
                )
    if isOutput:
        outputNodeStr += argStr
    else:
        inputNodeStr += argStr


fileStr += """
    inputs = [""" + inputNodeStr + """
    ]

    outputs = [""" + outputNodeStr + """
    ]
"""

outputFilepath = os.path.join(args.output, args.node + '.py')

if not args.force and os.path.exists(outputFilepath):
    print(f'Plugin "{args.node}" already exists "{outputFilepath}".')
    sys.exit(-1)

with open(outputFilepath, 'w') as pluginFile:
    pluginFile.write(fileStr)

print(f'New node exported to: "{outputFilepath}"')
