using System;
using System.Collections.Generic;
using NT8.Core.Common.Models;

namespace NT8.Core.Indicators
{
    /// <summary>
    /// Represents value area range around volume point of control.
    /// </summary>
    public class ValueArea
    {
        /// <summary>
        /// Volume point of control (highest volume price level).
        /// </summary>
        public double VPOC { get; set; }

        /// <summary>
        /// Value area high boundary.
        /// </summary>
        public double ValueAreaHigh { get; set; }

        /// <summary>
        /// Value area low boundary.
        /// </summary>
        public double ValueAreaLow { get; set; }

        /// <summary>
        /// Total profile volume.
        /// </summary>
        public double TotalVolume { get; set; }

        /// <summary>
        /// Value area volume.
        /// </summary>
        public double ValueAreaVolume { get; set; }

        /// <summary>
        /// Creates a value area model.
        /// </summary>
        /// <param name="vpoc">VPOC level.</param>
        /// <param name="valueAreaHigh">Value area high.</param>
        /// <param name="valueAreaLow">Value area low.</param>
        /// <param name="totalVolume">Total volume.</param>
        /// <param name="valueAreaVolume">Value area volume.</param>
        public ValueArea(double vpoc, double valueAreaHigh, double valueAreaLow, double totalVolume, double valueAreaVolume)
        {
            VPOC = vpoc;
            ValueAreaHigh = valueAreaHigh;
            ValueAreaLow = valueAreaLow;
            TotalVolume = totalVolume;
            ValueAreaVolume = valueAreaVolume;
        }
    }

    /// <summary>
    /// Analyzes volume profile by price level and derives VPOC/value areas.
    /// </summary>
    public class VolumeProfileAnalyzer
    {
        private readonly object _lock = new object();

        /// <summary>
        /// Gets VPOC from provided bars.
        /// </summary>
        /// <param name="bars">Bars in profile window.</param>
        /// <returns>VPOC price level.</returns>
        public double GetVPOC(List<BarData> bars)
        {
            if (bars == null)
                throw new ArgumentNullException("bars");

            lock (_lock)
            {
                var profile = BuildProfile(bars, 0.25);
                if (profile.Count == 0)
                    return 0.0;

                var maxVolume = double.MinValue;
                var vpoc = 0.0;

                foreach (var kv in profile)
                {
                    if (kv.Value > maxVolume)
                    {
                        maxVolume = kv.Value;
                        vpoc = kv.Key;
                    }
                }

                return vpoc;
            }
        }

        /// <summary>
        /// Returns high volume nodes where volume exceeds 1.5x average level volume.
        /// </summary>
        /// <param name="bars">Bars in profile window.</param>
        /// <returns>High volume node price levels.</returns>
        public List<double> GetHighVolumeNodes(List<BarData> bars)
        {
            if (bars == null)
                throw new ArgumentNullException("bars");

            lock (_lock)
            {
                var profile = BuildProfile(bars, 0.25);
                var result = new List<double>();
                if (profile.Count == 0)
                    return result;

                var avg = CalculateAverageVolume(profile);
                var threshold = avg * 1.5;

                foreach (var kv in profile)
                {
                    if (kv.Value >= threshold)
                        result.Add(kv.Key);
                }

                result.Sort();
                return result;
            }
        }

        /// <summary>
        /// Returns low volume nodes where volume is below 0.5x average level volume.
        /// </summary>
        /// <param name="bars">Bars in profile window.</param>
        /// <returns>Low volume node price levels.</returns>
        public List<double> GetLowVolumeNodes(List<BarData> bars)
        {
            if (bars == null)
                throw new ArgumentNullException("bars");

            lock (_lock)
            {
                var profile = BuildProfile(bars, 0.25);
                var result = new List<double>();
                if (profile.Count == 0)
                    return result;

                var avg = CalculateAverageVolume(profile);
                var threshold = avg * 0.5;

                foreach (var kv in profile)
                {
                    if (kv.Value <= threshold)
                        result.Add(kv.Key);
                }

                result.Sort();
                return result;
            }
        }

        /// <summary>
        /// Calculates 70% value area around VPOC.
        /// </summary>
        /// <param name="bars">Bars in profile window.</param>
        /// <returns>Calculated value area.</returns>
        public ValueArea CalculateValueArea(List<BarData> bars)
        {
            if (bars == null)
                throw new ArgumentNullException("bars");

            lock (_lock)
            {
                var profile = BuildProfile(bars, 0.25);
                if (profile.Count == 0)
                    return new ValueArea(0.0, 0.0, 0.0, 0.0, 0.0);

                var levels = new List<double>(profile.Keys);
                levels.Sort();

                var vpoc = GetVPOC(bars);
                var totalVolume = 0.0;
                for (var i = 0; i < levels.Count; i++)
                    totalVolume += profile[levels[i]];

                var target = totalVolume * 0.70;

                var included = new HashSet<double>();
                included.Add(vpoc);
                var includedVolume = profile.ContainsKey(vpoc) ? profile[vpoc] : 0.0;

                var vpocIndex = levels.IndexOf(vpoc);
                var left = vpocIndex - 1;
                var right = vpocIndex + 1;

                while (includedVolume < target && (left >= 0 || right < levels.Count))
                {
                    var leftVolume = left >= 0 ? profile[levels[left]] : -1.0;
                    var rightVolume = right < levels.Count ? profile[levels[right]] : -1.0;

                    if (rightVolume > leftVolume)
                    {
                        included.Add(levels[right]);
                        includedVolume += profile[levels[right]];
                        right++;
                    }
                    else if (left >= 0)
                    {
                        included.Add(levels[left]);
                        includedVolume += profile[levels[left]];
                        left--;
                    }
                    else
                    {
                        included.Add(levels[right]);
                        includedVolume += profile[levels[right]];
                        right++;
                    }
                }

                var vah = vpoc;
                var val = vpoc;
                foreach (var level in included)
                {
                    if (level > vah)
                        vah = level;
                    if (level < val)
                        val = level;
                }

                return new ValueArea(vpoc, vah, val, totalVolume, includedVolume);
            }
        }

        private static Dictionary<double, double> BuildProfile(List<BarData> bars, double tickSize)
        {
            var profile = new Dictionary<double, double>();

            for (var i = 0; i < bars.Count; i++)
            {
                var bar = bars[i];
                if (bar == null)
                    continue;

                var low = RoundToTick(bar.Low, tickSize);
                var high = RoundToTick(bar.High, tickSize);

                if (high < low)
                {
                    var temp = high;
                    high = low;
                    low = temp;
                }

                var levelsCount = ((high - low) / tickSize) + 1.0;
                if (levelsCount <= 0.0)
                    continue;

                var volumePerLevel = bar.Volume / levelsCount;

                var level = low;
                while (level <= high + 0.0000001)
                {
                    if (!profile.ContainsKey(level))
                        profile.Add(level, 0.0);

                    profile[level] = profile[level] + volumePerLevel;
                    level = RoundToTick(level + tickSize, tickSize);
                }
            }

            return profile;
        }

        private static double CalculateAverageVolume(Dictionary<double, double> profile)
        {
            if (profile == null || profile.Count == 0)
                return 0.0;

            var sum = 0.0;
            var count = 0;
            foreach (var kv in profile)
            {
                sum += kv.Value;
                count++;
            }

            return count > 0 ? sum / count : 0.0;
        }

        private static double RoundToTick(double value, double tickSize)
        {
            if (tickSize <= 0.0)
                return value;

            var ticks = Math.Round(value / tickSize);
            return ticks * tickSize;
        }
    }
}
