################################################################################
#
# Copyright (C) 2016-2024 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 .Common import architectureMap, gfxArch, printExit, printWarning, versionIsCompatible
from .SolutionStructs import Solution, ProblemSizes, ProblemType
from . import __version__
from . import SolutionLibrary
from .Utilities.ConditionalImports import yamlLoader, yamlDumper

from typing import NamedTuple, Optional

import os

try:
    import yaml
except ImportError:
    printExit(
        "You must install PyYAML to use Tensile (to parse config files). See http://pyyaml.org/wiki/PyYAML for installation instructions."
    )

try:
    import msgpack
except ImportError:
    print("Message pack python library not detected. Must use YAML backend instead.")


def updateProblemDatatypes(problemType):
    problemType["DataType"] = problemType["DataType"].value
    problemType["DestDataType"] = problemType["DestDataType"].value
    problemType["ComputeDataType"] = problemType["ComputeDataType"].value
    problemType["F32XdlMathOp"] = problemType["F32XdlMathOp"].value


###################
# Writing functions
###################
def write(filename_noExt, data, format="yaml"):
    """Writes data to file with specified format; extension is appended based on format."""
    if format == "yaml":
        writeYAML(filename_noExt + ".yaml", data)
    elif format == "msgpack":
        writeMsgPack(filename_noExt + ".dat", data)
    else:
        printExit("Unrecognized format {}".format(format))


def writeYAML(filename, data, **kwargs):
    """Writes data to file in YAML format."""
    # set default kwags for yaml dump
    if "explicit_start" not in kwargs:
        kwargs["explicit_start"] = True
    if "explicit_end" not in kwargs:
        kwargs["explicit_end"] = True
    if "default_flow_style" not in kwargs:
        kwargs["default_flow_style"] = None

    with open(filename, "w") as f:
        yaml.dump(data, f, yamlDumper, **kwargs)


def writeMsgPack(filename, data):
    """Writes data to file in Message Pack format."""
    with open(filename, "wb") as f:
        msgpack.pack(data, f)


def writeSolutions(filename, problemSizes, solutions, cache=False):
    """Writes solution YAML file."""

    # convert objects to nested dictionaries
    solutionStates = []

    if cache:
        solYaml = readYAML(filename)
        solutionStates = solYaml[2:]
    else:
        for solution in solutions:
            solutionState = solution.getAttributes()
            solutionState["ProblemType"] = solutionState["ProblemType"].state
            updateProblemDatatypes(solutionState["ProblemType"])
            solutionStates.append(solutionState)
    # write dictionaries
    with open(filename, "w") as f:
        f.write("- MinimumRequiredVersion: {}\n".format(__version__))
        f.write("- ProblemSizes:\n")
        if problemSizes:
            for sizeRange in problemSizes.ranges:
                f.write("  - Range: {}\n".format(sizeRange))
            for problemExact in problemSizes.exacts:
                #FIXME-problem, this ignores strides:
                f.write("  - Exact: {}\n".format(problemExact))

        yaml.dump(solutionStates, f, yamlDumper, default_flow_style=None)


###############################
# Reading and parsing functions
###############################
def readYAML(filename):
    """Reads and returns YAML data from file."""
    with open(filename, "r") as f:
        data = yaml.load(f, yamlLoader)
    return data


def parseSolutionsFile(filename):
    """Wrapper function to read and parse a solutions file."""
    return parseSolutionsData(readYAML(filename), filename)


def parseSolutionsData(data, srcFile="?"):
    """Parses problem sizes and solutions from the data of a solutions file."""
    if len(data) < 3:
        printExit("Solution file {} is missing required fields (len = {} < 3" \
                .format(srcFile, len(data)))

    versionString = data[0]["MinimumRequiredVersion"]
    if not versionIsCompatible(versionString):
        printWarning("Version = {} in solution file {} does not match Tensile version = {}" \
                .format(srcFile, versionString, __version__) )

    if "ProblemSizes" not in data[1]:
        printExit("Solution file {} doesn't begin with ProblemSizes".format(srcFile))

    problemSizesConfig = data[1]["ProblemSizes"]

    solutions = []
    for i in range(2, len(data)):
        solutionState = data[i]
        # force redo the deriving of parameters, make sure old version logic yamls can be validated
        solutionState["AssignedProblemIndependentDerivedParameters"] = False
        solutionState["AssignedDerivedParameters"] = False
        solutionObject = Solution(solutionState)
        solutions.append(solutionObject)
    problemType = solutions[0]["ProblemType"]
    problemSizes = ProblemSizes(problemType, problemSizesConfig)
    return (problemSizes, solutions)


class LibraryLogic(NamedTuple):
    """Return tuple for parseLibraryLogicData()"""
    schedule: str
    architecture: str
    problemType: ProblemType
    solutions: list
    exactLogic: list
    library: SolutionLibrary.MasterSolutionLibrary


def parseLibraryLogicFile(filename):
    """Wrapper function to read and parse a library logic file."""
    return parseLibraryLogicData(readYAML(filename), filename)


def parseLibraryLogicData(data, srcFile="?"):
    """Parses the data of a library logic file."""
    if type(data) is list:
        data = parseLibraryLogicList(data, srcFile)

    if "CUCount" not in data:
        data["CUCount"] = None

    if "IsAPU" not in data:
        data["IsAPU"] = None

    if "Fp16AltImpl" not in data:
        data["Fp16AltImpl"] = False

    if "Fp16AltImplRound" not in data:
        data["Fp16AltImplRound"] = False

    if "ArchitectureName" not in data:
        raise ValueError("'ArchitectureName' not found in logic file data" \
            f" Review the library logic file {srcFile}.")
    elif data["ArchitectureName"] not in architectureMap:
        raise ValueError(f"{data['ArchitectureName']} is not a supported architecture." \
            f" Review the library logic file {srcFile}.")

    if not versionIsCompatible(data["MinimumRequiredVersion"]):
        printWarning("Version = {} in library logic file {} does not match Tensile version = {}" \
                .format(srcFile, data["MinimumRequiredVersion"], __version__) )

    # unpack problemType
    problemType = ProblemType(data["ProblemType"])
    # unpack solutions
    solutions = []
    for solutionState in data["Solutions"]:
        if solutionState["ProblemType"] == "derive":
            solutionState["ProblemType"] = problemType
        if solutionState["KernelLanguage"] == "Assembly":
            # Set solution ISA based on the top-level gfx architecture in the library logic file
            solutionState["ISA"] = gfxArch(data["ArchitectureName"])
        else:
            solutionState["ISA"] = (0, 0, 0)
        # force redo the deriving of parameters, make sure old version logic yamls can be validated
        solutionState["AssignedProblemIndependentDerivedParameters"] = False
        solutionState["AssignedDerivedParameters"] = False
        solutionObject = Solution(solutionState)

        if solutionObject["ProblemType"] != problemType:
            printExit("ProblemType in library logic file {} doesn't match solution: {} != {}" \
                    .format(srcFile, problemType, solutionObject["ProblemType"]))
        solutions.append(solutionObject)

    newLibrary = SolutionLibrary.MasterSolutionLibrary.FromOriginalState(data, solutions)

    return LibraryLogic(data["ScheduleName"], data["ArchitectureName"], problemType, solutions, \
            data.get("ExactLogic"), newLibrary)


def parseLibraryLogicList(data, srcFile="?"):
    """Parses the data of a matching table style library logic file."""
    if len(data) < 9:
        printExit("Library logic file {} is missing required fields (len = {} < 9)" \
                .format(srcFile, len(data)))

    rv = {}
    rv["MinimumRequiredVersion"] = data[0]["MinimumRequiredVersion"]
    rv["ScheduleName"] = data[1]
    rv["DeviceNames"] = data[3]
    rv["ProblemType"] = data[4]
    rv["Solutions"] = data[5]

    if type(data[2]) is dict:
        rv["ArchitectureName"] = data[2]["Architecture"]
        if "CUCount" in data[2]:
            rv["CUCount"] = data[2]["CUCount"]
        if "IsAPU" in data[2]:
            rv["IsAPU"] = data[2]["IsAPU"]
    else:
        rv["ArchitectureName"] = data[2]
        rv["CUCount"] = None
        rv["IsAPU"] = None

    if architectureMap[rv["ArchitectureName"]] != rv["ScheduleName"]:
        raise ValueError(f"Architecture mismatch: {rv['ArchitectureName']} does not match " \
            f" {rv['ScheduleName']}. Review the library logic file {srcFile}.")

    rv["ExactLogic"] = data[7]
    # data[8] previously contained range logic, which has been retired

    # optional fields
    if len(data) > 10 and data[10]:
        rv["PerfMetric"] = data[10]

    if len(data) > 11 and data[11] == "Fp16AltImpl":
        rv["Fp16AltImpl"] = True

    if len(data) > 13 and data[13] == "Fp16AltImplRound":
        rv["Fp16AltImplRound"] = True

    # library logic fields
    rv["LibraryType"] = "Matching"
    rv["Library"] = {}
    rv["Library"]["indexOrder"] = data[6]
    rv["Library"]["table"] = data[7]
    rv["Library"]["distance"] = "Euclidean"
    if len(data) > 12 and data[12]:
        rv["Library"]["distance"] = data[12]

    return rv


def rawLibraryLogic(data):
    """Returns a tuple of the data in a library logic file."""
    versionString = data[0]
    scheduleName = data[1]
    architectureName = data[2]
    deviceNames = data[3]
    problemTypeState = data[4]
    solutionStates = data[5]
    indexOrder = data[6]
    exactLogic = data[7]
    rangeLogic = data[8]
    otherFields = []

    dataLength = len(data)
    if dataLength > 9:
        for idx in range(9, dataLength):
            otherFields.append(data[idx])

    return (versionString, scheduleName, architectureName, deviceNames,\
            problemTypeState, solutionStates, indexOrder, exactLogic, rangeLogic, otherFields)


########################
# Library logic creation
########################
def createLibraryLogic(schedulePrefix, architectureName, deviceNames, logicTuple, dictFormat):
    if not dictFormat:
        return createLibraryLogicList(schedulePrefix, architectureName, deviceNames, logicTuple)

    rv = {}
    rv["MinimumRequiredVersion"] = __version__
    rv["ScheduleName"] = schedulePrefix
    rv["DeviceNames"] = deviceNames
    rawProblemType = logicTuple[0]
    rawSolutions = logicTuple[1]

    # architecture
    if type(architectureName) is dict:
        rv["ArchitectureName"] = architectureName["Architecture"]
        if "CUCount" in architectureName:
            rv["CUCount"] = architectureName["CUCount"]
        if "IsAPU" in architectureName:
            rv["IsAPU"] = architectureName["IsAPU"]
    else:
        rv["ArchitectureName"] = architectureName

    # problem type
    problemTypeState = rawProblemType.state
    updateProblemDatatypes(problemTypeState)
    rv["ProblemType"] = problemTypeState

    # solutions
    solutionList = []
    for solution in rawSolutions:
        solutionState = solution.getAttributes()
        solutionState["ProblemType"] = "derive"
        solutionList.append(solutionState)

    rv["Solutions"] = solutionList

    # library logic
    exactLogic = logicTuple[3]
    exactLogicList = []
    for key in exactLogic:
        exactLogicList.append([list(key), exactLogic[key]])

    rv["LibraryType"] = "Matching"
    rv["Library"] = {}
    rv["Library"]["indexOrder"] = logicTuple[2]
    rv["Library"]["table"] = exactLogicList
    rv["Library"]["distance"] = "Euclidean"
    rv["PerfMetric"] = logicTuple[7]

    return rv


def createLibraryLogicList(schedulePrefix, architectureName, deviceNames, logicTuple):
    """Creates the data for a library logic file suitable for writing to YAML."""
    problemType = logicTuple[0]
    solutions = logicTuple[1]
    indexOrder = logicTuple[2]
    exactLogic = logicTuple[3]
    rangeLogic = logicTuple[4]

    tileSelection = False
    if len(logicTuple) > 5 and logicTuple[5]:
        tileSelection = True

    data = []
    # Tensile version
    data.append({"MinimumRequiredVersion": __version__})
    # schedule name
    data.append(schedulePrefix)  # change from Tensile to vega10
    data.append(architectureName)
    # schedule device names
    data.append(deviceNames)

    # problem type
    problemTypeState = problemType.state
    updateProblemDatatypes(problemTypeState)
    data.append(problemTypeState)

    # solutions
    solutionList = []
    for solution in solutions:
        solutionState = solution.getAttributes()
        solutionState["ProblemType"] = solutionState["ProblemType"].state
        updateProblemDatatypes(solutionState["ProblemType"])
        solutionList.append(solutionState)

    if tileSelection:
        tileSolutions = logicTuple[5]
        for solution in tileSolutions:
            solutionState = solution.getAttributes()
            solutionState["ProblemType"] = solutionState["ProblemType"].state
            updateProblemDatatypes(solutionState["ProblemType"])
            solutionList.append(solutionState)

    data.append(solutionList)
    # index order
    data.append(indexOrder)

    # exactLogic
    exactLogicList = []
    for key in exactLogic:
        exactLogicList.append([list(key), exactLogic[key]])
    data.append(exactLogicList)

    # rangeLogic
    data.append(rangeLogic)

    if tileSelection:
        tileSelectionLogic = {}
        tileSelectionIndices = logicTuple[6]
        tileSelectionLogic["TileSelectionIndices"] = tileSelectionIndices
        data.append(tileSelectionLogic)
    else:
        data.append(None)

    data.append(logicTuple[7])
    return data


def initAsmCapsCache(cacheFile: str) -> Optional[dict]:
    """
    If requested and exists, reads an assembler capabilities cache from disk.

    Arguments:
      cacheFile: Cache file (YAML format).

    Returns:
      newcache:  If cachFile is empty, None. Otherwise, if file exists, a dictionary containing
                 capabilities cache; if not, an empty dict
    """
    # if cacheFile str is empty, do not use cache
    if not cacheFile:
        return None

    cacheExists = os.path.exists(cacheFile)
    if not cacheExists:
        return {}

    cache  = readYAML(cacheFile)

    # gaurd against inconsistent state
    if not cache:
        return None

    toTuple = lambda s: tuple(int(i.strip()) for i in s.split(","))
    newcache = {toTuple(k): v for k,v in cache.items()}
    return newcache

def writeAsmCapsCache(cacheFile: str, data: dict):
    """
    Writes an asm cache to disk.

    Arguments:
      cacheFile: User spedified file to write the cache.
      data:      dict containg the computed cache.
    """
    newdata = {", ".join(map(str, k)): v for k, v in data.items()}
    writeYAML(cacheFile, newdata)
