using UnityEngine;
using System;
using System.Collections.Generic;
using UnityEditor.Graphing;
using UnityEditor.ShaderGraph.Drawing.Controls;
using UnityEditor.ShaderGraph.Internal;
using UnityEngine.Assertions;
using UnityEngine.UIElements;
using GraphDataStore = UnityEditor.ShaderGraph.DataStore<UnityEditor.ShaderGraph.GraphData>;

namespace UnityEditor.ShaderGraph.Drawing
{
    class ChangeExposedFlagAction : IGraphDataAction
    {
        internal ChangeExposedFlagAction(ShaderInput shaderInput, bool newIsExposed)
        {
            this.shaderInputReference = shaderInput;
            this.newIsExposedValue = newIsExposed;
            this.oldIsExposedValue = shaderInput.generatePropertyBlock;
        }

        void ChangeExposedFlag(GraphData graphData)
        {
            AssertHelpers.IsNotNull(graphData, "GraphData is null while carrying out ChangeExposedFlagAction");
            AssertHelpers.IsNotNull(shaderInputReference, "ShaderInputReference is null while carrying out ChangeExposedFlagAction");
            // The Undos are currently handled in ShaderInputPropertyDrawer but we want to move that out from there and handle here
            //graphData.owner.RegisterCompleteObjectUndo("Change Exposed Toggle");
            shaderInputReference.generatePropertyBlock = newIsExposedValue;
        }

        public Action<GraphData> modifyGraphDataAction => ChangeExposedFlag;

        // Reference to the shader input being modified
        internal ShaderInput shaderInputReference { get; private set; }

        // New value of whether the shader input should be exposed to the material inspector
        internal bool newIsExposedValue { get; private set; }
        internal bool oldIsExposedValue { get; private set; }
    }

    class ChangePropertyValueAction : IGraphDataAction
    {
        void ChangePropertyValue(GraphData graphData)
        {
            AssertHelpers.IsNotNull(graphData, "GraphData is null while carrying out ChangePropertyValueAction");
            AssertHelpers.IsNotNull(shaderInputReference, "ShaderPropertyReference is null while carrying out ChangePropertyValueAction");
            // The Undos are currently handled in ShaderInputPropertyDrawer but we want to move that out from there and handle here
            //graphData.owner.RegisterCompleteObjectUndo("Change Property Value");
            var material = graphData.owner.materialArtifact;
            switch (shaderInputReference)
            {
                case BooleanShaderProperty booleanProperty:
                    booleanProperty.value = ((ToggleData)newShaderInputValue).isOn;
                    if (material) material.SetFloat(shaderInputReference.referenceName, booleanProperty.value ? 1.0f : 0.0f);
                    break;
                case Vector1ShaderProperty vector1Property:
                    vector1Property.value = (float)newShaderInputValue;
                    if (material) material.SetFloat(shaderInputReference.referenceName, vector1Property.value);
                    break;
                case Vector2ShaderProperty vector2Property:
                    vector2Property.value = (Vector2)newShaderInputValue;
                    if (material) material.SetVector(shaderInputReference.referenceName, vector2Property.value);
                    break;
                case Vector3ShaderProperty vector3Property:
                    vector3Property.value = (Vector3)newShaderInputValue;
                    if (material) material.SetVector(shaderInputReference.referenceName, vector3Property.value);
                    break;
                case Vector4ShaderProperty vector4Property:
                    vector4Property.value = (Vector4)newShaderInputValue;
                    if (material) material.SetVector(shaderInputReference.referenceName, vector4Property.value);
                    break;
                case ColorShaderProperty colorProperty:
                    colorProperty.value = (Color)newShaderInputValue;
                    if (material) material.SetColor(shaderInputReference.referenceName, colorProperty.value);
                    break;
                case Texture2DShaderProperty texture2DProperty:
                    texture2DProperty.value.texture = (Texture)newShaderInputValue;
                    if (material) material.SetTexture(shaderInputReference.referenceName, texture2DProperty.value.texture);
                    break;
                case Texture2DArrayShaderProperty texture2DArrayProperty:
                    texture2DArrayProperty.value.textureArray = (Texture2DArray)newShaderInputValue;
                    if (material) material.SetTexture(shaderInputReference.referenceName, texture2DArrayProperty.value.textureArray);
                    break;
                case Texture3DShaderProperty texture3DProperty:
                    texture3DProperty.value.texture = (Texture3D)newShaderInputValue;
                    if (material) material.SetTexture(shaderInputReference.referenceName, texture3DProperty.value.texture);
                    break;
                case CubemapShaderProperty cubemapProperty:
                    cubemapProperty.value.cubemap = (Cubemap)newShaderInputValue;
                    if (material) material.SetTexture(shaderInputReference.referenceName, cubemapProperty.value.cubemap);
                    break;
                case Matrix2ShaderProperty matrix2Property:
                    matrix2Property.value = (Matrix4x4)newShaderInputValue;
                    break;
                case Matrix3ShaderProperty matrix3Property:
                    matrix3Property.value = (Matrix4x4)newShaderInputValue;
                    break;
                case Matrix4ShaderProperty matrix4Property:
                    matrix4Property.value = (Matrix4x4)newShaderInputValue;
                    break;
                case SamplerStateShaderProperty samplerStateProperty:
                    samplerStateProperty.value = (TextureSamplerState)newShaderInputValue;
                    break;
                case GradientShaderProperty gradientProperty:
                    gradientProperty.value = (Gradient)newShaderInputValue;
                    break;
                default:
                    throw new ArgumentOutOfRangeException();
            }
        }

        public Action<GraphData> modifyGraphDataAction => ChangePropertyValue;

        // Reference to the shader input being modified
        internal ShaderInput shaderInputReference { get; set; }

        // New value of the shader property

        internal object newShaderInputValue { get; set; }
    }

    class ChangeDisplayNameAction : IGraphDataAction
    {
        void ChangeDisplayName(GraphData graphData)
        {
            AssertHelpers.IsNotNull(graphData, "GraphData is null while carrying out ChangeDisplayNameAction");
            AssertHelpers.IsNotNull(shaderInputReference, "ShaderInputReference is null while carrying out ChangeDisplayNameAction");
            graphData.owner.RegisterCompleteObjectUndo("Change Display Name");
            if (newDisplayNameValue != shaderInputReference.displayName)
            {
                shaderInputReference.SetDisplayNameAndSanitizeForGraph(graphData, newDisplayNameValue);
            }
        }

        public Action<GraphData> modifyGraphDataAction => ChangeDisplayName;

        // Reference to the shader input being modified
        internal ShaderInput shaderInputReference { get; set; }

        internal string newDisplayNameValue { get; set; }
    }

    class ChangeReferenceNameAction : IGraphDataAction
    {
        void ChangeReferenceName(GraphData graphData)
        {
            AssertHelpers.IsNotNull(graphData, "GraphData is null while carrying out ChangeReferenceNameAction");
            AssertHelpers.IsNotNull(shaderInputReference, "ShaderInputReference is null while carrying out ChangeReferenceNameAction");
            // The Undos are currently handled in ShaderInputPropertyDrawer but we want to move that out from there and handle here
            //graphData.owner.RegisterCompleteObjectUndo("Change Reference Name");
            if (newReferenceNameValue != shaderInputReference.overrideReferenceName)
            {
                graphData.SanitizeGraphInputReferenceName(shaderInputReference, newReferenceNameValue);
            }
        }

        public Action<GraphData> modifyGraphDataAction => ChangeReferenceName;

        // Reference to the shader input being modified
        internal ShaderInput shaderInputReference { get; set; }

        internal string newReferenceNameValue { get; set; }
    }

    class ResetReferenceNameAction : IGraphDataAction
    {
        void ResetReferenceName(GraphData graphData)
        {
            AssertHelpers.IsNotNull(graphData, "GraphData is null while carrying out ResetReferenceNameAction");
            AssertHelpers.IsNotNull(shaderInputReference, "ShaderInputReference is null while carrying out ResetReferenceNameAction");
            graphData.owner.RegisterCompleteObjectUndo("Reset Reference Name");
            shaderInputReference.overrideReferenceName = null;
        }

        public Action<GraphData> modifyGraphDataAction => ResetReferenceName;

        // Reference to the shader input being modified
        internal ShaderInput shaderInputReference { get; set; }
    }

    class DeleteShaderInputAction : IGraphDataAction
    {
        void DeleteShaderInput(GraphData graphData)
        {
            AssertHelpers.IsNotNull(graphData, "GraphData is null while carrying out DeleteShaderInputAction");
            AssertHelpers.IsNotNull(shaderInputsToDelete, "ShaderInputsToDelete is null while carrying out DeleteShaderInputAction");
            // This is called by MaterialGraphView currently, no need to repeat it here, though ideally it would live here
            //graphData.owner.RegisterCompleteObjectUndo("Delete Graph Input(s)");

            foreach (var shaderInput in shaderInputsToDelete)
            {
                graphData.RemoveGraphInput(shaderInput);
            }
        }

        public Action<GraphData> modifyGraphDataAction => DeleteShaderInput;

        // Reference to the shader input(s) being deleted
        internal IList<ShaderInput> shaderInputsToDelete { get; set; } = new List<ShaderInput>();
    }

    class ShaderInputViewController : SGViewController<ShaderInput, ShaderInputViewModel>
    {
        // Exposed for PropertyView
        internal GraphData graphData => DataStore.State;

        internal ShaderInputViewController(ShaderInput shaderInput, ShaderInputViewModel inViewModel, GraphDataStore graphDataStore)
            : base(shaderInput, inViewModel, graphDataStore)
        {
            InitializeViewModel();

            m_SgBlackboardField = new SGBlackboardField(ViewModel);
            m_SgBlackboardField.controller = this;

            m_BlackboardRowView = new SGBlackboardRow(m_SgBlackboardField, null);
            m_BlackboardRowView.expanded = SessionState.GetBool($"Unity.ShaderGraph.Input.{shaderInput.objectId}.isExpanded", false);
        }

        void InitializeViewModel()
        {
            if (Model == null)
            {
                AssertHelpers.Fail("Could not initialize shader input view model as shader input was null.");
                return;
            }
            ViewModel.model = Model;
            ViewModel.isSubGraph = DataStore.State.isSubGraph;
            ViewModel.isInputExposed = (DataStore.State.isSubGraph || (Model.isExposable && Model.generatePropertyBlock));
            ViewModel.inputName = Model.displayName;
            switch (Model)
            {
                case AbstractShaderProperty shaderProperty:
                    ViewModel.inputTypeName = shaderProperty.GetPropertyTypeString();
                    // Handles upgrade fix for deprecated old Color property
                    shaderProperty.onBeforeVersionChange += (_) => graphData.owner.RegisterCompleteObjectUndo($"Change {shaderProperty.displayName} Version");
                    break;
                case ShaderKeyword shaderKeyword:
                    ViewModel.inputTypeName = shaderKeyword.keywordType + " Keyword";
                    ViewModel.inputTypeName = shaderKeyword.isBuiltIn ? "Built-in " + ViewModel.inputTypeName : ViewModel.inputTypeName;
                    break;
                case ShaderDropdown shaderDropdown:
                    ViewModel.inputTypeName = "Dropdown";
                    break;
            }

            ViewModel.requestModelChangeAction = this.RequestModelChange;
        }

        SGBlackboardRow m_BlackboardRowView;
        SGBlackboardField m_SgBlackboardField;

        internal SGBlackboardRow BlackboardItemView => m_BlackboardRowView;

        protected override void RequestModelChange(IGraphDataAction changeAction)
        {
            DataStore.Dispatch(changeAction);
        }

        // Called by GraphDataStore.Subscribe after the model has been changed
        protected override void ModelChanged(GraphData graphData, IGraphDataAction changeAction)
        {
            switch (changeAction)
            {
                case ChangeExposedFlagAction changeExposedFlagAction:
                    // ModelChanged is called overzealously on everything
                    // but we only care if the action pertains to our Model
                    if (changeExposedFlagAction.shaderInputReference == Model)
                    {
                        ViewModel.isInputExposed = Model.generatePropertyBlock;
                        if (changeExposedFlagAction.oldIsExposedValue != changeExposedFlagAction.newIsExposedValue)
                            DirtyNodes(ModificationScope.Graph);
                        m_SgBlackboardField.UpdateFromViewModel();
                    }
                    break;

                case ChangePropertyValueAction changePropertyValueAction:
                    if (changePropertyValueAction.shaderInputReference == Model)
                    {
                        DirtyNodes(ModificationScope.Graph);
                        m_SgBlackboardField.MarkDirtyRepaint();
                    }
                    break;

                case ResetReferenceNameAction resetReferenceNameAction:
                    if (resetReferenceNameAction.shaderInputReference == Model)
                    {
                        DirtyNodes(ModificationScope.Graph);
                    }
                    break;

                case ChangeReferenceNameAction changeReferenceNameAction:
                    if (changeReferenceNameAction.shaderInputReference == Model)
                    {
                        DirtyNodes(ModificationScope.Graph);
                    }
                    break;

                case ChangeDisplayNameAction changeDisplayNameAction:
                    if (changeDisplayNameAction.shaderInputReference == Model)
                    {
                        ViewModel.inputName = Model.displayName;
                        DirtyNodes(ModificationScope.Layout);
                        m_SgBlackboardField.UpdateFromViewModel();
                    }
                    break;
            }
        }

        // TODO: This should communicate to node controllers instead of searching for the nodes themselves everytime, but that's going to take a while...
        internal void DirtyNodes(ModificationScope modificationScope = ModificationScope.Node)
        {
            foreach (var observer in Model.InputObservers)
            {
                observer.OnShaderInputUpdated(modificationScope);
            }

            switch (Model)
            {
                case AbstractShaderProperty property:
                    var graphEditorView = m_BlackboardRowView.GetFirstAncestorOfType<GraphEditorView>();
                    if (graphEditorView == null)
                        return;
                    var colorManager = graphEditorView.colorManager;
                    var nodes = graphEditorView.graphView.Query<MaterialNodeView>().ToList();

                    colorManager.SetNodesDirty(nodes);
                    colorManager.UpdateNodeViews(nodes);
                    break;
                case ShaderKeyword keyword:
                    // Cant determine if Sub Graphs contain the keyword so just update them
                    foreach (var node in DataStore.State.GetNodes<SubGraphNode>())
                    {
                        node.Dirty(modificationScope);
                    }

                    break;
                case ShaderDropdown dropdown:
                    // Cant determine if Sub Graphs contain the dropdown so just update them
                    foreach (var node in DataStore.State.GetNodes<SubGraphNode>())
                    {
                        node.Dirty(modificationScope);
                    }

                    break;
                default:
                    throw new ArgumentOutOfRangeException();
            }
        }

        public override void Dispose()
        {
            if (m_SgBlackboardField == null)
                return;

            Model?.ClearObservers();

            base.Dispose();
            Cleanup();

            BlackboardItemView.Dispose();
            m_SgBlackboardField.Dispose();

            m_BlackboardRowView = null;
            m_SgBlackboardField = null;
        }
    }
}