using System;
using UniGLTF;
using UniGLTF.Extensions.VRMC_materials_mtoon;
using UnityEngine;
using VRMShaders;
using VRMShaders.VRM10.MToon10.Runtime;
using ColorSpace = VRMShaders.ColorSpace;

namespace UniVRM10
{
    public static class Vrm10MToonMaterialExporter
    {
        const string MTOON_SPEC_VERSION = "1.0-beta";

        public static bool TryExportMaterialAsMToon(Material src, ITextureExporter textureExporter, out glTFMaterial dst)
        {
            if (src.shader.name != MToon10Meta.UnityShaderName)
            {
                dst = null;
                return false;
            }

            // Get MToon10 Context
            var context = new MToon10Context(src);
            context.Validate();

            // base material
            dst = glTF_KHR_materials_unlit.CreateDefault();
            dst.name = src.name;

            // vrmc_materials_mtoon ext
            var mtoon = new UniGLTF.Extensions.VRMC_materials_mtoon.VRMC_materials_mtoon();
            mtoon.SpecVersion = MTOON_SPEC_VERSION;

            // Rendering
            dst.alphaMode = ExportAlphaMode(context.AlphaMode);
            mtoon.TransparentWithZWrite = context.TransparentWithZWriteMode == MToon10TransparentWithZWriteMode.On;
            dst.alphaCutoff = Mathf.Max(0, context.AlphaCutoff);
            mtoon.RenderQueueOffsetNumber = context.RenderQueueOffsetNumber;
            dst.doubleSided = context.DoubleSidedMode == MToon10DoubleSidedMode.On;

            // Lighting
            dst.pbrMetallicRoughness = new glTFPbrMetallicRoughness();
            dst.pbrMetallicRoughness.baseColorFactor = context.BaseColorFactorSrgb.ToFloat4(ColorSpace.sRGB, ColorSpace.Linear);
            var baseColorTextureIndex = textureExporter.RegisterExportingAsSRgb(context.BaseColorTexture, context.AlphaMode != MToon10AlphaMode.Opaque);
            if (baseColorTextureIndex != -1)
            {
                dst.pbrMetallicRoughness.baseColorTexture = new glTFMaterialBaseColorTextureInfo
                {
                    index = baseColorTextureIndex,
                };
            }
            mtoon.ShadeColorFactor = context.ShadeColorFactorSrgb.ToFloat3(ColorSpace.sRGB, ColorSpace.Linear);
            var shadeColorTextureIndex = textureExporter.RegisterExportingAsSRgb(context.ShadeColorTexture, needsAlpha: false);
            if (shadeColorTextureIndex != -1)
            {
                mtoon.ShadeMultiplyTexture = new TextureInfo
                {
                    Index = shadeColorTextureIndex,
                };
            }
            var normalTextureIndex = textureExporter.RegisterExportingAsNormal(context.NormalTexture);
            if (normalTextureIndex != -1)
            {
                dst.normalTexture = new glTFMaterialNormalTextureInfo
                {
                    index = normalTextureIndex,
                    scale = context.NormalTextureScale,
                };
            }
            mtoon.ShadingShiftFactor = context.ShadingShiftFactor;
            var shadingShiftTextureIndex = textureExporter.RegisterExportingAsLinear(context.ShadingShiftTexture, needsAlpha: false);
            if (shadingShiftTextureIndex != -1)
            {
                mtoon.ShadingShiftTexture = new ShadingShiftTextureInfo
                {
                    Index = shadingShiftTextureIndex,
                    Scale = context.ShadingShiftTextureScale,
                };
            }
            mtoon.ShadingToonyFactor = context.ShadingToonyFactor;

            // GI
            mtoon.GiEqualizationFactor = context.GiEqualizationFactor;

            // Emission
            // Emissive factor is stored in Linear space
            dst.emissiveFactor = context.EmissiveFactorLinear.ToFloat3(ColorSpace.Linear, ColorSpace.Linear);
            var emissiveTextureIndex = textureExporter.RegisterExportingAsSRgb(context.EmissiveTexture, needsAlpha: false);
            if (emissiveTextureIndex != -1)
            {
                dst.emissiveTexture = new glTFMaterialEmissiveTextureInfo
                {
                    index = emissiveTextureIndex,
                };
            }

            // Rim Lighting
            var matcapTextureIndex = textureExporter.RegisterExportingAsSRgb(context.MatcapTexture, needsAlpha: false);
            if (matcapTextureIndex != -1)
            {
                mtoon.MatcapTexture = new TextureInfo
                {
                    Index = matcapTextureIndex,
                };
            }
            mtoon.ParametricRimColorFactor = context.ParametricRimColorFactorSrgb.ToFloat3(ColorSpace.sRGB, ColorSpace.Linear);
            mtoon.ParametricRimFresnelPowerFactor = context.ParametricRimFresnelPowerFactor;
            mtoon.ParametricRimLiftFactor = context.ParametricRimLiftFactor;
            var rimMultiplyTextureIndex = textureExporter.RegisterExportingAsSRgb(context.RimMultiplyTexture, needsAlpha: false);
            if (rimMultiplyTextureIndex != -1)
            {
                mtoon.RimMultiplyTexture = new TextureInfo
                {
                    Index = rimMultiplyTextureIndex,
                };
            }
            mtoon.RimLightingMixFactor = context.RimLightingMixFactor;

            // Outline
            mtoon.OutlineWidthMode = ExportOutlineWidthMode(context.OutlineWidthMode);
            mtoon.OutlineWidthFactor = context.OutlineWidthFactor;
            var outlineWidthMultiplyTextureIndex = textureExporter.RegisterExportingAsLinear(context.OutlineWidthMultiplyTexture, needsAlpha: false);
            if (outlineWidthMultiplyTextureIndex != -1)
            {
                mtoon.OutlineWidthMultiplyTexture = new TextureInfo
                {
                    Index = outlineWidthMultiplyTextureIndex,
                };
            }
            mtoon.OutlineColorFactor = context.OutlineColorFactorSrgb.ToFloat3(ColorSpace.sRGB, ColorSpace.Linear);
            mtoon.OutlineLightingMixFactor = context.OutlineLightingMixFactor;

            // UV Animation
            var uvAnimationMaskTextureIndex = textureExporter.RegisterExportingAsLinear(context.UvAnimationMaskTexture, needsAlpha: false);
            if (uvAnimationMaskTextureIndex != -1)
            {
                mtoon.UvAnimationMaskTexture = new TextureInfo
                {
                    Index = uvAnimationMaskTextureIndex,
                };
            }
            mtoon.UvAnimationScrollXSpeedFactor = context.UvAnimationScrollXSpeedFactor;
            {
                // Coordinate Conversion
                const float invertY = -1f;
                mtoon.UvAnimationScrollYSpeedFactor = context.UvAnimationScrollYSpeedFactor * invertY;
            }
            mtoon.UvAnimationRotationSpeedFactor = context.UvAnimationRotationSpeedFactor;

            // Texture Transforms
            var scale = context.TextureScale;
            var offset = context.TextureOffset;
            ExportTextureTransform(dst.pbrMetallicRoughness.baseColorTexture, scale, offset);
            ExportTextureTransform(dst.emissiveTexture, scale, offset);
            ExportTextureTransform(dst.normalTexture, scale, offset);
            ExportTextureTransform(mtoon.ShadeMultiplyTexture, scale, offset);
            ExportTextureTransform(mtoon.ShadingShiftTexture, scale, offset);
            ExportTextureTransform(mtoon.MatcapTexture, scale, offset);
            ExportTextureTransform(mtoon.RimMultiplyTexture, scale, offset);
            ExportTextureTransform(mtoon.OutlineWidthMultiplyTexture, scale, offset);
            ExportTextureTransform(mtoon.UvAnimationMaskTexture, scale, offset);

            UniGLTF.Extensions.VRMC_materials_mtoon.GltfSerializer.SerializeTo(ref dst.extensions, mtoon);

            return true;
        }

        private static string ExportAlphaMode(MToon10AlphaMode alphaMode)
        {
            switch (alphaMode)
            {
                case MToon10AlphaMode.Opaque:
                    return "OPAQUE";
                case MToon10AlphaMode.Cutout:
                    return "MASK";
                case MToon10AlphaMode.Transparent:
                    return "BLEND";
                default:
                    throw new ArgumentOutOfRangeException(nameof(alphaMode), alphaMode, null);
            }
        }

        private static OutlineWidthMode ExportOutlineWidthMode(MToon10OutlineMode mode)
        {
            switch (mode)
            {
                case MToon10OutlineMode.None:
                    return OutlineWidthMode.none;
                case MToon10OutlineMode.World:
                    return OutlineWidthMode.worldCoordinates;
                case MToon10OutlineMode.Screen:
                    return OutlineWidthMode.screenCoordinates;
                default:
                    throw new ArgumentOutOfRangeException(nameof(mode), mode, null);
            }
        }

        public static void ExportTextureTransform(glTFTextureInfo textureInfo, Vector2 unityScale, Vector2 unityOffset)
        {
            if (textureInfo == null)
            {
                return;
            }
            var scale = unityScale;
            var offset = new Vector2(unityOffset.x, 1.0f - unityOffset.y - unityScale.y);

            glTF_KHR_texture_transform.Serialize(textureInfo, (offset.x, offset.y), (scale.x, scale.y));
        }

        public static void ExportTextureTransform(TextureInfo textureInfo, Vector2 unityScale, Vector2 unityOffset)
        {
            if (textureInfo == null)
            {
                return;
            }
            // Generate extension to empty holder.
            var gltfTextureInfo = new EmptyGltfTextureInfo();
            ExportTextureTransform(gltfTextureInfo, unityScale, unityOffset);

            // Copy extension from empty holder.
            textureInfo.Extensions = gltfTextureInfo.extensions;
        }

        public static void ExportTextureTransform(ShadingShiftTextureInfo textureInfo, Vector2 unityScale, Vector2 unityOffset)
        {
            if (textureInfo == null)
            {
                return;
            }
            // Generate extension to empty holder.
            var gltfTextureInfo = new EmptyGltfTextureInfo();
            ExportTextureTransform(gltfTextureInfo, unityScale, unityOffset);

            // Copy extension from empty holder.
            textureInfo.Extensions = gltfTextureInfo.extensions;
        }

        private sealed class EmptyGltfTextureInfo : glTFTextureInfo
        {

        }
    }
}
