#!/usr/bin/env python
import argparse
import os
import sys
import distutils.util

import meshroom
meshroom.setupEnvironment()

import meshroom.core.graph
import meshroom.core.taskManager
from meshroom import multiview

parser = argparse.ArgumentParser(description='Launch the full photogrammetry or Panorama HDR pipeline.')
parser.add_argument('-i', '--input', metavar='SFM/FOLDERS/IMAGES', type=str, nargs='*',
                    default=[],
                    help='Input folder containing images or folders of images or file (.sfm or .json) '
                         'with images paths and optionally predefined camera intrinsics.')
parser.add_argument('-I', '--inputRecursive', metavar='FOLDERS/IMAGES', type=str, nargs='*',
                    default=[],
                    help='Input folders containing all images recursively.')

parser.add_argument('-p', '--pipeline', metavar='photogrammetry/panoramaHdr/panoramaFisheyeHdr/MG_FILE', type=str, default='photogrammetry',
                    help='"photogrammetry" pipeline, "panotamaHdr" pipeline, "panotamaFisheyeHdr" pipeline or a Meshroom file containing a custom pipeline to run on input images. '
                         'Requirements: the graph must contain one CameraInit node, '
                         'and one Publish node if --output is set.')

parser.add_argument('--overrides', metavar='SETTINGS', type=str, default=None,
                    help='A JSON file containing the graph parameters override.')

parser.add_argument('--paramOverrides', metavar='NODETYPE:param=value NODEINSTANCE.param=value', type=str, default=None, nargs='*',
                    help='Override specific parameters directly from the command line (by node type or by node names).')

parser.add_argument('-o', '--output', metavar='FOLDER', type=str, required=False,
                    help='Output folder where results should be copied to. '
                         'If not set, results will have to be retrieved directly from the cache folder.')

parser.add_argument('--cache', metavar='FOLDER', type=str,
                    default=None,
                    help='Custom cache folder to write computation results. '
                         'If not set, the default cache folder will be used: ' + meshroom.core.defaultCacheFolder)

parser.add_argument('--save', metavar='FILE', type=str, required=False,
                    help='Save the configured Meshroom graph to a project file. It will setup the cache folder accordingly if not explicitly changed by --cache.')

parser.add_argument('--compute', metavar='<yes/no>', type=lambda x: bool(distutils.util.strtobool(x)), default=True, required=False,
                    help='You can set it to <no/false/0> to disable the computation.')

parser.add_argument('--scale', type=int, default=-1,
                    choices=[-1, 1, 2, 4, 8, 16],
                    help='Downscale factor override for DepthMap estimation. '
                         'By default (-1): use pipeline default value.')

parser.add_argument('--toNode', metavar='NODE', type=str, nargs='*',
                    default=None,
                    help='Process the node(s) with its dependencies.')

parser.add_argument('--forceStatus', help='Force computation if status is RUNNING or SUBMITTED.',
                    action='store_true')
parser.add_argument('--forceCompute', help='Compute in all cases even if already computed.',
                    action='store_true')

parser.add_argument('--submit', help='Submit on renderfarm instead of local computation.',
                    action='store_true')
parser.add_argument('--submitter',
                    type=str,
                    default='SimpleFarm',
                    help='Execute job with a specific submitter.')

args = parser.parse_args()


def getOnlyNodeOfType(g, nodeType):
    """ Helper function to get a node of 'nodeType' in the graph 'g' and raise if no or multiple candidates. """
    nodes = g.nodesOfType(nodeType)
    if len(nodes) != 1:
        raise RuntimeError("meshroom_batch requires a pipeline graph with exactly one '{}' node, {} found."
                           .format(nodeType, len(nodes)))
    return nodes[0]


if not args.input and not args.inputRecursive:
    print('Nothing to compute. You need to set --input or --inputRecursive.')
    sys.exit(1)

views, intrinsics = [], []
# Build image files list from inputImages arguments
filesByType = multiview.FilesByType()

hasSearchedForImages = False

if args.input:
    if len(args.input) == 1 and os.path.isfile(args.input[0]) and os.path.splitext(args.input[0])[-1] in ('.json', '.sfm'):
        # args.input is a sfmData file: setup pre-calibrated views and intrinsics
        from meshroom.nodes.aliceVision.CameraInit import readSfMData
        views, intrinsics = readSfMData(args.input[0])
    else:
        filesByType.extend(multiview.findFilesByTypeInFolder(args.input, recursive=False))
        hasSearchedForImages = True

if args.inputRecursive:
    filesByType.extend(multiview.findFilesByTypeInFolder(args.inputRecursive, recursive=True))
    hasSearchedForImages = True

if hasSearchedForImages and not filesByType.images:
    print("No image found")
    exit(-1)

graph = multiview.Graph(name=args.pipeline)

with multiview.GraphModification(graph):
    # initialize photogrammetry pipeline
    if args.pipeline.lower() == "photogrammetry":
        # default photogrammetry pipeline
        multiview.photogrammetry(inputViewpoints=views, inputIntrinsics=intrinsics, output=args.output, graph=graph)
    elif args.pipeline.lower() == "panoramahdr":
        # default panorama Hdr pipeline
        multiview.panoramaHdr(inputViewpoints=views, inputIntrinsics=intrinsics, output=args.output, graph=graph)
    elif args.pipeline.lower() == "panoramafisheyehdr":
        # default panorama Fisheye Hdr pipeline
        multiview.panoramaFisheyeHdr(inputViewpoints=views, inputIntrinsics=intrinsics, output=args.output, graph=graph)
    else:
        # custom pipeline
        graph.load(args.pipeline)
        # graph.update()

    cameraInit = getOnlyNodeOfType(graph, 'CameraInit')
    # reset graph inputs
    cameraInit.viewpoints.resetValue()
    cameraInit.intrinsics.resetValue()
    # add views and intrinsics (if any) read from args.input
    cameraInit.viewpoints.extend(views)
    cameraInit.intrinsics.extend(intrinsics)

    if not graph.canComputeLeaves:
        raise RuntimeError("Graph cannot be computed. Check for compatibility issues.")

    if args.output:
        publish = getOnlyNodeOfType(graph, 'Publish')
        publish.output.value = args.output

    if filesByType.images:
        views, intrinsics = cameraInit.nodeDesc.buildIntrinsics(cameraInit, filesByType.images)
        cameraInit.viewpoints.value = views
        cameraInit.intrinsics.value = intrinsics

    if args.overrides:
        import io
        import json
        with io.open(args.overrides, 'r', encoding='utf-8', errors='ignore') as f:
            data = json.load(f)
            for nodeName, overrides in data.items():
                for attrName, value in overrides.items():
                    graph.findNode(nodeName).attribute(attrName).value = value

    if args.paramOverrides:
        print("\n")
        import re
        reExtract = re.compile('(\w+)([:.])(\w+)=(.*)')
        for p in args.paramOverrides:
            result = reExtract.match(p)
            if not result:
                raise ValueError('Invalid param override: ' + str(p))
            node, t, param, value = result.groups()
            if t == ':':
                nodesOfType = graph.nodesOfType(node)
                if not nodesOfType:
                    raise ValueError('No node with the type "{}" in the scene.'.format(node))
                for n in nodesOfType:
                    print('Overrides {node}.{param}={value}'.format(node=node, param=param, value=value))
                    n.attribute(param).value = value
            elif t == '.':
                print('Overrides {node}.{param}={value}'.format(node=node, param=param, value=value))
                graph.findNode(node).attribute(param).value = value
            else:
                raise ValueError('Invalid param override: ' + str(p))
        print("\n")

    # setup DepthMap downscaling
    if args.scale > 0:
        for node in graph.nodesOfType('DepthMap'):
            node.downscale.value = args.scale

    # setup cache directory
    graph.cacheDir = args.cache if args.cache else meshroom.core.defaultCacheFolder

    if args.save:
        graph.save(args.save, setupProjectFile=not bool(args.cache))
        print('File successfully saved: "{}"'.format(args.save))

if not args.output:
    print('No output set, results will be available in the cache folder: "{}"'.format(graph.cacheDir))

# find end nodes (None will compute all graph)
toNodes = graph.findNodes(args.toNode) if args.toNode else None

if args.submit:
    if not args.save:
        raise ValueError('Need to save the project to file to submit on renderfarm.')
    # submit on renderfarm
    taskManager = meshroom.core.taskManager.TaskManager()
    taskManager.submitFromFile(args.save, args.submitter, toNode=toNodes)
elif args.compute:
    # start computation
    taskManager = meshroom.core.taskManager.TaskManager()
    taskManager.compute(graph, toNodes=toNodes, forceCompute=args.forceCompute, forceStatus=args.forceStatus)
