//+------------------------------------------------------------------+
//|                                      CorrelationHeatmapChart.mqh |
//+------------------------------------------------------------------+
#ifndef CORRELATIONHEATMAPCHART_MQH
#define CORRELATIONHEATMAPCHART_MQH

#include <Canvas\Canvas.mqh>
#include "CorrelationTypes.mqh"

//--- palette used by the heatmap, kept at file scope
const color CORR_COLOR_BACKGROUND = clrWhite;
const color CORR_COLOR_TEXT       = clrBlack;
const color CORR_COLOR_BORDER     = C'190,190,190';
const color CORR_COLOR_NEUTRAL    = C'250,250,250';
const color CORR_COLOR_POSITIVE   = C'190,60,20';
const color CORR_COLOR_NEGATIVE   = C'20,90,170';
const color CORR_COLOR_DIAGONAL   = C'225,225,225';
const color CORR_COLOR_UNDEFINED  = C'248,248,248';
const color CORR_COLOR_WARNING    = C'255,140,0';

//+--------------------------------------------------------------------------+
//| CCorrelationHeatmapChart                                                 |
//| Owns a CCanvas-backed chart panel and renders a square heatmap of every  |
//| symbol pair's Pearson correlation, with the numeric value printed inside |
//| each cell. Unlike a metric whose color scale is normalized against       |
//| whatever happens to be in the data, correlation already has a fixed,     |
//| universal range of -1 to 1, so this chart always maps that same fixed    |
//| range to color rather than rescaling it per corr_matrix, which keeps a   |
//| cell's color meaning identical from one redraw to the next.              |
//+--------------------------------------------------------------------------+
class CCorrelationHeatmapChart
  {
private:
   CCanvas           m_canvas;
   string            m_object_name;
   bool              m_canvas_created;
   int               m_created_width;
   int               m_created_height;
   int               m_left_margin;
   int               m_top_margin;
   int               m_cell_size;

   color             CorrelationColor(double value, bool is_defined) const;
   void              DrawLabels(const CCorrelationMatrix &corr_matrix);
   void              DrawCell(int row, int col, const CCorrelationMatrix &corr_matrix, double warning_threshold);

public:
                     CCorrelationHeatmapChart(const string object_name);
                    ~CCorrelationHeatmapChart(void);
   bool              Draw(const CCorrelationMatrix &corr_matrix, double warning_threshold, int x, int y, int width, int height);
   void              Clear(void);
  };

//+---------------------------------------------------------------------+
//| Constructor                                                         |
//| Stores the chart object name and establishes the default layout     |
//| geometry. The canvas is created lazily on the first call to Draw(), |
//| so construction never touches the chart.                            |
//+---------------------------------------------------------------------+
CCorrelationHeatmapChart::CCorrelationHeatmapChart(const string object_name)
  {
   m_object_name    = object_name;
   m_canvas_created = false;
   m_created_width  = 0;
   m_created_height = 0;
   m_left_margin    = 90;
   m_top_margin     = 40;
   m_cell_size      = 60;
  }

//+------------------------------------------------------------------------+
//| Destructor                                                             |
//| Intentionally empty. Unlike a script that runs once and finishes,      |
//| this chart is meant to be redrawn repeatedly for as long as the        |
//| Expert Advisor that owns it stays attached to the chart, so nothing    |
//| here should tear the panel down automatically. Clear() is the explicit,|
//| caller-driven way to remove it, normally called once, from OnDeinit(). |
//+------------------------------------------------------------------------+
CCorrelationHeatmapChart::~CCorrelationHeatmapChart(void)
  {
  }

//+------------------------------------------------------------------+
//| Clear                                                            |
//| Explicitly removes the chart panel and releases the underlying   |
//| graphical resource.                                              |
//+------------------------------------------------------------------+
void CCorrelationHeatmapChart::Clear(void)
  {
   if(m_canvas_created)
     {
      m_canvas.Destroy();
      m_canvas_created = false;
      m_created_width  = 0;
      m_created_height = 0;
     }
  }

//+-----------------------------------------------------------------------------+
//| CorrelationColor                                                            |
//| Maps a correlation value to a color on a scale fixed to the metric's        |
//| own theoretical range, not to whatever range happens to appear in this      |
//| particular corr_matrix. Zero gets its own explicit branch and renders as the|
//| flat neutral color, so a genuine zero correlation is never nudged toward    |
//| red or blue by floating-point noise. An undefined cell is rendered in its   |
//| own dedicated color, distinct from a defined-but-neutral cell, so "this     |
//| symbol pair could not be scored" never looks the same as "this symbol pair  |
//| scored at zero."                                                            |
//+-----------------------------------------------------------------------------+
color CCorrelationHeatmapChart::CorrelationColor(const double value,const bool is_defined) const
  {
   if(!is_defined)
      return(CORR_COLOR_UNDEFINED);
   if(value == 0.0)
      return(CORR_COLOR_NEUTRAL);

   double intensity = ::MathAbs(value); // the scale is fixed to [-1, 1], so the value itself is the intensity
   int    from_r    = (int)(CORR_COLOR_NEUTRAL & 0xFF);
   int    from_g    = (int)((CORR_COLOR_NEUTRAL >> 8) & 0xFF);
   int    from_b    = (int)((CORR_COLOR_NEUTRAL >> 16) & 0xFF);
   color  target    = (value > 0.0) ? CORR_COLOR_POSITIVE : CORR_COLOR_NEGATIVE;
   int    to_r      = (int)(target & 0xFF);
   int    to_g      = (int)((target >> 8) & 0xFF);
   int    to_b      = (int)((target >> 16) & 0xFF);

   int out_r = (int)::MathRound(from_r + (to_r - from_r) * intensity);
   int out_g = (int)::MathRound(from_g + (to_g - from_g) * intensity);
   int out_b = (int)::MathRound(from_b + (to_b - from_b) * intensity);
   return((color)(out_r | (out_g << 8) | (out_b << 16)));
  }

//+------------------------------------------------------------------------+
//| DrawLabels                                                             |
//| Draws the symbol name above each column and to the left of each row,   |
//| measuring every label with TextGetSize() first so row labels can be    |
//| right-aligned flush against the grid and column labels can be centered |
//| above their own column.                                                |
//+------------------------------------------------------------------------+
void CCorrelationHeatmapChart::DrawLabels(const CCorrelationMatrix &corr_matrix)
  {
   int size = corr_matrix.Size();
   m_canvas.FontSet("Arial", 10, FW_BOLD, 0);
   ::TextSetFont("Arial", 10, FW_BOLD, 0);
   for(int i = 0; i < size; i++)
     {
      string label = corr_matrix.GetSymbol(i);
      uint label_w = 0, label_h = 0;
      ::TextGetSize(label, label_w, label_h);

      //--- row label, right-aligned against the left edge of the grid
      int row_y = m_top_margin + i * m_cell_size + (m_cell_size - (int)label_h) / 2;
      m_canvas.TextOut(m_left_margin - (int)label_w - 8, row_y, label, ::ColorToARGB(CORR_COLOR_TEXT, 255));

      //--- column label, centered above its own column
      int col_x = m_left_margin + i * m_cell_size + (m_cell_size - (int)label_w) / 2;
      m_canvas.TextOut(col_x, m_top_margin - (int)label_h - 6, label, ::ColorToARGB(CORR_COLOR_TEXT, 255));
     }
  }

//+----------------------------------------------------------------------+
//| DrawCell                                                             |
//| Fills and outlines one cell, prints its numeric correlation value    |
//| inside it, and adds a thick warning border when the cell's magnitude |
//| meets or exceeds warning_threshold. The diagonal is deliberately     |
//| excluded from the warning border, since a symbol's correlation with  |
//| itself is trivially 1.0 and flagging it would only distract from the |
//| genuine concentration-risk pairs the border exists to highlight.     |
//| An undefined cell prints "N/A" instead of a number.                  |
//+----------------------------------------------------------------------+
void CCorrelationHeatmapChart::DrawCell(const int row,const int col,const CCorrelationMatrix &corr_matrix,
                                        const double warning_threshold)
  {
   int cell_x = m_left_margin + col * m_cell_size;
   int cell_y = m_top_margin  + row * m_cell_size;
   bool   is_diagonal = (row == col);
   bool   is_defined  = corr_matrix.IsDefined(row, col);
   double value       = corr_matrix.GetValue(row, col);

   color fill_color = is_diagonal ? CORR_COLOR_DIAGONAL : CorrelationColor(value, is_defined);
   m_canvas.FillRectangle(cell_x, cell_y, cell_x + m_cell_size, cell_y + m_cell_size, ::ColorToARGB(fill_color, 255));
   m_canvas.Rectangle(cell_x, cell_y, cell_x + m_cell_size, cell_y + m_cell_size, ::ColorToARGB(CORR_COLOR_BORDER, 255));

//--- a thick warning border on any off-diagonal cell at or beyond the threshold
   if(!is_diagonal && is_defined && ::MathAbs(value) >= warning_threshold)
     {
      for(int inset = 0; inset < 3; inset++)
         m_canvas.Rectangle(cell_x + inset, cell_y + inset, cell_x + m_cell_size - inset, cell_y + m_cell_size - inset,
                            ::ColorToARGB(CORR_COLOR_WARNING, 255));
     }

//--- numeric value, or an explicit "N/A" when the cell is undefined
   string value_text = is_defined ? ::DoubleToString(value, 2) : "N/A";
   m_canvas.FontSet("Arial", 11, FW_BOLD, 0);
   ::TextSetFont("Arial", 11, FW_BOLD, 0);
   uint text_w = 0, text_h = 0;
   ::TextGetSize(value_text, text_w, text_h);
   int text_x = cell_x + (m_cell_size - (int)text_w) / 2;
   int text_y = cell_y + (m_cell_size - (int)text_h) / 2;
   m_canvas.TextOut(text_x, text_y, value_text, ::ColorToARGB(CORR_COLOR_TEXT, 255));
  }

//+-------------------------------------------------------------+
//| Draw                                                        |
//| Renders the full correlation corr_matrix: labels,           |
//| then one cell per row and column pair. An empty corr_matrix,|
//| meaning no symbols were available to build one from, is     |
//| rendered as an explanatory message instead of an empty grid.|
//+-------------------------------------------------------------+
bool CCorrelationHeatmapChart::Draw(const CCorrelationMatrix &corr_matrix,const double warning_threshold,
                                    const int x,const int y,const int width,const int height)
  {
//--- (re)create the canvas whenever it has not been created yet, or when the
//--- requested size no longer matches the size it was created at. The matrix
//--- can grow as positions open, so a fixed canvas size would silently clip
//--- the grid once the symbol count outgrows it.
   if(!m_canvas_created || width != m_created_width || height != m_created_height)
     {
      if(m_canvas_created)
         m_canvas.Destroy();
      if(!m_canvas.CreateBitmapLabel(m_object_name, x, y, width, height, COLOR_FORMAT_ARGB_NORMALIZE))
        {
         ::Print("CCorrelationHeatmapChart: failed to create canvas for object '", m_object_name, "'.");
         m_canvas_created = false;
         return(false);
        }
      m_canvas_created = true;
      m_created_width  = width;
      m_created_height = height;
     }
   m_canvas.Erase(::ColorToARGB(CORR_COLOR_BACKGROUND, 255));

   int size = corr_matrix.Size();
   if(size <= 0)
     {
      m_canvas.FontSet("Arial", 11, 0, 0);
      ::TextSetFont("Arial", 11, 0, 0);
      string message = "No open positions to correlate.";
      m_canvas.TextOut(m_left_margin, m_top_margin, message, ::ColorToARGB(CORR_COLOR_TEXT, 255));
      m_canvas.Update(true);
      return(true);
     }

   DrawLabels(corr_matrix);
   for(int row = 0; row < size; row++)
     {
      for(int col = 0; col < size; col++)
         DrawCell(row, col, corr_matrix, warning_threshold);
     }

   m_canvas.Update(true);
   return(true);
  }

#endif // CORRELATIONHEATMAPCHART_MQH
//+------------------------------------------------------------------+