//+------------------------------------------------------------------+
//|                                             JsonConfigLoader.mqh |
//+------------------------------------------------------------------+
#ifndef JSONCONFIGLOADER_MQH
#define JSONCONFIGLOADER_MQH

#include "StrategyConfig.mqh"

//+------------------------------------------------------------------+
//| Class CJsonConfigLoader                                          |
//| Reads a flat-object JSON file and populates SStrategyConfig      |
//+------------------------------------------------------------------+
class CJsonConfigLoader
  {
private:
   string            m_filepath;
   datetime          m_last_load_time;
   //--- private helpers
   bool              Tokenize(const string &raw, SStrategyConfig &cfg);
   string            StripQuotes(const string &s);
   string            TrimWhitespace(const string &s);
public:
                     CJsonConfigLoader(void);
                    ~CJsonConfigLoader(void);
   void              SetFilePath(const string &path);
   bool              Load(SStrategyConfig &cfg);
   datetime          GetLastLoadTime(void);
  };

//+------------------------------------------------------------------+
//| Constructor                                                      |
//+------------------------------------------------------------------+
CJsonConfigLoader::CJsonConfigLoader(void)
  {
   m_filepath       = "";
   m_last_load_time = 0;
  }

//+------------------------------------------------------------------+
//| Destructor                                                       |
//+------------------------------------------------------------------+
CJsonConfigLoader::~CJsonConfigLoader(void)
  {
  }

//+------------------------------------------------------------------+
//| Sets the JSON file path relative to MQL5\Files                   |
//+------------------------------------------------------------------+
void CJsonConfigLoader::SetFilePath(const string &path)
  {
   m_filepath = path;
  }

//+------------------------------------------------------------------+
//| Opens the file, reads it fully, and delegates to Tokenize        |
//+------------------------------------------------------------------+
bool CJsonConfigLoader::Load(SStrategyConfig &cfg)
  {
   int handle = ::FileOpen(m_filepath, FILE_READ | FILE_TXT | FILE_ANSI);
   if(handle == INVALID_HANDLE)
     {
      ::Print("CJsonConfigLoader::Load - cannot open file '", m_filepath,
              "', error code ", ::GetLastError());
      return(false);
     }

   string raw = "";
   while(!::FileIsEnding(handle))
      raw += ::FileReadString(handle) + "\n";

   ::FileClose(handle);

   bool result = Tokenize(raw, cfg);
   if(result)
      m_last_load_time = ::TimeCurrent();

   return(result);
  }

//+------------------------------------------------------------------+
//| Hand-written flat-object JSON tokenizer                          |
//+------------------------------------------------------------------+
bool CJsonConfigLoader::Tokenize(const string &raw, SStrategyConfig &cfg)
  {
   int brace_pos = ::StringFind(raw, "{");
   if(brace_pos < 0)
     {
      ::Print("CJsonConfigLoader::Tokenize - opening brace not found");
      return(false);
     }

   int pos = brace_pos + 1;
   int len = ::StringLen(raw);

   while(pos < len)
     {
      //--- skip whitespace and comma separators between pairs
      while(pos < len && (::StringGetCharacter(raw, pos) == ' '  ||
                          ::StringGetCharacter(raw, pos) == '\t' ||
                          ::StringGetCharacter(raw, pos) == '\n' ||
                          ::StringGetCharacter(raw, pos) == '\r' ||
                          ::StringGetCharacter(raw, pos) == ','))
         pos++;

      if(pos >= len || ::StringGetCharacter(raw, pos) == '}')
         break;

      if(::StringGetCharacter(raw, pos) != '"')
        {
         ::Print("CJsonConfigLoader::Tokenize - expected key quote at position ", pos);
         return(false);
        }

      //--- locate key boundaries
      int key_start = pos + 1;
      int key_end   = ::StringFind(raw, "\"", key_start);
      if(key_end < 0)
        {
         ::Print("CJsonConfigLoader::Tokenize - unterminated key string");
         return(false);
        }
      string key = ::StringSubstr(raw, key_start, key_end - key_start);

      //--- locate colon separator
      int colon_pos = ::StringFind(raw, ":", key_end);
      if(colon_pos < 0)
        {
         ::Print("CJsonConfigLoader::Tokenize - missing colon after key '", key, "'");
         return(false);
        }

      //--- locate end of value, respecting quoted strings
      int value_start = colon_pos + 1;
      while(value_start < len && (::StringGetCharacter(raw, value_start) == ' ' ||
                                  ::StringGetCharacter(raw, value_start) == '\t'))
         value_start++;

      int  scan      = value_start;
      bool in_quotes = false;
      while(scan < len)
        {
         ushort ch = ::StringGetCharacter(raw, scan);
         if(ch == '"')
            in_quotes = !in_quotes;
         else
           {
            if(!in_quotes && (ch == ',' || ch == '}'))
               break;
           }
         scan++;
        }

      string value_raw = ::StringSubstr(raw, value_start, scan - value_start);
      value_raw = TrimWhitespace(value_raw);

      //--- resolve type and assign to the matching struct field
      string resolved_type = "unknown";
      if(::StringLen(value_raw) > 0 && ::StringGetCharacter(value_raw, 0) == '"')
        {
         string string_value = StripQuotes(value_raw);
         resolved_type = "string";
         if(key == "m_comment")
            cfg.m_comment = string_value;
        }
      else
        {
         resolved_type = "number";
         double number_value = ::StringToDouble(value_raw);
         if(key == "m_lot_size")
            cfg.m_lot_size = number_value;
         else
            if(key == "m_stop_loss_pts")
               cfg.m_stop_loss_pts = number_value;
            else
               if(key == "m_take_profit_pts")
                  cfg.m_take_profit_pts = number_value;
               else
                  if(key == "m_max_spread_pts")
                     cfg.m_max_spread_pts = number_value;
                  else
                     if(key == "m_magic_number")
                        cfg.m_magic_number = (long)number_value;
        }

      ::Print("CJsonConfigLoader::Tokenize - key='", key, "' type=", resolved_type,
              " value=", value_raw);

      pos = scan;
     }

   return(true);
  }

//+------------------------------------------------------------------+
//| Removes surrounding double quotes from a value token             |
//+------------------------------------------------------------------+
string CJsonConfigLoader::StripQuotes(const string &s)
  {
   int len = ::StringLen(s);
   if(len < 2)
      return(s);
   if(::StringGetCharacter(s, 0) == '"' && ::StringGetCharacter(s, len - 1) == '"')
      return(::StringSubstr(s, 1, len - 2));
   return(s);
  }

//+------------------------------------------------------------------+
//| Trims leading and trailing spaces and tabs                       |
//+------------------------------------------------------------------+
string CJsonConfigLoader::TrimWhitespace(const string &s)
  {
   string result = s;
   ::StringTrimLeft(result);
   ::StringTrimRight(result);
   return(result);
  }

//+------------------------------------------------------------------+
//| Returns the timestamp of the most recent successful load         |
//+------------------------------------------------------------------+
datetime CJsonConfigLoader::GetLastLoadTime(void)
  {
   return(m_last_load_time);
  }

#endif // JSONCONFIGLOADER_MQH
//+------------------------------------------------------------------+