################################################################################
#
# Copyright (C) 2016-2022 Advanced Micro Devices, Inc. All rights reserved.
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in
# all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
# SOFTWARE.
#
################################################################################

from . import Common
from . import ClientWriter
from . import LibraryIO
from .Contractions import ProblemType as ContractionsProblemType
from .SolutionStructs import ProblemSizes, ProblemType
from .Common import tPrint, printExit, printWarning, assignGlobalParameters, \
        restoreDefaultGlobalParameters, HR
from .Tensile import addCommonArguments, argUpdatedGlobalParameters
from . import __version__

import argparse
import os
import sys


def getGlobalParams(config):
    """Try to parse out global parameters from the places it could be"""
    globalParams = None
    try:
        globalParams = config["GlobalParameters"]
    except (TypeError, LookupError):
        pass

    return globalParams


def getProblemDict(config):
    """Try to parse out a problem type dict from the places it could be"""
    problemDict = None
    try:  # Tensile Config file (first entry)
        problemDict = config["BenchmarkProblems"][0][0]
        if len(config["BenchmarkProblems"]) > 1:
            printWarning("More than one BenchmarkProblem in config file: only using first")
    except (TypeError, LookupError):
        pass
    else:
        return problemDict

    try:  # Solution Selection "base" file
        problemDict = config["ProblemType"]
    except (TypeError, LookupError):
        pass

    return problemDict


def getSizeList(config):
    """
    Try to parse out a size list from the places it could be
    (including the whole config being the list)
    """
    sizeList = None
    try:  # Tensile config file (first entry)
        sizeList = config["BenchmarkProblems"][0][1]["BenchmarkFinalParameters"][0]["ProblemSizes"]
    except (TypeError, LookupError):
        pass
    else:
        return sizeList

    # check if whole config looks like a size list
    keep = True
    if type(config) is list:
        for i in config:
            if not (type(i) is dict and ("Exact" in i or "Range" in i)):
                keep = False
                break
    else:
        keep = False

    if keep:
        sizeList = config
    return sizeList


def parseConfig(config):
    """Parse out all data we can get from the config"""
    globalParams = getGlobalParams(config)
    problemDict = getProblemDict(config)
    sizeList = getSizeList(config)

    return (globalParams, problemDict, sizeList)


def TensileClientConfig(userArgs):
    tPrint(1, "")
    tPrint(1, HR)
    tPrint(1, "#")
    tPrint(1, "#  Tensile Client Config v{}".format(__version__))
    tPrint(1, "#")
    tPrint(1, HR)
    tPrint(1, "")

    # argument parsing
    # yapf: disable
    argParser = argparse.ArgumentParser()
    argParser.add_argument("ConfigYaml", type=os.path.realpath, nargs="+",
            help="Config yaml(s) containing parameters and problem and size information")
    argParser.add_argument("--output-config", "-o", dest="OutputConfig", type=os.path.realpath,
            required=True, help="Path to output resulting client config file")
    argParser.add_argument("--merge-sizes", dest="MergeSizes", action="store_true",
            help="Allow sizes from multiple config files")
    # yapf: enable

    addCommonArguments(argParser)
    args = argParser.parse_args(userArgs)

    # loop through all configs and parse out all the data we can
    configs = [LibraryIO.readYAML(x) for x in args.ConfigYaml]

    globalParams = {}
    problemDict = None
    sizeList = None

    for config in configs:
        (myGlobalParams, myProblemDict, mySizeList) = parseConfig(config)

        # if we got data from the config, keep it
        # if we already had that data, warn and exit
        if myGlobalParams is not None:
            if globalParams == {} or globalParams == myGlobalParams:
                globalParams = myGlobalParams
            else:
                printExit("Multiple definitions for GlobalParameters found:\n{}\nand\n{}".format(
                    globalParams, myGlobalParams))

        if myProblemDict is not None:
            if problemDict is None or problemDict == myProblemDict:
                problemDict = myProblemDict
            else:
                printExit("Multiple definitions for ProblemType found:\n{}\nand\n{}".format(
                    problemDict, myProblemDict))

        if mySizeList is not None:
            if sizeList is None or sizeList == mySizeList:
                sizeList = mySizeList
            elif args.MergeSizes:
                sizeList += mySizeList
            else:
                printExit("Multiple size lists found:\n{}\nand\n{}\n"
                          "Run with --merge-sizes to keep all size lists found".format(
                              sizeList, mySizeList))

    if problemDict is None:
        printExit("No ProblemType found; cannot produce output")
    if sizeList is None:
        printExit("No SizeList found; cannot produce output")

    ssProblemType = ProblemType(problemDict)
    conProblemType = ContractionsProblemType.FromOriginalState(ssProblemType)
    sizes = ProblemSizes(ssProblemType, sizeList)  # TODO doesn't seem to work for range sizes

    # update globals
    restoreDefaultGlobalParameters()
    assignGlobalParameters(globalParams)

    overrideParameters = argUpdatedGlobalParameters(args)
    for key, value in overrideParameters.items():
        tPrint(1, "Overriding {0}={1}".format(key, value))
        Common.globalParameters[key] = value

    # write output
    ClientWriter.writeClientConfigIni(sizes, conProblemType, "", [], "", args.OutputConfig, None)


def main():
    TensileClientConfig(sys.argv[1:])
