//+------------------------------------------------------------------+
//|                                             IndicatorExporter.mqh|
//+------------------------------------------------------------------+
#ifndef INDICATOREXPORTER_MQH
#define INDICATOREXPORTER_MQH

#include "IndicatorRow.mqh"
//+------------------------------------------------------------------+
//| Wraps one dynamic double array so a fixed-size array of these    |
//| structs can act as a jagged array of buffer results. MQL5 does   |
//| not allow a dynamic dimension anywhere but the first position of |
//| a multidimensional array, so double[8][] is not valid; wrapping  |
//| each slot's array inside a struct member gives each slot its own |
//| genuinely resizable array that CopyBuffer() can write into.      |
//+------------------------------------------------------------------+
struct CBufferSlot
  {
   double            data[];
  };
//+------------------------------------------------------------------+
//| Fetches indicator buffer values aligned to price bars and writes |
//| the result to a locale-safe CSV file.                            |
//+------------------------------------------------------------------+
class CIndicatorExporter
  {
private:
   string            m_symbol;            // instrument to export
   ENUM_TIMEFRAMES   m_timeframe;         // chart timeframe for the indicator
   string            m_indicator_name;    // indicator name as it appears in MQL5/Indicators
   double            m_params[4];         // up to 4 numeric input parameters
   int               m_param_count;       // how many of m_params are actually used
   int               m_buffer_indices[8]; // which buffer indices to export
   int               m_buffer_count;      // number of buffers being exported
   datetime          m_from;              // start of the export range
   datetime          m_to;                // end of the export range
   int               m_handle;            // indicator handle from iCustom()
   CIndicatorRow     m_rows[];            // accumulated export rows
   int               m_row_count;         // number of valid rows in m_rows[]

   bool              CreateHandle(void);
   bool              WaitForCalculation(int required_bars, int timeout_ms);
   string            FormatDouble(double value, int digits) const;

public:
                     CIndicatorExporter(void);
                    ~CIndicatorExporter(void);

   void              Init(const string &symbol, ENUM_TIMEFRAMES timeframe,
                        const string &indicator_name,
                        const double &params[], int param_count,
                        const int &buffer_indices[], int buffer_count,
                        datetime from, datetime to);
   bool              Fetch(void);
   bool              WriteCsv(const string &filename, const string &buffer_names[]);
   int               GetRowCount(void) const;
  };
//+------------------------------------------------------------------+
//| Constructor — sets all fields to safe initial values.            |
//+------------------------------------------------------------------+
CIndicatorExporter::CIndicatorExporter(void)
  {
   m_symbol         = "";
   m_timeframe      = PERIOD_CURRENT;
   m_indicator_name = "";
   m_param_count    = 0;
   m_buffer_count   = 0;
   m_from           = 0;
   m_to             = 0;
   m_handle         = INVALID_HANDLE;
   m_row_count      = 0;
   ::ArrayResize(m_rows, 0); // start with an empty row array
  }
//+------------------------------------------------------------------+
//| Destructor — releases the indicator handle if still open.        |
//+------------------------------------------------------------------+
CIndicatorExporter::~CIndicatorExporter(void)
  {
   if(m_handle != INVALID_HANDLE)
      ::IndicatorRelease(m_handle); // release the indicator instance
  }
//+------------------------------------------------------------------+
//| Stores every export parameter; performs no fetching or writing.  |
//+------------------------------------------------------------------+
void CIndicatorExporter::Init(const string &symbol, ENUM_TIMEFRAMES timeframe,
                              const string &indicator_name,
                              const double &params[], int param_count,
                              const int &buffer_indices[], int buffer_count,
                              datetime from, datetime to)
  {
   m_symbol         = symbol;
   m_timeframe      = timeframe;
   m_indicator_name = indicator_name;
   m_from           = from;
   m_to             = to;

//--- copy the numeric parameters, clamped to the 4-slot limit
   m_param_count = ::MathMin(param_count, 4);
   for(int i = 0; i < m_param_count; i++)
      m_params[i] = params[i];

//--- copy the requested buffer indices, clamped to the 8-slot limit
   m_buffer_count = ::MathMin(buffer_count, 8);
   for(int i = 0; i < m_buffer_count; i++)
      m_buffer_indices[i] = buffer_indices[i];
  }
//+-------------------------------------------------------------------+
//| Creates the indicator handle via iCustom(), dispatching to the    |
//| matching fixed-argument call based on m_param_count. MQL5 allows  |
//| iCustom() itself to accept a variable argument count, but any one |
//| call site must supply a fixed number of arguments known at        |
//| compile time, so a runtime-variable parameter count requires      |
//| this explicit switch across the supported counts.                 |
//+-------------------------------------------------------------------+
bool CIndicatorExporter::CreateHandle(void)
  {
   switch(m_param_count)
     {
      case 0:
         m_handle = ::iCustom(m_symbol, m_timeframe, m_indicator_name);
         break;
      case 1:
         m_handle = ::iCustom(m_symbol, m_timeframe, m_indicator_name,
                              m_params[0]);
         break;
      case 2:
         m_handle = ::iCustom(m_symbol, m_timeframe, m_indicator_name,
                              m_params[0], m_params[1]);
         break;
      case 3:
         m_handle = ::iCustom(m_symbol, m_timeframe, m_indicator_name,
                              m_params[0], m_params[1], m_params[2]);
         break;
      default:
         m_handle = ::iCustom(m_symbol, m_timeframe, m_indicator_name,
                              m_params[0], m_params[1],
                              m_params[2], m_params[3]);
         break;
     }

   if(m_handle == INVALID_HANDLE)
     {
      ::PrintFormat("CIndicatorExporter::CreateHandle: iCustom('%s') failed, error %d",
                    m_indicator_name, ::GetLastError());
      return(false);
     }
   return(true);
  }
//+------------------------------------------------------------------+
//| Polls BarsCalculated() until the indicator has processed at      |
//| least required_bars, or until timeout_ms elapses. Calling        |
//| CopyBuffer() before this completes is the most common cause of   |
//| a short or empty export on an indicator's first use.             |
//+------------------------------------------------------------------+
bool CIndicatorExporter::WaitForCalculation(int required_bars, int timeout_ms)
  {
   int elapsed  = 0;
   int interval = 50; // poll every 50 ms

   while(elapsed < timeout_ms)
     {
      int calculated = ::BarsCalculated(m_handle);
      //--- a negative result can be transient immediately after iCustom(),
      //--- before the terminal has finished registering the indicator
      //--- instance; treat it the same as "not enough bars yet" and retry
      //--- rather than failing on the first check
      if(calculated >= required_bars)
         return(true); // indicator has processed enough history

      ::Sleep(interval);
      elapsed += interval;
     }

   int final_calculated = ::BarsCalculated(m_handle);
   ::PrintFormat("CIndicatorExporter::WaitForCalculation: timed out after %d ms, "
                 "last BarsCalculated result %d of %d required",
                 timeout_ms, final_calculated, required_bars);
   return(false);
  }
//+------------------------------------------------------------------+
//| Formats a double for CSV output, always using a decimal point    |
//| regardless of the system's regional settings. Returns an empty   |
//| string for the EMPTY_VALUE sentinel so pandas reads it as NaN    |
//| rather than as a corrupted, enormous placeholder number.         |
//+------------------------------------------------------------------+
string CIndicatorExporter::FormatDouble(double value, int digits) const
  {
   if(value == EMPTY_VALUE)
      return(""); // warm-up period; no value available for this bar
   return(::DoubleToString(value, digits));
  }
//+------------------------------------------------------------------+
//| Creates the handle, waits for calculation, and retrieves price   |
//| data and every requested buffer over the configured date range.  |
//| If buffers return fewer bars than the price data (a rare warm-up |
//| or feed edge case), rows are truncated to the shortest series    |
//| returned so all columns remain aligned to the same bar times.    |
//+------------------------------------------------------------------+
bool CIndicatorExporter::Fetch(void)
  {
   if(!CreateHandle())
      return(false);

//--- determine how many bars the requested range covers and wait for
//--- the indicator to finish calculating across that many bars
   int required = ::Bars(m_symbol, m_timeframe, m_from, m_to);
   if(!WaitForCalculation(required, 10000))
      return(false);

//--- fetch price data for the range in a single call
   MqlRates rates[];
   int price_count = ::CopyRates(m_symbol, m_timeframe, m_from, m_to, rates);
   if(price_count <= 0)
     {
      ::PrintFormat("CIndicatorExporter::Fetch: CopyRates returned no bars, error %d",
                    ::GetLastError());
      return(false);
     }

   int min_count = price_count;

//--- fetch each requested buffer; buffers share the same time axis
//--- as the underlying price series for the same symbol and timeframe
   CBufferSlot buffer_data[8];
   for(int b = 0; b < m_buffer_count; b++)
     {
      int n = ::CopyBuffer(m_handle, m_buffer_indices[b], m_from, m_to, buffer_data[b].data);
      if(n <= 0)
        {
         ::PrintFormat("CIndicatorExporter::Fetch: CopyBuffer(index=%d) returned no data, error %d",
                       m_buffer_indices[b], ::GetLastError());
         return(false);
        }
      if(n < min_count)
         min_count = n; // shrink to the shortest series so columns stay aligned
     }

//--- assemble the final row set, one row per aligned bar
   ::ArrayResize(m_rows, min_count);
   for(int i = 0; i < min_count; i++)
     {
      m_rows[i].bar_time    = rates[i].time;
      m_rows[i].open        = rates[i].open;
      m_rows[i].high        = rates[i].high;
      m_rows[i].low         = rates[i].low;
      m_rows[i].close       = rates[i].close;
      m_rows[i].tick_volume = rates[i].tick_volume;

      for(int b = 0; b < m_buffer_count; b++)
         m_rows[i].values[b] = buffer_data[b].data[i];
     }

   m_row_count = min_count;
   return(true);
  }
//+------------------------------------------------------------------+
//| Writes the accumulated rows to a CSV file. Every field is        |
//| formatted explicitly and joined manually rather than relying on  |
//| FILE_CSV mode, avoiding locale-dependent delimiter or decimal    |
//| separator corruption on non-English regional settings.           |
//+------------------------------------------------------------------+
bool CIndicatorExporter::WriteCsv(const string &filename, const string &buffer_names[])
  {
   int handle = ::FileOpen(filename, FILE_WRITE | FILE_TXT | FILE_ANSI);
   if(handle == INVALID_HANDLE)
     {
      ::PrintFormat("CIndicatorExporter::WriteCsv: cannot open '%s', error %d",
                    filename, ::GetLastError());
      return(false);
     }

   int digits = (int)::SymbolInfoInteger(m_symbol, SYMBOL_DIGITS);

//--- build and write the header row
   string header = "time,open,high,low,close,tick_volume";
   for(int b = 0; b < m_buffer_count; b++)
      header += "," + buffer_names[b];
   ::FileWriteString(handle, header + "\r\n");

//--- build and write each data row
   for(int i = 0; i < m_row_count; i++)
     {
      string line = ::TimeToString(m_rows[i].bar_time,
                                   TIME_DATE | TIME_MINUTES | TIME_SECONDS);
      line += "," + FormatDouble(m_rows[i].open,  digits);
      line += "," + FormatDouble(m_rows[i].high,  digits);
      line += "," + FormatDouble(m_rows[i].low,   digits);
      line += "," + FormatDouble(m_rows[i].close, digits);
      line += "," + ::IntegerToString(m_rows[i].tick_volume);

      for(int b = 0; b < m_buffer_count; b++)
         line += "," + FormatDouble(m_rows[i].values[b], digits);

      ::FileWriteString(handle, line + "\r\n");
     }

   ::FileClose(handle);
   ::PrintFormat("CIndicatorExporter::WriteCsv: wrote %d rows to %s", m_row_count, filename);
   return(true);
  }
//+------------------------------------------------------------------+
//| Returns the number of rows fetched and ready for export.         |
//+------------------------------------------------------------------+
int CIndicatorExporter::GetRowCount(void) const
  {
   return(m_row_count);
  }

#endif // INDICATOREXPORTER_MQH
//+------------------------------------------------------------------+