#include "sierrachart.h"

#include <algorithm>
#include <cmath>
#include <vector>

SCDLLName("Custom Volume - Pixel Anchored")

namespace
{
    enum SubgraphIndexes
    {
        SG_VOLUME_BARS = 0,
        SG_VOLUME_SMA = 1
    };

    enum InputIndexes
    {
        IN_VOLUME_HEIGHT_PERCENT = 0,
        IN_BAR_GAP_PIXELS = 1,
        IN_SHOW_VOLUME_SMA = 2,
        IN_VOLUME_SMA_LENGTH = 3,
        IN_ENABLE_PERCENTILE_CLIPPING = 4,
        IN_TOP_PERCENT_TO_EXCLUDE = 5,
        IN_INCLUDE_CURRENT_BAR_IN_SCALE = 6
    };

    int ClampInt(const int Value, const int Minimum, const int Maximum)
    {
        if (Value < Minimum)
            return Minimum;

        if (Value > Maximum)
            return Maximum;

        return Value;
    }

    double ClampDouble(const double Value, const double Minimum, const double Maximum)
    {
        if (Value < Minimum)
            return Minimum;

        if (Value > Maximum)
            return Maximum;

        return Value;
    }

    void SetSolidGraphicsColor(SCStudyInterfaceRef sc, const COLORREF Color)
    {
        n_ACSIL::s_GraphicsBrush Brush;
        Brush.m_BrushType = n_ACSIL::s_GraphicsBrush::BRUSH_TYPE_SOLID;
        Brush.m_BrushColor.SetColorValue(Color);
        sc.Graphics.SetBrush(Brush);

        n_ACSIL::s_GraphicsPen Pen;
        Pen.m_PenColor.SetColorValue(Color);
        Pen.m_PenStyle =
            n_ACSIL::s_GraphicsPen::e_PenStyle::PEN_STYLE_SOLID;
        Pen.m_Width = 1;
        sc.Graphics.SetPen(Pen);
    }

    int ValueToLineYCoordinate(
        const double Value,
        const double ScaleMaximum,
        const int RegionBottom,
        const int VolumeBandHeight)
    {
        if (ScaleMaximum <= 0.0 || VolumeBandHeight <= 1)
            return RegionBottom - 1;

        const double Ratio = ClampDouble(Value / ScaleMaximum, 0.0, 1.0);
        const int LineRangeInPixels = VolumeBandHeight - 1;

        return RegionBottom - 1
            - static_cast<int>(std::lround(Ratio * LineRangeInPixels));
    }

    int ValueToRectangleTopCoordinate(
        const double Value,
        const double ScaleMaximum,
        const int RegionBottom,
        const int VolumeBandHeight)
    {
        if (Value <= 0.0 || ScaleMaximum <= 0.0 || VolumeBandHeight <= 0)
            return RegionBottom;

        const double Ratio = ClampDouble(Value / ScaleMaximum, 0.0, 1.0);
        int RectangleHeight =
            static_cast<int>(std::lround(Ratio * VolumeBandHeight));

        // Keep every non-zero volume bar visible, even when its proportional
        // height rounds to less than one pixel.
        if (RectangleHeight < 1)
            RectangleHeight = 1;

        if (RectangleHeight > VolumeBandHeight)
            RectangleHeight = VolumeBandHeight;

        return RegionBottom - RectangleHeight;
    }

    double DetermineVisibleScaleMaximum(
        SCStudyInterfaceRef sc,
        const int FirstVisibleBar,
        const int LastVisibleBar)
    {
        SCSubgraphRef VolumeBars = sc.Subgraph[SG_VOLUME_BARS];

        int LastBarForScale = LastVisibleBar;

        const bool IncludeCurrentBar =
            sc.Input[IN_INCLUDE_CURRENT_BAR_IN_SCALE].GetYesNo() != 0;

        if (!IncludeCurrentBar
            && LastBarForScale == sc.ArraySize - 1
            && LastBarForScale > FirstVisibleBar)
        {
            --LastBarForScale;
        }

        if (LastBarForScale < FirstVisibleBar)
            LastBarForScale = FirstVisibleBar;

        const bool PercentileClippingEnabled =
            sc.Input[IN_ENABLE_PERCENTILE_CLIPPING].GetYesNo() != 0;

        const double TopPercentToExclude = ClampDouble(
            sc.Input[IN_TOP_PERCENT_TO_EXCLUDE].GetFloat(),
            0.0,
            99.0);

        // Default path: one visible-range scan and no temporary allocation.
        if (!PercentileClippingEnabled || TopPercentToExclude <= 0.0)
        {
            double MaximumVolume = 0.0;

            for (int BarIndex = FirstVisibleBar;
                 BarIndex <= LastBarForScale;
                 ++BarIndex)
            {
                const double Volume = VolumeBars[BarIndex];

                if (Volume > MaximumVolume)
                    MaximumVolume = Volume;
            }

            return MaximumVolume;
        }

        // Optional path only: collect positive visible volumes and obtain the
        // requested upper-percentile threshold with nth_element. Bars above
        // this threshold are still drawn, but are clipped to the band top.
        std::vector<float> PositiveVisibleVolumes;
        PositiveVisibleVolumes.reserve(
            static_cast<std::size_t>(LastBarForScale - FirstVisibleBar + 1));

        for (int BarIndex = FirstVisibleBar;
             BarIndex <= LastBarForScale;
             ++BarIndex)
        {
            const float Volume = VolumeBars[BarIndex];

            if (Volume > 0.0f)
                PositiveVisibleVolumes.push_back(Volume);
        }

        if (PositiveVisibleVolumes.empty())
            return 0.0;

        // Convert the selected percentage to a whole number of bars. A
        // positive clipping percentage excludes at least one bar whenever
        // there are two or more positive visible-volume bars.
        std::size_t NumberToExclude = static_cast<std::size_t>(std::ceil(
            TopPercentToExclude / 100.0
            * static_cast<double>(PositiveVisibleVolumes.size())));

        if (PositiveVisibleVolumes.size() <= 1)
        {
            NumberToExclude = 0;
        }
        else if (NumberToExclude < 1)
        {
            NumberToExclude = 1;
        }
        else if (NumberToExclude >= PositiveVisibleVolumes.size())
        {
            NumberToExclude = PositiveVisibleVolumes.size() - 1;
        }

        const std::size_t NumberToKeep =
            PositiveVisibleVolumes.size() - NumberToExclude;
        const std::size_t ThresholdIndex = NumberToKeep - 1;

        std::nth_element(
            PositiveVisibleVolumes.begin(),
            PositiveVisibleVolumes.begin() + ThresholdIndex,
            PositiveVisibleVolumes.end());

        return PositiveVisibleVolumes[ThresholdIndex];
    }

    int DetermineBarSlotWidthPixels(
        SCStudyInterfaceRef sc,
        const int FirstVisibleBar)
    {
        if (sc.ArraySize >= 2)
        {
            int FirstBarForMeasurement = FirstVisibleBar;
            int SecondBarForMeasurement = FirstVisibleBar + 1;

            if (SecondBarForMeasurement >= sc.ArraySize)
            {
                FirstBarForMeasurement = sc.ArraySize - 2;
                SecondBarForMeasurement = sc.ArraySize - 1;
            }

            const int FirstCenterX =
                sc.BarIndexToXPixelCoordinate(FirstBarForMeasurement);
            const int SecondCenterX =
                sc.BarIndexToXPixelCoordinate(SecondBarForMeasurement);
            const int CenterDistance = SecondCenterX - FirstCenterX;

            if (CenterDistance > 0)
                return CenterDistance;

            if (CenterDistance < 0)
                return -CenterDistance;
        }

        return (sc.ChartBarSpacing > 0) ? sc.ChartBarSpacing : 1;
    }

    void DrawVolumeBarsForDirection(
        SCStudyInterfaceRef sc,
        const int FirstVisibleBar,
        const int LastVisibleBar,
        const bool DrawUpBars,
        const double ScaleMaximum,
        const int RegionLeft,
        const int RegionRight,
        const int RegionBottom,
        const int VolumeBandHeight,
        const int BarWidthPixels,
        const COLORREF BarColor)
    {
        SCSubgraphRef VolumeBars = sc.Subgraph[SG_VOLUME_BARS];

        SetSolidGraphicsColor(sc, BarColor);

        for (int BarIndex = FirstVisibleBar;
             BarIndex <= LastVisibleBar;
             ++BarIndex)
        {
            const bool IsUpBar =
                sc.BaseDataIn[SC_LAST][BarIndex]
                >= sc.BaseDataIn[SC_OPEN][BarIndex];

            if (IsUpBar != DrawUpBars)
                continue;

            const double Volume = VolumeBars[BarIndex];

            if (Volume <= 0.0)
                continue;

            const int BarCenterX = sc.BarIndexToXPixelCoordinate(BarIndex);
            int BarLeft = BarCenterX - BarWidthPixels / 2;
            int BarRight = BarLeft + BarWidthPixels;

            if (BarRight <= RegionLeft || BarLeft >= RegionRight)
                continue;

            BarLeft = ClampInt(BarLeft, RegionLeft, RegionRight);
            BarRight = ClampInt(BarRight, RegionLeft, RegionRight);

            if (BarRight <= BarLeft)
                continue;

            const int BarTop = ValueToRectangleTopCoordinate(
                Volume,
                ScaleMaximum,
                RegionBottom,
                VolumeBandHeight);

            if (BarTop >= RegionBottom)
                continue;

            sc.Graphics.DrawRectangle(
                BarLeft,
                BarTop,
                BarRight,
                RegionBottom);
        }
    }
}

void DrawPixelAnchoredVolumeOverlay(
    HWND WindowHandle,
    HDC DeviceContext,
    SCStudyInterfaceRef sc);

SCSFExport scsf_CustomVolumePixelAnchored(SCStudyInterfaceRef sc)
{
    SCSubgraphRef VolumeBars = sc.Subgraph[SG_VOLUME_BARS];
    SCSubgraphRef VolumeSMA = sc.Subgraph[SG_VOLUME_SMA];

    SCInputRef VolumeHeightPercent =
        sc.Input[IN_VOLUME_HEIGHT_PERCENT];
    SCInputRef BarGapPixels =
        sc.Input[IN_BAR_GAP_PIXELS];
    SCInputRef ShowVolumeSMA =
        sc.Input[IN_SHOW_VOLUME_SMA];
    SCInputRef VolumeSMALength =
        sc.Input[IN_VOLUME_SMA_LENGTH];
    SCInputRef EnablePercentileClipping =
        sc.Input[IN_ENABLE_PERCENTILE_CLIPPING];
    SCInputRef TopPercentToExclude =
        sc.Input[IN_TOP_PERCENT_TO_EXCLUDE];
    SCInputRef IncludeCurrentBarInScale =
        sc.Input[IN_INCLUDE_CURRENT_BAR_IN_SCALE];

    if (sc.SetDefaults)
    {
        sc.GraphName = "Custom Volume - Pixel Anchored";
        sc.GraphShortName = "[Volume][Bars+SMA]";
        sc.StudyDescription =
            "Pixel-anchored volume bars and a volume SMA in Chart Region 1. "
            "The visible-range maximum is dynamically mapped into a fixed "
            "percentage of the chart-region height, independently of all "
            "price and study scales.";

        sc.StudyVersion = 1;
        sc.AutoLoop = 0;
        sc.GraphRegion = 0;
        sc.DrawStudyUnderneathMainPriceGraph = 1;

        // The drawing callback itself recalculates the visible-range scale on
        // every chart redraw. Continuous UpdateAlways calls are unnecessary.
        sc.UpdateAlways = 0;

        VolumeBars.Name = "Volume Bars";
        VolumeBars.DrawStyle = DRAWSTYLE_IGNORE;
        VolumeBars.PrimaryColor = RGB(22, 41, 37);
        VolumeBars.SecondaryColor = RGB(62, 15, 13);
        VolumeBars.SecondaryColorUsed = 1;
        VolumeBars.LineWidth = 1;
        VolumeBars.DrawZeros = 0;

        VolumeSMA.Name = "Volume SMA";
        VolumeSMA.DrawStyle = DRAWSTYLE_IGNORE;
        VolumeSMA.PrimaryColor = RGB(0, 49, 179);
        VolumeSMA.LineWidth = 1;
        VolumeSMA.DrawZeros = 0;

        VolumeHeightPercent.Name = "Volume Height (% of Chart Region 1)";
        VolumeHeightPercent.SetFloat(25.0f);
        VolumeHeightPercent.SetFloatLimits(1.0f, 100.0f);

        BarGapPixels.Name = "Gap Between Volume Bars (Pixels)";
        BarGapPixels.SetInt(1);
        BarGapPixels.SetIntLimits(0, 20);

        ShowVolumeSMA.Name = "Show Volume Simple Moving Average";
        ShowVolumeSMA.SetYesNo(1);

        VolumeSMALength.Name = "Volume SMA Length (Bars)";
        VolumeSMALength.SetInt(20);
        VolumeSMALength.SetIntLimits(1, MAX_STUDY_LENGTH);

        EnablePercentileClipping.Name =
            "Enable Upper-Percentile Scale Clipping";
        EnablePercentileClipping.SetYesNo(0);

        TopPercentToExclude.Name =
            "Largest Visible Volume Bars to Exclude from Scale (%)";
        TopPercentToExclude.SetFloat(1.0f);
        TopPercentToExclude.SetFloatLimits(0.0f, 99.0f);

        IncludeCurrentBarInScale.Name =
            "Include Current/Live Bar in Scale Calculation";
        IncludeCurrentBarInScale.SetYesNo(1);

        return;
    }

    if (sc.LastCallToFunction)
    {
        sc.p_GDIFunction = nullptr;
        return;
    }

    // Set outside sc.SetDefaults so the pointer remains correct after the DLL
    // is unloaded and rebuilt while Sierra Chart is running.
    sc.p_GDIFunction = DrawPixelAnchoredVolumeOverlay;

    if (sc.ArraySize <= 0)
        return;

    int UpdateStartIndex = sc.UpdateStartIndex;

    if (UpdateStartIndex < 0)
        UpdateStartIndex = 0;

    if (UpdateStartIndex >= sc.ArraySize)
        UpdateStartIndex = sc.ArraySize - 1;

    for (int BarIndex = UpdateStartIndex;
         BarIndex < sc.ArraySize;
         ++BarIndex)
    {
        VolumeBars[BarIndex] = sc.BaseDataIn[SC_VOLUME][BarIndex];
    }

    const bool CalculateSMA = ShowVolumeSMA.GetYesNo() != 0;
    const int SMALength = ClampInt(
        VolumeSMALength.GetInt(),
        1,
        MAX_STUDY_LENGTH);

    sc.DataStartIndex = CalculateSMA ? SMALength - 1 : 0;

    if (!CalculateSMA)
        return;

    const int FirstSMAIndexToCalculate =
        (UpdateStartIndex > SMALength - 1)
            ? UpdateStartIndex
            : SMALength - 1;

    const int LastPreSMAIndex =
        (sc.ArraySize - 1 < SMALength - 2)
            ? sc.ArraySize - 1
            : SMALength - 2;

    for (int BarIndex = UpdateStartIndex;
         BarIndex <= LastPreSMAIndex;
         ++BarIndex)
    {
        VolumeSMA[BarIndex] = 0.0f;
    }

    if (FirstSMAIndexToCalculate >= sc.ArraySize)
        return;

    double RollingVolumeSum = 0.0;
    const int FirstWindowBar = FirstSMAIndexToCalculate - SMALength + 1;

    for (int BarIndex = FirstWindowBar;
         BarIndex <= FirstSMAIndexToCalculate;
         ++BarIndex)
    {
        RollingVolumeSum += VolumeBars[BarIndex];
    }

    VolumeSMA[FirstSMAIndexToCalculate] =
        static_cast<float>(RollingVolumeSum / SMALength);

    for (int BarIndex = FirstSMAIndexToCalculate + 1;
         BarIndex < sc.ArraySize;
         ++BarIndex)
    {
        RollingVolumeSum += VolumeBars[BarIndex];
        RollingVolumeSum -= VolumeBars[BarIndex - SMALength];

        VolumeSMA[BarIndex] =
            static_cast<float>(RollingVolumeSum / SMALength);
    }
}

void DrawPixelAnchoredVolumeOverlay(
    HWND WindowHandle,
    HDC DeviceContext,
    SCStudyInterfaceRef sc)
{
    (void)WindowHandle;
    (void)DeviceContext;

    if (sc.HideStudy || sc.ArraySize <= 0)
        return;

    const int RegionLeft = sc.StudyRegionLeftCoordinate;
    const int RegionRight = sc.StudyRegionRightCoordinate;
    const int RegionTop = sc.StudyRegionTopCoordinate;
    const int RegionBottom = sc.StudyRegionBottomCoordinate;

    if (RegionRight <= RegionLeft || RegionBottom <= RegionTop)
        return;

    int FirstVisibleBar = ClampInt(
        sc.IndexOfFirstVisibleBar,
        0,
        sc.ArraySize - 1);

    int LastVisibleBar = ClampInt(
        sc.IndexOfLastVisibleBar,
        0,
        sc.ArraySize - 1);

    if (LastVisibleBar < FirstVisibleBar)
        return;

    const double ScaleMaximum = DetermineVisibleScaleMaximum(
        sc,
        FirstVisibleBar,
        LastVisibleBar);

    if (ScaleMaximum <= 0.0)
        return;

    const int RegionHeight = RegionBottom - RegionTop;

    const double VolumeHeightPercent = ClampDouble(
        sc.Input[IN_VOLUME_HEIGHT_PERCENT].GetFloat(),
        1.0,
        100.0);

    int VolumeBandHeight = static_cast<int>(std::lround(
        RegionHeight * VolumeHeightPercent / 100.0));

    VolumeBandHeight = ClampInt(
        VolumeBandHeight,
        1,
        RegionHeight);

    const int BarSlotWidthPixels = DetermineBarSlotWidthPixels(
        sc,
        FirstVisibleBar);

    const int RequestedGapPixels = ClampInt(
        sc.Input[IN_BAR_GAP_PIXELS].GetInt(),
        0,
        20);

    // The slot width is measured directly between adjacent bar-center pixel
    // coordinates. The column therefore expands/contracts with chart zoom,
    // while the blank separation remains the requested fixed pixel count.
    // At extreme compression a one-pixel column is retained.
    const int BarWidthPixels =
        (BarSlotWidthPixels - RequestedGapPixels > 0)
            ? BarSlotWidthPixels - RequestedGapPixels
            : 1;

    SCSubgraphRef VolumeBars = sc.Subgraph[SG_VOLUME_BARS];

    // Two passes avoid repeated graphics brush/pen state changes when candle
    // direction alternates frequently.
    DrawVolumeBarsForDirection(
        sc,
        FirstVisibleBar,
        LastVisibleBar,
        true,
        ScaleMaximum,
        RegionLeft,
        RegionRight,
        RegionBottom,
        VolumeBandHeight,
        BarWidthPixels,
        VolumeBars.PrimaryColor);

    DrawVolumeBarsForDirection(
        sc,
        FirstVisibleBar,
        LastVisibleBar,
        false,
        ScaleMaximum,
        RegionLeft,
        RegionRight,
        RegionBottom,
        VolumeBandHeight,
        BarWidthPixels,
        VolumeBars.SecondaryColor);

    sc.Graphics.ResetBrush();
    sc.Graphics.ResetPen();

    if (sc.Input[IN_SHOW_VOLUME_SMA].GetYesNo() == 0)
        return;

    const int SMALength = ClampInt(
        sc.Input[IN_VOLUME_SMA_LENGTH].GetInt(),
        1,
        MAX_STUDY_LENGTH);

    SCSubgraphRef VolumeSMA = sc.Subgraph[SG_VOLUME_SMA];

    n_ACSIL::s_GraphicsPen SMAPen;
    SMAPen.m_PenColor.SetColorValue(VolumeSMA.PrimaryColor);
    SMAPen.m_PenStyle =
        n_ACSIL::s_GraphicsPen::e_PenStyle::PEN_STYLE_SOLID;
    SMAPen.m_Width = (VolumeSMA.LineWidth > 0)
        ? VolumeSMA.LineWidth
        : 1;
    sc.Graphics.SetPen(SMAPen);

    bool HasPreviousPoint = false;

    for (int BarIndex = FirstVisibleBar;
         BarIndex <= LastVisibleBar;
         ++BarIndex)
    {
        if (BarIndex < SMALength - 1)
            continue;

        const int PointX = ClampInt(
            sc.BarIndexToXPixelCoordinate(BarIndex),
            RegionLeft,
            RegionRight);
        const int PointY = ValueToLineYCoordinate(
            VolumeSMA[BarIndex],
            ScaleMaximum,
            RegionBottom,
            VolumeBandHeight);

        if (!HasPreviousPoint)
        {
            sc.Graphics.MoveTo(PointX, PointY);
            HasPreviousPoint = true;
        }
        else
        {
            sc.Graphics.LineTo(PointX, PointY);
        }
    }

    sc.Graphics.ResetPen();
}
