"""
All or portions of this file Copyright (c) Amazon.com, Inc. or its affiliates or
its licensors.

For complete copyright and license terms please see the LICENSE at the root of this
distribution (the "License"). All use of this software is governed by the License,
or, if provided, by the license below or the license accompanying this file. Do not
remove or modify any license notices. This file is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.



Lumberyard 1.8 Independent Texture Tiling Conversion Script


Why do I need to run this script?
================================================
In Lumberyard 1.8, the Independent Texture Tiling feature allows the
Second Diffuse Map, Emittance Map or Detail Maps to have their own
independent UV modulator for any Tiling, Rotation or Oscillation values
applied to them. Before 1.8, those values were ignored and a material
used the values applied in the main Diffuse Map's UV modulator. This script
will copy over values from the main Diffuse Map's UV modulator into the
Second Diffuse, Emittance and Detail Maps, ensuring those modulators
look and operate the same way they did in Lumberyard 1.7 or earlier.
Going forward, any changes made to the Tiling, Rotation or Oscillation
values in those map channels will be properly saved into the material
file and operate independent of the values saved in the main Diffuse Map.


Do I need to run this script?
================================================
If you have authored any material assets that have modified the
Tiling, Rotation or Oscillation values within the Diffuse Map channel
then you will need to run this script. If you have never made changes
to a material's Diffuse Map's Tiling, Rotation or Oscillation, than
you do not need to run this script. Because this script will only
convert materials that actually need it, it's better to err on the
side of caution and run this anyways.


How do I run this script from a command line?
================================================
1) Save this script into your build's scripts folder:
    - \dev\Editor\Scripts\
2) Open a command prompt and navigate to your build's python folder:
    - \dev\Tools\Python\2.7.11\windows
3) Type in the following and press enter:
    - python <your_build_path>\dev\Editor\Scripts\1.8_IndependentTilingConvertor.py
4) Optionally, if you want to run the script on a single project instead
of every project in your build, type in the following and press enter:
    - python <your_build_path>\dev\Editor\Scripts\1.8_IndependentTilingConvertor.py "my_poject_name"
5) Optionally, you can run this script on multiple projects using comma seperate project names:
    - python <your_build_path>\dev\Editor\Scripts\1.8_IndependentTilingConvertor.py "my_poject_name1,my_poject_name2,my_poject_name3"


How do I run this script from within Lumberyard?
================================================
1) Save this script into your build's scripts folder:
    - \dev\Editor\Scripts\
2) Open the Python Scripts tool:
    - Tools --> Other --> Python Scripts
3) Locate the 1.8_IndependentTilingConvertor.py script in the list and highlight it
4) Click on the "Execute" button in the lower right of the window.


As the script runs, it will validate each material to determine if it
needs to be converted. If so, it will convert the material and make
note of the material's filename in a log file (log_filename.log).
This log file will prevent the material from being converted again if
the script is ever run a second time. This script should only ever have
to be run once per project. Running the script without the optional
project name (Step #4) will convert all materials in all projects
within your Lumberyard build.
"""



IN_LUMBERYARD = False
CONVERTED_LOG_NAME = "1.8_IndependentTilingConvertedMats.log"
READ_ONLY_LOG_NAME = "1.8_IndependentTilingReadOnlyMats.log"
BUILD_PATH = None

# First determine if this is being executed from within the Lumberyard embedded python environment.
# This will determine how output is sent to the user; (command line prints or message boxes)

try:
    import winreg as winreg
except:
    import _winreg as winreg

def getLyInstallPath( ):
    aReg = winreg.ConnectRegistry( None, winreg.HKEY_CURRENT_USER )
    aKey = winreg.OpenKey( aReg, "SOFTWARE\\Amazon\\Lumberyard\\Settings" )

    for i in range( 1024 ):
        try:
            keyName, keyValue, last = winreg.EnumValue( aKey, i )
            if keyName == "ENG_RootPath":
                return keyValue
        except EnvironmentError:
            return None
    return None

try:
    import general
    BUILD_PATH = os.path.dirname(general.get_game_folder())
    IN_LUMBERYARD = True
except:
    IN_LUMBERYARD = False
    BUILD_PATH = getLyInstallPath()

# Normal imports
import os
import sys
import string
import xml.etree.ElementTree
import copy
import time


# This is a list of all TexMod parameters that need to be copied from the main Diffuse Map channel
# into the Second Diffuse, Emittance or Detail.
VALID_MODS = [
    "TexMod_RotateType",
    "TexMod_TexGenType",
    "RotateU",
    "TexMod_URotateCenter",
    "TileU",
    "OffsetU",
    "TexMod_UOscillatorType",
    "TexMod_UOscillatorRate",
    "TexMod_UOscillatorPhase",
    "TexMod_UOscillatorAmplitude",
    "RotateV",
    "TexMod_VRotateCenter",
    "TileV",
    "OffsetV",
    "TexMod_VOscillatorType",
    "TexMod_VOscillatorRate",
    "TexMod_VOscillatorPhase",
    "TexMod_VOscillatorAmplitude",
    "RotateW",
    "TexMod_WRotateRate",
    "TexMod_WRotatePhase",
    "TexMod_WRotateAmplitude",
    "IsTileU",
    "IsTileV" ]



class Material_File(object):
    """
    Class to perform any read, write or conversion operations on material (*.mtl) files.
    """
    def __init__(self, filename):
        self.filename = filename
        self.xml = None

        # These are the important elements in the mtl's xml that need to be modified
        self.diffuseTexModElement = []
        self.secondDiffuseElement = []
        self.detailElement = []
        self.emittanceElement = []
        self.diffuse_tileU = []
        self.diffuse_tileV = []
        self.subMats = []

        self.parse_xml()

    def is_valid_xml(self):
        """
        Performs a simple check to determine if the XML of the mtl is valid.
        This is to prevent an assert on a material conversion operation and
        preventing the script from finishing.

        It's possible for a material's xml to be malformed, which is why this check is needed.
        """
        try:
            if isinstance(self.xml.getroot(), xml.etree.ElementTree.Element):
                return True
            else:
                return False
        except:
            return False

    def parse_xml(self):
        """
        Open and parse the material's xml, storing it for access later.
        This will ensure only one disk seek is performed.
        """
        if os.path.exists(self.filename):
            try:
                self.xml = xml.etree.ElementTree.parse(self.filename)
                self.gather_submats()
                self.gather_elements()
            except:
                self.xml = None

    def gather_submats(self):
        """
        Returns a list of sub-materials if the material is a multi-material,
        a list containing the root if it is a single material,
        or an empty list if it is not valid xml.
        """
        if self.is_valid_xml():
            root = self.xml.getroot()
            if root.tag == "Material":
                for child in root:
                    if child.tag == "SubMaterials":
                        self.subMats = child.getchildren()
                        break

            if self.subMats == []:
                self.subMats = [root]

    def gather_elements(self):
        """
        Once the xml has been parsed, mine through it to find all of the
        neccessary elements that need to be modified.
        """

        if self.is_valid_xml():
            subMatId = -1
            for mat in self.subMats:
                subMatId += 1
                self.diffuseTexModElement.insert(subMatId, None)
                self.secondDiffuseElement.insert(subMatId, None)
                self.detailElement.insert(subMatId, None)
                self.emittanceElement.insert(subMatId, None)
                self.diffuse_tileU.insert(subMatId, None)
                self.diffuse_tileV.insert(subMatId, None)
                if mat.tag == "Material":
                    for child in mat.getchildren():
                        if child.tag == "Textures":
                            for tex in child.getchildren():
                                if tex.tag == "Texture":
                                    keys = tex.keys()
                                    if "Map" in keys:
                                        if tex.get("Map") == "Diffuse":
                                            for TexMod in tex.getchildren():
                                                if TexMod.tag == "TexMod":
                                                    for texModKey in TexMod.keys():
                                                        if texModKey in VALID_MODS:
                                                            self.diffuseTexModElement[subMatId] = TexMod
                                                        if texModKey == "TileU":
                                                            self.diffuse_tileU[subMatId] = float(TexMod.get(texModKey))
                                                        if texModKey == "TileV":
                                                            self.diffuse_tileV[subMatId] = float(TexMod.get(texModKey))
                                        if tex.get("Map") == "Custom":
                                            self.secondDiffuseElement[subMatId] = tex
                                        if tex.get("Map") == "Emittance":
                                            self.emittanceElement[subMatId] = tex
                                        if tex.get("Map") == "Detail":
                                            self.detailElement[subMatId] = tex

    def needs_conversion(self):
        """
        Determines if this material file needs to be converted by checking if
        the gather_elements method found the main diffuse map's TexMod element
        and at least one of the following other map channels:

        Second Diffuse, Emittance or Detail
        """
        subMatId = -1
        for mat in self.subMats:
            subMatId = subMatId + 1
            if isinstance(self.diffuseTexModElement[subMatId], xml.etree.ElementTree.Element):
                if isinstance(self.secondDiffuseElement[subMatId], xml.etree.ElementTree.Element):
                    return True
                if isinstance(self.emittanceElement[subMatId], xml.etree.ElementTree.Element):
                    return True
                if isinstance(self.detailElement[subMatId], xml.etree.ElementTree.Element):
                    return True
        return False

    def can_write(self):
        """
        Checks to make sure the mtl file is writable.
        This is to prevent the script from asseting during a
        save attempt and preventing the script from finishing.
        """
        if os.access(self.filename, os.W_OK):
            return True
        else:
            return False

    def convert(self):
        """
        Performs the actual modification of the xml data, copying the main Diffuse Map's
        TexMod element into any of the other 3 map channel's element.

        For the Second Diffuse & the Emittance maps, the process is:
        1) Remove the TexMod element if one exists.
        2) Copy over the TexMod element from the main diffuse into this texture.
        3) Remove any TexMod parameters that are not in the VALID_MODS list.

        For the Detail map, the process is slightly different:
        1) If the Detail map has a TexMod element, copy the TileU & TileV values from it for use later.
        1) Remove the TexMod element if one exists.
        2) Copy over the TexMod element from the main diffuse into this texture.
        3) Remove any TexMod parameters that are not in the VALID_MODS list.
        5) Multiply the TileU and TileV values from the copied TexMod, by the TileU & TileV
           values that were saved from the Detail map's original TexMod element.
        """
        subMatId = -1
        for mat in self.subMats:
            subMatId = subMatId + 1
            if isinstance(self.diffuseTexModElement[subMatId], xml.etree.ElementTree.Element):
                # Convert the second diffuse
                #=============================
                if isinstance(self.secondDiffuseElement[subMatId], xml.etree.ElementTree.Element):
                    # First remove the TexMod from the second diffuse if it exists
                    for TexMod in reversed(self.secondDiffuseElement[subMatId].getchildren()):
                        if TexMod.tag == "TexMod":
                            self.secondDiffuseElement[subMatId].remove(TexMod)
                            break

                    # Now copy the main diffuse into the second diffuse
                    self.secondDiffuseElement[subMatId].append(copy.deepcopy(self.diffuseTexModElement[subMatId]))

                    # Remove any non-valid keys from the copied TexMod
                    for TexMod in self.secondDiffuseElement[subMatId].getchildren():
                        if TexMod.tag == "TexMod":
                            for key in reversed(TexMod.keys()):
                                if not key in VALID_MODS:
                                    TexMod.attrib.pop(key)

                # Convert the emittance
                #=============================
                if isinstance(self.emittanceElement[subMatId], xml.etree.ElementTree.Element):
                   # First remove the TexMod from the emittance if it exists
                    for TexMod in reversed(self.emittanceElement[subMatId].getchildren()):
                        if TexMod.tag == "TexMod":
                            self.emittanceElement[subMatId].remove(TexMod)
                            break

                    # Now copy the main diffuse into the second diffuse
                    self.emittanceElement[subMatId].append(copy.deepcopy(self.diffuseTexModElement[subMatId]))

                    # Remove any non-valid keys from the copied TexMod
                    for TexMod in self.emittanceElement[subMatId].getchildren():
                        if TexMod.tag == "TexMod":
                            for key in reversed(TexMod.keys()):
                                if not key in VALID_MODS:
                                    TexMod.attrib.pop(key)

                # Convert the detail
                #=============================
                if isinstance(self.detailElement[subMatId], xml.etree.ElementTree.Element):
                   # Find the Detail Map's TexMod, if one exists, and save out its current TileU & TileV values
                    tileU = None
                    tileV = None
                    for TexMod in self.detailElement[subMatId].getchildren():
                        if TexMod.tag == "TexMod":
                            if TexMod.get("TileU"):
                                tileU = float(TexMod.get("TileU"))
                            if TexMod.get("TileV"):
                                tileV = float(TexMod.get("TileV"))

                    # Next, remove the TexMod from the detail if it exists
                    for TexMod in reversed(self.detailElement[subMatId].getchildren()):
                        if TexMod.tag == "TexMod":
                            self.detailElement[subMatId].remove(TexMod)
                            break

                    # Now copy the main diffuse into the second diffuse
                    self.detailElement[subMatId].append(copy.deepcopy(self.diffuseTexModElement[subMatId]))

                    # Remove any non-valid keys from the copied TexMod
                    for TexMod in self.detailElement[subMatId].getchildren():
                        if TexMod.tag == "TexMod":
                            for key in reversed(TexMod.keys()):
                                if not key in VALID_MODS:
                                    TexMod.attrib.pop(key)

                    # Finally, write over the TileU and TileV values
                    for TexMod in self.detailElement[subMatId].getchildren():
                        if TexMod.tag == "TexMod":
                            if TexMod.get("TileU") and tileU:
                                TexMod.set("TileU", str((float(TexMod.get("TileU")) * tileU)))
                            elif tileU:
                                TexMod.set("TileU", str(tileU))
                            if TexMod.get("TileV") and tileV:
                                TexMod.set("TileV", str((float(TexMod.get("TileV")) * tileV)))
                            elif tileV:
                                TexMod.set("TileV", str(tileV))

            self.xml.write(self.filename)


        return False



class Log_File(object):
    """
    Simple class to create, open & save a log file containing a list of filenames.
    """
    def __init__(self, filename, include_previous = True):
        self.filename = filename
        self.logFile = None
        self.include_previous = include_previous
        self.files = []
        self.create_log_file()

    def create_log_file(self):
        """
        Will either generate a new log file or open the existing log file and read its contents
        """
        if os.path.exists(self.filename):
            self.logFile = open(self.filename, 'r+')
            if self.include_previous:
                for fname in self.logFile.readlines():
                    self.add_file(fname)
        else:
            self.logFile = open(self.filename, 'w')

    def save(self):
        """
        Saves the log file back to disk.

        To ensure that filenames are not added twice, this method first saves an empty
        log to disc. Then saves it again with the list of filenames (self.files)
        stored in this class.
        """
        # First clear the log file's contents so they can be written back in
        self.logFile.close()
        open(self.filename, 'w').close()
        self.logFile = open(self.filename, 'w')

        for fname in self.files:
            self.logFile.write("{0}\n".format(fname))

        self.logFile.close()

    def get_files(self):
        return self.files

    def add_file(self, filename):
        """
        Adds a filename to the self.files list so it
        can be saved out to disc later.
        """
        fname = string.rstrip(filename.lower())
        if not fname in self.files:
            self.files.append(fname)

    def has_file(self, filename):
        """
        Checks to see if a specific material filename already exists in the list
        of converted maerials.
        """
        fname = string.rstrip(filename.lower())
        if fname in self.files:
            return True
        return False



###############################################################################
def main():
    '''sys.__name__ wrapper function'''

    msgStr = "This tool will scan all of your project's material files (*.mtl) and transfer any\n\
Tiling, Rotation or Oscillation values from the main Diffuse Map into the\n\
Second Diffuse or Emittance Map if present.\n\n\
Additionally, if any materials are using a Detail Map, the main Diffuse Map's\n\
Tiling values will be applied to the Detail Map's tiling values.\n\n\
This tool will maintain a log of all materials converted and if run again, will\n\
skip over any materials that have already been converted. It is recomended\n\
that you read the Lumberyard Version 1.8 Migration Notes for more\n\
information regarding the need for this tool. The log file can be found in:\n\n\
{0}\\{1}\n\n\
It is recomended that you first save a backup of your materials if you're\n\
not already using any form of source control.\n\n\
Would you like to continue with this batch conversion?".format(BUILD_PATH, CONVERTED_LOG_NAME)

    can_run = False
    projectFolders = []
    individual_projects = []

    if IN_LUMBERYARD:
        if general.message_box_yes_no("{0} This will convert all\nmaterials found in all projects.".format(msgStr)):
            can_run = True
    else:
        if len(sys.argv) > 1:
            individual_projects = sys.argv[1].split(',')

        # Contruct the message string based on any optional project names passed
        projectStr = ""
        if individual_projects == []:
            projectStr = "This will convert all\nmaterials found in all projects.\n"
        else:
            projectStr = "This will convert all\nmaterials in the following projects:\n\n"
            for proj in individual_projects:
                projectStr += "{0}\n".format(proj)

        # Prompt the user for an answer
        var = raw_input("{0} {1} \n\nY/N? --->: ".format(msgStr, projectStr))
        if var.lower() == "y":
            can_run = True

            # Lower any project names for easier matching
            for x in range(len(individual_projects)):
                individual_projects[x] = individual_projects[x].lower()
        else:
            print "Exiting..."

    if can_run:
        start_time = time.time()
        total_converted = 0

        print "\nStarting..."

        # First, gather a list of all project folders in the dev root.
        # This will help to reduce the amount of files that the script
        # has to walk through when searching for *.mtl files.
        for root, dirs, files in os.walk(BUILD_PATH):
            if root != BUILD_PATH:
                break
            for d in dirs:
                projectFile = "{0}\\{1}\\project.json".format(root, d)
                if os.path.exists(projectFile):
                    if individual_projects == []:
                        projectFolders.append(os.path.join(root, d))
                    elif d.lower() in individual_projects:
                        projectFolders.append(os.path.join(root, d))

        # Gather a list of all materials in all projects
        print "Gathering Material Assets..."
        allMats = []
        for projPath in projectFolders:
            for root, dirs, files in os.walk(projPath):
                for f in files:
                    if f.endswith(".mtl"):
                        allMats.append("{0}\\{1}".format(root, f))


        # Create a log file to store converted material filenames
        # and to check to see if the material has already been converted.
        convertedLogFile = Log_File(filename="{0}\\{1}".format(BUILD_PATH, CONVERTED_LOG_NAME))

        # Create a log file to store material filenames that need conversion
        # but cannot becuase they are read only.
        readOnlyLogFile = Log_File(filename="{0}\\{1}".format(BUILD_PATH, READ_ONLY_LOG_NAME), include_previous = False)


        # Go through each material to perform the conversion on it
        print "=============================="
        for matName in allMats:
            print matName
            if convertedLogFile.has_file(matName.lstrip(BUILD_PATH)):
                print "--> Previously converted, not doing"
            else:
                matFile = Material_File(matName)
                if matFile.needs_conversion():
                    if matFile.can_write():
                        matFile.convert()
                        convertedLogFile.add_file(matName.lstrip(BUILD_PATH))
                        print "--> Converted"
                        total_converted += 1
                    else:
                        readOnlyLogFile.add_file(matName.lstrip(BUILD_PATH))
                        print "--> Material is read-only. Not converted."
                else:
                    print "--> Conversion not need"
            if matName != allMats[-1]:
                print "\n"


        # Finally, save the log files to disk
        convertedLogFile.save()
        readOnlyLogFile.save()

        total_time = time.time() - start_time

        log_str = "You can view a list of converted materials in this log file:\n{0}\\{1}".format(BUILD_PATH, CONVERTED_LOG_NAME)

        read_only_str = "You can view a list of read-only materials in this log file:\n{0}\\{1}".format(BUILD_PATH, READ_ONLY_LOG_NAME)

        if IN_LUMBERYARD:
            general.message_box("Conversion completed\n\nConverted {0} materials(s)\n{2}\n\n{1} Material(s) needed conversion but are read-only.\n{3}".format(total_converted, len(readOnlyLogFile.get_files()), log_str, read_only_str))
        else:
            print "==============================\n"
            print "Conversion completed in {0} seconds.\n".format(total_time)
            print "Converted {0} material(s)".format(total_converted)

            # Inform the user about the log file
            print "{0}".format(log_str)

            if len(readOnlyLogFile.get_files()) > 0:
                print "\n{0} Material(s) needed conversion but are read-only.".format(len(readOnlyLogFile.get_files()))

                # Inform the user about read only mats
                print "{0}".format(read_only_str)


if __name__ == '__main__':
    # GLOBAL NOTE:
    # - All python scripts should execute through a main() function.
    # - Otherwise python will not properly garbage collect the top level variables.
    main()
