//+------------------------------------------------------------------+
//|                                         PatternRecorderPanel.mqh |
//|                                  Copyright 2026, MetaQuotes Ltd. |
//|                          https://www.mql5.com/en/users/lynnchris |
//+------------------------------------------------------------------+
#property copyright "Copyright 2026, MetaQuotes Ltd."
#property link      "https://www.mql5.com/en/users/lynnchris"
#property version   "1.00"

#ifndef PART84_PATTERNRECORDERPANEL_MQH
#define PART84_PATTERNRECORDERPANEL_MQH

#include "PatternLibrary.mqh"
//--- Pattern extraction converts the selection rectangle into candle data
#include "PatternExtractor.mqh"
//--- Validation measures the structure of a selected stored sample
#include "DoubleBottomValidator.mqh"

//+------------------------------------------------------------------+
//| Declares the chart-object recorder panel                         |
//+------------------------------------------------------------------+
class CRecorderPanel
  {
private:
   long              m_chart_id;
   int               m_subwin;
   int               m_x, m_y;
   int               m_width, m_height;
   string            m_prefix;

   //--- Base object names
   string            m_bg_name;
   string            m_title_name;
   string            m_pattern_name;
   string            m_status_name;
   string            m_select_info_name;
   string            m_notes_label_name;
   string            m_notes_edit_name;
   string            m_count_name;
   string            m_validation_info_name;
   string            m_btn_select_name;
   string            m_btn_record_name;
   string            m_btn_cancel_name;
   string            m_btn_view_name;
   string            m_btn_validate_name;
   string            m_btn_delete_name;
   string            m_btn_close_name;
   //--- Name reserved for the chart selection rectangle
   string            m_rect_name;

   //--- Dynamic sample buttons (prefix "sample_")
   string            m_sample_prefix;
   ulong             m_selected_sample_id;

   //--- Library and state
   CPatternLibrary   *m_library;
   bool               m_isSelectionMode;
   bool               m_hasPending;
   SPatternSample     m_pendingSample;
   SDoubleBottomSettings m_validation_settings;

public:
                     CRecorderPanel();
                    ~CRecorderPanel();

   bool              Create(CPatternLibrary *lib, long chart_id, int subwin,
                            int x, int y,
                            const SDoubleBottomSettings &settings);
   void              Destroy();
   void              OnChartEvent(const int id, const long &lparam, const double &dparam, const string &sparam);

   void              UpdateUI();
   void              RefreshSampleList();
   void              ShowStatus(string msg, bool is_error = false);

private:
   void              CreateObject(string name, ENUM_OBJECT type, int x, int y, int w, int h, string text = "");
   void              SetObjectText(string name, string text);
   void              SetTextRows(string name, string text, int rows, int y, color text_color);
   string            GetObjectText(string name);
   bool              ObjectExists(string name);
   void              DeleteObject(string name);
   void              DeleteObjectsWithPrefix(string prefix);

   void              OnSelectFormation();
   void              OnRecord();
   void              OnCancel();
   void              OnViewSamples();
   void              OnValidateSample();
   void              OnDeleteSample();
   void              OnClose();
   void              OnSampleClick(string name);

   void              ClearSelection();
   bool              FindActiveRectangle();
   bool              CaptureCurrentSelection();
   void              ShowPendingInfo();
   void              ShowValidationResult(const ulong sample_id,
                                          const SDoubleBottomValidation &result);
   void              ClearValidationInfo();
   bool              IsMyObject(string name);
  };

//+------------------------------------------------------------------+
//| Initializes recorder-panel state and object names                |
//+------------------------------------------------------------------+
CRecorderPanel::CRecorderPanel()
   : m_chart_id(0), m_subwin(0), m_x(0), m_y(0), m_width(410), m_height(695),
     m_library(NULL), m_isSelectionMode(false), m_hasPending(false),
     m_selected_sample_id(0)
  {
   m_prefix = "PatternRecorder_";
   m_bg_name = m_prefix + "bg";
   m_title_name = m_prefix + "title";
   m_pattern_name = m_prefix + "pattern";
   m_status_name = m_prefix + "status";
   m_select_info_name = m_prefix + "info";
   m_notes_label_name = m_prefix + "notes_label";
   m_notes_edit_name = m_prefix + "notes_edit";
   m_count_name = m_prefix + "count";
   m_validation_info_name = m_prefix + "validation_info";
   m_btn_select_name = m_prefix + "btn_select";
   m_btn_record_name = m_prefix + "btn_record";
   m_btn_cancel_name = m_prefix + "btn_cancel";
   m_btn_view_name = m_prefix + "btn_view";
   m_btn_validate_name = m_prefix + "btn_validate";
   m_btn_delete_name = m_prefix + "btn_delete";
   m_btn_close_name = m_prefix + "btn_close";
   m_rect_name = "PatternRect";
   m_sample_prefix = m_prefix + "sample_";
   InitDoubleBottomSettings(m_validation_settings, 2, 5, 20.0,
                            0.05, 40.0, 0.0, 10.0);
  }

//+------------------------------------------------------------------+
//| Removes recorder-panel chart objects during destruction          |
//+------------------------------------------------------------------+
CRecorderPanel::~CRecorderPanel()
  {
   Destroy();
  }

//+------------------------------------------------------------------+
//| Creates all recorder-panel chart objects                         |
//+------------------------------------------------------------------+
bool CRecorderPanel::Create(CPatternLibrary *lib, long chart_id, int subwin,
                            int x, int y,
                            const SDoubleBottomSettings &settings)
  {
   m_library = lib;
   m_chart_id = chart_id;
   m_subwin = subwin;
   m_x = x;
   m_y = y;
   m_validation_settings = settings;

//--- Create a border-only rectangle as the panel background
   CreateObject(m_bg_name, OBJ_RECTANGLE_LABEL, x, y, m_width, m_height, "");
   ObjectSetInteger(m_chart_id, m_bg_name, OBJPROP_BORDER_COLOR, clrBlack);
   ObjectSetInteger(m_chart_id, m_bg_name, OBJPROP_WIDTH, 1);

//--- Create the panel title
   CreateObject(m_title_name, OBJ_LABEL, x+10, y+5, m_width-20, 30, "  PATTERN RECORDER");
   ObjectSetInteger(m_chart_id, m_title_name, OBJPROP_COLOR, clrBlack);
   ObjectSetInteger(m_chart_id, m_title_name, OBJPROP_FONTSIZE, 12);
   ObjectSetInteger(m_chart_id, m_title_name, OBJPROP_ALIGN, ALIGN_LEFT);

//--- Identify the pattern category recorded by this EA
   CreateObject(m_pattern_name, OBJ_LABEL, x+10, y+40, m_width-20, 25, "Label: DOUBLE BOTTOM CANDIDATE");
   ObjectSetInteger(m_chart_id, m_pattern_name, OBJPROP_COLOR, clrBlack);
   ObjectSetInteger(m_chart_id, m_pattern_name, OBJPROP_FONTSIZE, 10);

//--- Reserve a status line for success and error feedback
   CreateObject(m_status_name, OBJ_LABEL, x+10, y+70, m_width-20, 25, "Ready.");
   ObjectSetInteger(m_chart_id, m_status_name, OBJPROP_COLOR, clrDarkGreen);
   ObjectSetInteger(m_chart_id, m_status_name, OBJPROP_FONTSIZE, 10);

//--- Create the button that starts rectangle selection
   CreateObject(m_btn_select_name, OBJ_BUTTON, x+10, y+100, m_width-20, 28, "SELECT FORMATION");
   ObjectSetInteger(m_chart_id, m_btn_select_name, OBJPROP_COLOR, clrBlack);
   ObjectSetInteger(m_chart_id, m_btn_select_name, OBJPROP_BORDER_COLOR, clrBlack);
   ObjectSetInteger(m_chart_id, m_btn_select_name, OBJPROP_FONTSIZE, 10);

//--- Create the record and cancel controls on one row
   int bw = (m_width-30)/2;
   CreateObject(m_btn_record_name, OBJ_BUTTON, x+10, y+135, bw, 28, "RECORD");
   ObjectSetInteger(m_chart_id, m_btn_record_name, OBJPROP_COLOR, clrBlack);
   ObjectSetInteger(m_chart_id, m_btn_record_name, OBJPROP_BORDER_COLOR, clrBlack);
   ObjectSetInteger(m_chart_id, m_btn_record_name, OBJPROP_FONTSIZE, 10);
   CreateObject(m_btn_cancel_name, OBJ_BUTTON, x+20+bw, y+135, bw, 28, "CANCEL");
   ObjectSetInteger(m_chart_id, m_btn_cancel_name, OBJPROP_COLOR, clrBlack);
   ObjectSetInteger(m_chart_id, m_btn_cancel_name, OBJPROP_BORDER_COLOR, clrBlack);
   ObjectSetInteger(m_chart_id, m_btn_cancel_name, OBJPROP_FONTSIZE, 10);

//--- Display the time range and bar count for the active selection
   CreateObject(m_select_info_name, OBJ_LABEL, x+10, y+170, m_width-20, 50, "No formation selected.");
   ObjectSetInteger(m_chart_id, m_select_info_name, OBJPROP_COLOR, clrBlack);
   ObjectSetInteger(m_chart_id, m_select_info_name, OBJPROP_FONTSIZE, 10);

//--- Provide a free-text field for the trader's observations
   CreateObject(m_notes_label_name, OBJ_LABEL, x+10, y+225, 40, 25, "Notes:");
   ObjectSetInteger(m_chart_id, m_notes_label_name, OBJPROP_COLOR, clrBlack);
   CreateObject(m_notes_edit_name, OBJ_EDIT, x+55, y+225, m_width-65, 25, "");
   ObjectSetInteger(m_chart_id, m_notes_edit_name, OBJPROP_COLOR, clrBlack);
   ObjectSetInteger(m_chart_id, m_notes_edit_name, OBJPROP_BORDER_COLOR, clrBlack);

//--- Display the number of samples currently in the library
   CreateObject(m_count_name, OBJ_LABEL, x+10, y+255, m_width-20, 25, "Recorded Samples: 0");
   ObjectSetInteger(m_chart_id, m_count_name, OBJPROP_COLOR, clrBlack);
   ObjectSetInteger(m_chart_id, m_count_name, OBJPROP_FONTSIZE, 10);

//--- Create sample-list and deletion controls
   CreateObject(m_btn_view_name, OBJ_BUTTON, x+10, y+285, bw, 28, "VIEW SAMPLES");
   ObjectSetInteger(m_chart_id, m_btn_view_name, OBJPROP_COLOR, clrBlack);
   ObjectSetInteger(m_chart_id, m_btn_view_name, OBJPROP_BORDER_COLOR, clrBlack);
   ObjectSetInteger(m_chart_id, m_btn_view_name, OBJPROP_FONTSIZE, 10);
   CreateObject(m_btn_delete_name, OBJ_BUTTON, x+20+bw, y+285, bw, 28, "DELETE SELECTED");
   ObjectSetInteger(m_chart_id, m_btn_delete_name, OBJPROP_COLOR, clrBlack);
   ObjectSetInteger(m_chart_id, m_btn_delete_name, OBJPROP_BORDER_COLOR, clrBlack);
   ObjectSetInteger(m_chart_id, m_btn_delete_name, OBJPROP_FONTSIZE, 10);

//--- Create the validation control and its multi-line result area
   CreateObject(m_btn_validate_name, OBJ_BUTTON, x+10, y+320,
                m_width-20, 28, "VALIDATE SELECTED");
   ObjectSetInteger(m_chart_id, m_btn_validate_name, OBJPROP_COLOR, clrBlack);
   ObjectSetInteger(m_chart_id, m_btn_validate_name, OBJPROP_BORDER_COLOR, clrBlack);
   ObjectSetInteger(m_chart_id, m_btn_validate_name, OBJPROP_FONTSIZE, 10);
   ClearValidationInfo();

//--- Create the close button at the bottom of the panel
   CreateObject(m_btn_close_name, OBJ_BUTTON, x+10, y+655, m_width-20, 28, "CLOSE");
   ObjectSetInteger(m_chart_id, m_btn_close_name, OBJPROP_COLOR, clrBlack);
   ObjectSetInteger(m_chart_id, m_btn_close_name, OBJPROP_BORDER_COLOR, clrBlack);
   ObjectSetInteger(m_chart_id, m_btn_close_name, OBJPROP_FONTSIZE, 10);

//--- Synchronize all dynamic labels and redraw the chart
   UpdateUI();
   RefreshSampleList();
   ShowStatus("Ready.", false);
   ChartRedraw(m_chart_id);
   return(true);
  }

//+------------------------------------------------------------------+
//| Destroy all objects                                              |
//+------------------------------------------------------------------+
void CRecorderPanel::Destroy()
  {
   DeleteObject(m_bg_name);
   DeleteObject(m_title_name);
   DeleteObject(m_pattern_name);
   DeleteObject(m_status_name);
   DeleteObject(m_select_info_name);
   DeleteObject(m_notes_label_name);
   DeleteObject(m_notes_edit_name);
   DeleteObject(m_count_name);
   DeleteObjectsWithPrefix(m_validation_info_name);
   DeleteObjectsWithPrefix(m_select_info_name + "_row_");
   DeleteObject(m_btn_select_name);
   DeleteObject(m_btn_record_name);
   DeleteObject(m_btn_cancel_name);
   DeleteObject(m_btn_view_name);
   DeleteObject(m_btn_validate_name);
   DeleteObject(m_btn_delete_name);
   DeleteObject(m_btn_close_name);
   DeleteObjectsWithPrefix(m_sample_prefix);
   ObjectDelete(m_chart_id, m_rect_name);
   ChartRedraw(m_chart_id);
  }

//+------------------------------------------------------------------+
//| Helper: Create an object                                         |
//+------------------------------------------------------------------+
void CRecorderPanel::CreateObject(string name, ENUM_OBJECT type, int x, int y, int w, int h, string text = "")
  {
   if(!ObjectCreate(m_chart_id, name, type, m_subwin, 0, 0))
      return;
   ObjectSetInteger(m_chart_id, name, OBJPROP_XDISTANCE, x);
   ObjectSetInteger(m_chart_id, name, OBJPROP_YDISTANCE, y);
   ObjectSetInteger(m_chart_id, name, OBJPROP_XSIZE, w);
   ObjectSetInteger(m_chart_id, name, OBJPROP_YSIZE, h);
   if(text != "")
      ObjectSetString(m_chart_id, name, OBJPROP_TEXT, text);
   if(type == OBJ_BUTTON)
     {
      ObjectSetInteger(m_chart_id, name, OBJPROP_STATE, false);
      ObjectSetInteger(m_chart_id, name, OBJPROP_BORDER_TYPE, BORDER_FLAT);
      //--- Style sample buttons as transparent clickable text
      if(StringFind(name, m_sample_prefix) == 0)
        {
         //--- Remove both fill and border from sample-list buttons
         ObjectSetInteger(m_chart_id, name, OBJPROP_BGCOLOR, clrNONE);
         ObjectSetInteger(m_chart_id, name, OBJPROP_BORDER_COLOR, clrNONE);
        }
     }
   if(type == OBJ_EDIT)
      ObjectSetInteger(m_chart_id, name, OBJPROP_READONLY, false);
   if(type == OBJ_LABEL)
      ObjectSetInteger(m_chart_id, name, OBJPROP_HIDDEN, false);
  }

//+------------------------------------------------------------------+
//| Helper: Set object text                                          |
//+------------------------------------------------------------------+
void CRecorderPanel::SetObjectText(string name, string text)
  {
   if(name == m_select_info_name)
     {
      SetTextRows(name, text, 3, m_y+170, clrBlack);
      return;
     }
   if(ObjectExists(name))
      ObjectSetString(m_chart_id, name, OBJPROP_TEXT, text);
  }

//+------------------------------------------------------------------+
//| Display wrapped text using one chart label per row               |
//+------------------------------------------------------------------+
void CRecorderPanel::SetTextRows(string name, string text, int rows,
                                 int y, color text_color)
  {
//--- Hide any former single-label text
   DeleteObject(name);
   string remaining = text;
   StringReplace(remaining, "\r", "");
   for(int row = 0; row < rows; row++)
     {
      //--- Honour explicit newlines and wrap long lines at spaces
      int cut = StringFind(remaining, "\n");
      if(cut < 0)
         cut = StringLen(remaining);
      bool newline = (cut < StringLen(remaining));
      if(cut > 44)
        {
         cut = 44;
         newline = false;
         for(int j = 44; j > 0; j--)
           {
            if(StringSubstr(remaining, j, 1) == " ")
              {
               cut = j;
               break;
              }
           }
        }
      string line = StringSubstr(remaining, 0, cut);
      remaining = StringSubstr(remaining, cut + (newline ? 1 : 0));
      StringTrimLeft(remaining);
      string object_name = name + "_row_" + IntegerToString(row);
      if(line == "")
        {
         DeleteObject(object_name);
         continue;
        }
      CreateObject(object_name, OBJ_LABEL, m_x+10, y+row*17,
                   m_width-20, 17, line);
      ObjectSetString(m_chart_id, object_name, OBJPROP_TEXT, line);
      ObjectSetInteger(m_chart_id, object_name, OBJPROP_FONTSIZE, 9);
      ObjectSetInteger(m_chart_id, object_name, OBJPROP_COLOR, text_color);
     }
  }

//+------------------------------------------------------------------+
//| Helper: Get object text                                          |
//+------------------------------------------------------------------+
string CRecorderPanel::GetObjectText(string name)
  {
   if(ObjectExists(name))
      return(ObjectGetString(m_chart_id, name, OBJPROP_TEXT));
   return("");
  }

//+------------------------------------------------------------------+
//| Helper: Check object exists                                      |
//+------------------------------------------------------------------+
bool CRecorderPanel::ObjectExists(string name)
  {
   return(ObjectFind(m_chart_id, name) >= 0);
  }

//+------------------------------------------------------------------+
//| Helper: Delete an object                                         |
//+------------------------------------------------------------------+
void CRecorderPanel::DeleteObject(string name)
  {
   if(ObjectExists(name))
      ObjectDelete(m_chart_id, name);
  }

//+------------------------------------------------------------------+
//| Delete all objects with a given prefix                           |
//+------------------------------------------------------------------+
void CRecorderPanel::DeleteObjectsWithPrefix(string prefix)
  {
   string obj_name;
   for(int i = ObjectsTotal(m_chart_id, m_subwin) - 1; i >= 0; i--)
     {
      obj_name = ObjectName(m_chart_id, i, m_subwin);
      if(StringFind(obj_name, prefix) == 0)
         ObjectDelete(m_chart_id, obj_name);
     }
  }

//+------------------------------------------------------------------+
//| Check if object belongs to this panel                            |
//+------------------------------------------------------------------+
bool CRecorderPanel::IsMyObject(string name)
  {
   return((StringFind(name, m_prefix) == 0) || (name == m_rect_name));
  }

//+------------------------------------------------------------------+
//| Chart event handler                                              |
//+------------------------------------------------------------------+
void CRecorderPanel::OnChartEvent(const int id, const long &lparam, const double &dparam, const string &sparam)
  {
   if(id == CHARTEVENT_OBJECT_CLICK)
     {
      string name = sparam;
      if(!IsMyObject(name))
         return;

      if(StringFind(name, m_sample_prefix) == 0)
         OnSampleClick(name);
      else
         if(name == m_btn_select_name)
            OnSelectFormation();
         else
            if(name == m_btn_record_name)
               OnRecord();
            else
               if(name == m_btn_cancel_name)
                  OnCancel();
               else
                  if(name == m_btn_view_name)
                     OnViewSamples();
                  else
                     if(name == m_btn_validate_name)
                        OnValidateSample();
                     else
                        if(name == m_btn_delete_name)
                           OnDeleteSample();
                        else
                           if(name == m_btn_close_name)
                              OnClose();
     }
  }

//+------------------------------------------------------------------+
//| Handle click on a sample button                                  |
//+------------------------------------------------------------------+
void CRecorderPanel::OnSampleClick(string name)
  {
   string text = ObjectGetString(m_chart_id, name, OBJPROP_TEXT);
   string id_str = text;
   StringReplace(id_str, "#", "");
   int space = StringFind(id_str, " ");
   if(space > 0)
      id_str = StringSubstr(id_str, 0, space);
   ulong id = (ulong)StringToInteger(id_str);
   if(id == 0)
      return;

   m_selected_sample_id = id;
   ClearValidationInfo();
   ShowStatus("Selected sample #" + IntegerToString(id) +
              ". Validate or delete it.", false);

//--- Highlight the selected sample and reset all other sample colors
   string obj_name;
   for(int i = ObjectsTotal(m_chart_id, m_subwin) - 1; i >= 0; i--)
     {
      obj_name = ObjectName(m_chart_id, i, m_subwin);
      if(StringFind(obj_name, m_sample_prefix) == 0)
        {
         if(obj_name == name)
            ObjectSetInteger(m_chart_id, obj_name, OBJPROP_COLOR, clrYellow);
         else
            ObjectSetInteger(m_chart_id, obj_name, OBJPROP_COLOR, clrBlack);
        }
     }
   ChartRedraw(m_chart_id);
  }

//+------------------------------------------------------------------+
//| Starts formation selection with a chart rectangle                |
//+------------------------------------------------------------------+
void CRecorderPanel::OnSelectFormation()
  {
   ClearSelection();
   if(!ObjectCreate(m_chart_id, m_rect_name, OBJ_RECTANGLE, m_subwin, 0, 0))
     {
      ShowStatus("Failed to create selection rectangle.", true);
      return;
     }
   ObjectSetInteger(m_chart_id, m_rect_name, OBJPROP_COLOR, clrDodgerBlue);
   ObjectSetInteger(m_chart_id, m_rect_name, OBJPROP_WIDTH, 2);
   ObjectSetInteger(m_chart_id, m_rect_name, OBJPROP_STYLE, STYLE_SOLID);
   ObjectSetInteger(m_chart_id, m_rect_name, OBJPROP_FILL, false);
   ObjectSetInteger(m_chart_id, m_rect_name, OBJPROP_BACK, false);
   ObjectSetInteger(m_chart_id, m_rect_name, OBJPROP_SELECTABLE, true);
   ObjectSetInteger(m_chart_id, m_rect_name, OBJPROP_SELECTED, true);
   ObjectSetInteger(m_chart_id, m_rect_name, OBJPROP_HIDDEN, false);
   datetime t1 = TimeCurrent() - PeriodSeconds() * 30;
   datetime t2 = TimeCurrent();
   double price_high = ChartGetDouble(m_chart_id, CHART_PRICE_MAX);
   double price_low = ChartGetDouble(m_chart_id, CHART_PRICE_MIN);
   ObjectSetDouble(m_chart_id, m_rect_name, OBJPROP_PRICE, 0, price_high);
   ObjectSetDouble(m_chart_id, m_rect_name, OBJPROP_PRICE, 1, price_low);
   ObjectSetInteger(m_chart_id, m_rect_name, OBJPROP_TIME, 0, t1);
   ObjectSetInteger(m_chart_id, m_rect_name, OBJPROP_TIME, 1, t2);
   ChartRedraw(m_chart_id);
   m_isSelectionMode = true;
   m_hasPending = false;
   SetObjectText(m_select_info_name, "Drag the blue rectangle to cover the formation.\nThen click RECORD.");
   ShowStatus("Select formation using the rectangle.", false);
   UpdateUI();
  }

//+------------------------------------------------------------------+
//| Captures and stores the selected formation                       |
//+------------------------------------------------------------------+
void CRecorderPanel::OnRecord()
  {
   if(!m_isSelectionMode)
     {
      ShowStatus("Please click SELECT FORMATION first.", true);
      return;
     }
   if(!FindActiveRectangle())
     {
      ShowStatus("Rectangle not found. Please redraw.", true);
      return;
     }
   if(!CaptureCurrentSelection())
     {
      ShowStatus("Failed to capture formation. Check rectangle covers at least 2 candles.", true);
      return;
     }
   m_pendingSample.notes = GetObjectText(m_notes_edit_name);
   if(StringLen(m_pendingSample.notes) > MAX_NOTES_LENGTH)
     {
      ShowStatus("Notes must contain at most 256 characters.", true);
      return;
     }
   ulong new_id = m_library.GetNewID();
   m_pendingSample.id = new_id;
   if(m_library.AddSample(m_pendingSample))
     {
      ShowStatus("Pattern Sample #" + IntegerToString(new_id) +
                 " recorded successfully.", false);
      m_hasPending = false;
      m_isSelectionMode = false;
      ClearSelection();
      UpdateUI();
      RefreshSampleList();
     }
   else
     {
      ShowStatus("Failed to save sample. Check storage folder permissions.", true);
     }
  }

//+------------------------------------------------------------------+
//| Cancels the active formation selection                           |
//+------------------------------------------------------------------+
void CRecorderPanel::OnCancel()
  {
   ClearSelection();
   m_isSelectionMode = false;
   m_hasPending = false;
   SetObjectText(m_select_info_name, "Selection cancelled.");
   ShowStatus("Cancelled.", false);
   UpdateUI();
  }

//+------------------------------------------------------------------+
//| Displays recent samples as clickable text buttons                |
//+------------------------------------------------------------------+
void CRecorderPanel::OnViewSamples()
  {
   DeleteObjectsWithPrefix(m_sample_prefix);

   if(m_library == NULL)
      return;
   SPatternSample samples[];
   m_library.GetAllSamples(samples);
   int count = ArraySize(samples);
   if(count == 0)
     {
      ShowStatus("No samples recorded yet.", false);
      return;
     }

   int x = m_x + 10;
   int y = m_y + 535;
   int w = m_width - 20;
   int h = 20;
   int max_visible = 5;
   int start = (count > max_visible) ? count - max_visible : 0;

   for(int i = start; i < count; i++)
     {
      string name = m_sample_prefix + IntegerToString(i);
      string text = "#" + IntegerToString(samples[i].id) + "  " +
                    IntegerToString(samples[i].bars) + " bars  " +
                    TimeToString(samples[i].start_time, TIME_DATE);
      //--- Use a button object so the displayed sample remains clickable
      CreateObject(name, OBJ_BUTTON, x, y, w, h, text);
      //--- Remove the background and border to imitate a text list
      ObjectSetInteger(m_chart_id, name, OBJPROP_BGCOLOR, clrNONE);
      ObjectSetInteger(m_chart_id, name, OBJPROP_BORDER_COLOR, clrNONE);
      ObjectSetInteger(m_chart_id, name, OBJPROP_COLOR, clrBlack);
      ObjectSetInteger(m_chart_id, name, OBJPROP_FONTSIZE, 9);
      //--- Keep the button flat while preserving its click state
      ObjectSetInteger(m_chart_id, name, OBJPROP_BORDER_TYPE, BORDER_FLAT);
      ObjectSetInteger(m_chart_id, name, OBJPROP_STATE, false);
      y += h + 2;
      if(y > m_y + m_height - 50)
         break;
     }
   ShowStatus("Click a sample to validate or delete it.", false);
   ChartRedraw(m_chart_id);
  }

//+------------------------------------------------------------------+
//| Validates the sample selected in the panel                       |
//+------------------------------------------------------------------+
void CRecorderPanel::OnValidateSample()
  {
   if(m_selected_sample_id == 0)
     {
      ShowStatus("No sample selected. Click a sample in the list first.", true);
      return;
     }

//--- Copy the stored sample without altering its original CSV record
   SPatternSample sample;
   if(!m_library.GetSample(m_selected_sample_id, sample))
     {
      ShowStatus("Selected sample could not be loaded from the library.", true);
      return;
     }

//--- Apply the configured structural rules and display every outcome
   SDoubleBottomValidation result;
   bool structurally_valid = ValidateDoubleBottom(sample,
                             m_validation_settings,
                             result);
   ShowValidationResult(m_selected_sample_id, result);
   string status = DoubleBottomStatusToString(result.status);
   if(structurally_valid)
      ShowStatus("Sample #" + IntegerToString(m_selected_sample_id) +
                 " validation: " + status + ".", false);
   else
      ShowStatus("Sample #" + IntegerToString(m_selected_sample_id) +
                 " validation: INVALID.", true);
   ChartRedraw(m_chart_id);
  }

//+------------------------------------------------------------------+
//| Displays validation measurements in the panel                    |
//+------------------------------------------------------------------+
void CRecorderPanel::ShowValidationResult(const ulong sample_id,
      const SDoubleBottomValidation &result)
  {
   string status = DoubleBottomStatusToString(result.status);
   string text = "Sample #" + IntegerToString(sample_id) +
                 "  Status: " + status;
   color text_color = clrRed;

   if(result.status != DOUBLE_BOTTOM_INVALID)
     {
      if(result.status == DOUBLE_BOTTOM_CONFIRMED)
         text += " at #" + IntegerToString(result.breakout_index);
      else
         if(result.status == DOUBLE_BOTTOM_FAILED)
            text += " at #" + IntegerToString(result.failure_index);
      text += "  Structure: " + DoubleToString(result.score, 1);
      text += "\nLows: #" + IntegerToString(result.first_low_index) +
              " and #" + IntegerToString(result.second_low_index) +
              "  Separation: " + IntegerToString(result.separation_bars);
      text += "\nNeckline: #" + IntegerToString(result.neckline_index) +
              " at " + DoubleToString(result.neckline_price, _Digits);
      text += "\nDepth: " + DoubleToString(result.depth_pct, 3) +
              "%  Low difference: " +
              DoubleToString(result.low_difference_pct, 1) + "%";
      text += "\nSymmetry: " + DoubleToString(result.symmetry_pct, 1) +
              "%  Recovery: " + DoubleToString(result.recovery_pct, 1) + "%";
      if(result.status == DOUBLE_BOTTOM_CONFIRMED)
         text_color = clrDarkGreen;
      else
         if(result.status == DOUBLE_BOTTOM_CANDIDATE)
            text_color = clrBlue;
     }
   else
      text += "\n" + result.reason;

   SetTextRows(m_validation_info_name, text, 10, m_y+355, text_color);
  }

//+------------------------------------------------------------------+
//| Clears the previous validation result                            |
//+------------------------------------------------------------------+
void CRecorderPanel::ClearValidationInfo()
  {
   SetTextRows(m_validation_info_name, "Validation: select a stored sample.",
               10, m_y+355, clrBlack);
  }

//+------------------------------------------------------------------+
//| Deletes the sample selected in the panel                         |
//+------------------------------------------------------------------+
void CRecorderPanel::OnDeleteSample()
  {
   if(m_selected_sample_id == 0)
     {
      ShowStatus("No sample selected. Click a sample in the list first.", true);
      return;
     }
   if(m_library.RemoveSample(m_selected_sample_id))
     {
      ShowStatus("Sample #" + IntegerToString(m_selected_sample_id) + " deleted.", false);
      m_selected_sample_id = 0;
      ClearValidationInfo();
      RefreshSampleList();
      //--- Refresh the visible list after deletion
      OnViewSamples();
     }
   else
     {
      ShowStatus("Failed to delete sample #" + IntegerToString(m_selected_sample_id), true);
     }
  }

//+------------------------------------------------------------------+
//| Closes the recorder panel and clears its selection               |
//+------------------------------------------------------------------+
void CRecorderPanel::OnClose()
  {
   ClearSelection();
   Destroy();
  }

//+------------------------------------------------------------------+
//| Clear selection rectangle                                        |
//+------------------------------------------------------------------+
void CRecorderPanel::ClearSelection()
  {
   ObjectDelete(m_chart_id, m_rect_name);
   ChartRedraw(m_chart_id);
  }

//+------------------------------------------------------------------+
//| Find active rectangle                                            |
//+------------------------------------------------------------------+
bool CRecorderPanel::FindActiveRectangle()
  {
   return(ObjectFind(m_chart_id, m_rect_name) >= 0);
  }

//+------------------------------------------------------------------+
//| Capture current selection                                        |
//+------------------------------------------------------------------+
bool CRecorderPanel::CaptureCurrentSelection()
  {
   string error;
   bool result = ExtractFromRectangle(Symbol(), Period(), m_rect_name, m_pendingSample, error);
   if(!result)
     {
      ShowStatus("Extraction error: " + error, true);
      return(false);
     }
   m_hasPending = true;
   ShowPendingInfo();
   return(true);
  }

//+------------------------------------------------------------------+
//| Show pending info                                                |
//+------------------------------------------------------------------+
void CRecorderPanel::ShowPendingInfo()
  {
   if(!m_hasPending)
      return;
   string info = "";
   info += "Start: " + TimeToString(m_pendingSample.start_time) + "\n";
   info += "End:   " + TimeToString(m_pendingSample.end_time) + "\n";
   info += "Bars:  " + IntegerToString(m_pendingSample.bars);
   SetObjectText(m_select_info_name, info);
  }

//+------------------------------------------------------------------+
//| Update UI buttons                                                |
//+------------------------------------------------------------------+
void CRecorderPanel::UpdateUI()
  {
   if(m_isSelectionMode)
     {
      SetObjectText(m_btn_record_name, "RECORD");
      SetObjectText(m_btn_cancel_name, "CANCEL");
     }
   else
     {
      SetObjectText(m_btn_record_name, "RECORD (inactive)");
      SetObjectText(m_btn_cancel_name, "CANCEL");
     }
  }

//+------------------------------------------------------------------+
//| Refresh sample count only (not the list)                         |
//+------------------------------------------------------------------+
void CRecorderPanel::RefreshSampleList()
  {
   if(m_library == NULL)
      return;
   SPatternSample samples[];
   m_library.GetAllSamples(samples);
   int count = ArraySize(samples);
   SetObjectText(m_count_name, "Recorded Samples: " + IntegerToString(count));
   m_selected_sample_id = 0;
   ClearValidationInfo();
   DeleteObjectsWithPrefix(m_sample_prefix);
   ChartRedraw(m_chart_id);
  }

//+------------------------------------------------------------------+
//| Show status message                                              |
//+------------------------------------------------------------------+
void CRecorderPanel::ShowStatus(string msg, bool is_error = false)
  {
   SetObjectText(m_status_name, msg);
   if(is_error)
      ObjectSetInteger(m_chart_id, m_status_name, OBJPROP_COLOR, clrRed);
   else
      ObjectSetInteger(m_chart_id, m_status_name, OBJPROP_COLOR, clrDarkGreen);
  }
//+------------------------------------------------------------------+
#endif // PART84_PATTERNRECORDERPANEL_MQH