//+------------------------------------------------------------------+
//|                                                  P4L BoxFibo.mq4 |
//|                               A significant rewrite by pips4life |
//|                 Original BoxFibo.mq4 by hanover + mods by others |
//+------------------------------------------------------------------+

#property copyright "v2.00+ by pips4life"
#property link      "https://www.forexfactory.com/thread/662529-modified-useful-mq4-utilities-indicators-and-related-tips" // TAG: v2_00  Update if posted in new thread
#property indicator_chart_window
#property strict

#property description "This indicator draws boxes around *completed* time-windows from: StartTime(inclusive) to EndTime(exclusive)"
#property description "A Fibo is drawn from the Low-to-High of the box.  The 'FibPercentLevels' are marked, with optional Description/Percent/Price"
#property description "On/Off button toggles the display.  The indicator is intended for Timeframes <= H1."
#property description "One may add more-than-one of this indicator so long as either the StartTime and/or EndTime differs."

#property version "2.01"  // TAG: v2_00
extern string _VERSION_v2_01___2023_Apr_26 = "By pips4life. (Orig: hanover)";

/*
VERSION HISTORY:

2023-Apr-26 v2_01 by pips4life
   Changed internal 'debugmicro' value from "TRUE" to "false", as was intended for a production release.
   
2023-Apr-25 v2_00 by pips4life
   NOTE: These are major changes, and it would not be unusual if there are still bugs!  Please report any you find.
   Optimized performance.   Previous version ran *every* tick, and depending up TF, might take 400 - 1000 microseconds (which is a problem during high-tick news events!)
     The previous version used very slow (wasteful) functions StrToTime and TimeToStr far too often. Replaced with faster equivalent calculations. 
     The previous version also recalculated ALL the previous boxes, for every tick, which is a total waste of CPU!  
     This version runs only once-per-second; OR, if a new bar; OR, if old bars have refreshed.  It does not recalculate old boxes unnecessarily.  Most ticks are 1-5 microseconds!
     (This version is not completely optimized to prevent ALL unnecessary re-calculations, but it's far better than before and good enough.)
   Because this version is much faster, the NumDays default is now 30 instead of 10 (Users may customize as desired).
   Added on/off button.
   Eliminated 'identity' variable, and replaced with internal *unique* 'createdObjectPrefix' (so long as either StartTime or EndTime are unique!)
   The default BoxColor & FiboColor are best for lightBG charts.  Added 'AutoFlipColorsIfDarkBG' (true) which will 'flip' the colors automatically
      if used on a dark background chart.  (*IF* the user wants specific colors for a dark BG chart, choose your colors AND make this var 'false'!)
   New var 'FibPercentLevels' (0 50 100 200 -100).  The user may customize the Fibo levels as desired.
   New var 'EXTRA_Corresponding_FibLevelDesc'(",,,,") to specify a prefix description for each Fibo level, as desired.
   New vars 'ShowPricePerlevel'(true) and 'ShowPercentPerLevel'(true) which determine what is displayed on each Fibo line (e.g. with both, "100 1.34502")
   New var 'DisplayPercentWithNumberOfDigits'(0) will display the %# level with specified accuracy, e.g. 0 displays "50", 1 displays "50.0"
   New var 'ShiftFiboTime2By_seconds'(3600) affects how far to the right the Fibo lines extend. 3600 is fairly consistent across <= H1 timeframes.
   New var 'SupressIndicatorForTFsAbove_minutes' (60) will suppress the BoxFibo's for TF's > 60 minutes (H1). (This indicator is really only 
      intended for <= H1 timeframes anyway). Note: Toggling the button will *ignore* this variable, and force-display on <= H4 timeframes.
   The previous version suffered from a few major bugs which might be infrequent, but would nevertheless occur:  
      * New boxes would overdraw old boxes, which would make them completely invisible!
      * The StartTime might include the previous bar on certain Symbols (e.g. XAUUSD (Gold) often does not have a bar at 00:00. The old version included the "23.00" bar.
   The previous version offered unique Fibo level text labels for specific Fibo % levels.  The same may be achieved using the
     comma-separated 'EXTRA_Corresponding_FibLevelDesc', which adds any non-blank text as a prefix to the corresponding Fibo level.
     To restore the original levels, comment out the new 2 lines and uncomment out the old 2 lines with the desired 'FibPercentLevels' and 'EXTRA_Corresponding_FibLevelDesc'

2009-2023 Various mods by others.

2009-Jun-07  Orig. by hanover. https://www.forexfactory.com/thread/post/2785607#post2785607

FUTURE PLANS/ENHANCEMENTS/ETC.:
   NEXT(2023-APR) : Rewrite the (pre-b600+) 'stringSplitBy*' functions to take advantage of the 'StringSplit()' function (available in MQL4 since b600+).
      A basic example to split by commas: fibCount = StringSplit(FibPercentLevels,StringGetChar(",",0),fibLevelStr);
   NEXT(2023-APR) : In the code there is the line: "if (dt2 != Time[i])  continue;"    For most ticks, "limit=1", which means this
      portion of the code repeats the drawing of the most recent Box and Fibo, over-and-over.  It would improve performance to draw that Box & Fibo only once, not repetitively.

*/


string createdObjectPrefix = "_BoxFibo_"; // This var (replaces 'identity' is modified in OnInit(), and will be unique so long as StartTime & EndTime are unique.

extern string StartTime                           = "00:00"; 
extern string EndTime                             = "09:00"; 
extern int    NumDays                             = 30;
extern string FibPercentLevels                    = "0, 50, 100, 200, -100";
extern string EXTRA_Corresponding_FibLevelDesc    = ",,,,";
// These next 2 lines were the levels & labels used in previous versions:
//extern string FibPercentLevels                    = "  0,        50,  100, -23.6,    -38.2,        -61.8, -100, 123.6,    138.2,        161.8, 200, -138.2, 238.2, -161.8, 261.8";
//extern string EXTRA_Corresponding_FibLevelDesc    = "Low, Mid Price, High, Entry, TP2 Stop, TP1/TP3 Stop,  TP2, Entry, TP2 Stop, TP1/TP3 Stop, TP2,    TP3,   TP3,    TP4,   TP4";
//
extern bool   ShowPercentPerLevel                 = true;
extern bool   ShowPricePerlevel                   = true;
// DisplayPercentWithNumberOfDigits: E.g. 0 displays '50', 1 displays '50.0'
extern int    DisplayPercentWithNumberOfDigits    = 0; 
extern color  BoxColor                            = clrLightBlue;
extern color  FiboColor                           = clrBlack;
extern bool   AutoFlipColorsIfDarkBG              = true;
extern int    SupressIndicatorForTFsAbove_minutes = 60;
extern int    ShiftFiboTime2By_seconds            = 3600; 
// ShiftFiboTime2By_seconds: The larger the number, the farther to the right the Fibo lines extend (depending upon TimeFrame). 3600 is consistent across <=H1

//Forex-Station button template start41; copy and paste
extern string             button_note1_          = "------------------------------";
extern int                btn_Subwindow          = 0;                               // What window to put the button on.  If <0, the button will use the same sub-window as the indicator.
extern ENUM_BASE_CORNER   btn_corner             = CORNER_LEFT_UPPER;               // button corner on chart for anchoring
extern string             btn_text               = "BoxFibo";                       // a button name
extern string             btn_Font               = "Arial";                         // button font name
extern int                btn_FontSize           = 9;                               // button font size               
extern color              btn_text_ON_color      = clrLime;                         // ON color when the button is turned on
extern color              btn_text_OFF_color     = clrLightSalmon;                  // OFF color when the button is turned off
extern color              btn_background_color   = clrDimGray;                      // background color of the button
extern color              btn_border_color       = clrBlack;                        // border color the button
extern int                button_x               = 20;                              // x coordinate of the button     
extern int                button_y               = 45;                              // y coordinate of the button     
extern int                btn_Width              = 60;                              // button width
extern int                btn_Height             = 20;                              // button height
extern bool               btn_Initial_Default_ON = true;                           // When first added to chart, true enables ON status of button.
extern string             button_note2           = "------------------------------";
string buttonId;
bool buttonStatus;
//


datetime dtStartTime;
datetime dtEndTime;
string fibLevelText = "";
string fibLevelsStr[];
string fibLevelsPrefix[];
double fibLevels[];
int fibCount;
bool debugmicro = false;

//+------------------------------------------------------------------+
int OnInit()
  {
   createdObjectPrefix = StringConcatenate(createdObjectPrefix,StartTime,"_",EndTime,"_");
   //===========  Determine if duplicate Indicator exists
   string shortName = createdObjectPrefix; // Must NOT be simply the same as 'WindowExpertName()', else it will see itself as a duplicate.
   int winFind = WindowFind(shortName);
   if(winFind >= 0)
   {  
      Alert("ERROR(?).  The chart may already have this indicator with the same StartTime & EndTime."); // Because of false errors, do not remove.  //This duplicate is auto-removed.");
      //return(INIT_FAILED); // Due to possible false errors, commented out to keep going.
   }  
   IndicatorShortName(shortName);
   //============  End of check, and adjustment, if duplicate Indicator added.
  
   FibPercentLevels = stringReplaceEveryMatch(FibPercentLevels,","," ");
   FibPercentLevels = stringReplaceEveryMatch(FibPercentLevels,"%"," ");
   FibPercentLevels = stringReplaceEveryMatch(FibPercentLevels,";"," ");
   FibPercentLevels = StringTrimLeft(StringTrimRight(FibPercentLevels));
   fibCount = stringSplitBySpaces(fibLevelsStr,FibPercentLevels);
   ArrayResize(fibLevels,fibCount);
   for(int c=0;c<fibCount; c++)
   {
      fibLevels[c] = StringToDouble(fibLevelsStr[c])/100.0;
   }
   
   if(ShowPricePerlevel) fibLevelText = " %$";
   
   stringSplitByDelimiters(fibLevelsPrefix, EXTRA_Corresponding_FibLevelDesc, ",", true, fibCount, "");
   
   
   dtStartTime = StrToTime("1970.01.01 "+StartTime);
   dtEndTime = StrToTime("1970.01.01 "+EndTime);
   //Alert("DEBUG dtStartTime=",dtStartTime,"  dtEndTime=",dtEndTime);
   
   // --------------- Button related ----------------- //
   //if(btn_Subwindow<0) btn_Subwindow = swin;
   // The leading "_" gives buttonId a *unique* prefix so it is not deleted if/when any other chart objects are deleted.
   buttonId = "_" + createdObjectPrefix + "_BT_";
   if (ObjectFind(buttonId)<0)
   { 
      createButton(buttonId, btn_text, btn_Width, btn_Height, btn_Font, btn_FontSize, btn_background_color, btn_border_color, btn_text_ON_color);
      if(!btn_Initial_Default_ON) ObjectSetInteger(0,buttonId, OBJPROP_STATE,false); // This can force the initially created button to start as "off".  (But just changing TF's keeps current status!)
   }
   ObjectSetInteger(0, buttonId, OBJPROP_YDISTANCE, button_y);
   ObjectSetInteger(0, buttonId, OBJPROP_XDISTANCE, button_x);
   buttonStatus = ObjectGetInteger(0, buttonId, OBJPROP_STATE);   
   if (buttonStatus) ObjectSetInteger(0,buttonId,OBJPROP_COLOR,btn_text_ON_color); 
   else ObjectSetInteger(0,buttonId,OBJPROP_COLOR,btn_text_OFF_color);
   // --------------- -------------- ----------------- //
   
   if(AutoFlipColorsIfDarkBG && !isBackgroundLight() )
   {
      BoxColor = colorToVisibleRGBflip(BoxColor);
      FiboColor = colorToVisibleRGBflip(FiboColor);
      //BoxColor = colorToRGBflip(BoxColor);
      //FiboColor = colorToRGBflip(FiboColor);
   }
   
   if(Period() <= SupressIndicatorForTFsAbove_minutes) plot_obj();
   return(INIT_SUCCEEDED);
}//end of OnInit()

//+------------------------------------------------------------------+
void OnDeinit(const int reason)
{
   // If just changing a TF', the button need not be deleted, therefore the 'OBJPROP_STATE' is also preserved.
   if ( reason != REASON_CHARTCHANGE
     && reason != REASON_CLOSE
     // && reason != REASON_RECOMPILE  // Leave commented, if personal preference is to delete it when Re-Compiled.
      ) ObjectDelete(buttonId);

   IndicatorShortName("any_random_name_just_before_deinit_is_done");
   del_obj();
   return;
}//end of OnDeinit()


//+------------------------------------------------------------------------------------------------------------------+
void OnChartEvent(const int id, //don't change anything here
                  const long &lparam,
                  const double &dparam,
                  const string &sparam)
{
   
   //// Method 1: At the very least, skip the bulk of what are usually unwanted event id's.  NOTE that these
   //// events might have been enabled not by this indicator but by some OTHER indicator on the same chart!
   //// This appears to make this indy compatible with other programs that enabled CHART_EVENT_OBJECT_CREATE and/or CHART_EVENT_OBJECT_DELETE :
   //if(id==CHARTEVENT_OBJECT_CREATE || id==CHARTEVENT_OBJECT_DELETE) return; 
   //// If this, or another program, enabled mouse-events, these are usually not needed, so skip unless actually needed.:
   //if(id==CHARTEVENT_MOUSE_MOVE    || id==CHARTEVENT_MOUSE_WHEEL)   return; 

   // Method 2 (Preferred):  Check for a specific id and a specific sparam (object name) of interest, and ONLY act on that event!
   if (id==CHARTEVENT_OBJECT_CLICK && sparam == buttonId)
   {
      buttonStatus = ObjectGetInteger(0, buttonId, OBJPROP_STATE);
      
      if (buttonStatus) // on
      {
         ObjectSetInteger(0,buttonId,OBJPROP_COLOR,btn_text_ON_color); 
         //Call a function (preferably not the entire start() function though it might work) to do whatever the indicator does here:
         del_obj();
         if(Period() < PERIOD_D1) plot_obj(); // The button *ignores* 'SupressIndicatorForTFsAbove_minutes' and can therefore force H4 (but not >= D1)         
      }
      else // off
      {
         ObjectSetInteger(0,buttonId,OBJPROP_COLOR,btn_text_OFF_color);
         // If this indy had created objects (besides the buttonId), this would be the place to remove them one time only, now that the indy is "off"
         // deleteCreatedObjects(IndicatorObjPrefix);
         del_obj();
      }
      ChartRedraw();
   }
}//end of OnChartEvent()

//+------------------------------------------------------------------------------------------------------------------+
// Normally one does not need to change anything here
void createButton(string buttonID,string buttonText,int width2,int height,string font,int fontSize,color bgColor,color borderColor,color txtColor, bool initial_state=true)
{
      ObjectDelete    (0,buttonID);
      ObjectCreate    (0,buttonID,OBJ_BUTTON,btn_Subwindow,0,0);
      ObjectSetInteger(0,buttonID,OBJPROP_COLOR,txtColor);
      ObjectSetInteger(0,buttonID,OBJPROP_BGCOLOR,bgColor);
      ObjectSetInteger(0,buttonID,OBJPROP_BORDER_COLOR,borderColor);
      ObjectSetInteger(0,buttonID,OBJPROP_BORDER_TYPE,BORDER_RAISED);
      ObjectSetInteger(0,buttonID,OBJPROP_XSIZE,width2);
      ObjectSetInteger(0,buttonID,OBJPROP_YSIZE,height);
      ObjectSetString (0,buttonID,OBJPROP_FONT,font);
      ObjectSetString (0,buttonID,OBJPROP_TEXT,buttonText);
      ObjectSetInteger(0,buttonID,OBJPROP_FONTSIZE,fontSize);
      ObjectSetInteger(0,buttonID,OBJPROP_SELECTABLE,0);
      ObjectSetInteger(0,buttonID,OBJPROP_CORNER,btn_corner);
      ObjectSetInteger(0,buttonID,OBJPROP_HIDDEN,1);
      ObjectSetInteger(0,buttonID,OBJPROP_XDISTANCE,9999);
      ObjectSetInteger(0,buttonID,OBJPROP_YDISTANCE,9999);
      // Upon creation, set the 'initial state'  If "true" which is "on" (the default), one will see the indicator by default
      ObjectSetInteger(0, buttonId, OBJPROP_STATE, initial_state);
}//end of createButton()

//+------------------------------------------------------------------+
int start()  
{  
   if(Period() > SupressIndicatorForTFsAbove_minutes) return(0);  // User can control whether to suppress ABOVE some TF, like > H1
   if(Period() >= PERIOD_D1) return(0);  // Indicator does not apply at all to D1 and above (and is not really intended for H4 either)
      
   int limit;
   int counted_bars = IndicatorCounted();
   //---- check for possible errors 
   if(counted_bars<0) return(-1); 
   ////---- the last counted bar will be recounted 
   //if(counted_bars>0) counted_bars--; 
   limit=Bars-counted_bars; 
   
   static datetime last_timecurrent = 0;
   bool isNewBar = IsNewBar(); 
   if(TimeCurrent() == last_timecurrent && !isNewBar && limit<=1 ) return(0);  // Run no more than once per second. 
   last_timecurrent = TimeCurrent();
   
   
   ulong microstart = 0;
   if(debugmicro) microstart = GetMicrosecondCount();
   plot_obj(limit);
//+------------------------------------------------------------------+
   if(debugmicro)
   {
      static int nn=0;
      static int uslimit = 25;
      ulong time_us = GetMicrosecondCount() - microstart;
      if(time_us > 400) 
      {
         if (nn > uslimit) 
         {
            nn=uslimit;
            uslimit = uslimit + 2;
         }
      }
      nn++;
      if (nn<=uslimit || isNewBar) Print("Box Fibo: Pass n=",nn," took: ",time_us," micro-seconds.  limit=",limit,"  Period()=",Period());
   }
  return(0);
}//end of start()

//+------------------------------------------------------------------+
void plot_obj(int limit=INT_MAX)  {
//+------------------------------------------------------------------+
   
   //datetime dt1 = StrToTime(TimeToStr(TimeCurrent(),TIME_DATE) + " " + StartTime + ":00"); // TimeToStr then StrToTime are VERY SLOW functions!
   //datetime dt2 = StrToTime(TimeToStr(TimeCurrent(),TIME_DATE) + " " + EndTime + ":00");
   datetime timecurrent = TimeCurrent();
   timecurrent -= (datetime) MathMod(timecurrent,86400);
   datetime dt1 = timecurrent + dtStartTime;
   datetime dt2 = timecurrent + dtEndTime;
   int      dys = 0, NumBars = (int) (dt2-dt1)/PeriodSeconds();
   limit = MathMin(limit,Bars-NumBars);
   for (int i=0; i<limit; i++)  
   {
      //dt1 = StrToTime(TimeToStr(Time[i],TIME_DATE) + " "  + StartTime + ":00");
      //dt2 = StrToTime(TimeToStr(Time[i],TIME_DATE) + " "  + EndTime + ":00");
      datetime timei = Time[i] - (datetime) MathMod(Time[i],86400);
      dt1 = timei + dtStartTime;
      dt2 = timei + dtEndTime;
      int ib1 = iBarShift(NULL,0,dt1);
      if(Time[ib1] < dt1 && ib1>=1) { ib1--; dt1 = Time[ib1]; } // If not an EXACT match for the bar time, the *next* bar is correct, so shift 1 bar forward.
      int ib2 = iBarShift(NULL,0,dt2);
      if(Time[ib2] < dt2 && ib2>=1) { ib2--; dt2 = Time[ib2]; }
      if (dt2 != Time[i])  continue;
      // This old 'for' loop is a little slower than using iHighest & iLowest
      ////double vHigh = 0, vLow = 999;
      //double vHigh = 0.0, vLow = DBL_MAX; 
      //for (int j=ib1; j>ib2; j--)  {
      //  vHigh = MathMax(vHigh,High[j]);
      //  vLow  = MathMin(vLow,Low[j]);
      //}
      int barHigh = iHighest(NULL,0,MODE_HIGH,ib1-ib2,ib2+1);
      int barLow = iLowest(NULL,0,MODE_LOW,ib1-ib2,ib2+1);
      double vHigh = High[barHigh];
      double vLow = Low[barLow];
      
      string objname = createdObjectPrefix+IntegerToString(dt1)+"_R";
      if( ObjectFind(objname) < 0) ObjectCreate(objname,OBJ_RECTANGLE,0,dt1,vHigh,dt2-60,vLow);
      else
      {
         ObjectSetInteger(0,objname,OBJPROP_TIME1,dt1);
         ObjectSetInteger(0,objname,OBJPROP_TIME2,dt2-60);
         ObjectSetDouble(0,objname,OBJPROP_PRICE1,vHigh);
         ObjectSetDouble(0,objname,OBJPROP_PRICE2,vLow);
      }
      ObjectSet(objname,OBJPROP_COLOR,BoxColor);
      ObjectSet(objname,OBJPROP_SELECTABLE,false); 
      
      if(fibCount > 0)
      {
         objname = createdObjectPrefix+IntegerToString(dt1)+"_F";
         if( ObjectFind(objname) < 0) ObjectCreate(objname,OBJ_FIBO,0,dt2-ShiftFiboTime2By_seconds,vHigh,dt2,vLow);
         else
         {
            ObjectSetInteger(0,objname,OBJPROP_TIME1,dt2-ShiftFiboTime2By_seconds);
            ObjectSetInteger(0,objname,OBJPROP_TIME2,dt2);
            ObjectSetDouble(0,objname,OBJPROP_PRICE1,vHigh);
            ObjectSetDouble(0,objname,OBJPROP_PRICE2,vLow);
         }
         ObjectSet(objname,OBJPROP_RAY,false);
         ObjectSet(objname,OBJPROP_SELECTABLE,false);
         ObjectSet(objname,OBJPROP_COLOR,BoxColor);
         ObjectSet(objname,OBJPROP_LEVELCOLOR,FiboColor);
         ObjectSet(objname,OBJPROP_LEVELWIDTH,2);
           
         ObjectSet(objname,OBJPROP_FIBOLEVELS,fibCount);
         ObjectSet(objname,OBJPROP_LEVELSTYLE,STYLE_SOLID);
         for(int c=0; c<fibCount; c++)
         {
            string use_fibLevelText = fibLevelText;
            ObjectSet(objname,OBJPROP_FIRSTLEVEL+c,fibLevels[c]);
            if(ShowPercentPerLevel) use_fibLevelText = DoubleToStr(fibLevels[c]*100.0,DisplayPercentWithNumberOfDigits)+" "+use_fibLevelText;
            if(!stringIsBlank(fibLevelsPrefix[c])) use_fibLevelText = fibLevelsPrefix[c]+"  "+use_fibLevelText;
            ObjectSetFiboDescription(objname,c,use_fibLevelText);
         }
      }
      dys++; if (dys >= NumDays)   break;
   }
}//end of plot_obj()

//+------------------------------------------------------------------+
void del_obj(string prefix="")  
{
   if(StringLen(prefix)==0) prefix = createdObjectPrefix;
   int limit = ObjectsTotal() - 1;
   for(int k=limit; k>= 0; k--) // When deleting objects, must *decrement*.
   {
      string objname = ObjectName(k);
      if (StringFind(objname,prefix,0) == 0)  
      ObjectDelete(objname);
   }    
}//end of del_obj()

//+------------------------------------------------------------------+ 
int stringSplitBySpaces(string& output[], string s_input) export
{
   int pos, arraysize;
   ArrayResize(output, 0);
   s_input = StringTrimLeft(StringTrimRight(s_input));  // Leading/trailing spaces must be stripped off first.
   while (true) 
   {
      pos = StringFind(s_input, " ");
      arraysize = ArraySize(output);
      ArrayResize(output, arraysize + 1);
      if (pos != -1) 
      {
         output[arraysize] = StringTrimLeft(StringTrimRight(StringSubstr(s_input, 0, pos)));
         s_input = StringTrimLeft(StringTrimRight(StringSubstr(s_input, pos + 1)));
      } else {
         output[arraysize] = StringTrimLeft(StringTrimRight(s_input));
         break;
      } // if
   } // while
   return(ArraySize(output));
}//end of stringSplitBySpaces()

//+------------------------------------------------------------------+
// Splits s_input string into output array according to *multiple* single-single character delimiters.
int stringSplitByDelimiters(string& output[], string s_input, string delimiters, bool trimLeftRightSpaces=true, int finalSize=0, string defaultValue=NULL) export
{
   int p, d, len, pos, size, cycles=0;
   int countDelim = StringLen(delimiters);
   if(countDelim == 0) return(-1); // Error, no delimiter specified.  (Should I assume... " "(space), or "/" or ... ?)
   int delim[];
   ArrayResize(delim,countDelim);
   //Note: A space is not a delimiter unless it is included as a character in the delimiters
   for(p=0; p<countDelim; p++) {delim[p] = (uchar) StringGetChar(delimiters,p);} //if(delim[p]== a space, do anything different?? Force trimLeftRightSpaces=true, or ??
   
   if(trimLeftRightSpaces) s_input = StringTrimRight(s_input); // Left is always trimmed in the loop
   ArrayResize(output, 0);
   
   //arrayPrint(delim,true);

   while (true) 
   {  //find each array element
      cycles++; if(cycles>10000) return(-1);
      
      if(trimLeftRightSpaces) s_input = StringTrimLeft(s_input);
      pos=0;
      len=StringLen(s_input);
      while(true)
      { //process each character
         cycles++; if(cycles>10000) return(-1);
         ushort asciiCode = StringGetCharacter(s_input,pos);
         bool delimFound=false;
         for(d=0; d<countDelim; d++)
         {
            if(pos>=len || asciiCode == delim[d])
            {
               //found a delim or EOS at pos
               delimFound=true;
               break;
            }
         }
         if(delimFound) break;
         pos++;
      }
      size = ArraySize(output);
      ArrayResize(output, size + 1);
      //if (pos == 0) output[size] = ""; //empty element 
      if (pos == 0) output[size] = defaultValue; //empty element
      else 
      {
         output[size] = StringSubstr(s_input, 0, pos);
         if(trimLeftRightSpaces) output[size] = StringTrimLeft(StringTrimRight(output[size]));
      }
      if(pos>=len) break;
      s_input = StringSubstr(s_input, pos + 1); // was len_token which is always 1
      
      //Alert("DEBUG (pre)size=",size,"  output[size]='",output[size],"'","  Remaining s_input='",s_input,"'  d=",d,"  pos=",pos);
   }
   
   int arSize = ArraySize(output);
   if(finalSize>arSize)
   {
      ArrayResize(output,finalSize);
      for(int k=arSize; k<finalSize; k++) {output[k] = defaultValue;}
      arSize = finalSize;
   }
   return(arSize);

   return(ArraySize(output));       
}//end of stringSplitByDelimiters()

//+------------------------------------------------------------------+
string stringReplaceEveryMatch(string str, string toFind, string toReplace, bool ignoreCaseOf_toFind=false) export {
   //Updated v3_15 to support ignoreCaseOf_toFind
   int len_toReplace = StringLen(toReplace);
   int len_toFind = StringLen(toFind);
   //int len = StringLen(toFind);
   int pos = 0;
   string leftPart, rightPart, result = str, TOFIND = toFind;
   if(ignoreCaseOf_toFind) StringToUpper(TOFIND);

   if (len_toFind == 0 || (!ignoreCaseOf_toFind && toFind == toReplace)) return (result); // Cannot find "", and replacing same-with-same is a waste of time.
   while (true) {
       // Careful, (pos or result) must change each loop or it's an infinite loop.
       if(ignoreCaseOf_toFind) pos = StringFind(stringToUC(result), TOFIND, pos);
       else pos = StringFind(result, toFind, pos);
       if (pos == -1) {
           break;
       }
       if (pos == 0) {
           leftPart = "";
       } else {
           leftPart = StringSubstr(result, 0, pos);
       }
       rightPart = StringSubstr(result, pos+len_toFind); 
       result = StringConcatenate(leftPart,toReplace,rightPart);
       pos = pos + len_toReplace;
   }    
   return (result);
}//end of stringReplaceEveryMatch()
//+------------------------------------------------------------------+
string stringToUC(string str) export {
   // Convert str to upper-case
   int lS = 97, lE = 122, uS = 65, uE = 90, diff = lS - uS;
   for (int i = 0; i < StringLen(str); i++) {
       int code = StringGetChar(str, i);
       if (code >= lS && code <= lE) {
           code -= diff;
           str = StringSetChar(str, i, (ushort) code);
       }
   }
   return (str);
}//end of stringToUC()
//+------------------------------------------------------------------+
bool IsNewBar() export // DEV to add: string symbol=_Symbol, int period=_Period)
{
   static datetime lastbar; // DEV Q: Would a single lastbar var work with multiple different calls and/or arguments to function? If no, how? Pass the &lastbar?
   datetime curbar = (datetime)SeriesInfoInteger(_Symbol,_Period,SERIES_LASTBAR_DATE);
   if(lastbar != curbar)
   {
      lastbar = curbar;
      return true;
   }
   return false;
}//end of IsNewBar()

//+------------------------------------------------------------------+
color colorToRGBflip(color c_mycolor) export { return( (color) (~c_mycolor & 0xFFFFFF) ); }
color colorToVisibleRGBflip(color c_mycolor) export 
{
   if(isBackgroundLight())
   {
      //Print("DEBUGC LIGHT orig=",IntegerToHexString(c_mycolor),"   pureFlip=",IntegerToHexString( (color) (~c_mycolor & 0xFFFFFF)),"  modFlip=",IntegerToHexString( (color) ((~c_mycolor & 0xFFFFFF) & 0xDDDDDD))); // 0xBBBBBB  0xDDDDDD
      return( (color) (~c_mycolor & 0xBBBBBB) );  // 0xBB = 1011 1011   This forces off a couple of bits each for RGB, so it won't be "too bright" for each of the RGB components of the color.
      //return( (color) ((~c_mycolor & 0xFFFFFF) & 0xCCCCCC) );
   }
   else
   {
      //Print("DEBUGC DARK orig=",IntegerToHexString(c_mycolor),"   pureFlip=",IntegerToHexString( (color) (~c_mycolor))," FlipAND=",IntegerToHexString( (color) ((~c_mycolor) & 0xFFFFFF)),
      //   "   pureFlip=",IntegerToHexString( (color) (~c_mycolor & 0xFFFFFF)),"  modFlip=",IntegerToHexString( (color) ((~c_mycolor & 0xFFFFFF) | 0x444444)));
      return( (color) ((~c_mycolor & 0xFFFFFF) | 0x444444) );
   }
   //return( (color) (~c_mycolor & 0xFFFFFF) ); 
}//end of colorToVisibleRGBflip()
//+------------------------------------------------------------------+
int   colorToRGBsum(color c_mycolor) export { return( (c_mycolor & 0xFF) + ((c_mycolor>>8) & 0xFF) + ((c_mycolor>>16) & 0xFF) ); }
bool  isBackgroundLight() export { if(colorToRGBsum( (int) ChartGetInteger(0,CHART_COLOR_BACKGROUND)) >382 ) return(true); else return(false); }
//+------------------------------------------------------------------+
bool stringIsBlank(string str, bool do_StringTrim_LR=true) export
{
   //NO Not reliable. if(str == "") return(false);
   //if(StringLen(str) > 0) return(false);
   //if(str == NULL) return(false); // MAYBE ok for build>=600
   if(do_StringTrim_LR) str = StringTrimLeft(StringTrimRight(str));
   //if(StringFind(str,"NULL",0) == 0) return(true); // Ok to check, but likely unnecessary
   if(StringGetChar(str,0) > 0) return(false);
   return(true);
}//end of stringIsBlank()
//+------------------------------------------------------------------+


// These previous functions to delete objects were flawed.  When deleting objects, one must decrement the counter; never increment.
////+------------------------------------------------------------------+
//void del_obj1()  {
////+------------------------------------------------------------------+
//  int k=0;
//  while (k<ObjectsTotal())   {
//    string objname = ObjectName(k);
//    if (StringSubstr(objname,0,3) == "BFB")  
//      ObjectDelete(objname);
//    else
//      k++;  // NEVER increment and ObjectDelete like this!  Only *decrement* the counter, starting from the max, down to 0.
//  }    
//}
//
//void del_obj2()  {
////+------------------------------------------------------------------+
//  int k=0;
//  while (k<ObjectsTotal())   {
//    string objname = ObjectName(k);
//    if (StringSubstr(objname,0,3) == "BFF")  
//      ObjectDelete(objname);
//    else
//      k++;
//  }    
//}


