using System.Reflection;
using System.Collections.Generic;
using System;
using Barotrauma;
using Barotrauma.Sounds;
using Barotrauma.Extensions;
using Barotrauma.Items.Components;
using Barotrauma.Networking;
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Graphics;
using System.Linq;

namespace BetterHealthUI {
    partial class BetterHealthUIMod {

        private int healthGUIRefreshTimer = 0;
        public static bool NeurotraumaEnabled = false;
        private static Random rnd = new Random();
        private static CharacterHealth prevOpenHealthWindow = null;

        public void InitClient() {

            // Check if Neurotrauma is enabled
            foreach (ContentPackage package in ContentPackageManager.EnabledPackages.All)
            {
                if (package.NameMatches("Neurotrauma"))
                {
                    NeurotraumaEnabled = true;
                    break;
                }
            }
            if (NeurotraumaEnabled)
            {
                ecgCurve = new CCurve();
                ecgCurve.Keys.Add(new CCurveKey(0,0));           // start
                ecgCurve.Keys.Add(new CCurveKey(0.1f,-0.1f));    // first low
                ecgCurve.Keys.Add(new CCurveKey(0.2f,1f));       // high
                ecgCurve.Keys.Add(new CCurveKey(0.3f,-0.3f));    // second low
                ecgCurve.Keys.Add(new CCurveKey(0.4f, 0f));      // end

                ecgCurveFib = new CCurve();
                ecgCurveFib.Keys.Add(new CCurveKey(0, -0.9f));    
                ecgCurveFib.Keys.Add(new CCurveKey(0.05f, -0.2f));
                ecgCurveFib.Keys.Add(new CCurveKey(0.1f, 0.3f));  
                ecgCurveFib.Keys.Add(new CCurveKey(0.15f, -0.2f));
                ecgCurveFib.Keys.Add(new CCurveKey(0.2f, -0.9f)); 
            }

            //LuaCsSetup.PrintCsMessage("BetterHealthUIMod.InitClient");
            foreach (Character character in Character.CharacterList)
            {
                CharacterHealth charHealth = character.CharacterHealth;
                ForceCustomized(charHealth);
            }

            void ForceCustomized(CharacterHealth selfHealth)
            {
                GUIListBox afflictionIconContainer = (GUIListBox)((typeof(CharacterHealth).GetField("afflictionIconList", BindingFlags.NonPublic | BindingFlags.Instance)).GetValue(selfHealth));

                var afflictionIconContainer2 = afflictionIconContainer != null ? (afflictionIconContainer.Parent.GetChildByUserData("afflictionIconContainer2")) : null;
                if (afflictionIconContainer2 == null && afflictionIconContainer!= null)
                {
                    // we havent added our custom stuff yet, do it NOW!
                    LuaCsSetup.PrintCsMessage("Forcing customization on " + selfHealth.Character.Name);

                    Dictionary<string, object> args = new Dictionary<string, object>();
                    args.Add("character", selfHealth.Character);
                    InitProjSpecific(selfHealth, args);
                }
            }

            // health window init
            // changes dimensions of the health window
            // changes job icon, character portrait and name layout and color
            GameMain.LuaCs.Hook.HookMethod("BetterHealthUIMod_InitProjSpecific",
                typeof(CharacterHealth).GetMethod("InitProjSpecific", BindingFlags.Instance | BindingFlags.NonPublic),
                (object self, Dictionary<string, object> args) => {
                    InitProjSpecific(self, args);
                    return null;
                }, LuaCsHook.HookMethodType.After, this);
            void InitProjSpecific(object self, Dictionary<string, object> args)
            {
                #region Reflection crap
                // get arguments
                Character character = (Character)(args["character"]);
                // get members
                CharacterHealth selfHealth = (CharacterHealth)self;
                GUITextBlock characterName = (GUITextBlock)(typeof(CharacterHealth).GetField("characterName", BindingFlags.NonPublic | BindingFlags.Instance).GetValue(self));
                GUIFrame healthWindow = (GUIFrame)(typeof(CharacterHealth).GetField("healthWindow", BindingFlags.NonPublic | BindingFlags.Instance).GetValue(self));
                GUIListBox afflictionIconContainer = (GUIListBox)(typeof(CharacterHealth).GetField("afflictionIconList", BindingFlags.NonPublic | BindingFlags.Instance).GetValue(self));
                #endregion

                var healthWindowVerticalLayout = (GUILayoutGroup)(healthWindow.GetChild(0));
                var characterIndicatorArea = (GUILayoutGroup)(healthWindowVerticalLayout.GetChild(3));

                // set size of health window
                healthWindow.RectTransform.RelativeSize = new Vector2(0.8f, 0.6f);

                // clear portrait, name and icon
                var topContainer = characterName.RectTransform.Parent;
                topContainer.ClearChildren();

                // create own layout
                topContainer.Parent.RelativeSize = new Vector2(0.975f, 0.95f);

                // job icon
                var jobIcon = new GUICustomComponent(new RectTransform(new Vector2(0.2f, 1.0f), topContainer),
                    onDraw: (spriteBatch, component) =>
                    {
                        character.Info?.DrawJobIcon(spriteBatch, component.Rect, character != Character.Controlled);
                    });
                //jobIcon.RectTransform.RelativeSize *= 1f;
                //jobIcon.RectTransform.SetPosition(Anchor.TopLeft);

                // char portrait
                var characterPortrait = new GUICustomComponent(new RectTransform(new Vector2(0.2f, 1.0f), topContainer, Anchor.CenterLeft),
                    onDraw: (spriteBatch, component) =>
                    {
                        character.Info?.DrawPortrait(spriteBatch, new Vector2(component.Rect.X, component.Rect.Center.Y - component.Rect.Width / 2), new Vector2(-40, 15), component.Rect.Width, false, character != Character.Controlled);
                    });
                characterPortrait.RectTransform.RelativeSize *= 0.7f;
                //characterPortrait.RectTransform.Anchor = Anchor.TopLeft;
                //characterPortrait.RectTransform.RelativeOffset = new Vector2(0f, 0.3f);

                // char name
                characterName = new GUITextBlock(new RectTransform(new Vector2(0.6f, 1f), topContainer, anchor: Anchor.CenterLeft), "", textAlignment: Alignment.CenterLeft, font: GUIStyle.LargeFont)
                {
                    AutoScaleHorizontal = true,
                    AutoScaleVertical = true
                };
                characterName.TextOffset = new Vector2(-50, 0);

                // color the name according to the job
                if (character.Info?.Job?.Prefab.UIColor != null)
                    characterName.TextColor = character.Info.Job.Prefab.UIColor;

                // resize the gene splicer slot
                selfHealth.InventorySlotContainer.RectTransform.RelativeSize *= 0.5f;

                // resize the cpr button
                var cprButton = (GUIButton)(characterIndicatorArea.GetChild(1));
                cprButton.RectTransform.RelativeSize = new Vector2(0.17f, 0.17f);

                // resize the limb man area
                var limbSelection = (GUICustomComponent)(characterIndicatorArea.GetChild(2));
                limbSelection.RectTransform.RelativeSize = new Vector2(0.3f, 1.0f);

                // resize the first affliction info list (non-buff)
                afflictionIconContainer.RectTransform.RelativeSize = new Vector2(0.4f, 1);
                // create the second affliction info list (buff)
                var afflictionIconContainer2 = new GUIListBox(new RectTransform(new Vector2(0.4f, 1.0f), characterIndicatorArea.RectTransform), style: null)
                {
                    UserData = "afflictionIconContainer2",
                };

                // not sure why i need to do this twice but if i dont then the inventoryslot isnt at the right place
                // the first time the health window is opened
                // nvm, even then it doesnt if in singleplayer
                characterIndicatorArea.Recalculate();

                // Neurotrauma specific elements
                if (NeurotraumaEnabled  && !BetterHealthUIMod.DisableHeartbeatBar) 
                {
                    var children = healthWindow.RectTransform.Children;
                    List<RectTransform> newChildren = new List<RectTransform>();
                    
                    var graphArea = new GUILayoutGroup(new RectTransform(new Vector2(0.5f, 0.15f), healthWindow.RectTransform, anchor : Anchor.TopRight))
                    {
                        Stretch = true,
                        RelativeSpacing = 0.02f,
                    };

                    // rearrange the health windows children so that the graph gets rendered behind the name
                    newChildren.Add(graphArea.RectTransform);
                    foreach(RectTransform child in healthWindow.RectTransform.Children)
                    {
                        if (child == graphArea.RectTransform) continue;
                        newChildren.Add(child);
                    }

                    ((List<RectTransform>)(healthWindow.RectTransform.Children)).Clear();
                    foreach (RectTransform child in newChildren)
                    {
                        ((List<RectTransform>)(healthWindow.RectTransform.Children)).Add(child);
                    }

                    var graph = new GUIFrame(new RectTransform(new Vector2(1.0f, 0.9f), graphArea.RectTransform), style: "GUIFrameListBox");
                    new GUICustomComponent(new RectTransform(new Vector2(0.9f, 0.98f), graph.RectTransform, Anchor.Center), DrawGraph, null);

                    void DrawGraph(SpriteBatch spriteBatch, GUICustomComponent container)
                    {
                        //if (item.Removed) { return; }
                        float maxLoad = 100;// loadGraph.Max();
                        float xOffset = 0;// graphTimer / updateGraphInterval;
                        Rectangle graphRect = new Rectangle(container.Rect.X, container.Rect.Y, container.Rect.Width, container.Rect.Height - (int)(5 * GUI.yScale));
                        DrawHeartGraph(heartGraph, spriteBatch, graphRect, xOffset);
                    }

                    void DrawHeartGraph(IList<float> graph, SpriteBatch spriteBatch, Rectangle rect, float xOffset)
                    {
                        Color color = Color.Green;
                        const float maxVal = 2;

                        Rectangle prevScissorRect = spriteBatch.GraphicsDevice.ScissorRectangle;
                        spriteBatch.End();
                        spriteBatch.GraphicsDevice.ScissorRectangle = rect;
                        spriteBatch.Begin(SpriteSortMode.Deferred, rasterizerState: GameMain.ScissorTestEnable);

                        float lineWidth = (float)rect.Width / (float)(graph.Count - 2);
                        float yScale = (float)rect.Height / maxVal;

                        Vector2 prevPoint = new Vector2(rect.Left, rect.Bottom - (graph[1] + (graph[0] - graph[1]) * xOffset) * yScale);

                        float currX = rect.Left + ((xOffset - 1.0f) * lineWidth);

                        for (int i = 1; i < graph.Count - 1; i++)
                        {
                            float age = 1 - ((i - heartGraphProgress + graph.Count) % graph.Count) / (float)graph.Count;

                            currX += lineWidth;

                            Vector2 newPoint = new Vector2(currX, rect.Bottom - graph[i] * yScale);

                            color = age < 0.025 ? Color.White:Color.Lerp(Color.LimeGreen, Color.Black, age);
                            GUI.DrawLine(spriteBatch, prevPoint, newPoint + new Vector2(1.0f, 0), color,0,age < 0.025?4:2);

                            prevPoint = newPoint;
                        }

                        Vector2 lastPoint = new Vector2(rect.Right,
                            rect.Bottom - (graph[graph.Count - 1] + (graph[graph.Count - 2] - graph[graph.Count - 1]) * xOffset) * yScale);


                        GUI.DrawLine(spriteBatch, prevPoint, lastPoint, color);


                        spriteBatch.End();
                        spriteBatch.GraphicsDevice.ScissorRectangle = prevScissorRect;
                        spriteBatch.Begin(SpriteSortMode.Deferred);
                    }
                }

                // reflection apply
                typeof(CharacterHealth).GetField("characterName", BindingFlags.NonPublic | BindingFlags.Instance).SetValue(self, characterName);
            }

            GameMain.LuaCs.Hook.HookMethod("BetterHealthUIMod_UpdateOxygenProjSpecific",
            typeof(CharacterHealth).GetMethod("UpdateOxygenProjSpecific", BindingFlags.Instance | BindingFlags.NonPublic),
            (object self, Dictionary<string, object> args) => {
                // prevent heart sounds through low oxygen if neurotrauma is enabled
                if(NeurotraumaEnabled)
                {
                    if (CharacterHealth.OpenHealthWindow == null&& flatlineSoundChannel!=null)
                    {
                        flatlineSoundChannel.Dispose();
                        flatlineSoundChannel = null;
                    }
                    return true;
                }
                return null;
            }, LuaCsHook.HookMethodType.Before, this);

            // CreateRecommendedTreatments override
            // sets the max amount of displayed treatments to 10
            const int maxDisplayedSuitableTreatments = 10;
            GameMain.LuaCs.Hook.HookMethod("BetterHealthUIMod_CreateRecommendedTreatments",
                typeof(CharacterHealth).GetMethod("CreateRecommendedTreatments", BindingFlags.Instance | BindingFlags.NonPublic),
                (object self, Dictionary<string, object> args) => {
                    // get members
                    CharacterHealth selfHealth = (CharacterHealth)self;
                    GUIListBox recommendedTreatmentContainer = (GUIListBox)(typeof(CharacterHealth).GetField("recommendedTreatmentContainer", BindingFlags.NonPublic | BindingFlags.Instance).GetValue(self));
                    int selectedLimbIndex = (int)(typeof(CharacterHealth).GetField("selectedLimbIndex", BindingFlags.NonPublic | BindingFlags.Instance).GetValue(self));
                    //GUIListBox afflictionIconContainer = (GUIListBox)(typeof(CharacterHealth).GetField("afflictionIconContainer", BindingFlags.NonPublic | BindingFlags.Instance).GetValue(self));

                    // override begins
                    ItemPrefab prevHighlightedItem = null;
                    if (GUI.MouseOn?.UserData is ItemPrefab && recommendedTreatmentContainer.Content.IsParentOf(GUI.MouseOn))
                    {
                        prevHighlightedItem = (ItemPrefab)GUI.MouseOn.UserData;
                    }

                    recommendedTreatmentContainer.Content.ClearChildren();

                    float characterSkillLevel = Character.Controlled == null ? 0.0f : Character.Controlled.GetSkillLevel("medical");

                    //key = item identifier
                    //float = suitability
                    Dictionary<Identifier, float> treatmentSuitability = new Dictionary<Identifier, float>();
                    selfHealth.GetSuitableTreatments(treatmentSuitability,
                        ignoreHiddenAfflictions: true,
                        limb: selectedLimbIndex == -1 ? null : selfHealth.Character.AnimController.Limbs.Find(l => l.HealthIndex == selectedLimbIndex),
                        user : Character.Controlled);

                    foreach (Identifier treatment in treatmentSuitability.Keys.ToList())
                    {
                        //prefer suggestions for items the player has
                        if (Character.Controlled.Inventory.FindItemByIdentifier(treatment, recursive: true) != null)
                        {
                            treatmentSuitability[treatment] *= 10.0f;
                        }
                    }

                    if (!treatmentSuitability.Any())
                    {
                        new GUITextBlock(new RectTransform(Vector2.One, recommendedTreatmentContainer.Content.RectTransform), TextManager.Get("none"), textAlignment: Alignment.Center)
                        {
                            CanBeFocused = false
                        };
                        recommendedTreatmentContainer.ScrollBarVisible = false;
                        recommendedTreatmentContainer.AutoHideScrollBar = false;
                    }
                    else
                    {
                        recommendedTreatmentContainer.ScrollBarVisible = true;
                        recommendedTreatmentContainer.AutoHideScrollBar = true;
                    }

                    List<KeyValuePair<Identifier, float>> treatmentSuitabilities = treatmentSuitability.OrderByDescending(t => t.Value).ToList();

                    int count = 0;
                    foreach (KeyValuePair<Identifier, float> treatment in treatmentSuitabilities)
                    {
                        if (treatment.Value < 0) { continue; }
                        count++;
                        if (count > maxDisplayedSuitableTreatments) { break; }
                        if (!(MapEntityPrefab.Find(name: null, identifier: treatment.Key, showErrorMessages: false) is ItemPrefab item)) { continue; }

                        var itemSlot = new GUIFrame(new RectTransform(new Vector2(1.0f / (maxDisplayedSuitableTreatments+1.0f), 1.0f), recommendedTreatmentContainer.Content.RectTransform, Anchor.TopLeft),
                            style: null)
                        {
                            UserData = item
                        };

                        var innerFrame = new GUIButton(new RectTransform(Vector2.One, itemSlot.RectTransform, Anchor.Center, Pivot.Center, scaleBasis: ScaleBasis.Smallest), style: "SubtreeHeader")
                        {
                            UserData = item,
                            DisabledColor = Color.White * 0.1f,
                            OnClicked = (btn, userdata) =>
                            {
                                if (!(userdata is ItemPrefab itemPrefab)) { return false; }
                                var item = Character.Controlled.Inventory.FindItem(it => it.Prefab == itemPrefab, recursive: true);
                                if (item == null) { return false; }
                                Limb targetLimb = selfHealth.Character.AnimController.Limbs.FirstOrDefault(l => l.HealthIndex == selectedLimbIndex);
                                item.ApplyTreatment(Character.Controlled, selfHealth.Character, targetLimb);
                                return true;
                            }
                        };

                        new GUIImage(new RectTransform(Vector2.One, innerFrame.RectTransform, Anchor.Center), style: "TalentBackgroundGlow")
                        {
                            CanBeFocused = false,
                            Color = GUIStyle.Green,
                            HoverColor = Color.White,
                            PressedColor = Color.DarkGray,
                            SelectedColor = Color.Transparent,
                            DisabledColor = Color.Transparent
                        };

                        Sprite itemSprite = item.InventoryIcon ?? item.Sprite;
                        Color itemColor = itemSprite == item.Sprite ? item.SpriteColor : item.InventoryIconColor;
                        var itemIcon = new GUIImage(new RectTransform(new Vector2(0.8f, 0.8f), innerFrame.RectTransform, Anchor.Center),
                            itemSprite, scaleToFit: true)
                        {
                            CanBeFocused = false,
                            Color = itemColor * 0.9f,
                            HoverColor = itemColor,
                            SelectedColor = itemColor,
                            DisabledColor = itemColor * 0.8f
                        };

                        if (item == prevHighlightedItem)
                        {
                            innerFrame.State = GUIComponent.ComponentState.Hover;
                            innerFrame.Children.ForEach(c => c.State = GUIComponent.ComponentState.Hover);
                        }
                    }

                    recommendedTreatmentContainer.RecalculateChildren();

                    // got rid of this because it makes the afflictions spazz around
                    /*afflictionIconContainer.Content.RectTransform.SortChildren((r1, r2) =>
                    {
                        var first = r1.GUIComponent.UserData as Affliction;
                        var second = r2.GUIComponent.UserData as Affliction;
                        int dmgPerSecond = Math.Sign(second.DamagePerSecond - first.DamagePerSecond);
                        return dmgPerSecond != 0 ? dmgPerSecond : Math.Sign(second.Strength - first.Strength);
                    });*/

                    if (count > 0)
                    {
                        var treatmentIconSize = recommendedTreatmentContainer.Content.Children.Sum(c => c.Rect.Width + recommendedTreatmentContainer.Spacing);
                        if (treatmentIconSize < recommendedTreatmentContainer.Content.Rect.Width)
                        {
                            var spacing = new GUIFrame(new RectTransform(new Point((recommendedTreatmentContainer.Content.Rect.Width - treatmentIconSize) / 2, 0), recommendedTreatmentContainer.Content.RectTransform), style: null)
                            {
                                CanBeFocused = false
                            };
                            spacing.RectTransform.SetAsFirstChild();
                        }
                    }

                    return true;
                }, LuaCsHook.HookMethodType.Before, this);

            // CreateAfflictionInfoElements override
            // The method responsible for the little info box that appears when hovering over an affliction in the health interface
            GameMain.LuaCs.Hook.HookMethod("BetterHealthUIMod_CreateAfflictionInfoElements",
                typeof(CharacterHealth).GetMethod("CreateAfflictionInfoElements", BindingFlags.Instance | BindingFlags.NonPublic),
                (object self, Dictionary<string, object> args) => {
                    // get arguments
                    GUIComponent parent = (GUIComponent)(args["parent"]);
                    Affliction affliction = (Affliction)(args["affliction"]);

                    // get members
                    CharacterHealth selfHealth = (CharacterHealth)self;
                    //LocalizedString[] strengthTexts = (LocalizedString[])(typeof(CharacterHealth).GetField("strengthTexts", BindingFlags.NonPublic | BindingFlags.Static).GetValue(self));

                    // override begins
                    var labelContainer = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 0.2f), parent.RectTransform), isHorizontal: true)
                    {
                        Stretch = true,
                        AbsoluteSpacing = 10,
                        UserData = "label",
                        CanBeFocused = false
                    };

                    var afflictionName = new GUITextBlock(new RectTransform(new Vector2(0.65f, 1.0f), labelContainer.RectTransform), affliction.Prefab.Name, textAlignment: Alignment.CenterLeft, font: GUIStyle.LargeFont)
                    {
                        CanBeFocused = false,
                        AutoScaleHorizontal = true
                    };
                    var afflictionStrength = new GUITextBlock(new RectTransform(new Vector2(0.35f, 0.6f), labelContainer.RectTransform), "", textAlignment: Alignment.TopRight, font: GUIStyle.SubHeadingFont)
                    {
                        UserData = "strength",
                        CanBeFocused = false
                    };
                    var vitality = new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.4f), labelContainer.RectTransform, Anchor.BottomRight), "", textAlignment: Alignment.BottomRight)
                    {
                        Padding = afflictionStrength.Padding,
                        IgnoreLayoutGroups = true,
                        UserData = "vitality",
                        CanBeFocused = false
                    };

                    var description = new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.3f), parent.RectTransform),
                        affliction.Prefab.GetDescription(
                            affliction.Strength,
                            selfHealth.Character == Character.Controlled ? AfflictionPrefab.Description.TargetType.Self : AfflictionPrefab.Description.TargetType.OtherCharacter)
                        , textAlignment: Alignment.TopLeft, wrap: true)
                    {
                        CanBeFocused = false
                    };

                    if (description.Font.MeasureString(description.WrappedText).Y > description.Rect.Height)
                    {
                        description.Font = GUIStyle.SmallFont;
                    }

                    Point nameDims = new Point(afflictionName.Rect.Width, (int)(GUIStyle.LargeFont.Size * 1.5f));

                    afflictionStrength.Text = affliction.GetStrengthText();

                    Vector2 strengthDims = GUIStyle.SubHeadingFont.MeasureString(afflictionStrength.Text);

                    labelContainer.RectTransform.Resize(new Point(labelContainer.Rect.Width, nameDims.Y));
                    afflictionName.RectTransform.Resize(new Point((int)(labelContainer.Rect.Width - strengthDims.X * 0.99f), nameDims.Y));
                    afflictionStrength.RectTransform.Resize(new Point(labelContainer.Rect.Width - afflictionName.Rect.Width, nameDims.Y));

                    afflictionStrength.TextColor = Color.Lerp(GUIStyle.Orange, GUIStyle.Red,
                        affliction.Strength / affliction.Prefab.MaxStrength);

                    description.RectTransform.Resize(new Point(description.Rect.Width, (int)(description.TextSize.Y + 10)));

                    int vitalityDecrease = (int)affliction.GetVitalityDecrease(selfHealth);
                    if (vitalityDecrease == 0)
                    {
                        vitality.Visible = false;
                    }
                    else
                    {
                        vitality.Visible = true;
                        vitality.Text = TextManager.Get("Vitality") + " -" + vitalityDecrease;
                        vitality.TextColor = vitalityDecrease <= 0 ? GUIStyle.Green :
                        Color.Lerp(GUIStyle.Orange, GUIStyle.Red, affliction.Strength / affliction.Prefab.MaxStrength);
                    }

                    vitality.AutoDraw = true;

                    return true;
                }, LuaCsHook.HookMethodType.Before, this);

            // DrawHealthWindow override
            // makes it so more than the most severe affliction is displayed (like in legacy (good))
            // also responsible for the limbs color in the health interface
            Vector2 limbGuyOffset = new Vector2(0, 0);
            GameMain.LuaCs.Hook.HookMethod("BetterHealthUIMod_DrawHealthWindow",
                typeof(CharacterHealth).GetMethod("DrawHealthWindow", BindingFlags.Instance | BindingFlags.NonPublic),
                (object self, Dictionary<string, object> args) => {
                    // get arguments
                    SpriteBatch spriteBatch = (SpriteBatch)(args["spriteBatch"]);
                    Rectangle drawArea = (Rectangle)(args["drawArea"]);
                    bool allowHighlight = (bool)(args["allowHighlight"]);

                    DrawHealthWindow(spriteBatch, drawArea, allowHighlight,self);
                    return true;
                }, LuaCsHook.HookMethodType.Before, this);
            void DrawHealthWindow(SpriteBatch spriteBatch, Rectangle drawArea, bool allowHighlight,object self)
            {
                #region Reflection crap
                // get members
                CharacterHealth selfHealth = (CharacterHealth)self;
                ForceCustomized(selfHealth);
                //LocalizedString[] strengthTexts = (LocalizedString[])(typeof(CharacterHealth).GetField("strengthTexts", BindingFlags.NonPublic | BindingFlags.Static).GetValue(self));
                List<CharacterHealth.LimbHealth> limbHealths = (List<CharacterHealth.LimbHealth>)(typeof(CharacterHealth).GetField("limbHealths", BindingFlags.NonPublic | BindingFlags.Instance).GetValue(self));
                Dictionary<Affliction, CharacterHealth.LimbHealth> afflictions = (Dictionary<Affliction, CharacterHealth.LimbHealth>)(typeof(CharacterHealth).GetField("afflictions", BindingFlags.NonPublic | BindingFlags.Instance).GetValue(self));
                SpriteSheet limbIndicatorOverlay = (SpriteSheet)(typeof(CharacterHealth).GetField("limbIndicatorOverlay", BindingFlags.NonPublic | BindingFlags.Instance).GetValue(self));
                float limbIndicatorOverlayAnimState = (float)(typeof(CharacterHealth).GetField("limbIndicatorOverlayAnimState", BindingFlags.NonPublic | BindingFlags.Instance).GetValue(self));
                int highlightedLimbIndex = (int)(typeof(CharacterHealth).GetField("highlightedLimbIndex", BindingFlags.NonPublic | BindingFlags.Instance).GetValue(self));
                int selectedLimbIndex = (int)(typeof(CharacterHealth).GetField("selectedLimbIndex", BindingFlags.NonPublic | BindingFlags.Instance).GetValue(self));
                List<Affliction> afflictionsDisplayedOnLimb = (List<Affliction>)(typeof(CharacterHealth).GetField("afflictionsDisplayedOnLimb", BindingFlags.NonPublic | BindingFlags.Static).GetValue(self));
                GUIListBox afflictionIconContainer = (GUIListBox)(typeof(CharacterHealth).GetField("afflictionIconList", BindingFlags.NonPublic | BindingFlags.Instance).GetValue(self));
                GUIListBox afflictionIconContainer2 = (GUIListBox)(afflictionIconContainer.Parent.GetChildByUserData("afflictionIconContainer2"));
                #endregion
                // override begins

                // clear heart graph if the open window changed
                if (prevOpenHealthWindow!= CharacterHealth.OpenHealthWindow)
                {
                    prevOpenHealthWindow = CharacterHealth.OpenHealthWindow;
                    if (NeurotraumaEnabled)
                    {
                        for (int j = 0; j < heartGraph.Length; j++) heartGraph[j] = 0;
                        heartGraphProgress = 0;
                    }
                }

                if (selfHealth.Character.Removed) { return; }

                spriteBatch.End();
                spriteBatch.Begin(SpriteSortMode.Immediate, blendState: BlendState.NonPremultiplied, rasterizerState: GameMain.ScissorTestEnable, effect: GameMain.GameScreen.GradientEffect);

                int i = 0;
                foreach (CharacterHealth.LimbHealth limbHealth in limbHealths)
                {
                    if (limbHealth.IndicatorSprite == null) { continue; }

                    Rectangle limbEffectiveArea = new Rectangle(limbHealth.IndicatorSprite.SourceRect.X + limbHealth.HighlightArea.X,
                                                                limbHealth.IndicatorSprite.SourceRect.Y + limbHealth.HighlightArea.Y,
                                                                limbHealth.HighlightArea.Width,
                                                                limbHealth.HighlightArea.Height);

                    float totalDamage = GetTotalDamage(limbHealth, afflictions, selfHealth);

                    //float negativeEffect = 0.0f, positiveEffect = 0.0f;

                    List<object[]> bottomcolors = new List<object[]>();
                    bottomcolors.Add(new object[] { Color.Gray/*new Color(50 / 255f, 66 / 255f, 168 / 255f)*/, 0.02f });
                    List<object[]> topcolors = new List<object[]>();
                    topcolors.Add(new object[] { Color.Gray/*new Color(50/255f, 66/255f, 168/255f)*/, 0.02f });

                    float topstrength = 0.02f;
                    float bottomstrength = 0.02f;

                    foreach (KeyValuePair<Affliction, CharacterHealth.LimbHealth> kvp in afflictions)
                    {
                        var affliction = kvp.Key;
                        if (affliction.Prefab.LimbSpecific)
                        {
                            if (kvp.Value != limbHealth) continue;
                        }
                        else
                        {
                            LimbType displayedType = LimbHealthToLimbType(limbHealth, limbHealths, selfHealth.Character.AnimController.Limbs);
                            if (NormalizeLimbType(affliction.Prefab.IndicatorLimb) != NormalizeLimbType(displayedType)) continue;
                        }

                        if (!affliction.ShouldShowIcon(selfHealth.Character)) { continue; }

                        float strength = MathHelper.Lerp(0.2f, 1, MathHelper.Clamp(affliction.Strength / affliction.Prefab.MaxStrength * 2, 0, 1));

                        if (affliction.Prefab.IsBuff)
                        {
                            topcolors.Add(new object[] { CharacterHealth.GetAfflictionIconColor(affliction), strength });
                            topstrength += strength;
                        }
                        else
                        {
                            bottomcolors.Add(new object[] { CharacterHealth.GetAfflictionIconColor(affliction), strength });
                            bottomstrength += strength;
                        }
                    }

                    float midPoint = (float)(limbEffectiveArea.Center.Y - limbEffectiveArea.Height / 4) / (float)limbHealth.IndicatorSprite.Texture.Height;
                    float fadeDist = 0.3f * (float)limbEffectiveArea.Height / (float)limbHealth.IndicatorSprite.Texture.Height;

                    Color color1 = topstrength < bottomstrength ? AverageColor(bottomcolors) : Color.Lerp(AverageColor(topcolors), AverageColor(bottomcolors), bottomstrength);
                    Color color2 = Color.Lerp(color1, AverageColor(topcolors), topstrength);

                    GameMain.GameScreen.GradientEffect.Parameters["color1"].SetValue(color1.ToVector4());
                    GameMain.GameScreen.GradientEffect.Parameters["color2"].SetValue(color2.ToVector4());
                    GameMain.GameScreen.GradientEffect.Parameters["midPoint"].SetValue(midPoint);
                    GameMain.GameScreen.GradientEffect.Parameters["fadeDist"].SetValue(fadeDist);

                    float scale = Math.Min(drawArea.Width / (float)limbHealth.IndicatorSprite.SourceRect.Width, drawArea.Height / (float)limbHealth.IndicatorSprite.SourceRect.Height);

                    limbHealth.IndicatorSprite.Draw(spriteBatch,
                        drawArea.Center.ToVector2()+limbGuyOffset, Color.White,
                        limbHealth.IndicatorSprite.Origin,
                        0, scale);

                    if (GameMain.DebugDraw)
                    {
                        Rectangle highlightArea = GetLimbHighlightArea(limbHealth, drawArea,selfHealth);

                        GUI.DrawRectangle(spriteBatch, highlightArea, Color.Red, false);
                        GUI.DrawRectangle(spriteBatch, drawArea, Color.Red, false);
                    }

                    i++;
                }

                spriteBatch.End();

                spriteBatch.Begin(SpriteSortMode.Deferred, Barotrauma.Lights.CustomBlendStates.Multiplicative);

                if (limbIndicatorOverlay != null)
                {
                    float overlayScale = Math.Min(
                        drawArea.Width / (float)limbIndicatorOverlay.FrameSize.X,
                        drawArea.Height / (float)limbIndicatorOverlay.FrameSize.Y);

                    int frame;
                    int frameCount = 17;
                    if (limbIndicatorOverlayAnimState >= frameCount * 2) limbIndicatorOverlayAnimState = 0.0f;
                    if (limbIndicatorOverlayAnimState < frameCount)
                    {
                        frame = (int)limbIndicatorOverlayAnimState;
                    }
                    else
                    {
                        frame = frameCount - (int)(limbIndicatorOverlayAnimState - (frameCount - 1));
                    }

                    limbIndicatorOverlay.Draw(spriteBatch, frame, drawArea.Center.ToVector2()+ limbGuyOffset, Color.Gray, origin: limbIndicatorOverlay.FrameSize.ToVector2() / 2, rotate: 0.0f,
                        scale: Vector2.One * overlayScale);
                }

                if (allowHighlight)
                {
                    i = 0;
                    foreach (CharacterHealth.LimbHealth limbHealth in limbHealths)
                    {
                        if (limbHealth.HighlightSprite == null) { continue; }

                        float scale = Math.Min(drawArea.Width / (float)limbHealth.HighlightSprite.SourceRect.Width, drawArea.Height / (float)limbHealth.HighlightSprite.SourceRect.Height);

                        int drawCount = 0;
                        if (i == highlightedLimbIndex) { drawCount++; }
                        if (i == selectedLimbIndex) { drawCount++; }
                        for (int j = 0; j < drawCount; j++)
                        {
                            limbHealth.HighlightSprite.Draw(spriteBatch,
                                drawArea.Center.ToVector2()+limbGuyOffset, Color.White,
                                limbHealth.HighlightSprite.Origin,
                                0, scale);
                        }
                        i++;
                    }
                }
                spriteBatch.End();
                spriteBatch.Begin(SpriteSortMode.Deferred, blendState: BlendState.NonPremultiplied, rasterizerState: GameMain.ScissorTestEnable);

                // drawing the preview icons on the limbs
                i = 0;
                foreach (CharacterHealth.LimbHealth limbHealth in limbHealths)
                {
                    afflictionsDisplayedOnLimb.Clear();
                    int negativecount = 0;
                    int positivecount = 0;
                    int undrawncount = 0;
                    foreach (var affliction in afflictions)
                    {
                        if (ShouldDisplayAfflictionOnLimb(affliction, limbHealth,selfHealth,limbHealths))
                        {
                            if (affliction.Key.Prefab.IsBuff)
                            {
                                if (positivecount >= 4) { undrawncount++; continue; }
                                positivecount++;
                            }
                            else
                            {
                                if (negativecount >= 4) { undrawncount++; continue; }
                                negativecount++;
                            }

                            afflictionsDisplayedOnLimb.Add(affliction.Key);

                        }
                    }

                    if (!afflictionsDisplayedOnLimb.Any()) { i++; continue; }
                    if (limbHealth.IndicatorSprite == null) { continue; }

                    float scale = Math.Min(drawArea.Width / (float)limbHealth.IndicatorSprite.SourceRect.Width, drawArea.Height / (float)limbHealth.IndicatorSprite.SourceRect.Height);

                    Rectangle highlightArea = GetLimbHighlightArea(limbHealth, drawArea,selfHealth);

                    float iconScale = 0.25f * scale;

                    int drawnPositve = 0;
                    int drawnNegative = 0;
                    foreach (Affliction affliction in afflictionsDisplayedOnLimb)
                    {
                        bool isBuff = affliction.Prefab.IsBuff;
                        Vector2 iconPos = highlightArea.Center.ToVector2();
                        if (negativecount > 0 && positivecount > 0)
                            iconPos += new Vector2(10 * (isBuff ? 1 : -1), 0);

                        float spacing = MathHelper.Clamp(40 / (Math.Max(negativecount, positivecount) / 1.5f), 10, 40);

                        iconPos += new Vector2(0, spacing * ((isBuff ? drawnPositve : drawnNegative) - 0.5f * (Math.Max(negativecount, positivecount) - 1)));

                        DrawLimbAfflictionIcon(spriteBatch, affliction, iconScale, ref iconPos);

                        if (isBuff) drawnPositve++;
                        else drawnNegative++;
                    }

                    // draw the "+x" if theres too many afflictions
                    if (undrawncount > 0)
                    {
                        string additionalAfflictionCount = $"+{undrawncount}";
                        Vector2 displace = GUIStyle.SubHeadingFont.MeasureString(additionalAfflictionCount);

                        Vector2 iconPos = highlightArea.Center.ToVector2();
                        if (negativecount > 0 && positivecount > 0)
                            iconPos += new Vector2(10, 0);

                        GUIStyle.SubHeadingFont.DrawString(spriteBatch, additionalAfflictionCount, iconPos + new Vector2(displace.X * 1.1f, -displace.Y * 0.45f), Color.Black * 0.75f);
                        GUIStyle.SubHeadingFont.DrawString(spriteBatch, additionalAfflictionCount, iconPos + new Vector2(displace.X, -displace.Y * 0.5f), Color.White);
                    }

                    i++;
                }

                if (selectedLimbIndex > -1 && (afflictionIconContainer.Content.CountChildren > 0 || afflictionIconContainer2.Content.CountChildren > 0))
                {
                    CharacterHealth.LimbHealth limbHealth = limbHealths[selectedLimbIndex];
                    if (limbHealth?.IndicatorSprite != null)
                    {
                        var target = afflictionIconContainer.Content.CountChildren > 0 ? afflictionIconContainer : afflictionIconContainer2;
                        Rectangle selectedLimbArea = GetLimbHighlightArea(limbHealth, drawArea,selfHealth);
                        GUI.DrawLine(spriteBatch,
                            new Vector2(target.Rect.X, target.Rect.Y),
                            selectedLimbArea.Center.ToVector2(),
                            Color.LightGray * 0.5f, width: 4);
                    }
                }

                if (NeurotraumaEnabled)
                    UpdateGraph();

                void DrawLimbAfflictionIcon(SpriteBatch spriteBatch, Affliction affliction, float iconScale, ref Vector2 iconPos)
                {
                    if (!affliction.ShouldShowIcon(selfHealth.Character) || affliction.Prefab.Icon == null) { return; }
                    Vector2 iconSize = affliction.Prefab.Icon.size * iconScale;

                    float showIconThreshold = Character.Controlled?.CharacterHealth == selfHealth ? affliction.Prefab.ShowIconThreshold : affliction.Prefab.ShowIconToOthersThreshold;

                    //afflictions that have a strength of less than 10 are faded out slightly
                    float alpha = MathHelper.Lerp(0.3f, 1.0f,
                        (affliction.Strength - showIconThreshold) / Math.Min(affliction.Prefab.MaxStrength - showIconThreshold, 10.0f));

                    affliction.Prefab.Icon.Draw(spriteBatch, iconPos - iconSize / 2.0f, CharacterHealth.GetAfflictionIconColor(affliction) * alpha, 0, iconScale);
                    iconPos += new Vector2(10.0f, 20.0f) * iconScale;
                }
            }

            bool ShouldDisplayAfflictionOnLimb(KeyValuePair<Affliction, CharacterHealth.LimbHealth> kvp, CharacterHealth.LimbHealth limbHealth, CharacterHealth selfHealth, List<CharacterHealth.LimbHealth> limbHealths)
            {
                if (!kvp.Key.ShouldShowIcon(selfHealth.Character)) { return false; }
                if (kvp.Value == limbHealth)
                {
                    return true;
                }
                else if (kvp.Value == null)
                {
                    Limb indicatorLimb = selfHealth.Character.AnimController.GetLimb(kvp.Key.Prefab.IndicatorLimb);
                    return indicatorLimb != null && indicatorLimb.HealthIndex == limbHealths.IndexOf(limbHealth);
                }
                return false;
            }

            // CreateAfflictionInfos override
            // makes it so the affliction descriptions are next to each other if buffs are present
            // also makes them smaller so theres room for more of them
            const int displayedAfflictionCountMax = 8;
            GameMain.LuaCs.Hook.HookMethod("BetterHealthUIMod_CreateAfflictionInfos",
                typeof(CharacterHealth).GetMethod("CreateAfflictionInfos", BindingFlags.Instance | BindingFlags.NonPublic),
                (object self, Dictionary<string, object> args) => {
                    CreateAfflictionInfos(self, args);
                    return true;
                }, LuaCsHook.HookMethodType.Before, this);
            void CreateAfflictionInfos(object self, Dictionary<string, object> args)
            {
                ForceCustomized((CharacterHealth)self);

                #region Reflection crap
                // get arguments
                IEnumerable<Affliction> afflictions = (IEnumerable<Affliction>)(args["afflictions"]);
                // get members
                GUIListBox afflictionIconContainer = (GUIListBox)(typeof(CharacterHealth).GetField("afflictionIconList", BindingFlags.NonPublic | BindingFlags.Instance).GetValue(self));
                GUIListBox afflictionIconContainer2 = (GUIListBox)(afflictionIconContainer.Parent.GetChildByUserData("afflictionIconContainer2"));
                List<(Affliction affliction, float strength)> displayedAfflictions = (List<(Affliction affliction, float strength)>)(typeof(CharacterHealth).GetField("displayedAfflictions", BindingFlags.NonPublic | BindingFlags.Instance).GetValue(self));
                #endregion
                // override begins
                afflictionIconContainer.ClearChildren();
                afflictionIconContainer2.ClearChildren();
                displayedAfflictions.Clear();

                afflictions = CharacterHealth.SortAfflictionsBySeverity(afflictions, false);
                Affliction mostSevereAffliction = afflictions.FirstOrDefault();
                GUIButton buttonToSelect = null;

                foreach (Affliction affliction in afflictions)
                {
                    displayedAfflictions.Add((affliction, affliction.Strength));

                    bool isBuff = affliction.Prefab.IsBuff;
                    var newParent = (isBuff ? afflictionIconContainer2 : afflictionIconContainer);

                    var frame = new GUIButton(new RectTransform(new Vector2(1.0f, 1f / displayedAfflictionCountMax),newParent.Content.RectTransform), style: "ListBoxElement")
                    {
                        UserData = affliction,
                        OnClicked = SelectAffliction
                    };

                    new GUIFrame(new RectTransform(Vector2.One, frame.RectTransform), style: "GUIFrameListBox") { CanBeFocused = false };

                    // houses the progress bar
                    var content = new GUILayoutGroup(new RectTransform(new Vector2(0.9f, 0.75f), frame.RectTransform, Anchor.Center), childAnchor: Anchor.CenterLeft)
                    {
                        Stretch = true,
                        CanBeFocused = false,
                        IsHorizontal = true
                    };
                    // spacing
                    new GUIFrame(new RectTransform(new Vector2(0.1f, 1f), content.RectTransform), style: "GUIFrameListBox") { CanBeFocused = false };

                    // houses the affliction icon and text
                    var content2 = new GUILayoutGroup(new RectTransform(new Vector2(1f, 0.95f), frame.RectTransform, Anchor.Center), childAnchor: Anchor.CenterLeft)
                    {
                        Stretch = true,
                        CanBeFocused = false,
                        IsHorizontal = true
                    };

                    var progressbarBg = new GUIProgressBar(new RectTransform(new Vector2(0.5f, 1), content.RectTransform), 0.0f, GUIStyle.Green, style: "GUIAfflictionBar")
                    {
                        UserData = "afflictionstrengthprediction",
                        CanBeFocused = false,
                        IsHorizontal = true
                    };
                    var afflictionStrengthBar = new GUIProgressBar(new RectTransform(Vector2.One, progressbarBg.RectTransform), 0.0f, Color.Transparent, showFrame: false, style: "GUIAfflictionBar")
                    {
                        UserData = "afflictionstrength",
                        CanBeFocused = false,
                        IsHorizontal = true
                    };
                    afflictionStrengthBar.BarSize = affliction.Strength / affliction.Prefab.MaxStrength;

                    //spacing
                    //new GUIFrame(new RectTransform(new Vector2(1.0f, 0.15f), content.RectTransform), style: null) { CanBeFocused = false };

                    if (affliction == mostSevereAffliction)
                    {
                        buttonToSelect = frame;
                    }

                    var afflictionIcon = new GUIImage(new RectTransform(Vector2.One * 1f, content2.RectTransform, anchor: Anchor.CenterLeft, pivot: Pivot.CenterLeft, scaleBasis: ScaleBasis.BothHeight), affliction.Prefab.Icon, scaleToFit: true)
                    {
                        Color = CharacterHealth.GetAfflictionIconColor(affliction),
                        CanBeFocused = false
                    };
                    afflictionIcon.PressedColor = afflictionIcon.Color;
                    afflictionIcon.HoverColor = Color.Lerp(afflictionIcon.Color, Color.White, 0.6f);
                    afflictionIcon.SelectedColor = Color.Lerp(afflictionIcon.Color, Color.White, 0.5f);

                    var nameText = new GUITextBlock(new RectTransform(new Vector2(1.1f, 0.0f), content2.RectTransform),
                        $"{affliction.Prefab.Name}\n({Math.Round(affliction.Strength / affliction.Prefab.MaxStrength * 100)}% | {Math.Round(affliction.Strength)}/{Math.Round(affliction.Prefab.MaxStrength)})", font: GUIStyle.SmallFont, textAlignment: Alignment.CenterLeft)
                    {
                        UserData = "afflictionname",
                        CanBeFocused = false,
                        OutlineColor = Color.Black,
                        Shadow = true
                    };
                    nameText.Text = ToolBox.LimitString(nameText.Text, nameText.Font, nameText.Rect.Width);
                    nameText.RectTransform.MinSize = new Point(0, (int)(nameText.TextSize.Y));
                    nameText.RectTransform.SizeChanged += () =>
                    {
                        nameText.Text = ToolBox.LimitString(nameText.Text, nameText.Font, nameText.Rect.Width);
                    };

                    content.Recalculate();
                    content2.Recalculate();
                }

                buttonToSelect?.OnClicked(buttonToSelect, buttonToSelect.UserData);
                afflictionIconContainer.RecalculateChildren();
                afflictionIconContainer.ForceLayoutRecalculation();
                afflictionIconContainer2.RecalculateChildren();
                afflictionIconContainer2.ForceLayoutRecalculation();

                bool SelectAffliction(GUIButton button, object userData)
                {
                    bool selected = button.Selected;
                    foreach (var child in afflictionIconContainer.Content.Children)
                    {
                        GUIButton btn = child.GetChild<GUIButton>();
                        if (btn != null)
                        {
                            btn.Selected = btn == button && !selected;
                        }
                    }
                    foreach (var child in afflictionIconContainer2.Content.Children)
                    {
                        GUIButton btn = child.GetChild<GUIButton>();
                        if (btn != null)
                        {
                            btn.Selected = btn == button && !selected;
                        }
                    }

                    return false;
                }
            }

            // UpdateAfflictionContainer override
            // got some custom double list crap going on here
            GameMain.LuaCs.Hook.HookMethod("BetterHealthUIMod_UpdateAfflictionContainer",
                typeof(CharacterHealth).GetMethod("UpdateAfflictionContainer", BindingFlags.Instance | BindingFlags.NonPublic),
                (object self, Dictionary<string, object> args) => {
                    UpdateAfflictionContainer((CharacterHealth.LimbHealth)(args["selectedLimb"]), (CharacterHealth)self);
                    return true;
                }, LuaCsHook.HookMethodType.Before, this);
            void UpdateAfflictionContainer(CharacterHealth.LimbHealth selectedLimb,CharacterHealth selfHealth)
            {
                ForceCustomized(selfHealth);
                GUIListBox afflictionIconContainer = (GUIListBox)(typeof(CharacterHealth).GetField("afflictionIconList", BindingFlags.NonPublic | BindingFlags.Instance).GetValue(selfHealth));
                GUIListBox afflictionIconContainer2 = (GUIListBox)(afflictionIconContainer.Parent.GetChildByUserData("afflictionIconContainer2"));
                List<(Affliction affliction, float strength)> displayedAfflictions = (List<(Affliction affliction, float strength)>)(typeof(CharacterHealth).GetField("displayedAfflictions", BindingFlags.NonPublic | BindingFlags.Instance).GetValue(selfHealth));
                Dictionary<Affliction, CharacterHealth.LimbHealth> afflictions = (Dictionary<Affliction, CharacterHealth.LimbHealth>)(typeof(CharacterHealth).GetField("afflictions", BindingFlags.NonPublic | BindingFlags.Instance).GetValue(selfHealth));
                CharacterHealth.LimbHealth currentDisplayedLimb = (CharacterHealth.LimbHealth)(typeof(CharacterHealth).GetField("currentDisplayedLimb", BindingFlags.NonPublic | BindingFlags.Instance).GetValue(selfHealth));
                List<CharacterHealth.LimbHealth> limbHealths = (List<CharacterHealth.LimbHealth>)(typeof(CharacterHealth).GetField("limbHealths", BindingFlags.NonPublic | BindingFlags.Instance).GetValue(selfHealth));

                if (selectedLimb == null)
                {
                    afflictionIconContainer.Content.ClearChildren();
                    afflictionIconContainer2.Content.ClearChildren();
                    return;
                }

                if (afflictionsDirty() || selectedLimb != currentDisplayedLimb)
                {
                    var currentAfflictions = afflictions.Where(a => ShouldDisplayAfflictionOnLimb(a, selectedLimb,selfHealth,limbHealths)).Select(a => a.Key);
                    Dictionary<string, object> args = new Dictionary<string, object>();
                    args.Add("afflictions", currentAfflictions);
                    CreateAfflictionInfos(selfHealth, args);
                    CreateRecommendedTreatments();
                }
                //update recommended treatments if the strength of some displayed affliction has changed by > 1
                else if (displayedAfflictions.Any(d => Math.Abs(d.strength - d.affliction.Strength) > 1.0f))
                {
                    CreateRecommendedTreatments();
                }

                bool afflictionsDirty()
                {
                    //not displaying one of the current afflictions -> dirty
                    foreach (KeyValuePair<Affliction, CharacterHealth.LimbHealth> kvp in afflictions)
                    {
                        if (!ShouldDisplayAfflictionOnLimb(kvp, selectedLimb,selfHealth,limbHealths)) { continue; }
                        if (!displayedAfflictions.Any(d => d.affliction == kvp.Key)) { return true; }
                    }
                    //displaying an affliction we no longer have -> dirty
                    foreach ((Affliction affliction, float strength) in displayedAfflictions)
                    {
                        if (!afflictions.Any(a => a.Key == affliction)) { return true; }
                    }
                    return false;
                }

                void CreateRecommendedTreatments()
                {
                    typeof(CharacterHealth).GetMethod("CreateRecommendedTreatments", BindingFlags.Instance | BindingFlags.NonPublic).Invoke(selfHealth, null);
                }
            }

            // UpdateAfflictionInfos override
            // makes it so the affliction progress bar is nicely colored
            GameMain.LuaCs.Hook.HookMethod("BetterHealthUIMod_UpdateAfflictionInfos",
                typeof(CharacterHealth).GetMethod("UpdateAfflictionInfos", BindingFlags.Instance | BindingFlags.NonPublic),
                (object self, Dictionary<string, object> args) => {
                    try
                    {
                        #region Reflection crap
                        // get arguments
                        IEnumerable<Affliction> afflictions = (IEnumerable<Affliction>)(args["afflictions"]);
                        // get members
                        CharacterHealth selfHealth = (CharacterHealth)self;
                        ForceCustomized(selfHealth);
                        GUIListBox afflictionIconContainer = (GUIListBox)(typeof(CharacterHealth).GetField("afflictionIconList", BindingFlags.NonPublic | BindingFlags.Instance).GetValue(self));
                        GUIListBox afflictionIconContainer2 = (GUIListBox)(afflictionIconContainer.Parent.GetChildByUserData("afflictionIconContainer2"));
                        GUIListBox afflictionTooltip = (GUIListBox)(typeof(CharacterHealth).GetField("afflictionTooltip", BindingFlags.NonPublic | BindingFlags.Instance).GetValue(self));
                        //LocalizedString[] strengthTexts = (LocalizedString[])(typeof(CharacterHealth).GetField("strengthTexts", BindingFlags.NonPublic | BindingFlags.Static).GetValue(self));
                        #endregion
                        // override begins

                        var potentialTreatment = Inventory.DraggingItems.FirstOrDefault();
                        if (potentialTreatment == null && GUI.MouseOn?.UserData is ItemPrefab itemPrefab)
                        {
                            potentialTreatment = Character.Controlled.Inventory.FindItem(it => it.Prefab == itemPrefab, recursive: true);
                        }
                        potentialTreatment ??= Inventory.SelectedSlot?.Item;

                        foreach (Affliction affliction in afflictions)
                        {
                            float afflictionVitalityDecrease = affliction.GetVitalityDecrease(selfHealth);
                            Color afflictionEffectColor = Color.Lerp(CharacterHealth.GetAfflictionIconColor(affliction), Color.Black, 0.5f);

                            bool isBuff = affliction.Prefab.IsBuff;

                            var child = (isBuff ? afflictionIconContainer2 : afflictionIconContainer).Content.FindChild(affliction);

                            var afflictionName = child.FindChild("afflictionname", true) as GUITextBlock;
                            afflictionName.Text = $"{affliction.Prefab.Name}\n({Math.Round(affliction.Strength / affliction.Prefab.MaxStrength * 100)}% | {Math.Round(affliction.Strength)}/{Math.Round(affliction.Prefab.MaxStrength)})";

                            var afflictionStrengthPredictionBar = child.FindChild("afflictionstrengthprediction", true) as GUIProgressBar;
                            afflictionStrengthPredictionBar.BarSize = 0.0f;
                            var afflictionStrengthBar = afflictionStrengthPredictionBar.GetChildByUserData("afflictionstrength") as GUIProgressBar;
                            afflictionStrengthBar.BarSize = MathHelper.Lerp(afflictionStrengthBar.BarSize, affliction.Strength / affliction.Prefab.MaxStrength, 0.1f);
                            afflictionStrengthBar.Color = afflictionEffectColor;

                            float afflictionStrengthPrediction = GetAfflictionStrengthPrediction(potentialTreatment, affliction);
                            if (!MathUtils.NearlyEqual(afflictionStrengthPrediction, affliction.Strength))
                            {
                                float t = (float)Math.Max(0.5f, (Math.Sin(Timing.TotalTime * 5) + 1.0f) / 2.0f);
                                if (afflictionStrengthPrediction < affliction.Strength)
                                {
                                    afflictionStrengthBar.Color = afflictionEffectColor;
                                    afflictionStrengthPredictionBar.Color = GUIStyle.Blue * t;
                                    afflictionStrengthPredictionBar.BarSize = afflictionStrengthBar.BarSize;
                                    afflictionStrengthBar.BarSize = afflictionStrengthPrediction / affliction.Prefab.MaxStrength;
                                }
                                else
                                {
                                    afflictionStrengthPredictionBar.Color = Color.Red * t;
                                    afflictionStrengthPredictionBar.BarSize = afflictionStrengthPrediction / affliction.Prefab.MaxStrength;
                                }
                            }

                            if (afflictionTooltip != null && afflictionTooltip.UserData == affliction)
                            {
                                UpdateAfflictionInfo(afflictionTooltip.Content, affliction);
                            }
                        }

                        void UpdateAfflictionInfo(GUIComponent parent, Affliction affliction)
                        {
                            var labelContainer = parent.GetChildByUserData("label");

                            var strengthText = labelContainer.GetChildByUserData("strength") as GUITextBlock;

                            strengthText.Text = affliction.GetStrengthText();

                            strengthText.TextColor = Color.Lerp(GUIStyle.Orange, GUIStyle.Red,
                                affliction.Strength / affliction.Prefab.MaxStrength);

                            var vitalityText = labelContainer.GetChildByUserData("vitality") as GUITextBlock;
                            int vitalityDecrease = (int)affliction.GetVitalityDecrease(selfHealth);
                            if (vitalityDecrease == 0)
                            {
                                vitalityText.Visible = false;
                            }
                            else
                            {
                                vitalityText.Visible = true;
                                vitalityText.Text = TextManager.Get("Vitality") + " -" + vitalityDecrease;
                                vitalityText.TextColor = vitalityDecrease <= 0 ? GUIStyle.Green :
                                Color.Lerp(GUIStyle.Orange, GUIStyle.Red, affliction.Strength / affliction.Prefab.MaxStrength);
                            }
                        }

                        float GetAfflictionStrengthPrediction(Item item, Affliction affliction)
                        {
                            float strength = affliction.Strength;
                            if (item == null) { return strength; }

                            foreach (ItemComponent ic in item.Components)
                            {
                                if (ic.statusEffectLists == null) { continue; }
                                if (!ic.statusEffectLists.TryGetValue(ActionType.OnUse, out List<StatusEffect> statusEffects)) { continue; }
                                foreach (StatusEffect effect in statusEffects)
                                {
                                    foreach (var reduceAffliction in effect.ReduceAffliction)
                                    {
                                        if (reduceAffliction.AfflictionIdentifier != affliction.Identifier && reduceAffliction.AfflictionIdentifier != affliction.Prefab.AfflictionType) { continue; }
                                        strength -= reduceAffliction.ReduceAmount * (effect.Duration > 0 ? effect.Duration : 1.0f);
                                    }
                                    foreach (var addAffliction in effect.Afflictions)
                                    {
                                        if (addAffliction.Prefab != affliction.Prefab) { continue; }
                                        strength += addAffliction.Strength * (effect.Duration > 0 ? effect.Duration : 1.0f);
                                    }
                                }
                            }
                            return strength;
                        }

                        healthGUIRefreshTimer--;
                        if (healthGUIRefreshTimer <= 0)
                        {
                            healthGUIRefreshTimer = 60;
                            afflictionIconContainer.Content.RectTransform.SortChildren((r1, r2) =>
                            {
                                var first = r1.GUIComponent.UserData as Affliction;
                                var second = r2.GUIComponent.UserData as Affliction;
                                return Math.Sign(second.Strength / second.Prefab.MaxStrength - first.Strength / first.Prefab.MaxStrength);
                            });
                            afflictionIconContainer2.Content.RectTransform.SortChildren((r1, r2) =>
                            {
                                var first = r1.GUIComponent.UserData as Affliction;
                                var second = r2.GUIComponent.UserData as Affliction;
                                return Math.Sign(second.Strength / second.Prefab.MaxStrength - first.Strength / first.Prefab.MaxStrength);
                            });
                        }

                    }
                    catch (Exception e)
                    {
                        LuaCsSetup.PrintCsMessage("shit. " + e);
                    }
                    

                    

                    return true;
                }, LuaCsHook.HookMethodType.Before, this);

            // UpdateLimbIndicators override
            // makes it so i can move the funny limb man
            GameMain.LuaCs.Hook.HookMethod("BetterHealthUIMod_UpdateLimbIndicators",
                typeof(CharacterHealth).GetMethod("UpdateLimbIndicators", BindingFlags.Instance | BindingFlags.NonPublic),
                (object self, Dictionary<string, object> args) => {
                    // get arguments
                    float deltaTime = (float)(args["deltaTime"]);
                    Rectangle drawArea = (Rectangle)(args["drawArea"]);

                    CharacterHealth selfHealth = (CharacterHealth)self;

                    UpdateLimbIndicators(deltaTime, drawArea, selfHealth);

                    return true;
                }, LuaCsHook.HookMethodType.Before, this);
            void UpdateLimbIndicators(float deltaTime, Rectangle drawArea, CharacterHealth selfHealth)
            {
                // get members
                float limbIndicatorOverlayAnimState = (float)(typeof(CharacterHealth).GetField("limbIndicatorOverlayAnimState", BindingFlags.NonPublic | BindingFlags.Instance).GetValue(selfHealth));
                int highlightedLimbIndex = (int)(typeof(CharacterHealth).GetField("highlightedLimbIndex", BindingFlags.NonPublic | BindingFlags.Instance).GetValue(selfHealth));
                int selectedLimbIndex = (int)(typeof(CharacterHealth).GetField("selectedLimbIndex", BindingFlags.NonPublic | BindingFlags.Instance).GetValue(selfHealth));
                List<CharacterHealth.LimbHealth> limbHealths = (List<CharacterHealth.LimbHealth>)(typeof(CharacterHealth).GetField("limbHealths", BindingFlags.NonPublic | BindingFlags.Instance).GetValue(selfHealth));
                // override begins

                if (!GameMain.Instance.Paused)
                {
                    limbIndicatorOverlayAnimState += deltaTime * 8.0f;
                    // reflection apply
                    typeof(CharacterHealth).GetField("limbIndicatorOverlayAnimState", BindingFlags.NonPublic | BindingFlags.Instance).SetValue(selfHealth, limbIndicatorOverlayAnimState);
                }

                highlightedLimbIndex = -1;
                int i = 0;
                foreach (CharacterHealth.LimbHealth limbHealth in limbHealths)
                {
                    if (limbHealth.IndicatorSprite == null) { continue; }

                    float scale = Math.Min(drawArea.Width / (float)limbHealth.IndicatorSprite.SourceRect.Width, drawArea.Height / (float)limbHealth.IndicatorSprite.SourceRect.Height);

                    Rectangle highlightArea = GetLimbHighlightArea(limbHealth, drawArea, selfHealth);

                    if (highlightArea.Contains(PlayerInput.MousePosition))
                    {
                        highlightedLimbIndex = i;
                    }
                    i++;
                }
                // reflection apply
                typeof(CharacterHealth).GetField("highlightedLimbIndex", BindingFlags.NonPublic | BindingFlags.Instance).SetValue(selfHealth, highlightedLimbIndex);

                if (PlayerInput.PrimaryMouseButtonClicked() && highlightedLimbIndex > -1)
                {
                    selectedLimbIndex = highlightedLimbIndex;
                    // reflection apply
                    typeof(CharacterHealth).GetField("selectedLimbIndex", BindingFlags.NonPublic | BindingFlags.Instance).SetValue(selfHealth, selectedLimbIndex);
                }
            }

            // SortAfflictionsBySeverity override
            // makes it so the afflictions dont spazz around
            GameMain.LuaCs.Hook.HookMethod("BetterHealthUIMod_SortAfflictionsBySeverity",
                typeof(CharacterHealth).GetMethod("SortAfflictionsBySeverity", BindingFlags.Static | BindingFlags.Public),
                (object self, Dictionary<string, object> args) => {
                    #region Reflection crap
                    // get arguments
                    IEnumerable<Affliction> afflictions = (IEnumerable<Affliction>)(args["afflictions"]);
                    bool excludeBuffs = (bool)(args["excludeBuffs"]);
                    #endregion
                    // override begins
                    return afflictions.Where(a => !excludeBuffs || !a.Prefab.IsBuff).OrderByDescending(a => a.Strength / a.Prefab.MaxStrength);
                }, LuaCsHook.HookMethodType.Before, this);

            float GetTotalDamage(CharacterHealth.LimbHealth limbHealth, Dictionary<Affliction, CharacterHealth.LimbHealth> afflictions,CharacterHealth self)
            {
                float totalDamage = 0.0f;
                foreach (KeyValuePair<Affliction, CharacterHealth.LimbHealth> kvp in afflictions)
                {
                    if (kvp.Value != limbHealth) { continue; }
                    var affliction = kvp.Key;
                    totalDamage += affliction.GetVitalityDecrease(self);
                }
                return totalDamage;
            }

            Rectangle GetLimbHighlightArea(CharacterHealth.LimbHealth limbHealth, Rectangle drawArea, CharacterHealth selfHealth)
            {
                float scale = Math.Min(drawArea.Width / (float)limbHealth.IndicatorSprite.SourceRect.Width, drawArea.Height / (float)limbHealth.IndicatorSprite.SourceRect.Height);
                return new Rectangle(
                    (int)(drawArea.Center.X + limbGuyOffset.X - (limbHealth.IndicatorSprite.SourceRect.Width / 2 - limbHealth.HighlightArea.X) * scale),
                    (int)(drawArea.Center.Y + limbGuyOffset.Y - (limbHealth.IndicatorSprite.SourceRect.Height / 2 - limbHealth.HighlightArea.Y) * scale),
                    (int)(limbHealth.HighlightArea.Width * scale),
                    (int)(limbHealth.HighlightArea.Height * scale));
            }

            Color AverageColor(List<object[]> colors)
            {
                float num = 0;
                float r = 0;
                float g = 0;
                float b = 0;

                foreach (object[] colarr in colors)
                {
                    Color col = (Color)colarr[0];
                    float strength = (float)colarr[1];
                    r += col.R * col.R * strength;
                    g += col.G * col.G * strength;
                    b += col.B * col.B * strength;
                    num += strength;
                }

                return new Color((float)Math.Sqrt(r / num)/255f, (float)Math.Sqrt(g / num) / 255f, (float)Math.Sqrt(b / num) / 255f);
            }

            LimbType NormalizeLimbType(LimbType type)
            {
                switch (type)
                {
                    case LimbType.Waist: return LimbType.Torso;
                    case LimbType.LeftHand:
                    case LimbType.LeftForearm:
                        return LimbType.LeftArm;
                    case LimbType.RightHand:
                    case LimbType.RightForearm:
                        return LimbType.RightArm;
                    case LimbType.LeftFoot:
                    case LimbType.LeftThigh:
                        return LimbType.LeftLeg;
                    case LimbType.RightFoot:
                    case LimbType.RightThigh:
                        return LimbType.RightLeg;
                }

                return type;
            }

            LimbType LimbHealthToLimbType(CharacterHealth.LimbHealth limbHealth, List<CharacterHealth.LimbHealth>  limbHealths, Limb[] Limbs)
            {
                int healthIndex = limbHealths.IndexOf(limbHealth);
                Limb l = Limbs.Find(l => l.HealthIndex == healthIndex);
                if (l == null) return LimbType.None;
                return l.type;
            }

        }

        // update heartrate monitor (Neurotrauma)
        const int GraphSize = 256;
        const float updateGraphInterval = 1 / 60f; // update at 60 fps
        const float NormalHeartrate = 60;
        const float MaxTachycardiaHeartrate = 180;
        const float MaxFibrillationHeartrate = 300;

        private static float graphTimer = 0;
        private static float[] heartGraph = new float[GraphSize];
        private static int heartGraphProgress = 0;
        private static float timeSinceBeat = 0;
        private static CCurve ecgCurve = null;
        private static CCurve ecgCurveFib = null;
        private static SoundChannel flatlineSoundChannel = null;

        private static void UpdateGraph()
        {
            const float deltaTime = 1 / 60f;

            graphTimer += deltaTime;
            timeSinceBeat += deltaTime;

            if (graphTimer >= updateGraphInterval)
            {
                Character character = CharacterHealth.OpenHealthWindow?.Character;
                UpdateHeartrateGraphData(heartGraph, GetHeartbeatAmplitude(character)+1);

                graphTimer = 0.0f;
            }
        }
        private static float GetHeartbeatAmplitude(Character character)
        {
            if (character == null) return 0.0f;

            // Проверяем, отключен ли звук пульса в конфигурации или частота равна 0
            if (BetterHealthUIMod.DisableHeartbeatSound || character.IsDead)
            {
                // Останавливаем плоский звук, если он включен
                if (flatlineSoundChannel != null)
                {
                    flatlineSoundChannel.Dispose();
                    flatlineSoundChannel = null;
                }
                return 0;
            }

            (float rate, float stability) heartrate = GetHeartrate(character);
            float chaos = MathHelper.Lerp(1, rnd.Next(0, 1000) / 1000f, Math.Min(1, (1 - heartrate.stability) * 2));

            float timePerBeat = heartrate.rate > 0
                ? 1 / (heartrate.rate / 60 * (1 + (1 - heartrate.stability) * 2 * (rnd.Next(0, 1000) / 1000f)))
                : float.PositiveInfinity;

            // Плоская линия, если частота сердцебиения равна 0
            if (heartrate.rate <= 0)
            {
                if (flatlineSoundChannel == null)
                {
                    flatlineSoundChannel = SoundPlayer.PlaySound("flatline2", 0.06f);
                    if (flatlineSoundChannel != null) flatlineSoundChannel.Looping = true;
                }
                return 0;
            }

            // Остановка звука плоской линии, если частота нормализовалась
            if (flatlineSoundChannel != null)
            {
                flatlineSoundChannel.Dispose();
                flatlineSoundChannel = null;
            }

            // Звук "бип" при каждом ударе сердца
            if (timeSinceBeat > timePerBeat)
            {
                timeSinceBeat -= timePerBeat;
                if (heartrate.rate > NormalHeartrate + 10 && rnd.Next(0, 1000) / 1000f < heartrate.stability)
                {
                    SoundPlayer.PlaySound("ecg1", 0.1f * Math.Min((heartrate.rate - NormalHeartrate) / 80, 1));
                }
            }

            return MathHelper.Lerp(ecgCurve.Evaluate(timeSinceBeat * 1.5f) * chaos, ecgCurveFib.Evaluate(timeSinceBeat * 1.5f), 1 - heartrate.stability);
        }

        private static (float rate,float stability) GetHeartrate(Character character)
        {
            if (character == null || character.CharacterHealth==null || character.IsDead) return (0,0);

            float rate = NormalHeartrate;
            float stability = 1;

            Affliction cardiacarrest = character.CharacterHealth.GetAffliction("cardiacarrest");

            // return 0 rate and stability if in cardiac arrest
            if (cardiacarrest != null && cardiacarrest.Strength >= 0.5f) return (0,0);

            Affliction tachycardia = character.CharacterHealth.GetAffliction("tachycardia");
            Affliction fibrillation = character.CharacterHealth.GetAffliction("fibrillation");

            if (fibrillation!=null)
            { 
                rate = MathHelper.Lerp(MaxTachycardiaHeartrate, MaxFibrillationHeartrate, fibrillation.Strength/100 * (0.25f+(rnd.Next(0, 1000) / 1000f)));
                stability = 1 - fibrillation.Strength / 100;
            }
            else if (tachycardia != null)
            {
                rate = MathHelper.Lerp(NormalHeartrate, MaxTachycardiaHeartrate, tachycardia.Strength / 100);
            }

            return (rate,stability);
        }

        private static void UpdateHeartrateGraphData(IList<float> graph, float newValue)
        {
            graph[heartGraphProgress] = newValue*0.8f;
            heartGraphProgress = (heartGraphProgress + 1) % graph.Count;
        }
    }

    // i hate this
    // trying to use the already existing curve class throws an error that its in two assemblies
    // i dont know how to fix it so here comes the duplicate classes!

    public enum CCurveLoopType
    {
        Constant,
        Cycle,
        CycleOffset,
        Oscillate,
        Linear
    }

    public enum CCurveContinuity
    {
        Smooth,
        Step
    }

    public enum CCurveTangent
    {
        Flat,
        Linear,
        Smooth
    }

    public class CCurve
    {
        #region Private Fields

        private CCurveKeyCollection keys;
        private CCurveLoopType postLoop;
        private CCurveLoopType preLoop;

        #endregion Private Fields

        #region Public Properties

        public bool IsConstant
        {
            get { return keys.Count <= 1; }
        }

        public CCurveKeyCollection Keys
        {
            get { return keys; }
        }

        public CCurveLoopType PostLoop
        {
            get { return postLoop; }
            set { postLoop = value; }
        }

        public CCurveLoopType PreLoop
        {
            get { return preLoop; }
            set { preLoop = value; }
        }

        #endregion Public Properties

        #region Public Constructors

        public CCurve()
        {
            keys = new CCurveKeyCollection();
        }

        #endregion Public Constructors

        #region Public Methods

        public CCurve Clone()
        {
            CCurve curve = new CCurve();

            curve.keys = keys.Clone();
            curve.preLoop = preLoop;
            curve.postLoop = postLoop;

            return curve;
        }

        public float Evaluate(float position)
        {
            CCurveKey first = keys[0];
            CCurveKey last = keys[keys.Count - 1];

            if (position < first.Position)
            {
                switch (PreLoop)
                {
                    case CCurveLoopType.Constant:
                        //constant
                        return first.Value;

                    case CCurveLoopType.Linear:
                        // linear y = a*x +b with a tangeant of last point
                        return first.Value - first.TangentIn * (first.Position - position);

                    case CCurveLoopType.Cycle:
                        //start -> end / start -> end
                        int cycle = GetNumberOfCycle(position);
                        float virtualPos = position - (cycle * (last.Position - first.Position));
                        return GetCurvePosition(virtualPos);

                    case CCurveLoopType.CycleOffset:
                        //make the curve continue (with no step) so must up the curve each cycle of delta(value)
                        cycle = GetNumberOfCycle(position);
                        virtualPos = position - (cycle * (last.Position - first.Position));
                        return (GetCurvePosition(virtualPos) + cycle * (last.Value - first.Value));

                    case CCurveLoopType.Oscillate:
                        //go back on curve from end and target start 
                        // start-> end / end -> start
                        cycle = GetNumberOfCycle(position);
                        if (0 == cycle % 2f) //if pair
                            virtualPos = position - (cycle * (last.Position - first.Position));
                        else
                            virtualPos = last.Position - position + first.Position +
                                            (cycle * (last.Position - first.Position));
                        return GetCurvePosition(virtualPos);
                }
            }
            else if (position > last.Position)
            {
                int cycle;
                switch (PostLoop)
                {
                    case CCurveLoopType.Constant:
                        //constant
                        return last.Value;

                    case CCurveLoopType.Linear:
                        // linear y = a*x +b with a tangeant of last point
                        return last.Value + first.TangentOut * (position - last.Position);

                    case CCurveLoopType.Cycle:
                        //start -> end / start -> end
                        cycle = GetNumberOfCycle(position);
                        float virtualPos = position - (cycle * (last.Position - first.Position));
                        return GetCurvePosition(virtualPos);

                    case CCurveLoopType.CycleOffset:
                        //make the curve continue (with no step) so must up the curve each cycle of delta(value)
                        cycle = GetNumberOfCycle(position);
                        virtualPos = position - (cycle * (last.Position - first.Position));
                        return (GetCurvePosition(virtualPos) + cycle * (last.Value - first.Value));

                    case CCurveLoopType.Oscillate:
                        //go back on curve from end and target start 
                        // start-> end / end -> start
                        cycle = GetNumberOfCycle(position);
                        virtualPos = position - (cycle * (last.Position - first.Position));
                        if (0 == cycle % 2f) //if pair
                            virtualPos = position - (cycle * (last.Position - first.Position));
                        else
                            virtualPos = last.Position - position + first.Position +
                                            (cycle * (last.Position - first.Position));
                        return GetCurvePosition(virtualPos);
                }
            }

            //in curve
            return GetCurvePosition(position);
        }

        #endregion Public Methods

        #region Private Methods

        private int GetNumberOfCycle(float position)
        {
            float cycle = (position - keys[0].Position) / (keys[keys.Count - 1].Position - keys[0].Position);
            if (cycle < 0f)
                cycle--;
            return (int)cycle;
        }

        private float GetCurvePosition(float position)
        {
            //only for position in curve
            CCurveKey prev = keys[0];
            CCurveKey next;
            for (int i = 1; i < keys.Count; i++)
            {
                next = Keys[i];
                if (next.Position >= position)
                {
                    if (prev.Continuity == CCurveContinuity.Step)
                    {
                        if (position >= 1f)
                        {
                            return next.Value;
                        }
                        return prev.Value;
                    }
                    float t = (position - prev.Position) / (next.Position - prev.Position); //to have t in [0,1]
                    float ts = t * t;
                    float tss = ts * t;
                    //After a lot of search on internet I have found all about spline function
                    // and bezier (phi'sss ancien) but finaly use hermite curve 
                    //http://en.wikipedia.org/wiki/Cubic_Hermite_spline
                    //P(t) = (2*t^3 - 3t^2 + 1)*P0 + (t^3 - 2t^2 + t)m0 + (-2t^3 + 3t^2)P1 + (t^3-t^2)m1
                    //with P0.value = prev.value , m0 = prev.tangentOut, P1= next.value, m1 = next.TangentIn
                    return (2 * tss - 3 * ts + 1f) * prev.Value + (tss - 2 * ts + t) * prev.TangentOut + (3 * ts - 2 * tss) * next.Value +
                            (tss - ts) * next.TangentIn;
                }
                prev = next;
            }
            return 0f;
        }

        #endregion
    }

    public class CCurveKey : IEquatable<CCurveKey>, IComparable<CCurveKey>
    {
        #region Private Fields

        private CCurveContinuity continuity;
        private float position;
        private float tangentIn;
        private float tangentOut;
        private float value;

        #endregion Private Fields

        #region Properties

        public CCurveContinuity Continuity
        {
            get { return continuity; }
            set { continuity = value; }
        }

        public float Position
        {
            get { return position; }
        }

        public float TangentIn
        {
            get { return tangentIn; }
            set { tangentIn = value; }
        }

        public float TangentOut
        {
            get { return tangentOut; }
            set { tangentOut = value; }
        }

        public float Value
        {
            get { return value; }
            set { this.value = value; }
        }

        #endregion

        #region Constructors

        public CCurveKey(float position, float value)
            : this(position, value, 0, 0, CCurveContinuity.Smooth)
        {
        }

        public CCurveKey(float position, float value, float tangentIn, float tangentOut)
            : this(position, value, tangentIn, tangentOut, CCurveContinuity.Smooth)
        {
        }

        public CCurveKey(float position, float value, float tangentIn, float tangentOut, CCurveContinuity continuity)
        {
            this.position = position;
            this.value = value;
            this.tangentIn = tangentIn;
            this.tangentOut = tangentOut;
            this.continuity = continuity;
        }

        #endregion Constructors

        #region Public Methods

        #region IComparable<CurveKey> Members

        public int CompareTo(CCurveKey other)
        {
            return position.CompareTo(other.position);
        }

        #endregion

        #region IEquatable<CCurveKey> Members

        public bool Equals(CCurveKey other)
        {
            return (this == other);
        }

        #endregion

        public static bool operator !=(CCurveKey a, CCurveKey b)
        {
            return !(a == b);
        }

        public static bool operator ==(CCurveKey a, CCurveKey b)
        {
            if (Equals(a, null))
                return Equals(b, null);

            if (Equals(b, null))
                return Equals(a, null);

            return (a.position == b.position)
                   && (a.value == b.value)
                   && (a.tangentIn == b.tangentIn)
                   && (a.tangentOut == b.tangentOut)
                   && (a.continuity == b.continuity);
        }

        public CCurveKey Clone()
        {
            return new CCurveKey(position, value, tangentIn, tangentOut, continuity);
        }

        public override bool Equals(object obj)
        {
            return (obj is CCurveKey) ? ((CCurveKey)obj) == this : false;
        }

        public override int GetHashCode()
        {
            return position.GetHashCode() ^ value.GetHashCode() ^ tangentIn.GetHashCode() ^
                   tangentOut.GetHashCode() ^ continuity.GetHashCode();
        }

        #endregion
    }

    public class CCurveKeyCollection
    {
        #region Private Fields

        private List<CCurveKey> innerlist;
        private bool isReadOnly = false;

        #endregion Private Fields

        #region Properties

        public CCurveKey this[int index]
        {
            get { return innerlist[index]; }
            set
            {
                if (value == null)
                    throw new ArgumentNullException();

                if (index >= innerlist.Count)
                    throw new IndexOutOfRangeException();

                if (innerlist[index].Position == value.Position)
                    innerlist[index] = value;
                else
                {
                    innerlist.RemoveAt(index);
                    innerlist.Add(value);
                }
            }
        }

        public int Count
        {
            get { return innerlist.Count; }
        }

        public bool IsReadOnly
        {
            get { return isReadOnly; }
        }

        #endregion Properties

        #region Constructors

        public CCurveKeyCollection()
        {
            innerlist = new List<CCurveKey>();
        }

        #endregion Constructors

        #region Public Methods

        public void Add(CCurveKey item)
        {
            if (item == null)
                throw new ArgumentNullException("Value cannot be null.", (Exception)null);

            if (innerlist.Count == 0)
            {
                innerlist.Add(item);
                return;
            }

            for (int i = 0; i < innerlist.Count; i++)
            {
                if (item.Position < innerlist[i].Position)
                {
                    innerlist.Insert(i, item);
                    return;
                }
            }

            innerlist.Add(item);
        }

        public void Clear()
        {
            innerlist.Clear();
        }

        public bool Contains(CCurveKey item)
        {
            return innerlist.Contains(item);
        }

        public void CopyTo(CCurveKey[] array, int arrayIndex)
        {
            innerlist.CopyTo(array, arrayIndex);
        }

        public IEnumerator<CCurveKey> GetEnumerator()
        {
            return innerlist.GetEnumerator();
        }

        public bool Remove(CCurveKey item)
        {
            return innerlist.Remove(item);
        }



        public CCurveKeyCollection Clone()
        {
            CCurveKeyCollection ckc = new CCurveKeyCollection();
            foreach (CCurveKey key in innerlist)
                ckc.Add(key);
            return ckc;
        }

        public int IndexOf(CCurveKey item)
        {
            return innerlist.IndexOf(item);
        }

        public void RemoveAt(int index)
        {
            if (index != Count && index > -1)
                innerlist.RemoveAt(index);
            else
                throw new ArgumentOutOfRangeException(
                    "Index was out of range. Must be non-negative and less than the size of the collection.\r\nParameter name: index",
                    (Exception)null);
        }

        #endregion Public Methods
    }
}