//+------------------------------------------------------------------+
//|                                               CalendarParser.mqh |
//+------------------------------------------------------------------+
#ifndef CALENDARPARSER_MQH
#define CALENDARPARSER_MQH

#include "NewsEvent.mqh"

//+------------------------------------------------------------------+
//| CCalendarParser                                                  |
//| Opens the economic calendar file, reads it line by line, and     |
//| converts each valid row into a CNewsEvent record. Malformed rows |
//| are logged and skipped without aborting the parse.               |
//+------------------------------------------------------------------+
class CCalendarParser
  {
private:
   int               m_skipped_rows;

   bool              ParseLine(const string line, CNewsEvent &event);
   int               SplitCsvLine(const string line, string &fields[]);
   bool              ParseEventTime(const string date_field, const string time_field, datetime &result);
   int               MonthNumber(const string month_name);
   bool              ParseClockTime(const string time_field, int &hour, int &minute);

public:
                     CCalendarParser(void);
                    ~CCalendarParser(void);

   int               Parse(const string filename, CNewsEvent &events[]);
   ENUM_NEWS_IMPACT  MapImpact(const string impact_text);
   int               SkippedRows(void) const { return(m_skipped_rows); }
  };

//+------------------------------------------------------------------+
//| Constructor: Resets the skipped-row counter.                     |
//+------------------------------------------------------------------+
CCalendarParser::CCalendarParser(void)
  {
//--- start with no skipped rows recorded
   m_skipped_rows = 0;
  }

//+------------------------------------------------------------------+
//| Destructor: No resources are owned, so nothing to release.       |
//+------------------------------------------------------------------+
CCalendarParser::~CCalendarParser(void)
  {
  }

//+------------------------------------------------------------------+
//| Parse                                                            |
//| Opens the economic calendar file from MQL5/Files, skips the      |
//| header row, parses each remaining line into a CNewsEvent, and    |
//| returns the number of event records loaded, or -1 if the file    |
//| could not be opened.                                             |
//+------------------------------------------------------------------+
int CCalendarParser::Parse(const string filename, CNewsEvent &events[])
  {
//--- reset state from any previous parse
   m_skipped_rows = 0;
   ::ArrayResize(events, 0);
//--- open the economic calendar file in text mode for line reading
   int handle = ::FileOpen(filename, FILE_READ | FILE_TXT | FILE_ANSI);
   if(handle == INVALID_HANDLE)
     {
      ::PrintFormat("CCalendarParser: cannot open '%s', error %d", filename, ::GetLastError());
      return(-1);
     }
//--- read and discard the header row
   if(!::FileIsEnding(handle))
      ::FileReadString(handle);
//--- read every remaining line and parse it into an event record
   int count = 0;
   while(!::FileIsEnding(handle))
     {
      string line = ::FileReadString(handle);
      //--- ignore blank lines silently, they carry no data
      if(::StringLen(line) == 0)
         continue;
      //--- attempt to parse the line; log and skip on failure
      CNewsEvent event;
      if(!ParseLine(line, event))
        {
         m_skipped_rows++;
         ::PrintFormat("CCalendarParser: skipping malformed row: %s", line);
         continue;
        }
      //--- append the parsed event record to the output array
      ::ArrayResize(events, count + 1);
      events[count] = event;
      count++;
     }
//--- release the file handle and report the total
   ::FileClose(handle);
   return(count);
  }

//+------------------------------------------------------------------+
//| ParseLine                                                        |
//| Converts one CSV row into a CNewsEvent. Requires at least the    |
//| five leading columns (Date, Time, Currency, Impact, Event); the  |
//| Actual, Forecast, and Previous columns may be empty or absent.   |
//+------------------------------------------------------------------+
bool CCalendarParser::ParseLine(const string line, CNewsEvent &event)
  {
//--- split the row into fields while respecting quoted commas
   string fields[];
   int    field_count = SplitCsvLine(line, fields);
   if(field_count < 5)
      return(false);
//--- convert the quoted date and am/pm time into a datetime
   datetime event_time = 0;
   if(!ParseEventTime(fields[0], fields[1], event_time))
      return(false);
//--- reject rows with a missing currency code
   string currency = fields[2];
   ::StringTrimLeft(currency);
   ::StringTrimRight(currency);
   if(::StringLen(currency) != 3)
      return(false);
//--- populate the event record
   event.event_time = event_time;
   event.currency   = currency;
   event.impact     = MapImpact(fields[3]);
   event.title      = fields[4];
   return(true);
  }

//+------------------------------------------------------------------+
//| SplitCsvLine                                                     |
//| Splits a CSV line on commas while treating commas inside double  |
//| quotes as literal text. Quote characters are consumed and do not |
//| appear in the output fields. Returns the number of fields.       |
//+------------------------------------------------------------------+
int CCalendarParser::SplitCsvLine(const string line, string &fields[])
  {
//--- prepare an empty output array and scanning state
   ::ArrayResize(fields, 0);
   string current   = "";
   bool   in_quotes = false;
   int    length    = ::StringLen(line);
   int    count     = 0;
//--- walk the line character by character
   for(int i = 0; i < length; i++)
     {
      ushort ch = ::StringGetCharacter(line, i);
      //--- a quote toggles the in-quotes state and is not stored
      if(ch == '"')
        {
         in_quotes = !in_quotes;
         continue;
        }
      //--- an unquoted comma closes the current field
      if(ch == ',' && !in_quotes)
        {
         ::ArrayResize(fields, count + 1);
         fields[count] = current;
         count++;
         current = "";
         continue;
        }
      //--- any other character is appended to the current field
      current += ::ShortToString(ch);
     }
//--- store the final field after the last separator
   ::ArrayResize(fields, count + 1);
   fields[count] = current;
   count++;
   return(count);
  }

//+------------------------------------------------------------------+
//| ParseEventTime                                                   |
//| Combines a date field like "Jul 14, 2026" and a time field like  |
//| "8:30am" into a single datetime by building the canonical        |
//| "yyyy.mm.dd hh:mi" string and passing it to StringToTime().      |
//+------------------------------------------------------------------+
bool CCalendarParser::ParseEventTime(const string date_field, const string time_field, datetime &result)
  {
//--- normalize the date field and drop the embedded comma
   string date_clean = date_field;
   ::StringTrimLeft(date_clean);
   ::StringTrimRight(date_clean);
   ::StringReplace(date_clean, ",", "");
//--- split "Jul 14 2026" into month, day, and year tokens
   string tokens[];
   if(::StringSplit(date_clean, ' ', tokens) != 3)
      return(false);
//--- resolve the month name to its number
   int month = MonthNumber(tokens[0]);
   if(month == 0)
      return(false);
//--- convert the day and year tokens to integers
   int day  = (int)::StringToInteger(tokens[1]);
   int year = (int)::StringToInteger(tokens[2]);
   if(day < 1 || day > 31 || year < 2000)
      return(false);
//--- convert the am/pm clock time to 24-hour components
   int hour   = 0;
   int minute = 0;
   if(!ParseClockTime(time_field, hour, minute))
      return(false);
//--- assemble the canonical datetime string and convert it
   string stamp = ::StringFormat("%04d.%02d.%02d %02d:%02d", year, month, day, hour, minute);
   result = ::StringToTime(stamp);
   return(result > 0);
  }

//+------------------------------------------------------------------+
//| MonthNumber                                                      |
//| Maps a three-letter English month abbreviation to its number     |
//| from 1 to 12, or returns 0 when the name is not recognized.      |
//+------------------------------------------------------------------+
int CCalendarParser::MonthNumber(const string month_name)
  {
//--- compare against each abbreviation used by the export format
   if(month_name == "Jan")
      return(1);
   if(month_name == "Feb")
      return(2);
   if(month_name == "Mar")
      return(3);
   if(month_name == "Apr")
      return(4);
   if(month_name == "May")
      return(5);
   if(month_name == "Jun")
      return(6);
   if(month_name == "Jul")
      return(7);
   if(month_name == "Aug")
      return(8);
   if(month_name == "Sep")
      return(9);
   if(month_name == "Oct")
      return(10);
   if(month_name == "Nov")
      return(11);
   if(month_name == "Dec")
      return(12);
//--- unknown month name
   return(0);
  }

//+------------------------------------------------------------------+
//| ParseClockTime                                                   |
//| Converts an am/pm time string like "8:30am" or "12:30pm" into    |
//| 24-hour components. Handles the two 12-hour traps: 12:xxam maps  |
//| to hour 0 (midnight) and 12:xxpm stays at hour 12 (noon).        |
//+------------------------------------------------------------------+
bool CCalendarParser::ParseClockTime(const string time_field, int &hour, int &minute)
  {
//--- normalize case and whitespace
   string clock = time_field;
   ::StringTrimLeft(clock);
   ::StringTrimRight(clock);
   ::StringToLower(clock);
   int length = ::StringLen(clock);
   if(length < 6)
      return(false);
//--- separate the am/pm marker from the numeric part
   string marker  = ::StringSubstr(clock, length - 2, 2);
   string numeric = ::StringSubstr(clock, 0, length - 2);
   if(marker != "am" && marker != "pm")
      return(false);
//--- split the numeric part into hour and minute
   string parts[];
   if(::StringSplit(numeric, ':', parts) != 2)
      return(false);
   hour   = (int)::StringToInteger(parts[0]);
   minute = (int)::StringToInteger(parts[1]);
   if(hour < 1 || hour > 12 || minute < 0 || minute > 59)
      return(false);
//--- apply the 12-hour conversion rules
   if(marker == "am" && hour == 12)
      hour = 0;
   if(marker == "pm" && hour != 12)
      hour += 12;
   return(true);
  }

//+------------------------------------------------------------------+
//| MapImpact                                                        |
//| Maps the impact string from the economic calendar file to the    |
//| ENUM_NEWS_IMPACT enum. Unrecognized strings default to           |
//| NEWS_IMPACT_LOW, which never triggers a block on its own.        |
//+------------------------------------------------------------------+
ENUM_NEWS_IMPACT CCalendarParser::MapImpact(const string impact_text)
  {
//--- normalize the input before comparing
   string impact = impact_text;
   ::StringTrimLeft(impact);
   ::StringTrimRight(impact);
//--- map the three recognized impact level strings
   if(impact == "High")
      return(NEWS_IMPACT_HIGH);
   if(impact == "Medium")
      return(NEWS_IMPACT_MEDIUM);
   if(impact == "Low")
      return(NEWS_IMPACT_LOW);
//--- safe default for anything unrecognized
   return(NEWS_IMPACT_LOW);
  }

#endif // CALENDARPARSER_MQH
//+------------------------------------------------------------------+