//+-------------------------------------------------------------------+
//|                                                       Surface.mqh |
//|                              Copyright 2026, Sandro Begashvili    |
//|                   https://www.mql5.com/en/users/sandrobegashvil   |
//|                          Telegram: https://t.me/MrMQL5Developer   |
//|                  Cairo G2D - 2D vector engine - PART 1 code       |
//|                                                                   |
//|  THE OUTPUT LAYER - the bridge between our pixel buffer and the   |
//|  MetaTrader chart.                                                |
//|                                                                   |
//|  A surface owns three things:                                     |
//|                                                                   |
//|    1. uint m_px[]        the ARGB pixels we rasterise into        |
//|    2. a dynamic resource ("::name") holding a copy of them        |
//|    3. one OBJ_BITMAP_LABEL on the chart displaying that resource  |
//|                                                                   |
//|  THE WHOLE POINT: no matter how many shapes are drawn - ten, or   |
//|  ten thousand - the terminal only ever tracks ONE chart object.   |
//|  Complexity costs us CPU time inside our own buffer; it costs the |
//|  terminal nothing.                                                |
//|                                                                   |
//|  A NOTE ON RESOURCE NAMES                                         |
//|  A dynamic resource is scoped to the EX5 PROGRAM, not to the      |
//|  chart. Two copies of the same EA on two charts that both create  |
//|  "::panel" share ONE pixel buffer, and whichever uploaded last    |
//|  wins on BOTH charts. CairoSurfaceName() below builds a name that |
//|  cannot collide; use it rather than a bare string literal.        |
//+-------------------------------------------------------------------+
#ifndef CAIRO5_SURFACE_MQH
#define CAIRO5_SURFACE_MQH

#include "Color.mqh"

//+-------------------------------------------------------------------+
//| Build a name that is unique to one surface on one chart.          |
//|                                                                   |
//| A chart id alone is not enough. The same chart can hold several   |
//| copies of the same program, a program can be re-initialised, and  |
//| a subwindow is a separate drawing area with its own coordinates.  |
//| Folding all three into the name makes a collision impossible      |
//| without making the name unreadable in the object list:            |
//|                                                                   |
//|     panel  ->  g2d_panel_130874521003_0                           |
//|                                                                   |
//| The chart id is unsigned here because ChartID() returns a long    |
//| and a negative number in an object name is only confusing.        |
//+-------------------------------------------------------------------+
string CairoSurfaceName(const string base, const long chart_id = 0,
                        const int subwin = 0)
  {
   const long id = (chart_id == 0) ? ChartID() : chart_id;

   return StringFormat("g2d_%s_%I64u_%d", base, (ulong)id, subwin);
  }

//+-------------------------------------------------------------------+
//| CCairoSurface                                                     |
//+-------------------------------------------------------------------+
class CCairoSurface
  {
private:
   int               m_width;
   int               m_height;
   string            m_object;        // chart object name
   string            m_resource;      // dynamic resource name ("::something")
   long              m_chart;
   int               m_subwin;
   bool              m_created;
   bool              m_bound;         // is OBJPROP_BMPFILE already set?

   void              Reset();         // back to the constructed state

public:
   //-----------------------------------------------------------------
   //  THE PIXEL BUFFER IS PUBLIC, ON PURPOSE.
   //
   //  MQL5 has no pointer-to-array type, so an accessor returning the
   //  buffer is not expressible - `uint *Pixels()` does not compile.
   //  The array must therefore be reached directly and passed by
   //  reference to the rasteriser:
   //
   //      ras.Fill( path, color, surface.m_px,
   //                surface.Width(), surface.Height() );
   //
   //  This is the same choice the standard library makes with
   //  CCanvas::m_pixels, and for the same reason.
   //-----------------------------------------------------------------
   uint              m_px[];          // the ARGB pixel buffer, row-major

                     CCairoSurface();
                    ~CCairoSurface();

   //--- lifecycle
   bool              Create(const string name, const int x, const int y,
                            const int w, const int h,
                            const long chart_id = 0, const int subwin = 0);
   void              Destroy();

   //--- geometry and state
   int               Width()     { return m_width;  }
   int               Height()    { return m_height; }
   bool              IsCreated() { return m_created; }

   //--- drawing support
   void              Clear(const uint argb = CAIRO_TRANSPARENT);
   void              Move(const int x, const int y);

   //--- push the buffer to the chart
   bool              Flush(const bool redraw = true);
  };

//+-------------------------------------------------------------------+
//| Constructor                                                       |
//+-------------------------------------------------------------------+
CCairoSurface::CCairoSurface()
  {
   m_width   = 0;
   m_height  = 0;
   m_chart   = 0;
   m_subwin  = 0;
   m_created = false;
   m_bound   = false;
  }

//+-------------------------------------------------------------------+
//| Destructor                                                        |
//+-------------------------------------------------------------------+
CCairoSurface::~CCairoSurface()
  {
   Destroy();
  }

//+-------------------------------------------------------------------+
//| Allocate the buffer, create the chart object, and bind the two    |
//| together through a dynamic resource.                              |
//|                                                                   |
//| The object is anchored to the UPPER-LEFT corner of the chart and  |
//| positioned in pixels, which is what a GUI wants: it must not      |
//| drift when the chart is scrolled or the price scale changes.      |
//+-------------------------------------------------------------------+
bool CCairoSurface::Create(const string name, const int x, const int y,
                           const int w, const int h,
                           const long chart_id, const int subwin)
  {
   Destroy();

   if(w <= 0 || h <= 0 || name == "")
      return false;

   m_width    = w;
   m_height   = h;
   m_chart    = chart_id;
   m_subwin   = subwin;
   m_object   = name;
   m_resource = "::" + name;

//--- from here on, every failure has to undo what came before it.
//--- Reset() is enough while nothing exists on the chart yet; once
//--- the object is created, only Destroy() will do.
   if(ArrayResize(m_px, w * h) != w * h)
     {
      Reset();
      return false;
     }

   Clear();

   if(!ObjectCreate(m_chart, m_object, OBJ_BITMAP_LABEL, m_subwin, 0, 0))
     {
      Reset();
      return false;
     }

   ObjectSetInteger(m_chart, m_object, OBJPROP_CORNER,     CORNER_LEFT_UPPER);
   ObjectSetInteger(m_chart, m_object, OBJPROP_ANCHOR,     ANCHOR_LEFT_UPPER);
   ObjectSetInteger(m_chart, m_object, OBJPROP_XDISTANCE,  x);
   ObjectSetInteger(m_chart, m_object, OBJPROP_YDISTANCE,  y);
   ObjectSetInteger(m_chart, m_object, OBJPROP_XSIZE,      w);
   ObjectSetInteger(m_chart, m_object, OBJPROP_YSIZE,      h);
   ObjectSetInteger(m_chart, m_object, OBJPROP_BACK,       false);
   ObjectSetInteger(m_chart, m_object, OBJPROP_SELECTABLE, false);
   ObjectSetInteger(m_chart, m_object, OBJPROP_HIDDEN,     true);

//--- Flush() refuses to run on a surface that is not created, so the
//--- flag has to go up before the first upload rather than after it.
//--- If that upload fails we are in a half-built state, and the only
//--- honest answer is to tear the whole thing down again.
   m_created = true;

   if(!Flush(false))
     {
      Destroy();
      return false;
     }

   return true;
  }

//+-------------------------------------------------------------------+
//| Remove the chart object and release the resource.                 |
//|                                                                   |
//| Then wipe every field. A destroyed surface must be indistinguish- |
//| able from a freshly constructed one - otherwise a stale m_width   |
//| outlives the buffer it described, and the next bug is a very      |
//| confusing one.                                                    |
//+-------------------------------------------------------------------+
void CCairoSurface::Destroy()
  {
   if(m_created)
     {
      ObjectDelete(m_chart, m_object);

      if(m_resource != "")
         ResourceFree(m_resource);
     }

   Reset();
  }

//+-------------------------------------------------------------------+
//| Drop every field back to the constructed state, and give the      |
//| pixel memory back to the terminal.                                |
//|                                                                   |
//| This touches nothing on the chart. It is the bookkeeping half of  |
//| Destroy(), split out so the failure paths in Create() can reuse   |
//| it before any chart object exists.                                |
//+-------------------------------------------------------------------+
void CCairoSurface::Reset()
  {
   m_width    = 0;
   m_height   = 0;
   m_object   = "";
   m_resource = "";
   m_chart    = 0;
   m_subwin   = 0;
   m_created  = false;
   m_bound    = false;

   ArrayResize(m_px, 0);
  }

//+-------------------------------------------------------------------+
//| Fill the whole buffer with one value.                             |
//|                                                                   |
//| The default is fully TRANSPARENT, not black: a GUI panel almost   |
//| always wants the chart visible around its rounded corners, and    |
//| starting from transparent is what makes that work.                |
//+-------------------------------------------------------------------+
void CCairoSurface::Clear(const uint argb)
  {
   ArrayFill(m_px, 0, ArraySize(m_px), argb);
  }

//+-------------------------------------------------------------------+
//| Move the bitmap on the chart. No repaint needed - the pixels are  |
//| unchanged, only the object's position is.                         |
//+-------------------------------------------------------------------+
void CCairoSurface::Move(const int x, const int y)
  {
   if(!m_created)
      return;

   ObjectSetInteger(m_chart, m_object, OBJPROP_XDISTANCE, x);
   ObjectSetInteger(m_chart, m_object, OBJPROP_YDISTANCE, y);
  }

//+-------------------------------------------------------------------+
//| Upload the buffer to the terminal and point the object at it.     |
//|                                                                   |
//| COLOR_FORMAT_ARGB_NORMALIZE is the format that honours the alpha  |
//| channel and expects STRAIGHT (non-premultiplied) pixels - exactly |
//| what our own blending will produce from Part 4 onwards. Using     |
//| COLOR_FORMAT_XRGB_NOALPHA here would silently throw away every    |
//| transparent corner.                                               |
//|                                                                   |
//| ResourceCreate copies the array, so the buffer may be modified    |
//| again immediately afterwards.                                     |
//|                                                                   |
//| THE OBJECT IS BOUND TO THE RESOURCE ONCE, NOT ONCE PER FRAME.     |
//| ObjectSetString( ... OBJPROP_BMPFILE ... ) tells the object which |
//| resource to display. That answer never changes for the life of a  |
//| surface - m_resource is fixed in Create() - so setting it again   |
//| on every flush is pure cost. ResourceCreate replaces the CONTENT  |
//| under a name that stays the same, and the object follows it.      |
//+-------------------------------------------------------------------+
bool CCairoSurface::Flush(const bool redraw)
  {
   if(!m_created)
      return false;

   if(!ResourceCreate(m_resource, m_px, m_width, m_height,
                      0, 0, 0, COLOR_FORMAT_ARGB_NORMALIZE))
      return false;

   if(!m_bound)
     {
      if(!ObjectSetString(m_chart, m_object, OBJPROP_BMPFILE, m_resource))
         return false;

      m_bound = true;
     }

   if(redraw)
      ChartRedraw(m_chart);

   return true;
  }

#endif // CAIRO5_SURFACE_MQH
