#include "sierrachart.h"
#include <map>
#include <string>
#include <utility>
#include <vector>
#include <fstream>
#include <cstdint>
#include <cmath>
#include <chrono>

SCDLLName("Volume Rate")

struct VolumeRateBar
{
    float Peak = 0.0f;
    float Close = 0.0f;
    bool Closed = false;
    bool Rebuilt = false;
    int RecordIndex = 0;
    double RecordVolume = 0.0;
    float RecordPeak = 0.0f;
    double FinalVolume = 0.0;
    float Dominance = 0.0f;
    bool Observed = false;
    bool FileMode = false;
    int64_t FileIndex = -1;
};

struct VolumeRateFile
{
    std::ifstream Stream;
    uint32_t HeaderSize = 0;
    uint32_t RecordSize = 0;
    int64_t Count = 0;
    int64_t Next = -1;

    bool Open(const char* Path)
    {
        Stream.open(Path, std::ios::binary);
        char Magic[4] = {};
        Stream.read(Magic, 4);
        Stream.read(reinterpret_cast<char*>(&HeaderSize), 4);
        Stream.read(reinterpret_cast<char*>(&RecordSize), 4);
        if (!Stream || std::string(Magic, 4) != "SCID"
            || HeaderSize < 56 || RecordSize != sizeof(s_IntradayRecord))
            return false;
        Stream.seekg(0, std::ios::end);
        const int64_t Bytes = static_cast<int64_t>(Stream.tellg());
        Count = Bytes > HeaderSize ? (Bytes - HeaderSize) / RecordSize : 0;
        return true;
    }

    bool Read(int64_t Index, s_IntradayRecord& Record)
    {
        if (Index < 0 || Index >= Count)
            return false;
        if (Index != Next)
        {
            Stream.clear();
            Stream.seekg(HeaderSize + Index * RecordSize);
        }
        Stream.read(reinterpret_cast<char*>(&Record), sizeof(Record));
        Next = Index + 1;
        return static_cast<bool>(Stream);
    }

    int64_t Find(double Time)
    {
        int64_t Low = 0, High = Count;
        s_IntradayRecord Record;
        while (Low < High)
        {
            const int64_t Middle = Low + (High - Low) / 2;
            if (!Read(Middle, Record))
                return Count;
            if (Record.DateTime.GetAsDouble() < Time)
                Low = Middle + 1;
            else
                High = Middle;
        }
        return Low;
    }
};

struct VolumeRateRecord
{
    double Time = 0.0;
    double Volume = 0.0;
    float Peak = 0.0f;
    float Close = 0.0f;
    float Dominance = 0.0f;
    int32_t Occurrence = 0;
    uint32_t Version = 1;
    uint32_t Checksum = 0;
};

static uint32_t VolumeRateChecksum(const VolumeRateRecord& Record)
{
    const unsigned char* Bytes = reinterpret_cast<const unsigned char*>(&Record);
    uint32_t Hash = 2166136261u;
    for (size_t i = 0; i < offsetof(VolumeRateRecord, Checksum); ++i)
        Hash = (Hash ^ Bytes[i]) * 16777619u;
    return Hash;
}

struct VolumeRateState
{
    std::map<std::pair<double, int>, VolumeRateBar> Bars;
    std::string Chart;
    int LastIndex = -1;
    double LastTime = 0.0;
    double LastEnd = 0.0;
    int Replay = 0;
    int PendingIndex = -1;
    bool MissingDataLogged = false;
    std::vector<VolumeRateBar*> IndexedBars;
    std::ofstream Cache;
    std::vector<VolumeRateRecord> Writes;
    bool CacheWarningLogged = false;
    double FirstTime = 0.0;
    std::map<std::string, VolumeRateFile> Files;
    n_ACSIL::s_BarPeriod Period;
    double CaptureStartTime = 0.0;
    int CaptureOccurrence = 0;
    int ShowStart = 0;
    int HistoricalBars = -2;
};

static void VolumeRateSave(VolumeRateState& State, double Time, int Occurrence,
    const VolumeRateBar& Bar)
{
    if (State.Replay || !State.Cache.is_open())
        return;
    VolumeRateRecord Record;
    Record.Time = Time;
    Record.Volume = Bar.FinalVolume;
    Record.Peak = Bar.Peak;
    Record.Close = Bar.Close;
    Record.Dominance = Bar.Dominance;
    Record.Occurrence = Occurrence;
    Record.Checksum = VolumeRateChecksum(Record);
    State.Writes.push_back(Record);
}

static void VolumeRateFlush(SCStudyInterfaceRef sc, VolumeRateState& State)
{
    if (State.Writes.empty())
        return;
    State.Cache.write(reinterpret_cast<const char*>(State.Writes.data()),
        static_cast<std::streamsize>(State.Writes.size() * sizeof(VolumeRateRecord)));
    State.Cache.flush();
    if (!State.Cache && !State.CacheWarningLogged)
    {
        sc.AddMessageToLog("Peak cache write failed", 0);
        State.CacheWarningLogged = true;
    }
    State.Writes.clear();
}

static void VolumeRateLoad(SCStudyInterfaceRef sc, VolumeRateState& State)
{
    uint64_t Hash = 14695981039346656037ull;
    for (unsigned char Character : State.Chart)
        Hash = (Hash ^ Character) * 1099511628211ull;
    SCString Path;
    Path.Format("%s\\VolumeRate_%016llX.cache", sc.DataFilesFolder().GetChars(),
        static_cast<unsigned long long>(Hash));
    std::ifstream File(Path.GetChars(), std::ios::binary);
    if (File)
    {
        VolumeRateRecord Record;
        while (File.read(reinterpret_cast<char*>(&Record), sizeof(Record)))
        {
            if (Record.Version != 1 || Record.Checksum != VolumeRateChecksum(Record)
                || !std::isfinite(Record.Time) || !std::isfinite(Record.Volume)
                || !std::isfinite(Record.Peak) || !std::isfinite(Record.Close)
                || !std::isfinite(Record.Dominance) || Record.Peak < Record.Close
                || Record.Close < 0.0f || Record.Occurrence < 0)
            {
                sc.AddMessageToLog("Peak cache invalid", 0);
                return;
            }
            VolumeRateBar& Bar = State.Bars[std::make_pair(Record.Time, Record.Occurrence)];
            Bar.Peak = Record.Peak;
            Bar.Close = Record.Close;
            Bar.FinalVolume = Record.Volume;
            Bar.Dominance = Record.Dominance;
            Bar.Closed = true;
            Bar.Rebuilt = true;
        }
        if (File.gcount() != 0)
        {
            sc.AddMessageToLog("Peak cache incomplete", 0);
            return;
        }
        File.close();
    }
    State.Cache.open(Path.GetChars(), std::ios::binary | std::ios::app);
    if (!State.Cache)
        sc.AddMessageToLog("Peak cache unavailable", 0);
}

SCSFExport scsf_VolumeRate(SCStudyInterfaceRef sc)
{
    if (sc.SetDefaults)
    {
        sc.GraphName = "Volume Rate";
        sc.StudyDescription = "Volume per second histogram.";
        sc.AutoLoop = 0;
        sc.GraphRegion = 1;
        sc.GraphDrawType = GDT_CUSTOM;
        sc.GraphUsesChartColors = 0;
        sc.MaintainAdditionalChartDataArrays = 1;
        sc.UpdateAlways = 1;
        sc.ValueFormat = 2;
        sc.ScaleRangeType = SCALE_ZEROBASED;

        sc.Input[0].Name = "Color Gradation";
        sc.Input[0].SetYesNo(1);
        sc.Input[1].Name = "Show Historical Bars (-1 All, 0 None)";
        sc.Input[1].SetInt(100);
        sc.Input[1].SetIntLimits(-1, 1000);

        sc.Subgraph[SC_OPEN].Name = "Open";
        sc.Subgraph[SC_HIGH].Name = "Peak";
        sc.Subgraph[SC_LOW].Name = "Low";
        sc.Subgraph[SC_LAST].Name = "Volume per Second";

        for (int s = 0; s < 4; ++s)
        {
            sc.Subgraph[s].PrimaryColor = RGB(40, 40, 40);
            sc.Subgraph[s].SecondaryColor = RGB(40, 40, 40);
            sc.Subgraph[s].DrawZeros = 1;
        }
        sc.Subgraph[SC_OPEN].DrawStyle = DRAWSTYLE_IGNORE;
        sc.Subgraph[SC_LOW].DrawStyle = DRAWSTYLE_IGNORE;
        sc.Subgraph[SC_HIGH].DrawStyle = DRAWSTYLE_BAR;
        sc.Subgraph[SC_HIGH].LineWidth = 1;
        sc.Subgraph[SC_LAST].DrawStyle = DRAWSTYLE_BAR;
        sc.Subgraph[SC_LAST].LineWidth = 7;

        sc.Subgraph[4].Name = "Weak";
        sc.Subgraph[5].Name = "Moderate";
        sc.Subgraph[6].Name = "Strong";
        sc.Subgraph[7].Name = "Very Strong";
        sc.Subgraph[4].PrimaryColor = RGB(70, 105, 85);
        sc.Subgraph[5].PrimaryColor = RGB(30, 125, 70);
        sc.Subgraph[6].PrimaryColor = RGB(0, 160, 65);
        sc.Subgraph[7].PrimaryColor = RGB(0, 220, 75);
        sc.Subgraph[4].SecondaryColor = RGB(120, 75, 75);
        sc.Subgraph[5].SecondaryColor = RGB(165, 50, 50);
        sc.Subgraph[6].SecondaryColor = RGB(205, 25, 25);
        sc.Subgraph[7].SecondaryColor = RGB(255, 0, 0);
        for (int s = 4; s < 8; ++s)
        {
            sc.Subgraph[s].DrawStyle = DRAWSTYLE_IGNORE;
            sc.Subgraph[s].SecondaryColorUsed = 1;
        }
        return;
    }

    VolumeRateState* State = static_cast<VolumeRateState*>(sc.GetPersistentPointer(1));
    if (sc.LastCallToFunction)
    {
        if (State)
            VolumeRateFlush(sc, *State);
        delete State;
        sc.SetPersistentPointer(1, nullptr);
        return;
    }
    if (sc.ArraySize == 0)
        return;
    if (State == nullptr)
    {
        State = new VolumeRateState;
        sc.SetPersistentPointer(1, State);
    }

    const int Replay = sc.ReplayStatus != REPLAY_STOPPED;
    n_ACSIL::s_BarPeriod& Period = State->Period;
    SCString Chart;
    Chart = State->Chart.c_str();
    if (sc.IsFullRecalculation || State->LastIndex < 0 || State->Replay != Replay)
    {
        sc.GetBarPeriodParameters(Period);
        Chart.Format("%s|%d|%d|%d|%d|%d|%d|%d|%d|%s",
            sc.Symbol.GetChars(), Period.ChartDataType,
            Period.IntradayChartBarPeriodType,
            Period.IntradayChartBarPeriodParameter1,
            Period.IntradayChartBarPeriodParameter2,
            Period.IntradayChartBarPeriodParameter3,
            Period.IntradayChartBarPeriodParameter4,
            Period.HistoricalChartBarPeriodType,
            Period.HistoricalChartDaysPerBar,
            Period.ACSILCustomChartStudyName.GetChars());
        SCString Settings;
        Settings.Format("|%s|%d|%d|%d|%d|%d|%d|%d", sc.ChartbookName().GetChars(),
            sc.ChartNumber, sc.StudyGraphInstanceID, sc.StartTime1, sc.EndTime1,
            sc.StartTime2, sc.EndTime2, sc.UseSecondStartEndTimes);
        Chart += Settings;
        sc.Subgraph[4].Name = "Weak";
        sc.Subgraph[5].Name = "Moderate";
        sc.Subgraph[6].Name = "Strong";
        sc.Subgraph[7].Name = "Very Strong";
    }

    const int Last = sc.ArraySize - 1;
    const double LastTime = sc.BaseDateTimeIn[Last].GetAsDouble();
    const double LastEnd = sc.BaseDataEndDateTime[Last].GetAsDouble();
    const bool Reset = State->Chart != Chart.GetChars()
        || State->Replay != Replay
        || (Replay && (LastTime < State->LastTime || LastEnd < State->LastEnd));
    if (Reset)
    {
        VolumeRateFlush(sc, *State);
        State->Cache.close();
        State->Cache.clear();
        State->Bars.clear();
        State->Files.clear();
        State->IndexedBars.clear();
        State->Chart = Chart.GetChars();
        State->LastIndex = -1;
        State->PendingIndex = -1;
        State->MissingDataLogged = false;
        State->CacheWarningLogged = false;
        State->Replay = Replay;
        State->CaptureStartTime = LastTime;
        State->CaptureOccurrence = 0;
        for (int j = Last; j > 0
            && sc.BaseDateTimeIn[j] == sc.BaseDateTimeIn[j - 1]; --j)
            ++State->CaptureOccurrence;
        if (!Replay)
            VolumeRateLoad(sc, *State);
    }
    const bool Remap = Reset || sc.IsFullRecalculation
        || State->HistoricalBars != sc.Input[1].GetInt()
        || State->FirstTime != sc.BaseDateTimeIn[0].GetAsDouble()
        || State->IndexedBars.size() > static_cast<size_t>(sc.ArraySize);
    if (Remap)
    {
        State->HistoricalBars = sc.Input[1].GetInt();
        int Low = 0, High = sc.ArraySize;
        while (Low < High)
        {
            const int Middle = Low + (High - Low) / 2;
            if (sc.BaseDateTimeIn[Middle].GetAsDouble() < State->CaptureStartTime)
                Low = Middle + 1;
            else
                High = Middle;
        }
        if (Low < sc.ArraySize
            && sc.BaseDateTimeIn[Low].GetAsDouble() == State->CaptureStartTime)
            Low += State->CaptureOccurrence;
        if (Low > Last)
            Low = Last;
        State->ShowStart = State->HistoricalBars < 0 ? 0 : Low - State->HistoricalBars;
        if (State->ShowStart < 0)
            State->ShowStart = 0;
        for (int i = 0; i < State->ShowStart; ++i)
            for (int s = 0; s < 4; ++s)
                sc.Subgraph[s][i] = 0.0f;
        State->IndexedBars.clear();
        State->PendingIndex = Last - 1;
    }
    State->IndexedBars.resize(sc.ArraySize, nullptr);
    sc.DataStartIndex = State->ShowStart;

    int Start = Remap || State->LastIndex < 0 ? 0 : State->LastIndex;
    if (Start < 0 || Start > Last)
        Start = 0;
    if (Start < State->ShowStart)
        Start = State->ShowStart;

    int Occurrence = 0;
    for (int j = Start; j > 0
        && sc.BaseDateTimeIn[j] == sc.BaseDateTimeIn[j - 1]; --j)
        ++Occurrence;

    for (int i = Start; i <= Last; ++i)
    {
        if (i > Start)
            Occurrence = sc.BaseDateTimeIn[i] == sc.BaseDateTimeIn[i - 1]
                ? Occurrence + 1 : 0;
        const double Time = sc.BaseDateTimeIn[i].GetAsDouble();
        VolumeRateBar*& Slot = State->IndexedBars[i];
        if (!Slot)
        {
            Slot = &State->Bars[std::make_pair(Time, Occurrence)];
            if (Slot->Closed && (i == Last || Slot->FinalVolume != sc.Volume[i]))
                *Slot = VolumeRateBar();
        }
        VolumeRateBar& Bar = *Slot;

        if (!Bar.Closed)
        {
            const bool Closed = sc.GetBarHasClosedStatus(i) == BHCS_BAR_HAS_CLOSED;
            double Seconds;
            if (Closed)
                Seconds = sc.TimeSpanOfBar(i).GetAsDouble() * 86400.0;
            else
                Seconds = (sc.BaseDataEndDateTime[i].GetAsDouble() - Time) * 86400.0;
            if (Seconds < 1.0)
                Seconds = 1.0;

            const float Volume = sc.Volume[i] > 0.0f ? sc.Volume[i] : 0.0f;
            const float ClassifiedVolume = sc.AskVolume[i] + sc.BidVolume[i];
            Bar.Dominance = ClassifiedVolume > 0.0f
                ? (sc.AskVolume[i] - sc.BidVolume[i]) / ClassifiedVolume : 0.0f;
            Bar.Close = static_cast<float>(Volume / Seconds);
            if (Bar.Close > Bar.Peak)
                Bar.Peak = Bar.Close;
            Bar.Closed = Closed;
            if (Closed)
            {
                Bar.FinalVolume = Volume;
                if (Bar.Observed)
                {
                    Bar.Rebuilt = true;
                    VolumeRateSave(*State, Time, Occurrence, Bar);
                }
                else if (i > State->PendingIndex)
                    State->PendingIndex = i;
            }
            else
                Bar.Observed = true;
        }

        sc.Subgraph[SC_OPEN][i] = 0.0f;
        sc.Subgraph[SC_HIGH][i] = Bar.Peak;
        sc.Subgraph[SC_LOW][i] = 0.0f;
        sc.Subgraph[SC_LAST][i] = Bar.Close;

        COLORREF BodyColor = sc.Subgraph[SC_LAST].PrimaryColor;
        COLORREF WickColor = sc.Subgraph[SC_HIGH].PrimaryColor;
        if (sc.Input[0].GetYesNo() && Bar.Dominance != 0.0f)
        {
            const float Strength = Bar.Dominance > 0.0f ? Bar.Dominance : -Bar.Dominance;
            const int Level = Strength < 0.10f ? 4 : Strength < 0.25f ? 5
                : Strength < 0.50f ? 6 : 7;
            BodyColor = Bar.Dominance > 0.0f ? sc.Subgraph[Level].PrimaryColor
                : sc.Subgraph[Level].SecondaryColor;
            WickColor = BodyColor;
        }
        sc.Subgraph[SC_HIGH].DataColor[i] = WickColor;
        sc.Subgraph[SC_LAST].DataColor[i] = BodyColor;
    }

    int Earliest = Remap ? 0 : Start;
    int RecordBudget = 2000;
    int BarBudget = 100;
    const auto Deadline = std::chrono::steady_clock::now() + std::chrono::milliseconds(2);
    bool Locked = false;
    s_IntradayRecord Record;
    while (State->PendingIndex >= State->ShowStart && RecordBudget > 0 && BarBudget > 0)
    {
        const int i = State->PendingIndex;
        int Duplicate = 0;
        for (int j = i; j > 0
            && sc.BaseDateTimeIn[j] == sc.BaseDateTimeIn[j - 1]; --j)
            ++Duplicate;
        const double Time = sc.BaseDateTimeIn[i].GetAsDouble();
        VolumeRateBar& Bar = *State->IndexedBars[i];
        if (!Bar.Closed)
            break;
        if (Bar.Rebuilt)
        {
            --State->PendingIndex;
            --BarBudget;
            continue;
        }

        const double StartUTC = sc.ConvertDateTimeFromChartTimeZone(
            sc.BaseDateTimeIn[i], TIMEZONE_UTC).GetAsDouble();
        const double EndUTC = sc.ConvertDateTimeFromChartTimeZone(
            sc.BaseDataEndDateTime[i], TIMEZONE_UTC).GetAsDouble();
        SCString Contract;
        sc.GetContinuousContractSymbolForBarIndex(i, Contract);
        if (!Contract.GetChars()[0])
            Contract = sc.Symbol;
        if (std::string(Contract.GetChars()) != sc.Symbol.GetChars())
            Bar.FileMode = true;
        VolumeRateFile* Raw = nullptr;
        if (Bar.FileMode && Period.ChartDataType == INTRADAY_DATA)
        {
            auto Found = State->Files.find(Contract.GetChars());
            if (Found == State->Files.end())
            {
                VolumeRateFile& File = State->Files[Contract.GetChars()];
                SCString Path;
                Path.Format("%s\\%s.scid", sc.DataFilesFolder().GetChars(), Contract.GetChars());
                File.Open(Path.GetChars());
                Raw = &File;
            }
            else
                Raw = &Found->second;
            if (Bar.FileIndex < 0)
                Bar.FileIndex = Raw->Find(StartUTC);
        }
        bool Finished = Period.ChartDataType != INTRADAY_DATA;
        while (!Finished && RecordBudget > 0)
        {
            if ((RecordBudget & 63) == 0 && std::chrono::steady_clock::now() >= Deadline)
            {
                RecordBudget = 0;
                break;
            }
            int Result = 0;
            if (Bar.FileMode)
            {
                Result = Raw && Raw->Read(Bar.FileIndex++, Record);
                if (Result && Record.DateTime.GetAsDouble() > EndUTC)
                    Result = 0;
            }
            else
            {
                Result = sc.ReadIntradayFileRecordForBarIndexAndSubIndex(
                    i, Bar.RecordIndex, Record,
                    Locked ? IFLA_NO_CHANGE : IFLA_LOCK_READ_HOLD);
                Locked = true;
            }
            --RecordBudget;
            if (!Result)
            {
                Finished = true;
                break;
            }

            ++Bar.RecordIndex;
            Bar.RecordVolume += Record.TotalVolume;
            double Seconds = (Record.DateTime.GetAsDouble() - StartUTC) * 86400.0;
            if (Seconds < 1.0)
                Seconds = 1.0;
            const float Rate = static_cast<float>(Bar.RecordVolume / Seconds);
            if (Rate > Bar.RecordPeak)
                Bar.RecordPeak = Rate;
        }

        if (Finished)
        {
            double Difference = Bar.RecordVolume - Bar.FinalVolume;
            if (Difference < 0.0)
                Difference = -Difference;
            if (Bar.RecordIndex > 0 && Difference < 0.5)
            {
                if (Bar.RecordPeak > Bar.Peak)
                    Bar.Peak = Bar.RecordPeak;
                VolumeRateSave(*State, Time, Duplicate, Bar);
            }
            else if (!Bar.FileMode && Period.ChartDataType == INTRADAY_DATA)
            {
                Bar.FileMode = true;
                Bar.RecordIndex = 0;
                Bar.RecordVolume = 0.0;
                Bar.RecordPeak = 0.0f;
                --BarBudget;
                continue;
            }
            else if (Bar.FinalVolume > 0.0 && !State->MissingDataLogged)
            {
                sc.AddMessageToLog("Historical peak data unavailable", 0);
                State->MissingDataLogged = true;
            }
            Bar.Rebuilt = true;
            sc.Subgraph[SC_HIGH][i] = Bar.Peak;
            if (i < Earliest)
                Earliest = i;
            --State->PendingIndex;
            --BarBudget;
        }
    }
    if (Locked)
        sc.ReadIntradayFileRecordForBarIndexAndSubIndex(-1, -1, Record, IFLA_RELEASE_AFTER_READ);

    VolumeRateFlush(sc, *State);
    sc.UpdateAlways = State->PendingIndex >= State->ShowStart;
    if (!sc.UpdateAlways)
        State->Files.clear();
    if (!sc.IsFullRecalculation)
        sc.EarliestUpdateSubgraphDataArrayIndex = Earliest;
    State->LastIndex = Last;
    State->LastTime = LastTime;
    State->LastEnd = LastEnd;
    State->Replay = Replay;
    State->FirstTime = sc.BaseDateTimeIn[0].GetAsDouble();
}
