private static SecondarySpriteTexture?CreateSecondarySpriteTexture(string dataAssetPath, SpritesheetMaterialData materialData)
        {
            if (materialData == null)
            {
                return(null);
            }

            string textureName;

            // Only a couple of roles are supported for secondary textures
            switch (materialData.MaterialRole)
            {
            case MaterialRole.Mask:
                textureName = "_MaskTex";
                break;

            case MaterialRole.Normal:
                textureName = "_NormalMap";
                break;

            default:
                Log($"Material role {materialData.MaterialRole} is unrecognized and the associated image won't be processed", LogLevel.Warning);
                return(null);
            }

            Log($"Material \"{materialData.name}\" with role {materialData.role} will have the secondary texture name \"{textureName}\"", LogLevel.Verbose);

            string    secondaryTexturePath = Path.Combine(Path.GetDirectoryName(dataAssetPath), materialData.file);
            Texture2D secondaryTexture     = AssetDatabase.LoadAssetAtPath <Texture2D>(secondaryTexturePath);

            if (secondaryTexture == null)
            {
                throw new InvalidOperationException($"Expected to find a secondary texture at asset path \"{secondaryTexturePath}\" based on .ssdata file at \"{dataAssetPath}\"");
            }

            return(new SecondarySpriteTexture {
                name = textureName,
                texture = secondaryTexture
            });
        }
        static void OnPostprocessAllAssets(string[] importedAssets, string[] deletedAssets, string[] movedAssets, string[] movedFromAssetPaths)
        {
            List <string> processedSpritesheets = new List <string>();

            var assets = importedAssets.Concat(movedAssets).ToList();

            foreach (string inputAssetPath in assets)
            {
                string assetPath = inputAssetPath.Replace('/', '\\');

                SpritesheetData spritesheetData;

                #region Find and load spritesheet data file
                // Regardless of whether an .ssdata file or an image file is being imported, we try to load the
                // spritesheet data and process everything it references, to keep all of the files in sync
                if (assetPath.EndsWith(".ssdata"))
                {
                    spritesheetData = LoadSpritesheetDataFile(assetPath);
                }
                else
                {
                    // Check if this is a texture which may have a .ssdata associated
                    if (AssetDatabase.LoadAssetAtPath <Texture2D>(assetPath) == null)
                    {
                        continue;
                    }

                    spritesheetData = FindSpritesheetData(assetPath);
                }
                #endregion

                if (spritesheetData == null)
                {
                    continue;
                }


                // When loading in several images that represent a texture and its secondary textures, we want to avoid
                // repeating work, so we only process each spritesheet data file once per import
                if (processedSpritesheets.Contains(spritesheetData.dataFilePath))
                {
                    Log($"Spritesheet at path \"{spritesheetData.dataFilePath} has already been processed in this import operation; skipping", LogLevel.Verbose);
                    continue;
                }

                processedSpritesheets.Add(spritesheetData.dataFilePath);

                var spritesheetImporter = AssetImporter.GetAtPath(spritesheetData.dataFilePath) as SpritesheetDataImporter;

                if (spritesheetImporter == null)
                {
                    Log($"SpritesheetDataImporter for asset \"{inputAssetPath}\" unavailable; it's probably running later in this import operation", LogLevel.Verbose);
                    continue;
                }

                string successMessage = $"Import of assets referenced in \"{spritesheetData.dataFilePath}\" completed successfully. ";

                SliceSpritesheets(spritesheetData, spritesheetImporter);

                #region Set up secondary textures
#if SECONDARY_TEXTURES_AVAILABLE
                if (spritesheetData.materialData.Count > 0)
                {
                    SpritesheetMaterialData[] albedoMaterials = spritesheetData.materialData.Where(mat => mat.MaterialRole == MaterialRole.Albedo).ToArray();
                    SpritesheetMaterialData[] maskMaterials   = spritesheetData.materialData.Where(mat => mat.MaterialRole == MaterialRole.Mask).ToArray();
                    SpritesheetMaterialData[] normalMaterials = spritesheetData.materialData.Where(mat => mat.MaterialRole == MaterialRole.Normal).ToArray();
                    SpritesheetMaterialData[] otherMaterials  = spritesheetData.materialData.Where(mat => mat.MaterialRole == MaterialRole.UnassignedOrUnrecognized).ToArray();

                    #region Validate material data
                    if (albedoMaterials.Length != 1)
                    {
                        throw new InvalidOperationException($"There should be exactly 1 albedo material; found {albedoMaterials.Length} for data file {spritesheetData.dataFilePath}");
                    }

                    if (maskMaterials.Length > 1)
                    {
                        throw new InvalidOperationException($"There should be at most 1 mask material; found {maskMaterials.Length} for data file {spritesheetData.dataFilePath}");
                    }

                    if (normalMaterials.Length > 1)
                    {
                        throw new InvalidOperationException($"There should be at most 1 normal material; found {normalMaterials.Length} for data file {spritesheetData.dataFilePath}");
                    }

                    if (otherMaterials.Length > 0)
                    {
                        Log($"Data file {spritesheetData.dataFilePath} references {otherMaterials.Length} materials of unknown purpose. These will need to be configured manually.", LogLevel.Warning);
                    }
                    #endregion

                    SpritesheetMaterialData albedo = albedoMaterials[0];
                    SpritesheetMaterialData mask   = maskMaterials.Length > 0 ? maskMaterials[0] : null;
                    SpritesheetMaterialData normal = normalMaterials.Length > 0 ? normalMaterials[0] : null;

                    Log($"Asset has mask material texture: {mask != null}", LogLevel.Verbose);
                    Log($"Asset has normal material texture: {normal != null}", LogLevel.Verbose);

                    #region Create secondary textures
                    List <SecondarySpriteTexture> secondarySpriteTextures = new List <SecondarySpriteTexture>();

                    if (mask != null)
                    {
                        secondarySpriteTextures.Add(CreateSecondarySpriteTexture(spritesheetData.dataFilePath, mask).Value);
                    }

                    if (normal != null)
                    {
                        secondarySpriteTextures.Add(CreateSecondarySpriteTexture(spritesheetData.dataFilePath, normal).Value);
                    }
                    #endregion

                    // Secondary textures are handled through the TextureImporter, and making changes to the importer during OnPostprocessAllAssets
                    // results in them being applied the next time the asset is imported. We therefore have to trigger a reimport ourselves, but
                    // being careful only to do so if something has changed, or else we'll get stuck in an infinite import loop.
                    string          albedoTextureAssetPath = Path.Combine(Path.GetDirectoryName(spritesheetData.dataFilePath), albedo.file);
                    TextureImporter albedoTextureImporter  = AssetImporter.GetAtPath(albedoTextureAssetPath) as TextureImporter;
                    bool            importSettingsChanged  = false;

                    #region Check for changes in import settings
                    if (secondarySpriteTextures.Count != albedoTextureImporter.secondarySpriteTextures.Length)
                    {
                        importSettingsChanged = true;
                    }
                    else
                    {
                        // Compare each element between the two sets pairwise. We always import in a consistent
                        // order so we don't need to worry about that.
                        for (int i = 0; i < secondarySpriteTextures.Count; i++)
                        {
                            var newSecondaryTexture = secondarySpriteTextures[i];
                            var oldSecondaryTexture = albedoTextureImporter.secondarySpriteTextures[i];

                            string newAssetPath = AssetDatabase.GetAssetPath(newSecondaryTexture.texture);
                            string oldAssetPath = AssetDatabase.GetAssetPath(oldSecondaryTexture.texture);

                            importSettingsChanged = importSettingsChanged || (newSecondaryTexture.name != oldSecondaryTexture.name) || (newAssetPath != oldAssetPath);
                        }
                    }
                    #endregion

                    if (importSettingsChanged)
                    {
                        Log("A change has occurred in the secondary textures; triggering a reimport", LogLevel.Verbose);
                        albedoTextureImporter.secondarySpriteTextures = secondarySpriteTextures.ToArray();
                        albedoTextureImporter.SaveAndReimport();
                    }

                    successMessage += $"Configured {secondarySpriteTextures.Count} secondary textures. ";
                }
#endif
                #endregion

                #region Create/update animations
                if (spritesheetImporter.createAnimations && spritesheetData.animations.Count > 0)
                {
                    Log($"Going to create or replace {spritesheetData.animations.Count} animation clips for data file at \"{spritesheetData.dataFilePath}\"", LogLevel.Verbose);

                    string mainImageAssetPath    = GetMainImagePath(spritesheetData);
                    string assetDirectory        = Path.GetDirectoryName(mainImageAssetPath);
                    UnityEngine.Object[] sprites = AssetDatabase.LoadAllAssetRepresentationsAtPath(mainImageAssetPath);

                    Log($"Loaded {sprites.Length} sprites from main image asset path \"{mainImageAssetPath}\"", LogLevel.Verbose);

                    foreach (var animationData in spritesheetData.animations)
                    {
                        bool multipleAnimationsFromSameSource = spritesheetData.animations.Where(anim => anim.name == animationData.name).Count() > 1;

                        // Only include rotation in name if needed to disambiguate
                        string rotation = multipleAnimationsFromSameSource ? "_rot" + animationData.rotation.ToString().PadLeft(3, '0') : "";
                        string clipName = FormatAssetName(spritesheetData.baseObjectName + "_" + animationData.name + rotation + ".anim");

                        string clipPath = assetDirectory;
                        if (multipleAnimationsFromSameSource && spritesheetImporter.placeAnimationsInSubfolders)
                        {
                            string subfolderName = SpritesheetImporterSettings.animationSubfolderNameFormat.value
                                                   .Replace("{anim}", FormatAssetName(animationData.name))
                                                   .Replace("{obj}", FormatAssetName(spritesheetData.baseObjectName));

                            clipPath = Path.Combine(clipPath, subfolderName);
                            Log($"Creating subfolder at \"{clipPath}\" to contain animations named \"{animationData.name}\"", LogLevel.Verbose);
                            Directory.CreateDirectory(clipPath);
                        }

                        clipPath = Path.Combine(clipPath, clipName);
                        AnimationClip clip = CreateAnimationClip(spritesheetData, animationData, sprites);

                        Log($"Animation clip saving at path \"{clipPath}\"", LogLevel.Verbose);
                        AssetDatabase.CreateAsset(clip, clipPath);
                    }

                    Log("Done creating/replacing animation assets; now saving asset database", LogLevel.Verbose);
                    AssetDatabase.SaveAssets();

                    successMessage += $"Created or updated {spritesheetData.animations.Count} animation clips. ";
                }
                #endregion

                Log(successMessage, LogLevel.Info);
            }
        }