//+------------------------------------------------------------------+
//|                                                    JsonValue.mqh |
//| Minimal recursive JSON value + parser, just enough to decode the |
//| MSGARCH ZMQ server's responses (objects, arrays, strings,        |
//| numbers, bool, null).                                            |
//+------------------------------------------------------------------+
#include <Arrays/ArrayObj.mqh>

enum ENUM_JSON_TYPE
  {
   JSON_NULL,
   JSON_BOOL,
   JSON_NUMBER,
   JSON_STRING,
   JSON_ARRAY,
   JSON_OBJECT
  };

//+------------------------------------------------------------------+
//| A single JSON value (leaf or container).                         |
//+------------------------------------------------------------------+
class CJsonValue : public CObject
  {
public:
   ENUM_JSON_TYPE    m_type;
   double            m_num;
   bool              m_bool;
   string            m_str;
   CArrayObj         m_items;   // children, for JSON_ARRAY / JSON_OBJECT
   string            m_keys[];  // parallel to m_items, for JSON_OBJECT only

                     CJsonValue(void)
     {
      m_type = JSON_NULL;
      m_num  = 0.0;
      m_bool = false;
      m_str  = "";
     }

                    ~CJsonValue(void)
     {
      m_items.Clear(); // CArrayObj free-mode (default true) deletes owned children
     }

   bool              IsNull(void)   const { return m_type == JSON_NULL; }
   double            AsDouble(void) const { return m_num; }
   int               AsInt(void)    const { return (int)m_num; }
   bool              AsBool(void)   const { return m_bool; }
   string            AsString(void) const { return m_str; }
   int               Size(void)     const { return m_items.Total(); }

   //--- array access
   CJsonValue      *At(const int idx) const
     {
      if(idx < 0 || idx >= m_items.Total())
         return NULL;
      return (CJsonValue *)m_items.At(idx);
     }

   //--- object access by key (NULL if missing)
   CJsonValue      *Get(const string key) const
     {
      int n = ArraySize(m_keys);
      for(int i = 0; i < n; i++)
         if(m_keys[i] == key)
            return (CJsonValue *)m_items.At(i);
      return NULL;
     }

   //--- convenience: object access with a numeric/bool/string default
   double            GetDouble(const string key, const double def_val) const
     {
      CJsonValue *v = Get(key);
      if(v == NULL || v.IsNull())
         return def_val;
      return v.AsDouble();
     }

   string            GetString(const string key, const string def_val) const
     {
      CJsonValue *v = Get(key);
      if(v == NULL || v.IsNull())
         return def_val;
      return v.AsString();
     }

   bool              GetBool(const string key, const bool def_val) const
     {
      CJsonValue *v = Get(key);
      if(v == NULL || v.IsNull())
         return def_val;
      return v.AsBool();
     }

   //--- fills a double[] array from a JSON_ARRAY of numbers
   void              ToDoubleArray(double &out[]) const
     {
      int n = Size();
      ArrayResize(out, n);
      for(int i = 0; i < n; i++)
         out[i] = At(i).AsDouble();
     }

   void              AddChild(CJsonValue *child)
     {
      m_items.Add(child);
     }

   void              AddKeyedChild(const string key, CJsonValue *child)
     {
      int n = ArraySize(m_keys);
      ArrayResize(m_keys, n + 1);
      m_keys[n] = key;
      m_items.Add(child);
     }
  };

//+------------------------------------------------------------------+
//| Recursive-descent JSON parser.                                   |
//+------------------------------------------------------------------+
class CJsonParser
  {
private:
   string            m_src;
   int               m_pos;
   int               m_len;

   void              SkipWhitespace(void)
     {
      while(m_pos < m_len)
        {
         ushort c = StringGetCharacter(m_src, m_pos);
         if(c == ' ' || c == '\t' || c == '\n' || c == '\r')
            m_pos++;
         else
            break;
        }
     }

   ushort            Peek(void) { return (m_pos < m_len) ? StringGetCharacter(m_src, m_pos) : 0; }
   ushort            Next(void) { return (m_pos < m_len) ? StringGetCharacter(m_src, m_pos++) : 0; }

   string            ParseRawString(void)
     {
      string result = "";
      Next(); // opening quote
      while(m_pos < m_len)
        {
         ushort c = Next();
         if(c == '"')
            break;
         if(c == '\\')
           {
            ushort e = Next();
            switch(e)
              {
               case 'n':  result += "\n"; break;
               case 't':  result += "\t"; break;
               case 'r':  result += "\r"; break;
               case '"':  result += "\""; break;
               case '\\': result += "\\"; break;
               case '/':  result += "/";  break;
               default:   result += ShortToString(e); break;
              }
           }
         else
            result += ShortToString(c);
        }
      return result;
     }

   CJsonValue       *ParseValue(void)
     {
      SkipWhitespace();
      ushort c = Peek();
      if(c == '"') return ParseString();
      if(c == '{') return ParseObject();
      if(c == '[') return ParseArray();
      if(c == 't' || c == 'f') return ParseBool();
      if(c == 'n') return ParseNull();
      return ParseNumber();
     }

   CJsonValue       *ParseString(void)
     {
      CJsonValue *v = new CJsonValue();
      v.m_type = JSON_STRING;
      v.m_str  = ParseRawString();
      return v;
     }

   CJsonValue       *ParseNumber(void)
     {
      int start = m_pos;
      if(Peek() == '-')
         Next();
      while(m_pos < m_len)
        {
         ushort c = Peek();
         if((c >= '0' && c <= '9') || c == '.' || c == 'e' || c == 'E' || c == '+' || c == '-')
            Next();
         else
            break;
        }
      string numstr = StringSubstr(m_src, start, m_pos - start);
      CJsonValue *v = new CJsonValue();
      v.m_type = JSON_NUMBER;
      v.m_num  = StringToDouble(numstr);
      return v;
     }

   CJsonValue       *ParseBool(void)
     {
      CJsonValue *v = new CJsonValue();
      v.m_type = JSON_BOOL;
      if(Peek() == 't') { m_pos += 4; v.m_bool = true;  } // "true"
      else              { m_pos += 5; v.m_bool = false; } // "false"
      return v;
     }

   CJsonValue       *ParseNull(void)
     {
      m_pos += 4; // "null"
      CJsonValue *v = new CJsonValue();
      v.m_type = JSON_NULL;
      return v;
     }

   CJsonValue       *ParseArray(void)
     {
      CJsonValue *v = new CJsonValue();
      v.m_type = JSON_ARRAY;
      Next(); // '['
      SkipWhitespace();
      if(Peek() == ']') { Next(); return v; }
      while(true)
        {
         v.AddChild(ParseValue());
         SkipWhitespace();
         ushort c = Next();
         if(c == ']')
            break;
         SkipWhitespace(); // c was ',' -> continue to next element
        }
      return v;
     }

   CJsonValue       *ParseObject(void)
     {
      CJsonValue *v = new CJsonValue();
      v.m_type = JSON_OBJECT;
      Next(); // '{'
      SkipWhitespace();
      if(Peek() == '}') { Next(); return v; }
      while(true)
        {
         SkipWhitespace();
         string key = ParseRawString();
         SkipWhitespace();
         Next(); // ':'
         v.AddKeyedChild(key, ParseValue());
         SkipWhitespace();
         ushort c = Next();
         if(c == '}')
            break;
         SkipWhitespace(); // c was ',' -> continue to next pair
        }
      return v;
     }

public:
   //--- caller owns the returned CJsonValue* and must delete it
   CJsonValue       *Parse(const string json_text)
     {
      m_src = json_text;
      m_pos = 0;
      m_len = StringLen(m_src);
      if(m_len == 0)
         return NULL;
      return ParseValue();
     }
  };
//+------------------------------------------------------------------+
