using System;
using Monocle;
using System.Xml;

using Microsoft.Xna.Framework.Graphics;
using Microsoft.Xna.Framework;
using MonoMod.Utils;


using Celeste.Mod.CelesteNet.Client;
using Celeste.Mod.CelesteNet.Client.Entities;
using Celeste.Mod.CelesteNet.DataTypes;
using System.Reflection;

using Matrix = Microsoft.Xna.Framework.Matrix;
using MonoMod.RuntimeDetour;
using MonoMod.Cil;
using Mono.Cecil.Cil;
using YamlDotNet.Core;
using Mono.Cecil;
using Version = System.Version;
using System.Collections.Generic;

using Celeste.Mod.Hateline;
using Celeste.Mod.SkinModHelper;
using Celeste.Mod.SkinModHelper.Interop;

namespace Celeste.Mod.Eevee_Skinmod
{
    //Mods referenced: SkinModHelper by BigKahuna, AvaliSkin by Yoshi, Styline by Popax21 and ProBananSkin by Pro Banana
    //Mod updated to work only with SMH+, due to crashing if you have the mod plus normal SMH
    public class EeveeModule : EverestModule
    {
        public static EeveeModule Instance;
        public ColorGradeManager palette;
        public ColorGradeManager miniEevee;
        private static Effect FxEeveeGrading;
        private static Effect FxEeveeNetGrading;
        private Random rand = new Random();
        private bool cNet;
        private bool updateSkin;
        private bool pet;
        private int updateDash = 1;
        private static int dash = 1;

        private Hook spriteRender;

        private static ILHook updateSpriteHook;
        private static MethodInfo playerUpdateSpriteInfo =
            typeof(Player).GetMethod("orig_UpdateSprite", BindingFlags.Instance | BindingFlags.NonPublic);

        public static string version = "1_2_10";



        public EeveeModule() {
            Instance = this;
        }

        public override Type SettingsType => typeof(EeveeSettings);
        public static EeveeSettings Settings => (EeveeSettings)Instance._Settings;

        public static EeveeConfig PlayerConfig {
            get => new EeveeConfig {
                Enabled = CheckSMHPlus() || CheckHateline(),
                Dash = EeveeModule.dash,
                palette = Settings.fluff,
            };
        }

        public static EeveeConfig AllEeveeConfig {
            get => new EeveeConfig {
                Enabled = true,
                Dash = 1,
                palette = PlayerConfig.palette,
            };
        }

        public static EverestModuleMetadata CelesteNetMeta = new EverestModuleMetadata() {
            Name = "CelesteNet.Client",
            Version = new Version(2, 2, 2)
        };


        public static EverestModuleMetadata StrawberryPetMeta = new EverestModuleMetadata() {
            Name = "StrawberryFriend",
            Version = new Version(2, 1, 3)
        };

        public static EverestModuleMetadata HatelineMeta = new EverestModuleMetadata() {
            Name = "Hateline",
            Version = new Version(0, 2, 0)
        };

        private bool SMHPlusloaded = false;
        //SMH+ introduces an API to know which skin is being used at the moment, to use it just add the mod's dll to your references and use the method
        //SkinModHelperInterop.GetPlayerSkinNameForGlobal().
        private static bool CheckSMHPlus() {

        return SkinModHelperInterop.GetPlayerSkinNameForGlobal() == "Zaro_Eevee" || SkinModHelperInterop.GetPlayerSkinNameForGlobal() == "Zaro_Eevee_Extra" || SkinModHelperInterop.GetPlayerSkinNameForGlobal() == "Zaro_Eevee_Silhouette" || SkinModHelperInterop.GetPlayerSkinNameForGlobal() == "Zaro_Eevee_Extra_Silhouette";
        
        }

        //The eevee skin introduces the palette system, this system is not only limited to the player, so it can be extended to other stuff, in this case
        //we apply the palette to an special hat for hateline, so to know when the hat is selected we load the mod and ask this information.
        private static bool CheckHateline() {
            if(Everest.Loader.TryGetDependency(new EverestModuleMetadata() { Name = "Hateline", Version = new Version(0, 2, 0) }, out var mod)) {
                Hateline_Settings = mod._Settings;
                return Hateline_Settings != null && DynamicData.For(Hateline_Settings).Get<bool>("_enabled") && DynamicData.For(Hateline_Settings).Get<string>("SelectedHat") == "eevee_palette";
            } else {
                return false;
            }

        }

        private static EverestModuleSettings SkinModHelper_Settings;
        private static EverestModuleSettings Hateline_Settings;

        public override void Load(){
            Logger.SetLogLevel("EeveeSkin", LogLevel.Info);
            On.Celeste.PlayerSprite.ctor += onCelesteSpriteEeveeConstructor;
            On.Celeste.Player.Render += onCelesteEeveeRender;
            On.Celeste.Player.UpdateHair += OnCelesteEeveeHair;
            On.Celeste.Player.GetCurrentTrailColor += OnCelesteEeveeTrail;

            spriteRender = new Hook(typeof(Sprite).GetMethod("Render", BindingFlags.Instance | BindingFlags.Public), onEeveeImageRender);

            //With stuff as portraits and textboxes i highly recommend sticking to the usual method of smh+, even if you are making a code skinmod like this, they shouldnt be mutually exclusive
            //but in case you really need control over these, here we use il hooks to change the routine of textboxes, since they work weirdly different as just changing the textbox field in 
            //Portraits.xml isnt enought.
            //First we create the hook for the Textbox Routine, where the magic happens.

            IL.Celeste.FancyText.Parse += ilEeveePortraits;
            IL.Celeste.CS06_Campfire.Question.ctor += ilEeveeCampfire;


            On.Celeste.Lookout.Update += onEeveeLookout;
            On.Celeste.Payphone.Update += onEeveePayphone;

            //The same apply for markers, better leave this stuff to smh+, but in case you want them to be changed, here is how, this code was adapted from AAA1416's skinmodhelper.

            On.Monocle.Atlas.GetAtlasSubtextures += OnEeveeTexture;
            On.Monocle.Sprite.ctor_Atlas_string += onEeveeAtlas;


            //Here we call a new event for when the player is rendered inside the crashes info page, allowing us to apply colorgrades
            //since its a new event, if someone where to play the game with a previous version of everest, and they were using the skin, id would be likely the game would explode, so we use reflections
            //to prevent that. 
            //Note that we need to unload the method too.--------------------------------
            EventInfo onBeforePlayerRender = typeof(UI.CriticalErrorHandler).GetEvent("OnBeforePlayerRender", BindingFlags.Static | BindingFlags.Public);
            if (onBeforePlayerRender is not null) {
                Type type = onBeforePlayerRender.EventHandlerType;
                MethodInfo method = typeof(EeveeModule).GetMethod("EeveeBeforeErrorHandler");
                Delegate handler = Delegate.CreateDelegate(type, null, method);
                onBeforePlayerRender.AddEventHandler(null, handler);

            }
            //UI.CriticalErrorHandler.OnBeforePlayerRender += EeveeBeforeErrorHandler;

            SMHPlusloaded = Everest.Loader.TryGetDependency(new EverestModuleMetadata() { Name = "SkinModHelperPlus", Version = new Version(0, 9, 5) }, out var mod);

            On.Monocle.SpriteBank.Create += onMonocleEeveeCreate;
            On.Monocle.SpriteBank.CreateOn += onMonocleEeveeCreateOn;
            On.Celeste.PlayerDeadBody.Render += onCelesteEeveeDeadbody;
            On.Celeste.WaveDashPlaybackTutorial.Render += onCelesteEeveeTutorial;
            On.Celeste.WaveDashPlaybackTutorial.CreateTrail += onCelesteEeveeTutorialTrail;


            On.Celeste.Player.DashUpdate += onEeveeDashUpdate;
            cNet = Everest.Loader.DependencyLoaded(CelesteNetMeta);
            pet = Everest.Loader.DependenciesLoaded(StrawberryPetMeta);
            if (cNet) {
                On.Celeste.PlayerSprite.Render += onPlayerSpriteRenderCelestenet;
                updateSkin = CheckSMHPlus();
            }
            On.Celeste.Player.SuperJump += onEeveeSuperJump;
            On.Celeste.Player.SuperWallJump += onEeveeSuperWallJump;
            updateSpriteHook = new ILHook(playerUpdateSpriteInfo, ilEeveeUpdateSprite);


        }


        public override void Unload() {

            palette = null;
            On.Celeste.PlayerSprite.ctor -= onCelesteSpriteEeveeConstructor;
            On.Celeste.Player.Render -= onCelesteEeveeRender;
            On.Celeste.Player.UpdateHair -= OnCelesteEeveeHair;
            On.Celeste.Player.GetCurrentTrailColor -= OnCelesteEeveeTrail;

            spriteRender.Dispose();

            IL.Celeste.FancyText.Parse -= ilEeveePortraits;
            IL.Celeste.CS06_Campfire.Question.ctor -= ilEeveeCampfire;


            On.Celeste.Lookout.Update -= onEeveeLookout;
            On.Celeste.Payphone.Update -= onEeveePayphone;

            On.Monocle.Atlas.GetAtlasSubtextures -= OnEeveeTexture;
            On.Monocle.Sprite.ctor_Atlas_string -= onEeveeAtlas;

            EventInfo onBeforePlayerRender = typeof(UI.CriticalErrorHandler).GetEvent("OnBeforePlayerRender", BindingFlags.Static | BindingFlags.Public);
            if (onBeforePlayerRender is not null) {
                Type type = onBeforePlayerRender.EventHandlerType;
                MethodInfo method = typeof(EeveeModule).GetMethod("EeveeBeforeErrorHandler");
                Delegate handler = Delegate.CreateDelegate(type, null, method);
                onBeforePlayerRender.RemoveEventHandler(null, handler);

            }

            //UI.CriticalErrorHandler.OnBeforePlayerRender -= EeveeBeforeErrorHandler;

            On.Monocle.SpriteBank.Create -= onMonocleEeveeCreate;
            On.Monocle.SpriteBank.CreateOn -= onMonocleEeveeCreateOn;
            On.Celeste.PlayerDeadBody.Render -= onCelesteEeveeDeadbody;
            On.Celeste.WaveDashPlaybackTutorial.Render -= onCelesteEeveeTutorial;
            On.Celeste.WaveDashPlaybackTutorial.CreateTrail -= onCelesteEeveeTutorialTrail;
            On.Celeste.Player.DashUpdate -= onEeveeDashUpdate;


            if (Everest.Loader.DependencyLoaded(CelesteNetMeta)) {
                On.Celeste.PlayerSprite.Render -= onPlayerSpriteRenderCelestenet;
            }

            On.Celeste.Player.SuperJump -= onEeveeSuperJump;
            On.Celeste.Player.SuperWallJump -= onEeveeSuperWallJump;
            updateSpriteHook?.Dispose();
            updateSpriteHook = null;


            FxEeveeGrading.Dispose();
        }

        public override void LoadContent(bool firstLoad) {
            base.LoadContent(firstLoad);

            IGraphicsDeviceService graphicsDeviceService =
                Engine.Instance.Content.ServiceProvider
                .GetService(typeof(IGraphicsDeviceService))
                as IGraphicsDeviceService;

            ModAsset asset = Everest.Content.Get("Effect/SkinModHelperShader.cso", true);
            FxEeveeGrading = new Effect(graphicsDeviceService.GraphicsDevice, asset.Data);

            asset = Everest.Content.Get("Effect/EeveeShader.cso", true);
            FxEeveeNetGrading = new Effect(graphicsDeviceService.GraphicsDevice, asset.Data);

        }

        public override void Initialize() {
            ///we create our colorgrade holder once Celeste has properly initialized
            palette = new ColorGradeManager();
            miniEevee = new ColorGradeManager();
            Settings.updatePet = true;
            //we also get the full list of palettes availables.
            Settings.ReadContent("palettes");
            if (EeveeReader.tryGet(Settings.palettePath)) {
                Settings.externalList = EeveeReader.readContents(Settings.palettePath);
                Settings.createList = EeveeConverter.getNames(Settings.palettePath);
            }
            //to encourage testing different palettes, when launching for the first time it will select a random palette
            if (Settings.fluff == null) {
                Settings.InternalOpt = rand.Next(9);
                Settings.fluff = SkinPalette.StringtoColor(Settings.paletteList[Settings.getList1(Settings.InternalOpt)]);
            }
            if(Settings.petFluff == null) {
                Settings.InternalPet = rand.Next(9);
                Settings.petFluff = SkinPalette.StringtoColor(Settings.paletteList[Settings.getList1(Settings.InternalPet)]);
            }
            if(Settings.markerType == 0) {
                Settings.markerNum = rand.Next(3);
            }
            if ((!Settings.eeveeShiny && rand.Next(100) > 98) || (Settings.eeveeShiny && !Settings.updated && rand.Next(20) > 18)) {
                Settings.eeveeShiny = true;
                Settings.updated = true;
                Settings.paletteList.Add("Shiny", Settings.test);
                Settings.InternalOpt = 9;
                Settings.Selection = 0;
                Settings.fluff = SkinPalette.StringtoColor(Settings.paletteList["Shiny"]);
            } else if (Settings.eeveeShiny) {
                Settings.paletteList.Add("Shiny", Settings.test);
            }
            
        }
        //Hooks for markers: there is no specific class for markers, so basically we hijack whenever a texture is asked, similar with what is done on the create/createOn hooks
        private static List<Monocle.MTexture> OnEeveeTexture(On.Monocle.Atlas.orig_GetAtlasSubtextures orig, Atlas self, string path) {

            if (CheckSMHPlus() && self != null){
                string location = "";
                if ((Settings.markerMode == 0 && Settings.InternalOpt == 9 && Settings.Selection == 0) || Settings.markerMode == 2) {
                    location = "Test/";
                }
                else {
                    location = "Eevee/";
                }
                switch(Settings.markerNum) {
                    case 0:
                        location += "";
                        break;
                    case 1:
                        location += "sleeping/";
                        break;
                    case 2:
                        location += "turning/";
                        break;
                }

                if (self.HasAtlasSubtextures(location + path)){
                    path = location + path;
                }
                return orig(self, path);
            }
            else {
                return orig(self, path);
            }
        }
        private static void onEeveeAtlas(On.Monocle.Sprite.orig_ctor_Atlas_string orig, Sprite self, Atlas atlas, string path) {
            if (CheckSMHPlus() && atlas != null) {
                string location = "";
                if ((Settings.markerMode == 0 && Settings.InternalOpt == 9 && Settings.Selection == 0) || Settings.markerMode == 2)
                {
                    location = "Test/";
                }
                else
                {
                    location = "Eevee/";
                }

                switch(Settings.markerNum) {
                    case 0:
                        location += "";
                        break;
                    case 1:
                        location += "sleeping/";
                        break;
                    case 2:
                        location += "turning/";
                        break;
                }

                if (atlas.HasAtlasSubtextures(location+path)) {
                    path = location + path;
                }
                orig(self, atlas, path);
            }
            else {
                orig(self, atlas, path);
            }
        }

        //Il hook for textboxes, basically we insert a function whenever an object type FancyText.Portrait is created

        private void ilEeveePortraits(ILContext il) {
            ILCursor cursor = new ILCursor(il);
            if (cursor.TryGotoNext(MoveType.Before, instr => instr.MatchStfld<FancyText.Portrait>("Sprite"))) {
                cursor.EmitDelegate<Func<string, string>>(portraitFunction);
            }
        }

        private static string portraitFunction(string self) {
            string eeveeid = "";
            if ((Settings.portraitMode == 0 && Settings.InternalOpt == 9 && Settings.Selection == 0) || Settings.portraitMode == 2) {
                eeveeid = self + "_test";
            } else {
                eeveeid = self + "_eevee";
            }
            string skinId = "portrait_" + eeveeid;
            if (GFX.PortraitsSpriteBank.Has(skinId) && CheckSMHPlus())
                self = skinId.Substring(9);

            return self;
        }


        private void ilEeveeCampfire(ILContext il) {
            ILCursor cursor = new ILCursor(il);

            while (cursor.TryGotoNext(MoveType.After, instr => instr.MatchIsinst<FancyText.Portrait>())) {
            }
            Instruction prev = cursor.Prev;

            if (cursor.TryGotoNext(MoveType.After, instr => instr.MatchLdstr("_ask"), instr => instr.MatchCall(out MethodReference value) && value.Name == "Concat") ){
                cursor.EmitDelegate<Func<string, string>>(eeveeTextboxChecker);
            }
        }
        private static string eeveeTextboxChecker(string self) {
            if (CheckSMHPlus()) {
                string text = self.Split('_')[0].Replace("textbox/", "");
                string mode = ((Settings.portraitMode == 0 && Settings.InternalOpt == 9 && Settings.Selection == 0) || Settings.portraitMode == 2) ? "test" : "eevee";
                if (text.ToLower() == "madeline") {
                    self = "textbox/" + mode + "_ask";
                }
                
            }
            return self;
        }

        //This hooks were taken/inspired by SkinModHelperPlus by AAA1459
        private void onEeveePayphone(On.Celeste.Payphone.orig_Update orig, Payphone self) {

            DynamicData payphone = DynamicData.For(self.Sprite);

            bool? enabled = payphone.Get<bool?>("Eevee_Enable");

            if (enabled == null) {
                if (CheckSMHPlus()) {
                    payphone.Set("Eevee_Enable", true);
                } else {
                    payphone.Set("Eevee_Enable", false);
                }
            }

            orig(self);
        }

        private void onEeveeLookout(On.Celeste.Lookout.orig_Update orig, Lookout self) {
            Sprite look = DynamicData.For(self).Get<Sprite>("sprite");

            DynamicData lookout = DynamicData.For((object)look);

            bool? enabled = lookout.Get<bool?>("Eevee_Enable");

            if (enabled == null) {
                if (CheckSMHPlus()) {
                    lookout.Set("Eevee_Enable", true);
                } else {
                    lookout.Set("Eevee_Enable", false);
                }
            }

            orig(self);

        }

        private void onEeveeImageRender(Action<Sprite> orig, Sprite self) {
            DynamicData image = DynamicData.For((Object)self);
            bool? enable = image.Get<bool?>("Eevee_Enable");

            if(enable != null && (bool)enable) {

                Engine.Graphics.GraphicsDevice.Textures[1] = null;

                Effect fxColorGrading = GFX.FxColorGrading;
                fxColorGrading.CurrentTechnique = fxColorGrading.Techniques["ColorGradeSingle"];

                DynData<SpriteBatch> spriteData = new DynData<SpriteBatch>(Draw.SpriteBatch);
                Matrix matrix = (Matrix)spriteData["transformMatrix"];


                Engine.Graphics.GraphicsDevice.Textures[1] = palette.colorGrade;
                Draw.SpriteBatch.End();
                Draw.SpriteBatch.Begin(SpriteSortMode.Deferred, BlendState.AlphaBlend, SamplerState.PointWrap, DepthStencilState.None, RasterizerState.CullNone, fxColorGrading, matrix);

                orig(self);

                Draw.SpriteBatch.End();
                Draw.SpriteBatch.Begin(SpriteSortMode.Deferred, BlendState.AlphaBlend, SamplerState.PointWrap, DepthStencilState.None, RasterizerState.CullNone, null, matrix);
            } else if(pet && Settings.overwritePet) {
                enable = image.Get<bool?>("Custom_Enable");

                if(enable != null && (bool)enable) {

                    Engine.Graphics.GraphicsDevice.Textures[1] = null;

                    Effect fxColorGrading = GFX.FxColorGrading;
                    fxColorGrading.CurrentTechnique = fxColorGrading.Techniques["ColorGradeSingle"];

                    DynData<SpriteBatch> spriteData = new DynData<SpriteBatch>(Draw.SpriteBatch);
                    Matrix matrix = (Matrix)spriteData["transformMatrix"];


                    Engine.Graphics.GraphicsDevice.Textures[1] = miniEevee.colorGrade;
                    Draw.SpriteBatch.End();
                    Draw.SpriteBatch.Begin(SpriteSortMode.Deferred, BlendState.AlphaBlend, SamplerState.PointWrap, DepthStencilState.None, RasterizerState.CullNone, fxColorGrading, matrix);

                    orig(self);

                    Draw.SpriteBatch.End();
                    Draw.SpriteBatch.Begin(SpriteSortMode.Deferred, BlendState.AlphaBlend, SamplerState.PointWrap, DepthStencilState.None, RasterizerState.CullNone, null, matrix);

                } else {
                    orig(self);
                }
            } else {
                orig(self);
            }
        }

        public void EeveeBeforeErrorHandler(ref SpriteSortMode sortMode, ref BlendState blendState, ref SamplerState samplerState, ref DepthStencilState depthStencilState, ref RasterizerState rasterizerState, ref Effect effect, ref Matrix matrix) {
            if (CheckSMHPlus()) { 
            Engine.Graphics.GraphicsDevice.Textures[1] = null;

            Effect fxColorGrading = GFX.FxColorGrading;
            fxColorGrading.CurrentTechnique = fxColorGrading.Techniques["ColorGradeSingle"];

            EeveeModule.Instance.palette.writeColor(Settings.fluff, 1);
            Engine.Graphics.GraphicsDevice.Textures[1] = EeveeModule.Instance.palette.colorGrade;
            effect = fxColorGrading;
            }
        }
        
        //if you struggled like me to get the hair working with the feather, remember to use playerSprite.CreateFramesMetadata
        //due to how smh+ handles skin, if you are making a hibrid skinmod (codemod + smh+), this function becomes obsolete
        public void onCelesteSpriteEeveeConstructor(On.Celeste.PlayerSprite.orig_ctor orig,PlayerSprite self, PlayerSpriteMode mode) {

            orig(self,mode);
            /*
            bool SMHenabled = CheckSMHPlus();
            if (SMHenabled){
                switch (mode) {
                    case PlayerSpriteMode.Madeline:
                        GFX.SpriteBank.CreateOn(self, "eevee");
                        PlayerSprite.CreateFramesMetadata("eevee");
                        break;
                    case PlayerSpriteMode.MadelineNoBackpack:
                        GFX.SpriteBank.CreateOn(self,"eevee_no_backpack");
                        PlayerSprite.CreateFramesMetadata("eevee_no_Backpack");
                        break;
                    case PlayerSpriteMode.Playback:
                        GFX.SpriteBank.CreateOn(self, "eevee_playback");
                        PlayerSprite.CreateFramesMetadata("eevee_playback");
                        break;
                }
            }
            */

        }
        public void onCelesteEeveeRender(On.Celeste.Player.orig_Render orig, Player self) {
            if(CheckSMHPlus()) {



                dash = self.Dashes;

                if(cNet && (dash != updateDash)) {
                    Settings.updatePaletteCelestenet();
                    updateDash = dash;
                }

                DynData<Player> player = new DynData<Player>(self);
                bool flash = player.Get<bool>("flash");
                bool isTired = player.Get<bool>("IsTired");

                Engine.Graphics.GraphicsDevice.Textures[1] = null;

                FxEeveeGrading.CurrentTechnique = FxEeveeGrading.Techniques["ColorGrade"];

                DynData<SpriteBatch> spriteData = new DynData<SpriteBatch>(Draw.SpriteBatch);
                Matrix matrix = (Matrix)spriteData["transformMatrix"];
                if(!Settings.dashEnabled) {
                    //palette.CustomDash(self.Hair.Color);

                    Color DashAccent = self.Hair.Color;
                    DashAccent.R = (Byte)(DashAccent.R > 50 ? DashAccent.R - 50 : 0);
                    DashAccent.G = (Byte)(DashAccent.G > 50 ? DashAccent.G - 50 : 0);
                    DashAccent.B = (Byte)(DashAccent.B > 50 ? DashAccent.B - 50 : 0);

                    Color DashDarkerAccent = self.Hair.Color;
                    DashDarkerAccent.R = (Byte)(DashDarkerAccent.R > 100 ? DashDarkerAccent.R - 100 : 0);
                    DashDarkerAccent.G = (Byte)(DashDarkerAccent.G > 100 ? DashDarkerAccent.G - 100 : 0);
                    DashDarkerAccent.B = (Byte)(DashDarkerAccent.B > 100 ? DashDarkerAccent.B - 100 : 0);

                    Settings.fluff.fluff1[Calc.Clamp(0, 4, self.Dashes)] = self.Hair.Color;
                    Settings.fluff.fluff2[Calc.Clamp(0, 4, self.Dashes)] = DashAccent;
                    if(cNet) {
                        Settings.updatePaletteCelestenet();
                    }
                }

                palette.writeColor(Settings.fluff, Math.Min(Math.Max(self.Dashes, 0), 4));
                
                if(self.MaxDashes == 0) {
                    palette.writeColor(Settings.fluff, 1);
                }
                //if (!Settings.dashEnabled) {
                //    palette.CustomDash(self.Hair.Color);
                //}


                Engine.Graphics.GraphicsDevice.Textures[1] = palette.colorGrade;
                Draw.SpriteBatch.End();
                Draw.SpriteBatch.Begin(SpriteSortMode.Deferred, BlendState.AlphaBlend, SamplerState.PointWrap, DepthStencilState.None, RasterizerState.CullNone, FxEeveeGrading , matrix);

                orig(self);

                Draw.SpriteBatch.End();
                Draw.SpriteBatch.Begin(SpriteSortMode.Deferred, BlendState.AlphaBlend, SamplerState.PointWrap, DepthStencilState.None, RasterizerState.CullNone, null, matrix);


            } else if(CheckHateline()) {
                dash = Settings.hatDash;

                Engine.Graphics.GraphicsDevice.Textures[1] = null;

                Effect fxColorGrading = GFX.FxColorGrading;
                fxColorGrading.CurrentTechnique = fxColorGrading.Techniques["ColorGradeSingle"];

                FxEeveeGrading.CurrentTechnique = FxEeveeGrading.Techniques["ColorGrade"];

                DynData<SpriteBatch> spriteData = new DynData<SpriteBatch>(Draw.SpriteBatch);
                Matrix matrix = (Matrix)spriteData["transformMatrix"];
                
                    palette.writeColor(Settings.fluff, Settings.hatDash);
               

                //if (!Settings.dashEnabled) {
                //    palette.CustomDash(self.Hair.Color);
                //}


                Engine.Graphics.GraphicsDevice.Textures[1] = palette.colorGrade;
                Draw.SpriteBatch.End();
                Draw.SpriteBatch.Begin(SpriteSortMode.Deferred, BlendState.AlphaBlend, SamplerState.PointWrap, DepthStencilState.None, RasterizerState.CullNone,  fxColorGrading, matrix);

                orig(self);

                Draw.SpriteBatch.End();
                Draw.SpriteBatch.Begin(SpriteSortMode.Deferred, BlendState.AlphaBlend, SamplerState.PointWrap, DepthStencilState.None, RasterizerState.CullNone, null, matrix);

            } else {
                orig(self);
            }
            if(cNet && (updateSkin != CheckSMHPlus())) {
                Settings.updatePaletteCelestenet();
                updateSkin = CheckSMHPlus();
            }
        }
        public void OnCelesteEeveeHair(On.Celeste.Player.orig_UpdateHair orig, Player self, bool applyGravity) {

            orig(self, applyGravity);

            if(Settings.dashEnabled && self.StateMachine.State != Player.StStarFly && CheckSMHPlus()) {
                self.Hair.Color = Settings.fluff.dash[Math.Min(Math.Max(self.Dashes, 0), 4)];
            }       
            
        }
        public Color OnCelesteEeveeTrail(On.Celeste.Player.orig_GetCurrentTrailColor orig, Player self) {

            if (Settings.dashEnabled && CheckSMHPlus()) { 
                return Settings.fluff.dash[Math.Min(Math.Max(self.Dashes,0), 4)];
            }
            
            return orig(self);
        }
        //taken from the Avali skin, changes the pixel's color when dashing, for more info on how it works i recommend checking it out ^^
        private int onEeveeDashUpdate(On.Celeste.Player.orig_DashUpdate orig, Player self) {
            if (Settings.dashEnabled && CheckSMHPlus()) {
                Color color = Settings.fluff.dash[Math.Min(Math.Max(self.Dashes,0),4)];

                ParticleType bakDashA = Player.P_DashA;
                ParticleType bakDashB = Player.P_DashB;
                ParticleType bakDashBadB = Player.P_DashBadB;

                Player.P_DashA = new ParticleType(Player.P_DashA) {
                    Color = color,
                    Color2 = color,
                };
                Player.P_DashB = new ParticleType(Player.P_DashA);
                Player.P_DashBadB = new ParticleType(Player.P_DashA);

                int result = orig(self);

                Player.P_DashA = bakDashA;
                Player.P_DashB = bakDashB;
                Player.P_DashBadB = bakDashBadB;

                return result;
            } else {
                return orig(self);
            }
        }
        //Similar to onCelesteSpriteEeveeConstructor, whenever the functions "Create" or "CreateOn" are called, we ask if the id can be found on the mod's Sprite.xml file
        //maybe it could have been a better idea to instead change the object's constructor than this, but here we can change almost all sprites just by adding them to
        //the Sprite.xml file.
        //One way to appy the colorgrades to modded animation without needing to hook thos mods is to add an special tag to the sprite.xml, the problem is
        //that this make more complex this function, but the ability to appla such colorgrades outweight this issue imo.

        //This function also works for changing the file selection screen portraits, so keep that in mind when deciding where to add the tag.

        //Custom variable works to add a different palette to the strayberry friend mod
        private Sprite onMonocleEeveeCreate(On.Monocle.SpriteBank.orig_Create orig, SpriteBank self, string id) {
            string eeveeid = "eevee_" + id;
            string eeveePaletteid = "eevee_palette_" + id;
            string custom = "custom_palette_" + id;

            string portrait =  "";
            if((Settings.portraitMode == 0 && Settings.InternalOpt == 9 && Settings.Selection == 0) || Settings.portraitMode == 2) {
                portrait = id + "_test";
            } else {
                portrait = id + "_eevee";
            }


            if(pet && self.SpriteData.ContainsKey(custom)) {
                string oldId = id;
                id = custom;
                if(Settings.updatePet) {
                    miniEevee.writeColor(Settings.petFluff, Settings.PetState);
                    Settings.updatePet = false;
                }

                Sprite dummy = orig(self, id);
                DynamicData test = DynamicData.For((Object)dummy);


                if(!test.TryGet("Custom_Enable", out var _)) {
                    test.Set("Custom_Enable", Settings.overwritePet);
                }
                if(Settings.overwritePet) {

                    return dummy;
                }
                id = oldId;
            }

            bool active = CheckSMHPlus();

            if(self.SpriteData.ContainsKey(eeveeid) && active) {
                id = eeveeid;

            } else if(self.SpriteData.ContainsKey(eeveePaletteid) && active) {
                id = eeveePaletteid;
                Sprite dummy = orig(self, id);
                DynamicData test = DynamicData.For((Object)dummy);


                if(!test.TryGet("Eevee_Enable", out var _)) {
                    test.Set("Eevee_Enable", active);
                }

                return dummy;

            }

            if(self.SpriteData.ContainsKey(portrait) && active) {
                id = portrait;

            }
            return orig(self,id);
        }

        private Sprite onMonocleEeveeCreateOn(On.Monocle.SpriteBank.orig_CreateOn orig, SpriteBank self, Sprite sprite, string id) {
            string eeveeid = "eevee_" + id;

            if (self.SpriteData.ContainsKey(eeveeid) && CheckSMHPlus()) {
                id = eeveeid;
            }

            return orig(self,sprite, id);
        }
        private void onCelesteEeveeDeadbody(On.Celeste.PlayerDeadBody.orig_Render orig, PlayerDeadBody self) {
            DynData<PlayerDeadBody> deadBody = new DynData<PlayerDeadBody>(self);
            int dashes = deadBody.Get<Player>("player").Dashes;
            Player player = deadBody.Get<Player>("player");
            Color deadColor = Settings.fluff.dash[Math.Min(Math.Max(dashes, 0), 4)];

            if (CheckSMHPlus()) {

                Engine.Graphics.GraphicsDevice.Textures[1] = null;
                Effect fxColorGrading = GFX.FxColorGrading;
                fxColorGrading.CurrentTechnique = fxColorGrading.Techniques["ColorGradeSingle"];

                DynData<SpriteBatch> spriteData = new DynData<SpriteBatch>(Draw.SpriteBatch);
                Matrix matrix = (Matrix)spriteData["transformMatrix"];

                palette.writeColor(Settings.fluff, Math.Min(Math.Max(dashes,0), 4));
                
                if (!Settings.dashEnabled) {
                    palette.CustomDash(deadColor);
                }

                Engine.Graphics.GraphicsDevice.Textures[1] = palette.colorGrade;
                Draw.SpriteBatch.End();
                Draw.SpriteBatch.Begin(SpriteSortMode.Deferred, BlendState.AlphaBlend, SamplerState.PointWrap, DepthStencilState.None, RasterizerState.CullNone, fxColorGrading, matrix);

                orig(self);

                Draw.SpriteBatch.End();
                Draw.SpriteBatch.Begin(SpriteSortMode.Deferred, BlendState.AlphaBlend, SamplerState.PointWrap, DepthStencilState.None, RasterizerState.CullNone, null, matrix);
                return;
            } else {
                orig(self);
            }

        }
        //This function were made by SMH24, so special thanks to them for their help ^^
        private void onEeveeSuperJump(On.Celeste.Player.orig_SuperJump orig, Player self) {
            orig(self);
            if(CheckSMHPlus() && Settings.useSuperJumpSprite > 0) {
                if(Settings.useSuperJumpSprite == 2 || rand.Chance(0.05f)) {
                    self.Sprite.Play("extra_jumpSuper");
                }
            }
        }
        private void onEeveeSuperWallJump(On.Celeste.Player.orig_SuperWallJump orig, Player self, int dir) {
            orig(self, dir);
            if(CheckSMHPlus() && Settings.useWallBounceSprite > 0) {
                if(Settings.useWallBounceSprite == 2 || rand.Chance(0.05f)) {
                    self.Sprite.Play("extra_wallBounce");
                }
            }
        }
        private void ilEeveeUpdateSprite(ILContext il) {
            ILCursor cursor = new(il);
            if(cursor.TryGotoNext(MoveType.After, instr => instr.MatchLdstr("duck"))) { // first instance is for demo specifically
                cursor.Emit(OpCodes.Ldarg_0);
                cursor.EmitDelegate(changeDemoAnim);
            }
            if(cursor.TryGotoNext(MoveType.After, instr => instr.MatchLdstr("dash"))) {
                cursor.Emit(OpCodes.Ldarg_0);
                cursor.EmitDelegate(changeDashAnim);
            }
        }
        private static string changeDemoAnim(string anim, Player player) {
            // extra DashAttacking check not strictly necessary
            // but if the IL match ever messes up and gets the other one for some reason it's probably good to have
            if(CheckSMHPlus() && Settings.useDemoDashSprite && player.DashAttacking) {
                anim = "extra_dashCrouch";
            }
            return anim;
        }
        private static string changeDashAnim(string anim, Player player) {
            if(CheckSMHPlus() && Settings.useDirectionalDashSprites) {
                if(Math.Abs(player.DashDir.X) < 0.3f) {
                    if(player.DashDir.Y <= -0.5f) {
                        anim = "extra_dash_Up";
                    } else if(player.DashDir.Y >= 0.5f) {
                        anim = "extra_dash_Down";
                    }
                } else {
                    if(player.DashDir.Y <= -0.5f) {
                        anim = "extra_dash_SideUp";
                    } else if(player.DashDir.Y >= 0.5f) {
                        anim = "extra_dash_SideDown";
                    }
                }
            }
            return anim;
        }

        //we cant access the render function of the tutorial's Madeline, so we have to implement it from scratch
        //This way the palette can be aplied to it
        private void onCelesteEeveeTutorial(On.Celeste.WaveDashPlaybackTutorial.orig_Render orig, WaveDashPlaybackTutorial self, Vector2 position, float scale) {

            DynData<WaveDashPlaybackTutorial> tutorial = new DynData<WaveDashPlaybackTutorial>(self);
            int tag = tutorial.Get<int>("tag");
            bool hasUpdated = tutorial.Get<bool>("hasUpdated");
            PlayerPlayback Playback = tutorial.Get<PlayerPlayback>("Playback");

            if (CheckSMHPlus()) {
                Engine.Graphics.GraphicsDevice.Textures[1] = null;

                Effect fxColorGrading = GFX.FxColorGrading;
                fxColorGrading.CurrentTechnique = fxColorGrading.Techniques["ColorGradeSingle"];

                Engine.Graphics.GraphicsDevice.Textures[1] = palette.colorGrade;
                Matrix transformationMatrix = Matrix.CreateScale(4f) * Matrix.CreateTranslation(position.X, position.Y, 0f);
                Draw.SpriteBatch.End();
                Draw.SpriteBatch.Begin(SpriteSortMode.Deferred, BlendState.AlphaBlend, SamplerState.PointClamp, null, null, fxColorGrading, transformationMatrix);
                foreach (Entity entity in Engine.Scene.Tracker.GetEntities<TrailManager.Snapshot>()) {
                    if (entity.Tag == tag) {
                        entity.Render();
                    }
                }
                foreach (Entity entity2 in Engine.Scene.Tracker.GetEntities<SlashFx>()) {
                    if (entity2.Tag == tag && entity2.Visible) {
                        entity2.Render();
                    }
                }
                foreach (Entity entity3 in Engine.Scene.Tracker.GetEntities<SpeedRing>()) {
                    if (entity3.Tag == tag) {
                        entity3.Render();
                    }
                }
                if (Playback.Visible && hasUpdated) {
                    Playback.Render();
                }
                if (self.OnRender != null) {
                    self.OnRender();
                }
                Draw.SpriteBatch.End();
                Draw.SpriteBatch.Begin();
                if (Playback.Visible && hasUpdated) {
                    Playback.Render();
                }

            } else {
                orig(self, position, scale);

            }
        }
        private void onCelesteEeveeTutorialTrail(On.Celeste.WaveDashPlaybackTutorial.orig_CreateTrail orig, WaveDashPlaybackTutorial self) {
            DynData<WaveDashPlaybackTutorial> tutorial = new DynData<WaveDashPlaybackTutorial>(self);
            int tag = tutorial.Get<int>("tag");
            PlayerPlayback Playback = tutorial.Get<PlayerPlayback>("Playback");

            if (CheckSMHPlus()) {
                TrailManager.Add(Playback.Position, Playback.Sprite, Playback.Hair, Playback.Sprite.Scale,Settings.fluff.dash[0], 0).Tag = tag;
            } else {
                orig(self);
            }
        }
        // CelesteNet must be loaded when calling this function, this code was taken and adapted from the AvaliSkin
        private void onPlayerSpriteRenderCelestenet(On.Celeste.PlayerSprite.orig_Render orig, PlayerSprite self) {
            CelesteNetClient client = CelesteNetClientModule.Instance.Client;
            Ghost ghost;
            EeveeConfig config;
            if (self.Scene != null && self.Entity != null && self.Entity is Ghost ghost2 && ghost2.PlayerInfo != null && client != null  && client.Data.TryGetBoundRef<DataPlayerInfo, DataPlayerEeveeSkin>(
                    ghost2.PlayerInfo.ID,
                    out DataPlayerEeveeSkin data
                ) && data != null && data.Config.IsEnabled(ghost2)  ) {
                ghost = ghost2;
                config = data.Config;
            } else if (false && self.Scene != null && self.Entity != null && self.Entity is Ghost ghost3 && AllEeveeConfig.IsEnabled(ghost3) ) {
                ghost = ghost3;
                //Logger.Log(LogLevel.Info, "ALL Eevee IN", $"Testing if entering");
                config = AllEeveeConfig;
            } else if ( self.Scene != null && self.Entity != null && self.Entity is Ghost ) {
                trySwapSkin(self, false);
                orig(self);
                return;
            } else {
                orig(self);
                return;
            }
            if (self.Color.A < .8) {
                self.Color *= 1f / (self.Color.A + 0.00001f);
            }
            //somehow the all eevee option is broken so i removed the option, im not completely erasing it in case i deacide to give it a go in the future
            //if (config.palette == null) {
            //    config = AllEeveeConfig;
            //}
            trySwapSkin(self,true);

            Engine.Graphics.GraphicsDevice.Textures[1] = null;
            FxEeveeGrading.CurrentTechnique = FxEeveeGrading.Techniques["ColorGrade"];


            DynData<SpriteBatch> spriteData = new DynData<SpriteBatch>(Draw.SpriteBatch);
            Matrix matrix = (Matrix)spriteData["transformMatrix"];

            palette.writeColorNet(config.palette, Math.Min(Math.Max(config.Dash, 0), 4));
            //palette.writeColorNet(config.palette, config.Dash);

            Engine.Graphics.GraphicsDevice.Textures[1] = palette.colorGrade;
 
            Draw.SpriteBatch.End();
            Draw.SpriteBatch.Begin(SpriteSortMode.Deferred, BlendState.AlphaBlend, SamplerState.PointWrap, DepthStencilState.None, RasterizerState.CullNone, FxEeveeNetGrading, matrix);

            orig(self);

            Draw.SpriteBatch.End();
            Draw.SpriteBatch.Begin(SpriteSortMode.Deferred, BlendState.AlphaBlend, SamplerState.PointWrap, DepthStencilState.None, RasterizerState.CullNone, null, matrix);

        }
        //Function taken from the Avaliskin, with this one we can allow the player to use another skin with cNet functionability, otherwise everything would be Madeline
        //These fucntion are obsolete if you are making a skin compatible with smh+, still gonna elave them cuz im sure they are really importat for when tinting the celestenet players :v
        public void trySwapSkin(PlayerSprite sprite, bool enable) {
            DynamicData dd = DynamicData.For(sprite);
            bool oldEnabled = false;
            if (dd.TryGet<bool?>("eeveeskin_enabled", out bool? ddoldEnabled)) {
                oldEnabled = (bool)ddoldEnabled;
            }
            if (oldEnabled != enable) {
                string spriteId = "";
                switch (sprite.Mode) {
                    case PlayerSpriteMode.Madeline:
                        spriteId = enable ? "player":"player";
                        break;
                    case PlayerSpriteMode.MadelineNoBackpack:
                        spriteId = enable ? "player_no_backpack" : "player_no_backpack";
                        break;
                    case PlayerSpriteMode.Playback:
                        spriteId = enable ? "player_playback" : "player_playback";
                        break;
                    default: return;
                }

                dd.Set("eeveeskin_enabled", (bool?)enable);

                Vector2 pos = sprite.Position;
                Color color = sprite.Color;
                string currentAnimationID2 = sprite.CurrentAnimationID;
                int currentAnimationFrame2 = sprite.CurrentAnimationFrame;

                GFX.SpriteBank.CreateOn(sprite, spriteId);

                sprite.Position = pos;
                sprite.SetColor(color);
                if (currentAnimationID2 != "") {
                    sprite.Play(currentAnimationID2);
                    sprite.SetAnimationFrame(currentAnimationFrame2);
                }
            }
        }

    }

}
