using System;
using System.Linq;
using System.Collections.Generic;
using UnityEngine;
using UnityEditor.ShaderGraph.Internal;
using UnityEngine.Serialization;

namespace UnityEditor.ShaderGraph
{
    [Serializable]
    class ShaderKeyword : ShaderInput
    {
        public const string kVariantLimitWarning = "Graph is generating too many variants. Either delete Keywords, reduce Keyword variants or increase the Shader Variant Limit in Preferences > Shader Graph or Project Settings > Shader Graph.";

        internal override bool canPromoteToFinalShader => true;

        public ShaderKeyword()
        {
        }

        public ShaderKeyword(KeywordType keywordType)
        {
            this.displayName = keywordType.ToString();
            this.keywordType = keywordType;

            // Add sensible default entries for Enum type
            if (keywordType == KeywordType.Enum)
            {
                m_Entries = new List<KeywordEntry>();
                m_Entries.Add(new KeywordEntry(1, "A", "A"));
                m_Entries.Add(new KeywordEntry(2, "B", "B"));
                m_Entries.Add(new KeywordEntry(3, "C", "C"));
            }
        }

        public static ShaderKeyword CreateBuiltInKeyword(KeywordDescriptor descriptor)
        {
            if (descriptor.entries != null)
            {
                for (int i = 0; i < descriptor.entries.Length; i++)
                {
                    if (descriptor.entries[i].id == -1)
                        descriptor.entries[i].id = i + 1;
                }
            }

            return new ShaderKeyword()
            {
                isBuiltIn = true,
                displayName = descriptor.displayName,
                overrideReferenceName = descriptor.referenceName,
                keywordType = descriptor.type,
                keywordDefinition = descriptor.definition,
                keywordScope = descriptor.scope,
                value = descriptor.value,
                entries = descriptor.entries.ToList(),
            };
        }

        [SerializeField]
        private KeywordType m_KeywordType = KeywordType.Boolean;

        public KeywordType keywordType
        {
            get => m_KeywordType;
            set => m_KeywordType = value;
        }

        internal bool IsDynamic => m_KeywordDefinition == KeywordDefinition.DynamicBranch;

        // If true, indicates that ShaderGraph may pre-compute permutations containing this keyword
        internal bool IsPermutable => !IsDynamic && !IsShaderBuildSettingsCompatible;

        [SerializeField]
        private KeywordDefinition m_KeywordDefinition = KeywordDefinition.ShaderFeature;

        public KeywordDefinition keywordDefinition
        {
            get => m_KeywordDefinition;
            set => m_KeywordDefinition = value;
        }

        [SerializeField]
        private KeywordScope m_KeywordScope = KeywordScope.Local;

        public KeywordScope keywordScope
        {
            get => m_KeywordScope;
            set => m_KeywordScope = value;
        }

        [SerializeField]
        private KeywordShaderStage m_KeywordStages = KeywordShaderStage.All;

        public KeywordShaderStage keywordStages
        {
            get => m_KeywordStages;
            set => m_KeywordStages = value;
        }

        [SerializeField]
        private List<KeywordEntry> m_Entries;

        public List<KeywordEntry> entries
        {
            get => m_Entries;
            set => m_Entries = value;
        }

        public bool HasNoneEntry => entries.Count > 0 && entries[0].IsNoneKeyword;

        [SerializeField]
        private int m_Value;

        public int value
        {
            get => m_Value;
            set => m_Value = value;
        }

        [SerializeField]
        private bool m_IsEditable = true;       // this serializes !isBuiltIn

        [SerializeField]
        private bool m_IsShaderBuildSettingsCompatible = true;

        // Note that if this field is true, we must effectively treat this keyword as a dynamic branch for code gen
        // purposes as its definition may be overridden by project settings.
        public bool IsShaderBuildSettingsCompatible
        {
            get => m_IsShaderBuildSettingsCompatible;
            set => m_IsShaderBuildSettingsCompatible = value;
        }

        public bool isBuiltIn
        {
            get => !m_IsEditable;
            set => m_IsEditable = !value;
        }

        internal override bool isCustomSlotAllowed => false;

        internal override bool isExposable => !isBuiltIn && (keywordDefinition != KeywordDefinition.Predefined);

        internal override bool isRenamable => !isBuiltIn;

        internal override ConcreteSlotValueType concreteShaderValueType => keywordType.ToConcreteSlotValueType();

        public IEnumerable<(KeywordEntry entry, int value)> GetEntriesExcludingNone()
        {
            for (int i = HasNoneEntry ? 1 : 0; i < entries.Count; ++i)
                yield return (entries[i], i);
        }

        // Enumerates the enum's entries in the order one would emit 'if', 'else if', and 'else' branches
        public IEnumerable<(KeywordEntry entry, int value)> GetEntriesInBranchOrder()
        {
            foreach (var entry in GetEntriesExcludingNone())
                yield return entry;
            if (HasNoneEntry)
                yield return (entries[0], 0);
        }

        public override string GetOldDefaultReferenceName()
        {
            // _ON suffix is required for exposing Boolean type to Material
            var suffix = string.Empty;
            if (keywordType == KeywordType.Boolean)
            {
                suffix = "_ON";
            }

            return $"{keywordType.ToString()}_{objectId}{suffix}".ToUpper();
        }

        public void AppendPropertyBlockStrings(ShaderStringBuilder builder)
        {
            if (isExposed)
            {
                switch (keywordType)
                {
                    case KeywordType.Enum:
                        string enumTagString = $"[KeywordEnum({string.Join(", ", entries.Select(x => x.displayName))})]";
                        builder.AppendLine($"{enumTagString}{referenceName}(\"{displayName}\", Float) = {value}");
                        break;
                    case KeywordType.Boolean:
                        if (referenceName.EndsWith("_ON"))
                            builder.AppendLine($"[Toggle]{referenceName.Remove(referenceName.Length - 3, 3)}(\"{displayName}\", Float) = {value}");
                        else
                            builder.AppendLine($"[Toggle({referenceName})]{referenceName}(\"{displayName}\", Float) = {value}");
                        break;
                    default:
                        break;
                }
            }
        }

        public void AppendKeywordDeclarationStrings(ShaderStringBuilder builder)
        {
            if (keywordDefinition != KeywordDefinition.Predefined)
            {
                if (keywordType == KeywordType.Boolean)
                    KeywordUtil.GenerateBooleanKeywordPragmaStrings(referenceName, keywordDefinition, keywordScope, keywordStages, str => builder.AppendLine(str));
                else
                    KeywordUtil.GenerateEnumKeywordPragmaStrings(referenceName, keywordDefinition, keywordScope, keywordStages, entries, str => builder.AppendLine(str));
            }
        }

        public string GetKeywordPreviewDeclarationString()
        {
            if (this.IsDynamic)
            {
                switch(keywordType)
                {
                    case KeywordType.Boolean:
                        return $"#define {referenceName} {(value == 0 ? "false" : " true")}";
                    case KeywordType.Enum:
                        string result = entries[value].IsNoneKeyword ?
                            string.Empty : $"#define {referenceName}_{entries[value].referenceName} true";
                        foreach ((KeywordEntry entry, int entryValue) in GetEntriesExcludingNone())
                        {
                            if (entryValue != value)
                            {
                                string newline = result.Length == 0 ? string.Empty : "\n";
                                result += $"{newline}#define {referenceName}_{entry.referenceName} false";
                            }
                        }
                        return result;
                    default:
                        throw new ArgumentOutOfRangeException();
                }
            }
            else switch (keywordType)
            {
                case KeywordType.Boolean:
                    return value == 1 ? $"#define {referenceName} true" : $"static const bool {referenceName} = false;";
                case KeywordType.Enum:
                    string enumResult = entries[value].IsNoneKeyword ?
                            string.Empty : $"#define {referenceName}_{entries[value].referenceName} true";
                    foreach ((KeywordEntry entry, int entryValue) in GetEntriesExcludingNone())
                    {
                        if (entryValue != value)
                        {
                            string newline = enumResult.Length == 0 ? string.Empty : "\n";
                            enumResult += $"{newline}static const bool {referenceName}_{entry.referenceName} = false;";
                        }
                    }
                    return enumResult;
                default:
                    throw new ArgumentOutOfRangeException();
            }
        }

        internal override ShaderInput Copy()
        {
            // Keywords copy reference name
            // This is because keywords are copied between graphs
            // When copying dependent nodes
            return new ShaderKeyword()
            {
                displayName = displayName,
                value = value,
                isBuiltIn = isBuiltIn,
                keywordType = keywordType,
                keywordDefinition = keywordDefinition,
                keywordScope = keywordScope,
                entries = entries,
                keywordStages = keywordStages,
                overrideReferenceName = overrideReferenceName,
                IsShaderBuildSettingsCompatible = IsShaderBuildSettingsCompatible,
            };
        }

        public override int latestVersion => 2;
        public override void OnAfterDeserialize(string json)
        {
            if (sgVersion == 0)
            {
                // Version 1 allows keywords to control whether they are exposed (for Material control) or not.
                // old exposable keywords set their exposed state to maintain previous behavior
                // (where bool keywords only showed up in the material when ending in "_ON")
                if (isExposable)
                {
                    if (m_KeywordType == KeywordType.Boolean)
                        generatePropertyBlock = referenceName.EndsWith("_ON");
                    else // KeywordType.Enum
                        generatePropertyBlock = true;
                }
                ChangeVersion(1);
            }
            if (sgVersion < 2)
            {
                // Version 2 marks the point at which we changed KeywordNode code gen to be compatible with
                // Shader Build Settings. Legacy shader graphs should default to incompatible KeywordNode
                // code gen.
                IsShaderBuildSettingsCompatible = false;
                ChangeVersion(2);
            }
        }
    }
}