import { gStyle, settings, constants, clTAxis, clTGaxis, isFunc, isStr } from '../core.mjs';
import { select as d3_select, drag as d3_drag, timeFormat as d3_timeFormat, utcFormat as d3_utcFormat,
         scaleTime as d3_scaleTime, scaleSymlog as d3_scaleSymlog,
         scaleLog as d3_scaleLog, scaleLinear as d3_scaleLinear } from '../d3.mjs';
import { floatToString, makeTranslate, addHighlightStyle } from '../base/BasePainter.mjs';
import { ObjectPainter, EAxisBits, kAxisLabels, kAxisNormal, kAxisFunc, kAxisTime } from '../base/ObjectPainter.mjs';
import { FontHandler } from '../base/FontHandler.mjs';


/** @summary Return time offset value for given TAxis object
  * @private */
function getTimeOffset(axis) {
   const dflt_time_offset = 788918400000;

   if (!axis)
      return dflt_time_offset;
   const idF = axis.fTimeFormat.indexOf('%F');
   if (idF < 0)
      return gStyle.fTimeOffset * 1000;
   let sof = axis.fTimeFormat.slice(idF + 2);
   // default string in axis offset
   if (sof.indexOf('1995-01-01 00:00:00s0') === 0)
      return dflt_time_offset;
   // another default string with unix time
   if (sof.indexOf('1970-01-01 00:00:00s0') === 0)
      return 0;
   // special case, used from DABC painters
   if ((sof === '0') || (sof === ''))
      return 0;

   // decode time from ROOT string
   const next = (separ, min, max) => {
      const pos = sof.indexOf(separ);
      if (pos < 0)
         return min;
      const val = parseInt(sof.slice(0, pos));
      sof = sof.slice(pos + 1);
      if (!Number.isInteger(val) || (val < min) || (val > max))
         return min;
      return val;
   };
   // eslint-disable-next-line one-var
   const year = next('-', 1900, 2900),
         month = next('-', 1, 12) - 1,
         day = next(' ', 1, 31),
         hour = next(':', 0, 23),
         min = next(':', 0, 59),
         sec = next('s', 0, 59),
         msec = next(' ', 0, 999);

   let offset = Date.UTC(year, month, day, hour, min, sec, msec);

   // now also handle suffix like GMT or GMT -0600
   sof = sof.toUpperCase();

   if (sof.indexOf('GMT') === 0) {
      sof = sof.slice(4).trim();
      if (sof.length > 3) {
         let p = 0, sign = 1000;
         if (sof[0] === '-') {
            p = 1;
            sign = -1000;
         }
         offset -= sign * (parseInt(sof.slice(p, p + 2)) * 3600 + parseInt(sof.slice(p + 2, p + 4)) * 60);
      }
   }

   return offset;
}

/** @summary Return true when GMT option configured in time format
  * @private */
function getTimeGMT(axis) {
   const fmt = axis?.fTimeFormat ?? '';
   return (fmt.indexOf('gmt') > 0) || (fmt.indexOf('GMT') > 0);
}

/** @summary Tries to choose time format for provided time interval
  * @private */
function chooseTimeFormat(awidth, ticks) {
   if (awidth < 0.5)
      return ticks ? '%S.%L' : '%H:%M:%S.%L';
   if (awidth < 30)
      return ticks ? '%Mm%S' : '%H:%M:%S';
   awidth /= 60;
   if (awidth < 30)
      return ticks ? '%Hh%M' : '%d/%m %H:%M';
   awidth /= 60;
   if (awidth < 12)
      return ticks ? '%d-%Hh' : '%d/%m/%y %Hh';
   awidth /= 24;
   if (awidth < 15.218425)
      return ticks ? '%d/%m' : '%d/%m/%y';
   awidth /= 30.43685;
   if (awidth < 6)
      return '%d/%m/%y';
   awidth /= 12;
   if (awidth < 2)
      return ticks ? '%m/%y' : '%d/%m/%y';
   return '%Y';
}

/**
  * @summary Base axis painter methods
  *
  * @private
  */

const AxisPainterMethods = {

   initAxisPainter() {
      this.name = 'yaxis';
      this.kind = kAxisNormal;
      this.func = null;
      this.order = 0; // scaling order for axis labels

      this.full_min = 0;
      this.full_max = 1;
      this.scale_min = 0;
      this.scale_max = 1;
      this.ticks = []; // list of major ticks
   },

   /** @summary Cleanup axis painter */
   cleanupAxisPainter() {
      this.ticks = [];
      delete this.format;
      delete this.func;
      delete this.tfunc1;
      delete this.tfunc2;
      delete this.gr;
   },

   /** @summary Assign often used members of frame painter */
   assignFrameMembers(fp, axis) {
      fp[`gr${axis}`] = this.gr;                 // fp.grx
      fp[`log${axis}`] = this.log;               // fp.logx
      fp[`scale_${axis}min`] = this.scale_min;   // fp.scale_xmin
      fp[`scale_${axis}max`] = this.scale_max;   // fp.scale_xmax
   },

   /** @summary Convert axis value into the Date object */
   convertDate(v) {
      const dt = new Date(this.timeoffset + v * 1000);
      let res = dt;
      if (!this.timegmt && settings.TimeZone) {
         try {
            const ms = dt.getMilliseconds();
            res = new Date(dt.toLocaleString('en-US', { timeZone: settings.TimeZone }));
            res.setMilliseconds(ms);
         } catch {
            res = dt;
         }
      }
      return res;
   },

   /** @summary Convert graphical point back into axis value */
   revertPoint(pnt) {
      const value = this.func.invert(pnt);
      return this.kind === kAxisTime ? (value - this.timeoffset) / 1000 : value;
   },

   /** @summary Provide label for time axis */
   formatTime(dt, asticks) {
      return asticks ? this.tfunc1(dt) : this.tfunc2(dt);
   },

   /** @summary Provide label for log axis */
   formatLog(d, asticks, fmt) {
      const val = parseFloat(d), rnd = Math.round(val);
      if (!asticks)
         return ((rnd === val) && (Math.abs(rnd) < 1e9)) ? rnd.toString() : floatToString(val, fmt || gStyle.fStatFormat);
      if (val <= 0)
         return null;
      let vlog = Math.log10(val);
      const base = this.logbase;
      if (base !== 10)
         vlog /= Math.log10(base);
      if (this.moreloglabels || (Math.abs(vlog - Math.round(vlog)) < 0.001)) {
         if (!this.noexp && (asticks !== 2))
            return this.formatExp(base, Math.floor(vlog + 0.01), val);
         if (Math.abs(base - Math.E) < 0.001)
            return floatToString(val, fmt || gStyle.fStatFormat);
         return (vlog < 0) ? val.toFixed(Math.round(-vlog + 0.5)) : val.toFixed(0);
      }
      return null;
   },

   /** @summary Detect if tick is extra-ticks */
   isExtraLogTick(val) {
      if (!this.log)
         return false;
      let vlog = Math.log10(val);
      if (this.logbase !== 10)
         vlog /= Math.log10(this.logbase);

      return Math.abs(vlog - Math.round(vlog)) >= 0.001;
   },

   /** @summary Provide label for normal axis */
   formatNormal(d, asticks, fmt) {
      let val = parseFloat(d);
      if (asticks && this.order)
         val /= Math.pow(10, this.order);

      if (gStyle.fStripDecimals && (val === Math.round(val)))
         return Math.abs(val) < 1e9 ? val.toFixed(0) : val.toExponential(4);

      if (asticks) {
         if (this.ndig > 10)
            return val.toExponential(this.ndig - 11);
         let res = val.toFixed(this.ndig);
         const p = res.indexOf('.');
         if ((p > 0) && settings.StripAxisLabels) {
            while ((res.length >= p) && ((res.at(-1) === '0') || (res.at(-1) === '.')))
               res = res.slice(0, res.length - 1);
         }
         return res;
      }

      return floatToString(val, fmt || '8.6g');
   },

   /** @summary Provide label for exponential form */
   formatExp(base, order, value) {
      let res = '';
      const sbase = Math.abs(base - Math.E) < 0.001 ? 'e' : base.toString();
      if (value) {
         value = Math.round(value / Math.pow(base, order));
         if (settings.StripAxisLabels) {
            if (order === 0)
               return value.toString();
            else if ((order === 1) && (value === 1))
               return sbase;
         }
         if (value !== 1)
            res = value.toString() + (settings.Latex ? '#times' : 'x');
      }
      res += sbase;
      if (settings.Latex > constants.Latex.Symbols)
         return res + `^{${order}}`;
      const superscript_symbols = {
         0: '\u2070', 1: '\xB9', 2: '\xB2', 3: '\xB3', 4: '\u2074', 5: '\u2075',
         6: '\u2076', 7: '\u2077', 8: '\u2078', 9: '\u2079', '-': '\u207B'
      }, str = order.toString();
      for (let n = 0; n < str.length; ++n)
         res += superscript_symbols[str[n]];
      return res;
   },

   /** @summary Convert 'raw' axis value into text */
   axisAsText(value, fmt) {
      if (this.kind === kAxisTime)
         value = this.convertDate(value);
      if (this.format)
         return this.format(value, false, fmt);
      return value.toPrecision(4);
   },

   /** @summary Produce ticks for d3.scaleLog
     * @desc Fixing following problem, described [here]{@link https://stackoverflow.com/questions/64649793} */
   poduceLogTicks(func, number) {
      const linearArray = arr => {
         if (arr.length < 2)
            return false;
         let sum1 = 0, sum2 = 0;
         for (let k = 1; k < arr.length; ++k) {
            const diff = (arr[k] - arr[k - 1]);
            sum1 += diff;
            sum2 += diff ** 2;
         }
         const mean = sum1 / (arr.length - 1),
               dev = sum2 / (arr.length - 1) - mean ** 2;

         if (dev <= 0)
            return true;
         if (Math.abs(mean) < 1e-100)
            return false;
         return Math.sqrt(dev) / mean < 1e-6;
      };

      let arr = func.ticks(number);

      while ((number > 4) && linearArray(arr)) {
         number = Math.round(number * 0.8);
         arr = func.ticks(number);
      }

      // if still linear array, try to sort out 'bad' ticks
      if ((number < 5) && linearArray(arr) && this.logbase && (this.logbase !== 10)) {
         const arr2 = [];
         arr.forEach(val => {
            const pow = Math.log10(val) / Math.log10(this.logbase);
            if (Math.abs(Math.round(pow) - pow) < 0.01)
               arr2.push(val);
         });
         if (arr2.length)
            arr = arr2;
      }

      return arr;
   },

   /** @summary Produce axis ticks */
   produceTicks(ndiv, ndiv2) {
      if (!this.noticksopt) {
         const total = ndiv * (ndiv2 || 1);

         if (this.log)
            return this.poduceLogTicks(this.func, total);

         const dom = this.func.domain(),
               check = ticks => {
                  if (ticks.length <= total)
                     return true;
                  if (ticks.length > total + 1)
                     return false;
                  return (ticks[0] === dom[0]) || (ticks[total] === dom[1]); // special case of N+1 ticks, but match any range
               }, res1 = this.func.ticks(total);
         if (ndiv2 || check(res1))
            return res1;

         const res2 = this.func.ticks(Math.round(total * 0.7));
         return (res2.length > 2) && check(res2) ? res2 : res1;
      }

      const dom = this.func.domain(), ticks = [];
      if (ndiv2)
         ndiv = (ndiv - 1) * ndiv2;
      for (let n = 0; n <= ndiv; ++n)
         ticks.push((dom[0] * (ndiv - n) + dom[1] * n) / ndiv);
      return ticks;
   },

   /** @summary Method analyze mouse wheel event and returns item with suggested zooming range */
   analyzeWheelEvent(evnt, dmin, item, test_ignore) {
      if (!item)
         item = {};

      let delta = 0, delta_left = 1, delta_right = 1;

      if ('dleft' in item) {
         delta_left = item.dleft;
         delta = 1;
      }
      if ('dright' in item) {
         delta_right = item.dright;
         delta = 1;
      }

      if (item.delta)
         delta = item.delta;
      else if (evnt)
         delta = evnt.wheelDelta ? -evnt.wheelDelta : (evnt.deltaY || evnt.detail);

      if (!delta || (test_ignore && item.ignore))
         return;

      delta = (delta < 0) ? -0.2 : 0.2;
      delta_left *= delta;
      delta_right *= delta;

      const lmin = item.min = this.scale_min,
            lmax = item.max = this.scale_max,
            gmin = this.full_min,
            gmax = this.full_max;

      if ((item.min === item.max) && (delta < 0)) {
         item.min = gmin;
         item.max = gmax;
      }

      if (item.min >= item.max)
         return;

      if (item.reverse)
         dmin = 1 - dmin;

      if ((dmin > 0) && (dmin < 1)) {
         if (this.log) {
            let factor = (item.min > 0) ? Math.log10(item.max / item.min) : 2;
            if (factor > 10)
               factor = 10;
            else if (factor < 0.01)
               factor = 0.01;
            item.min /= Math.pow(10, factor * delta_left * dmin);
            item.max *= Math.pow(10, factor * delta_right * (1 - dmin));
            // special handling for Z scale - limit zooming of color scale
            if (this.minposbin && this.name === 'zaxis')
               item.min = Math.max(item.min, 0.3 * this.minposbin);
         } else if ((delta_left === -delta_right) && !item.reverse) {
            // shift left/right, try to keep range constant
            let delta_shift = (item.max - item.min) * delta_right * dmin;

            if ((Math.round(item.max) === item.max) && (Math.round(item.min) === item.min) && (Math.abs(delta_shift) > 1))
               delta_shift = Math.round(delta_shift);

            if (item.min + delta_shift < gmin)
               delta_shift = gmin - item.min;
            else if (item.max + delta_shift > gmax)
               delta_shift = gmax - item.max;

            if (delta_shift) {
               item.min += delta_shift;
               item.max += delta_shift;
            } else {
               delete item.min;
               delete item.max;
            }
         } else {
            let rx_left = (item.max - item.min), rx_right = rx_left;
            if (delta_left > 0)
               rx_left = 1.001 * rx_left / (1 - delta_left);
            item.min += -delta_left * dmin * rx_left;
            if (delta_right > 0)
               rx_right = 1.001 * rx_right / (1 - delta_right);
            item.max -= -delta_right * (1 - dmin) * rx_right;
         }
         if (item.min >= item.max)
            item.min = item.max = undefined;
         else if (delta_left !== delta_right) {
            // extra check case when moving left or right
            if (((item.min < gmin) && (lmin === gmin)) ||
                ((item.max > gmax) && (lmax === gmax)))
               item.min = item.max = undefined;
         } else {
            item.min = Math.max(item.min, gmin);
            item.max = Math.min(item.max, gmax);
         }
      } else
         item.min = item.max = undefined;

      item.changed = (item.min !== undefined) && (item.max !== undefined);

      return item;
   }

}; // AxisPainterMethods


/**
 * @summary Painter for TAxis object
 *
 * @private
 */

class TAxisPainter extends ObjectPainter {

   /** @summary constructor
     * @param {object|string} dom - identifier or dom element
     * @param {object} axis - object to draw
     * @param {boolean} embedded - if true, painter used in other objects painters */
   constructor(dom, axis, embedded) {
      super(dom, axis);

      this.is_gaxis = axis?._typename === clTGaxis;

      Object.assign(this, AxisPainterMethods);
      this.initAxisPainter();

      this.embedded = embedded; // indicate that painter embedded into the histogram painter
      this.invert_side = false;
      this.lbls_both_sides = false; // draw labels on both sides
   }

   /** @summary cleanup painter */
   cleanup() {
      this.cleanupAxisPainter();
      delete this.hist_painter;
      delete this.hist_axis;
      delete this.is_gaxis;
      super.cleanup();
   }

   /** @summary Use in GED to identify kind of axis */
   getAxisType() { return clTAxis; }

   /** @summary Configure axis painter
     * @desc Axis can be drawn inside frame <g> group with offset to 0 point for the frame
     * Therefore one should distinguish when calculated coordinates used for axis drawing itself or for calculation of frame coordinates
     * @private */
   configureAxis(name, min, max, smin, smax, vertical, range, opts) {
      const axis = this.getObject();

      this.name = name;
      this.full_min = min;
      this.full_max = max;
      this.kind = kAxisNormal;
      this.vertical = vertical;
      this.log = opts.log || 0;
      this.minposbin = opts.minposbin;
      this.ignore_labels = opts.ignore_labels;
      this.noexp_changed = opts.noexp_changed;
      this.symlog = opts.symlog || false;
      this.reverse = opts.reverse || false;
      // special flag to change align of labels on vertical axis
      // it is workaround shown in TGaxis docu
      this.reverseAlign = this.vertical && this.reverse && this.is_gaxis && (axis.fX1 !== axis.fX2);
      this.swap_side = opts.swap_side || false;
      this.fixed_ticks = opts.fixed_ticks || null;
      this.maxTickSize = opts.maxTickSize || 0;
      this.value_axis = opts.value_axis ?? false; // use fMinimum/fMaximum from source object

      if (opts.time_scale || axis.fTimeDisplay) {
         this.kind = kAxisTime;
         this.timeoffset = getTimeOffset(axis);
         this.timegmt = getTimeGMT(axis);
      } else if (opts.axis_func)
         this.kind = kAxisFunc;
      else
         this.kind = !axis.fLabels || this.ignore_labels ? kAxisNormal : kAxisLabels;

      if (this.kind === kAxisTime)
         this.func = d3_scaleTime().domain([this.convertDate(smin), this.convertDate(smax)]);
      else if (this.log) {
         if ((this.log === 1) || (this.log === 10))
            this.logbase = 10;
         else if (this.log === 3)
            this.logbase = Math.E;
         else
            this.logbase = Math.round(this.log);

         if (smax <= 0)
            smax = 1;

         if (opts.log_min_nz)
            this.log_min_nz = opts.log_min_nz;
         else if (axis && opts.logcheckmin) {
            let v = 0;
            for (let i = 0; i < axis.fNbins; ++i) {
               v = axis.GetBinLowEdge(i + 1);
               if (v > 0)
                  break;
               v = axis.GetBinCenter(i + 1);
               if (v > 0)
                  break;
            }
            if (v > 0)
               this.log_min_nz = v;
         }

         if ((smin <= 0) && this.log_min_nz)
            smin = this.log_min_nz;

         if ((smin <= 0) || (smin >= smax))
            smin = smax * (opts.logminfactor || 1e-4);

         if (this.kind === kAxisFunc)
            this.func = this.createFuncHandle(opts.axis_func, this.logbase, smin, smax);
         else
            this.func = d3_scaleLog().base(this.logbase).domain([smin, smax]);
      } else if (this.symlog) {
         let v = Math.max(Math.abs(smin), Math.abs(smax));
         if (Number.isInteger(this.symlog) && (this.symlog > 0))
            v *= Math.pow(10, -1 * this.symlog);
         else
            v *= 0.01;
         this.func = d3_scaleSymlog().constant(v).domain([smin, smax]);
      } else if (this.kind === kAxisFunc)
         this.func = this.createFuncHandle(opts.axis_func, 0, smin, smax);
      else
         this.func = d3_scaleLinear().domain([smin, smax]);

      if (this.vertical ^ this.reverse)
         [range[0], range[1]] = [range[1], range[0]];

      this.func.range(range);

      this.scale_min = smin;
      this.scale_max = smax;

      if (this.kind === kAxisTime)
         this.gr = val => this.func(this.convertDate(val));
      else if (this.log)
         this.gr = val => { return (val < this.scale_min) ? (this.vertical ? this.func.range()[0] + 5 : -5) : this.func(val); };
      else
         this.gr = this.func;

      delete this.format;// remove formatting func

      let ndiv = 508;
      if (this.is_gaxis)
         ndiv = axis.fNdiv;
      else if (axis)
         ndiv = axis.fNdivisions ? Math.max(axis.fNdivisions, 4) : 0;

      this.nticks = ndiv % 100;
      this.nticks2 = (ndiv % 10000 - this.nticks) / 100;
      this.nticks3 = Math.floor(ndiv / 10000);

      if (axis && !this.is_gaxis)
         this.nticks = Math.min(this.nticks, 20);

      let gr_range = Math.abs(this.func.range()[1] - this.func.range()[0]);
      if (gr_range <= 0)
         gr_range = 100;

      if (this.kind === kAxisTime) {
         this.nticks = Math.min(this.nticks, 8);

         const scale_range = this.scale_max - this.scale_min,
               idF = axis.fTimeFormat.indexOf('%F'),
               tf2 = chooseTimeFormat(scale_range / gr_range, false);
         let tf1 = (idF >= 0) ? axis.fTimeFormat.slice(0, idF) : axis.fTimeFormat;

         if (!tf1 || (scale_range < 0.1 * (this.full_max - this.full_min)))
            tf1 = chooseTimeFormat(scale_range / this.nticks, true);

         this.tfunc1 = this.tfunc2 = this.timegmt ? d3_utcFormat(tf1) : d3_timeFormat(tf1);
         if (tf2 !== tf1)
            this.tfunc2 = this.timegmt ? d3_utcFormat(tf2) : d3_timeFormat(tf2);

         this.format = this.formatTime;
      } else if (this.log) {
         if (this.nticks2 > 1) {
            this.nticks *= this.nticks2; // all log ticks (major or minor) created centrally
            this.nticks2 = 1;
         }
         this.noexp = axis?.TestBit(EAxisBits.kNoExponent);
         if ((this.scale_max < 300) && (this.scale_min > 0.3) && !this.noexp_changed && (this.log === 1))
            this.noexp = true;
         this.moreloglabels = axis?.TestBit(EAxisBits.kMoreLogLabels);
         this.format = this.formatLog;
      } else if (this.kind === kAxisLabels) {
         this.nticks = 50; // for text output allow max 50 names
         const scale_range = this.scale_max - this.scale_min;
         if (this.nticks > scale_range)
            this.nticks = Math.round(scale_range);

         this.regular_labels = true;

         if (axis?.fNbins && axis?.fLabels) {
            if ((axis.fNbins !== Math.round(axis.fXmax - axis.fXmin)) ||
                axis.fXmin || (axis.fXmax !== axis.fNbins))
               this.regular_labels = false;
         }

         this.nticks2 = 1;

         this.format = this.formatLabels;
      } else {
         this.order = 0;
         this.ndig = 0;
         this.format = this.formatNormal;
      }
   }

   /** @summary Check zooming value for log scale
    * @private */
   checkZoomMin(value) {
      return this.log && this.log_min_nz ? Math.max(value, this.log_min_nz) : value;
   }

   /** @summary Return scale min */
   getScaleMin() {
      return this.func?.domain()[0] ?? 0;
   }

   /** @summary Return scale max */
   getScaleMax() {
      return this.func?.domain()[1] ?? 0;
   }

   /** @summary Return true if labels may be removed while they are not fit to graphical range */
   cutLabels() {
      if (!settings.CutAxisLabels)
         return false;
      if (isStr(settings.CutAxisLabels))
         return settings.CutAxisLabels.indexOf(this.name) >= 0;
      return this.vertical; // cut vertical axis by default
   }

   /** @summary Provide label for axis value */
   formatLabels(d) {
      const a = this.getObject();
      let indx = parseFloat(d);
      if (!this.regular_labels)
         indx = Math.round((indx - a.fXmin) / (a.fXmax - a.fXmin) * a.fNbins);
      else
         indx = Math.floor(indx);
      if ((indx < 0) || (indx >= a.fNbins))
         return null;
      const arr = a.fLabels.arr;
      for (let i = 0; i < arr.length; ++i) {
         if (arr[i].fUniqueID === indx + 1)
            return arr[i].fString;
      }
      return null;
   }

   /** @summary Creates array with minor/middle/major ticks */
   createTicks(only_major_as_array, optionNoexp, optionNoopt, optionInt) {
      if (optionNoopt && this.nticks && (this.kind === kAxisNormal))
         this.noticksopt = true;

      const handle = { painter: this, nminor: 0, nmiddle: 0, nmajor: 0, func: this.func, minor: [], middle: [], major: [] };
      let ticks = [];

      if (this.fixed_ticks) {
         this.fixed_ticks.forEach(v => {
            if ((v >= this.scale_min) && (v <= this.scale_max))
               ticks.push(v);
         });
      } else if (this.kind === kAxisLabels) {
         handle.lbl_pos = [];
         const axis = this.getObject();
         for (let n = 0; n <= axis.fNbins; ++n) {
            const x = this.regular_labels ? n : axis.fXmin + n / axis.fNbins * (axis.fXmax - axis.fXmin);
            if ((x >= this.scale_min) && (x <= this.scale_max)) {
               handle.lbl_pos.push(x);
               ticks.push(x);
            }
         }
      } else
         ticks = this.produceTicks(this.nticks);

      handle.minor = handle.middle = handle.major = ticks;

      if (only_major_as_array) {
         const res = handle.major, delta = (this.scale_max - this.scale_min) * 1e-5;
         if (res.at(0) > this.scale_min + delta)
            res.unshift(this.scale_min);
         if (res.at(-1) < this.scale_max - delta)
            res.push(this.scale_max);
         return res;
      }

      if ((this.nticks2 > 1) && (!this.log || (this.logbase === 10)) && !this.fixed_ticks) {
         handle.minor = handle.middle = this.produceTicks(handle.major.length, this.nticks2);
         const gr_range = Math.abs(this.func.range()[1] - this.func.range()[0]);
         // avoid black filling by middle-size
         if ((handle.middle.length <= handle.major.length) || (handle.middle.length > gr_range))
            handle.minor = handle.middle = handle.major;
         else if ((this.nticks3 > 1) && !this.log) {
            handle.minor = this.produceTicks(handle.middle.length, this.nticks3);
            if ((handle.minor.length <= handle.middle.length) || (handle.minor.length > gr_range))
               handle.minor = handle.middle;
         }
      }

      handle.reset = function() {
         this.nminor = this.nmiddle = this.nmajor = 0;
      };

      handle.next = function(doround) {
         if (this.nminor >= this.minor.length)
            return false;

         this.tick = this.minor[this.nminor++];
         this.grpos = this.func(this.tick);
         if (doround)
            this.grpos = Math.round(this.grpos);
         this.kind = 3;

         if ((this.nmiddle < this.middle.length) && (Math.abs(this.grpos - this.func(this.middle[this.nmiddle])) < 1)) {
            this.nmiddle++;
            this.kind = 2;
         }

         if ((this.nmajor < this.major.length) && (Math.abs(this.grpos - this.func(this.major[this.nmajor])) < 1)) {
            this.nmajor++;
            this.kind = 1;
         }
         return true;
      };

      handle.last_major = function() {
         return (this.kind !== 1) ? false : this.nmajor === this.major.length;
      };

      handle.next_major_grpos = function() {
         return this.nmajor >= this.major.length ? null : this.func(this.major[this.nmajor]);
      };

      handle.get_modifier = function() {
         return this.painter.findLabelModifier(this.painter.getObject(), this.nmajor - 1, this.major);
      };

      this.order = 0;
      this.ndig = 0;

      // at the moment when drawing labels, we can try to find most optimal text representation for them
      if (((this.kind === kAxisNormal) || (this.kind === kAxisFunc)) && !this.log && handle.major.length) {
         let maxorder = 0, minorder = 0, exclorder3 = false;

         if (!optionNoexp && !this.cutLabels()) {
            const maxtick = Math.max(Math.abs(handle.major.at(0)), Math.abs(handle.major.at(-1))),
                  mintick = Math.min(Math.abs(handle.major.at(0)), Math.abs(handle.major.at(-1))),
                  ord1 = (maxtick > 0) ? Math.round(Math.log10(maxtick) / 3) * 3 : 0,
                  ord2 = (mintick > 0) ? Math.round(Math.log10(mintick) / 3) * 3 : 0;

            exclorder3 = (maxtick < 2e4); // do not show 10^3 for values below 20000

            if (maxtick || mintick) {
               maxorder = Math.max(ord1, ord2) + 3;
               minorder = Math.min(ord1, ord2) - 3;
            }
         }

         // now try to find best combination of order and ndig for labels
         let bestorder = 0, bestndig = this.ndig, bestlen = 1e10;

         for (let order = minorder; order <= maxorder; order += 3) {
            if (exclorder3 && (order === 3))
               continue;
            this.order = order;
            this.ndig = 0;
            let lbls = [], indx = 0, totallen = 0;
            while (indx < handle.major.length) {
               const v0 = handle.major[indx],
                     lbl = this.format(v0, true);

               let bad_value = lbls.indexOf(lbl) >= 0;
               if (!bad_value) {
                  try {
                     const v1 = parseFloat(lbl) * Math.pow(10, order);
                     bad_value = (Math.abs(v0) > 1e-30) && (Math.abs(v1 - v0) / Math.abs(v0) > 1e-8);
                  } catch {
                     console.warn('Failure by parsing of', lbl);
                     bad_value = true;
                  }
               }
               if (bad_value) {
                  if (++this.ndig > 15) {
                     totallen += 1e10;
                     break; // not too many digits, anyway it will be exponential
                  }
                  lbls = [];
                  indx = totallen = 0;
               } else {
                  lbls.push(lbl);
                  const p = lbl.indexOf('.');
                  if (!order && !optionNoexp && ((p > gStyle.fAxisMaxDigits) || ((p < 0) && (lbl.length > gStyle.fAxisMaxDigits)))) {
                     totallen += 1e10; // do not use order = 0 when too many digits are there
                     exclorder3 = false;
                  }
                  totallen += lbl.length;
                  indx++;
               }
            }

            // for order === 0 we should virtually remove '0.' and extra label on top
            if (!order && (this.ndig < 4))
               totallen -= handle.major.length * 2 + 3;

            if (totallen < bestlen) {
               bestlen = totallen;
               bestorder = this.order;
               bestndig = this.ndig;
            }
         }

         this.order = bestorder;
         this.ndig = bestndig;

         if (optionInt) {
            if (this.order)
               console.warn(`Axis painter - integer labels are configured, but axis order ${this.order} is preferable`);
            if (this.ndig)
               console.warn(`Axis painter - integer labels are configured, but ${this.ndig} decimal digits are required`);
            this.ndig = 0;
            this.order = 0;
         }
      }

      return handle;
   }

   /** @summary Is labels should be centered */
   isCenteredLabels() {
      if (this.kind === kAxisLabels)
         return true;
      if (this.log)
         return false;
      return this.getObject()?.TestBit(EAxisBits.kCenterLabels);
   }

   /** @summary Is labels should be rotated */
   isRotateLabels() {
      return this.getObject()?.TestBit(EAxisBits.kLabelsVert);
   }

   /** @summary Is title should be rotated */
   isRotateTitle() {
      return this.getObject()?.TestBit(EAxisBits.kRotateTitle);
   }

   /** @summary Add interactive elements to draw axes title */
   addTitleDrag(title_g, vertical, offset_k, reverse, axis_length) {
      if (!settings.MoveResize || this.isBatchMode())
         return;

      let drag_rect = null, x_0, y_0, i_0,
          acc_x, acc_y, new_x, new_y, sign_0, alt_pos, curr_indx, can_indx0 = true;
      const drag_move = d3_drag().subject(Object);

      drag_move.on('start', evnt => {
         evnt.sourceEvent.preventDefault();
         evnt.sourceEvent.stopPropagation();

         const box = title_g.node().getBBox(), // check that elements visible, request precise value
               title_length = vertical ? box.height : box.width;

         x_0 = new_x = acc_x = title_g.property('shift_x');
         y_0 = new_y = acc_y = title_g.property('shift_y');

         sign_0 = vertical ? (acc_x > 0) : (acc_y > 0); // sign should remain
         can_indx0 = !this.hist_painter?.getSnapId(); // online canvas does not allow alternate position

         alt_pos = vertical ? [axis_length, axis_length / 2, 0] : [0, axis_length / 2, axis_length]; // possible positions
         const off = vertical ? -title_length / 2 : title_length / 2;
         if (this.title_align === 'middle') {
            alt_pos[0] += off;
            alt_pos[2] -= off;
         } else if (this.title_align === 'begin') {
            alt_pos[1] -= off;
            alt_pos[2] -= 2 * off;
         } else { // end
            alt_pos[0] += 2 * off;
            alt_pos[1] += off;
         }

         if (this.titleCenter)
            curr_indx = 1;
         else if ((reverse ^ this.titleOpposite) && can_indx0)
            curr_indx = 0;
         else
            curr_indx = 2;

         alt_pos[curr_indx] = vertical ? acc_y : acc_x;
         i_0 = curr_indx;

         drag_rect = title_g.append('rect')
            .attr('x', box.x)
            .attr('y', box.y)
            .attr('width', box.width)
            .attr('height', box.height)
            .style('cursor', 'move')
            .call(addHighlightStyle, true);
         //   .style('pointer-events','none'); // let forward double click to underlying elements
      }).on('drag', evnt => {
         if (!drag_rect)
            return;

         evnt.sourceEvent.preventDefault();
         evnt.sourceEvent.stopPropagation();

         acc_x += evnt.dx;
         acc_y += evnt.dy;

         let set_x, set_y, besti = can_indx0 ? 0 : 1;
         const p = vertical ? acc_y : acc_x;

         for (let i = 1; i < 3; ++i) {
            if (Math.abs(p - alt_pos[i]) < Math.abs(p - alt_pos[besti]))
               besti = i;
         }

         if (vertical) {
            set_x = acc_x;
            set_y = alt_pos[besti];
         } else {
            set_y = acc_y;
            set_x = alt_pos[besti];
         }

         if (sign_0 === (vertical ? (set_x > 0) : (set_y > 0))) {
            new_x = set_x;
            new_y = set_y;
            curr_indx = besti;
            makeTranslate(title_g, new_x, new_y);
         }
      }).on('end', evnt => {
         if (!drag_rect)
            return;

         evnt.sourceEvent.preventDefault();
         evnt.sourceEvent.stopPropagation();

         title_g.property('shift_x', new_x)
                .property('shift_y', new_y);

         const axis = this.getObject(), axis2 = this.source_axis,
               setBit = (bit, on) => {
                  axis?.SetBit(bit, on);
                  axis2?.SetBit(bit, on);
               };

         this.titleOffset = (vertical ? new_x : new_y) / offset_k;
         const offset = this.titleOffset / this.offsetScaling / this.titleSize;
         if (axis)
            axis.fTitleOffset = offset;
         if (axis2)
            axis2.fTitleOffset = offset;

         if (curr_indx === 1) {
            setBit(EAxisBits.kCenterTitle, true);
            this.titleCenter = true;
            setBit(EAxisBits.kOppositeTitle, false);
            this.titleOpposite = false;
         } else if (curr_indx === 0) {
            setBit(EAxisBits.kCenterTitle, false);
            this.titleCenter = false;
            setBit(EAxisBits.kOppositeTitle, true);
            this.titleOpposite = true;
         } else {
            setBit(EAxisBits.kCenterTitle, false);
            this.titleCenter = false;
            setBit(EAxisBits.kOppositeTitle, false);
            this.titleOpposite = false;
         }

         drag_rect.remove();
         drag_rect = null;

         if ((x_0 !== new_x) || (y_0 !== new_y) || (i_0 !== curr_indx))
            this.submitAxisExec(`SetTitleOffset(${offset});;SetBit(${EAxisBits.kCenterTitle},${this.titleCenter ? 1 : 0})`);

         if (this.hist_painter && this.hist_axis)
            this.hist_painter.getCanvPainter()?.producePadEvent('select', this.hist_painter.getPadPainter(), this);
      });

      title_g.style('cursor', 'move').call(drag_move);
   }

   /** @summary Configure hist painter which creates axis - to be able submit execs
     * @private */
   setHistPainter(hist_painter, axis_name) {
      this.hist_painter = hist_painter;
      this.hist_axis = axis_name;
   }

   /** @summary Submit exec for the axis - if possible
     * @private */
   submitAxisExec(exec, only_gaxis) {
      const snapid = this.hist_painter?.getSnapId();
      if (snapid && this.hist_axis && !only_gaxis)
         this.submitCanvExec(exec, `${snapid}#${this.hist_axis}`);
      else if (this.is_gaxis)
         this.submitCanvExec(exec);
   }

   /** @summary Produce svg path for axis ticks */
   produceTicksPath(handle, side, tickSize, ticksPlusMinus, secondShift, real_draw) {
      let path1 = '', path2 = '';
      this.ticks = [];

      while (handle.next(true)) {
         let h1 = Math.round(tickSize / 4), h2 = 0;

         if (handle.kind < 3)
            h1 = Math.round(tickSize / 2);

         if (handle.kind === 1) {
            // if not showing labels, not show large tick
            // FIXME: for labels last tick is smaller,
            if (!this.isExtraLogTick(handle.tick) && (this.format(handle.tick, true) !== null))
               h1 = tickSize;
            this.ticks.push(handle.grpos); // keep graphical positions of major ticks
         }

         if (ticksPlusMinus > 0)
            h2 = -h1;
         else if (side < 0) {
            h2 = -h1;
            h1 = 0;
         }

         path1 += this.vertical ? `M${h1},${handle.grpos}H${h2}` : `M${handle.grpos},${-h1}V${-h2}`;

         if (secondShift)
            path2 += this.vertical ? `M${secondShift - h1},${handle.grpos}H${secondShift - h2}` : `M${handle.grpos},${secondShift + h1}V${secondShift + h2}`;
      }

      return real_draw ? path1 + path2 : '';
   }

   /** @summary Returns modifier for axis label */
   findLabelModifier(axis, nlabel, positions) {
      if (!axis.fModLabs)
         return null;
      for (let n = 0; n < axis.fModLabs.arr.length; ++n) {
         const mod = axis.fModLabs.arr[n];

         if ((mod.fLabValue !== undefined) && (mod.fLabNum === 0)) {
            const eps = this.log ? positions[nlabel] * 1e-6 : (this.scale_max - this.scale_min) * 1e-6;
            if (Math.abs(mod.fLabValue - positions[nlabel]) < eps)
               return mod;
         }

         if ((mod.fLabNum === nlabel + 1) ||
             ((mod.fLabNum < 0) && (nlabel === positions.length + mod.fLabNum)))
            return mod;
      }
      return null;
   }

   /** @summary Draw axis labels
     * @return {Promise} with array label size and max width */
   async drawLabels(axis_g, axis, w, h, handle, labelsside, labelsFont, labeloffset, tickSize, ticksPlusMinus, max_text_width, frame_ygap) {
      const center_lbls = this.isCenteredLabels(),
            label_g = [axis_g.append('svg:g').attr('class', 'axis_labels')],
            lbl_pos = handle.lbl_pos || handle.major,
            tilt_angle = gStyle.AxisTiltAngle ?? 25;
      let rotate_lbls = this.isRotateLabels(),
          textscale = 1, flipscale = 1, maxtextlen = 0, applied_scale = 0,
          lbl_tilt = false, any_modified = false, max_textwidth = 0, max_tiltsize = 0;

      if (this.lbls_both_sides)
         label_g.push(axis_g.append('svg:g').attr('class', 'axis_labels').attr('transform', this.vertical ? `translate(${w})` : `translate(0,${-h})`));

      if (frame_ygap > 0)
         max_tiltsize = frame_ygap / Math.sin(tilt_angle / 180 * Math.PI) - Math.tan(tilt_angle / 180 * Math.PI);

      // function called when text is drawn to analyze width, required to correctly scale all labels
      // must be function to correctly handle 'this' argument
      function process_drawtext_ready(painter) {
         const textwidth = this.result_width;
         max_textwidth = Math.max(max_textwidth, textwidth);

         const maxwidth = !this.gap_before ? 0.9 * this.gap_after : (!this.gap_after ? 0.9 * this.gap_before : this.gap_before * 0.45 + this.gap_after * 0.45);

         if (!painter.vertical && !rotate_lbls && this.result_height && maxwidth)
            flipscale = Math.min(flipscale, maxwidth / this.result_height);

         if (textwidth && ((!painter.vertical && !rotate_lbls) || (painter.vertical && rotate_lbls)) && !painter.log)
            textscale = Math.min(textscale, maxwidth / textwidth);
         else if (painter.vertical && max_text_width && this.normal_side && (max_text_width - labeloffset > 20) && (textwidth > max_text_width - labeloffset))
            textscale = Math.min(textscale, (max_text_width - labeloffset) / textwidth);

         if ((textscale > 0.0001) && (textscale < 0.7) && !any_modified &&
              !painter.vertical && !rotate_lbls && (label_g.length === 1) && (lbl_tilt === false)) {
            if (maxtextlen > 5)
               lbl_tilt = true;
         }

         let scale = textscale;

         if (lbl_tilt) {
            if (max_tiltsize && max_textwidth) {
               scale = Math.min(1, 0.8 * max_tiltsize / max_textwidth);
               if (scale < textscale) {
                  // if due to tilt scale is even smaller - ignore tilting
                  lbl_tilt = 0;
                  scale = textscale;
               }
            } else
               scale *= 3;
         }

         if (((scale > 0.0001) && (scale < 1)) || (lbl_tilt !== false)) {
            applied_scale = 1 / scale;
            painter.scaleTextDrawing(applied_scale, label_g[0]);
         }
      }

      // check if short labels can be rotated
      if (!this.vertical && this.regular_labels && !rotate_lbls) {
         let tlen = 0;
         for (let nmajor = 0; nmajor < lbl_pos.length; ++nmajor) {
            const text = this.format(lbl_pos[nmajor], true);
            if (text)
               tlen = Math.max(tlen, text.length);
         }

         if ((tlen > 2) && (tlen <= 5) && (lbl_pos.length * labelsFont.size > w / 2)) {
            rotate_lbls = true;
            lbl_tilt = 0;
         }
      }

      const draw_labels = (lcnt, side) => {
         return this.startTextDrawingAsync(labelsFont, 'font', label_g[lcnt]).then(() => {
            let lastpos = 0;
            const fix_coord = this.vertical ? -labeloffset * side : labeloffset * side + ticksPlusMinus * tickSize;

            for (let nmajor = 0; nmajor < lbl_pos.length; ++nmajor) {
               let text = this.format(lbl_pos[nmajor], true);
               if (text === null)
                  continue;

               const mod = this.findLabelModifier(axis, nmajor, lbl_pos);
               if (mod?.fTextSize === 0)
                  continue;

               if (mod)
                  any_modified = true;
               if (mod?.fLabText)
                  text = mod.fLabText;

               const arg = { text, color: labelsFont.color, latex: 1, draw_g: label_g[lcnt], normal_side: (lcnt === 0) };
               let pos = Math.round(this.func(lbl_pos[nmajor]));

               // exclude labels for extra log ticks
               if (lastpos && this.vertical && (Math.abs(pos - lastpos) < labelsFont.size * 1.1) && this.isExtraLogTick(lbl_pos[nmajor])) {
                  lastpos = pos;
                  continue;
               }

               if (mod?.fTextColor > 0)
                  arg.color = this.getColor(mod.fTextColor);

               arg.gap_before = (nmajor > 0) ? Math.abs(Math.round(pos - this.func(lbl_pos[nmajor - 1]))) : 0;

               arg.gap_after = (nmajor < lbl_pos.length - 1) ? Math.abs(Math.round(this.func(lbl_pos[nmajor + 1]) - pos)) : 0;

               if (center_lbls) {
                  const gap = arg.gap_after || arg.gap_before;
                  pos = Math.round(pos - ((this.vertical !== this.reverse) ? 0.5 * gap : -0.5 * gap));
                  if ((pos < -5) || (pos > (this.vertical ? h : w) + 5))
                     continue;
               }

               maxtextlen = Math.max(maxtextlen, text.length);

               if (this.vertical) {
                  arg.x = fix_coord;
                  arg.y = pos;
                  const flag = this.optionLeft || this.reverseAlign || (side < 0);
                  arg.align = rotate_lbls ? (flag ? 23 : 21) : (flag ? 12 : 32);
                  if (this.cutLabels()) {
                     const gap = labelsFont.size * (rotate_lbls ? 1.5 : 0.6);
                     if ((pos < gap) || (pos > h - gap))
                        continue;
                  }
               } else {
                  arg.x = pos;
                  arg.y = fix_coord;
                  arg.align = rotate_lbls ? ((side < 0) ? 12 : 32) : ((side < 0) ? 21 : 23);
                  if (this.log && !this.noexp && !this.vertical && arg.align === 23) {
                     arg.align = 21;
                     arg.y += labelsFont.size;
                  } else if (arg.align % 10 === 3)
                     arg.y -= labelsFont.size * 0.1; // font takes 10% more by top align

                  if (this.cutLabels()) {
                     const gap = labelsFont.size * (rotate_lbls ? 0.4 : 1.5);
                     if ((pos < gap) || (pos > w - gap))
                        continue;
                  }
               }

               if (rotate_lbls)
                  arg.rotate = 270;
               else if (mod && mod.fTextAngle !== -1)
                  arg.rotate = -mod.fTextAngle;

               // only for major text drawing scale factor need to be checked
               // for modified labels ignore scaling
               if ((lcnt === 0) && !mod?.fLabText)
                  arg.post_process = process_drawtext_ready;

               this.drawText(arg);

               // workaround for symlog where labels can be compressed to close
               if (this.symlog && lastpos && (pos !== lastpos) && ((this.vertical && !rotate_lbls) || (!this.vertical && rotate_lbls))) {
                  const axis_step = Math.abs(pos - lastpos);
                  textscale = Math.min(textscale, 1.1 * axis_step / labelsFont.size);
               }

               lastpos = pos;
            }

            if (this.order) {
               let xoff = 0, yoff = 0;
               if (this.name === 'xaxis') {
                  xoff = gStyle.fXAxisExpXOffset || 0;
                  yoff = gStyle.fXAxisExpYOffset || 0;
               } else if (this.name === 'yaxis') {
                  xoff = gStyle.fYAxisExpXOffset || 0;
                  yoff = gStyle.fYAxisExpYOffset || 0;
               }

               if (xoff)
                  xoff = Math.round(xoff * (this.getPadPainter()?.getPadWidth() ?? 0));
               if (yoff)
                  yoff = Math.round(yoff * (this.getPadPainter()?.getPadHeight() ?? 0));

               this.drawText({
                  color: labelsFont.color,
                  x: xoff + (this.vertical ? side * 5 : w + 5),
                  y: yoff + (this.has_obstacle ? fix_coord : (this.vertical ? -3 : -3 * side)),
                  align: this.vertical ? ((side < 0) ? 30 : 10) : ((this.has_obstacle ^ (side < 0)) ? 13 : 10),
                  latex: 1,
                  text: '#times' + this.formatExp(10, this.order),
                  draw_g: label_g[lcnt]
               });
            }

            if ((lcnt > 0) && applied_scale)
               this.scaleTextDrawing(applied_scale, label_g[lcnt]);

            return this.finishTextDrawing(label_g[lcnt], true);
         });
      };

      return draw_labels(0, labelsside).then(() => {
         return label_g.length < 2 ? true : draw_labels(1, -labelsside);
      }).then(() => {
         this._maxlbllen = maxtextlen; // for internal use in palette painter

         if (lbl_tilt) {
            label_g[0].selectAll('text').each(function() {
               const txt = d3_select(this), tr = txt.attr('transform');
               if (lbl_tilt)
                  txt.attr('transform', `${tr} rotate(${tilt_angle})`).style('text-anchor', 'start');
            });
         }

         return max_textwidth;
      });
   }

   /** @summary Extract major draw attributes, which are also used in interactive operations
     * @private  */
   extractDrawAttributes(scalingSize, w, h) {
      const axis = this.getObject();
      let pp = this.getPadPainter();
      if (axis.$use_top_pad)
         pp = pp?.getPadPainter(); // workaround for ratio plot
      const pad_w = pp?.getPadWidth() || scalingSize || w / 0.8, // use factor 0.8 as ratio between frame and pad size
            pad_h = pp?.getPadHeight() || scalingSize || h / 0.8,
            // if no external scaling size use scaling as in TGaxis.cxx:1448 - NDC axis length is in the scaling factor
            tickScalingSize = scalingSize || (this.vertical ? h / pad_h * pad_w : w / pad_w * pad_h),
            bit_plus = axis.TestBit(EAxisBits.kTickPlus), bit_minus = axis.TestBit(EAxisBits.kTickMinus);

      let tickSize, titleColor, titleFontId, offset;

      this.scalingSize = scalingSize || Math.max(Math.min(pad_w, pad_h), 10);

      if (this.is_gaxis) {
         const optionSize = axis.fChopt.indexOf('S') >= 0;
         this.optionUnlab = axis.fChopt.indexOf('U') >= 0;
         this.optionMinus = (axis.fChopt.indexOf('-') >= 0) || bit_minus;
         this.optionPlus = (axis.fChopt.indexOf('+') >= 0) || bit_plus;
         this.optionNoopt = (axis.fChopt.indexOf('N') >= 0);  // no ticks position optimization
         this.optionInt = (axis.fChopt.indexOf('I') >= 0);  // integer labels
         this.optionText = (axis.fChopt.indexOf('T') >= 0);  // text scaling?
         this.optionLeft = (axis.fChopt.indexOf('L') >= 0);  // left text align
         this.optionRight = (axis.fChopt.indexOf('R') >= 0);  // right text align
         this.optionCenter = (axis.fChopt.indexOf('C') >= 0);  // center text align
         this.createAttLine({ attr: axis });
         tickSize = optionSize ? axis.fTickSize : 0.03;
         titleColor = this.getColor(axis.fTextColor);
         titleFontId = axis.fTextFont;
         offset = axis.fLabelOffset;
         // workaround for old reverse axes where offset is not properly working
         if (this.reverse && (!this.vertical || (!this.optionMinus && (axis.fX1 !== axis.fX2))))
            offset = -offset;
      } else {
         this.optionUnlab = false;
         if (!bit_plus && !bit_minus) {
            this.optionMinus = this.vertical ^ this.invert_side;
            this.optionPlus = !this.optionMinus;
         } else {
            this.optionPlus = bit_plus;
            this.optionMinus = bit_minus;
         }
         this.optionNoopt = false;  // no ticks position optimization
         this.optionInt = false;  // integer labels
         this.optionText = false;
         this.createAttLine({ color: axis.fAxisColor, width: 1, style: 1 });
         tickSize = axis.fTickLength;
         titleColor = this.getColor(axis.fTitleColor);
         titleFontId = axis.fTitleFont;
         offset = axis.fLabelOffset;
      }

      offset += (this.vertical ? 0.002 : 0.005);

      if (this.kind === kAxisLabels)
         this.optionText = true;

      this.optionNoexp = axis.TestBit(EAxisBits.kNoExponent);

      this.ticksSize = Math.round(tickSize * tickScalingSize);
      if (scalingSize && (this.ticksSize < 0))
         this.ticksSize = -this.ticksSize;

      if (this.maxTickSize && (this.ticksSize > this.maxTickSize))
         this.ticksSize = this.maxTickSize;

      // now used only in 3D drawing
      this.ticksColor = this.lineatt.color;
      this.ticksWidth = this.lineatt.width;

      const k = this.optionText ? 0.66666 : 1; // set TGaxis.cxx, line 1504
      this.labelSize = Math.round((axis.fLabelSize < 1) ? k * axis.fLabelSize * this.scalingSize : k * axis.fLabelSize);
      this.labelsOffset = Math.round(offset * this.scalingSize);
      this.labelsFont = new FontHandler(axis.fLabelFont, this.labelSize, scalingSize);
      if ((this.labelSize <= 0) || (Math.abs(axis.fLabelOffset) > 1.1))
         this.optionUnlab = true; // disable labels when size not specified
      this.labelsFont.setColor(this.getColor(axis.fLabelColor));

      this.fTitle = axis.fTitle;
      if (this.fTitle) {
         this.titleSize = (axis.fTitleSize >= 1) ? axis.fTitleSize : Math.round(axis.fTitleSize * this.scalingSize);
         this.titleFont = new FontHandler(titleFontId, this.titleSize, scalingSize);
         this.titleFont.setColor(titleColor);
         this.offsetScaling = (axis.fTitleSize >= 1) ? 1 : (this.vertical ? pad_w : pad_h) / this.scalingSize;
         this.titleOffset = axis.fTitleOffset;
         if (!this.titleOffset && this.name[0] === 'x')
            this.titleOffset = gStyle.fXaxis.fTitleOffset;
         this.titleOffset *= this.titleSize * this.offsetScaling;
         this.titleCenter = axis.TestBit(EAxisBits.kCenterTitle);
         this.titleOpposite = axis.TestBit(EAxisBits.kOppositeTitle);
      } else {
         delete this.titleSize;
         delete this.titleFont;
         delete this.offsetScaling;
         delete this.titleOffset;
         delete this.titleCenter;
         delete this.titleOpposite;
      }
   }

   /** @summary function draws TAxis or TGaxis object
     * @return {Promise} for drawing ready */
   async drawAxis(layer, w, h, transform, secondShift, disable_axis_drawing, max_text_width, calculate_position, frame_ygap) {
      const axis = this.getObject(),
            swap_side = this.swap_side || false;
      let axis_g = layer, draw_lines = true;

      // shift for second ticks set (if any)
      if (!secondShift)
         secondShift = 0;
      else if (this.invert_side)
         secondShift = -secondShift;

      this.extractDrawAttributes(undefined, w, h);

      if (this.is_gaxis)
         draw_lines = Boolean(axis.fLineColor);

      if (!this.is_gaxis || (this.name === 'zaxis')) {
         axis_g = layer.selectChild(`.${this.name}_container`);
         if (axis_g.empty())
            axis_g = layer.append('svg:g').attr('class', `${this.name}_container`);
         else
            axis_g.selectAll('*').remove();
      }

      let axis_lines = '';
      if (draw_lines) {
         axis_lines = 'M0,0' + (this.vertical ? `v${h}` : `h${w}`);
         if (secondShift)
            axis_lines += this.vertical ? `M${secondShift},0v${h}` : `M0,${secondShift}h${w}`;
      }

      axis_g.attr('transform', transform);

      let side = 1, ticksPlusMinus = 0;

      if (this.optionPlus && this.optionMinus)
         side = ticksPlusMinus = 1;
      else if (this.optionMinus)
         side = (swap_side ^ this.vertical) ? 1 : -1;
      else if (this.optionPlus)
         side = (swap_side ^ this.vertical) ? -1 : 1;

      // first draw ticks

      const handle = this.createTicks(false, this.optionNoexp, this.optionNoopt, this.optionInt);

      axis_lines += this.produceTicksPath(handle, side, this.ticksSize, ticksPlusMinus, secondShift, draw_lines && !disable_axis_drawing && !this.disable_ticks);

      if (!disable_axis_drawing && axis_lines && !this.lineatt.empty()) {
         axis_g.append('svg:path')
               .attr('d', axis_lines)
               .call(this.lineatt.func);
      }

      let title_shift_x = 0, title_shift_y = 0, title_g, labelsMaxWidth = 0;
      // draw labels (sometime on both sides)
      const labelSize = Math.max(this.labelsFont.size, 5),
            pr = (disable_axis_drawing || this.optionUnlab)
                ? Promise.resolve(0)
                : this.drawLabels(axis_g, axis, w, h, handle, side, this.labelsFont, this.labelsOffset, this.ticksSize, ticksPlusMinus, max_text_width, frame_ygap);

      return pr.then(maxw => {
         labelsMaxWidth = maxw;

         if (settings.Zooming && !this.disable_zooming && !this.isBatchMode()) {
            const r = axis_g.append('svg:rect')
                            .attr('class', 'axis_zoom')
                            .style('opacity', '0')
                            .style('cursor', 'crosshair');

            if (this.vertical) {
               const rw = Math.max(labelsMaxWidth, 2 * labelSize) + 3;
               r.attr('x', (side > 0) ? -rw : 0).attr('y', 0)
                .attr('width', rw).attr('height', h);
            } else {
               r.attr('x', 0).attr('y', (side > 0) ? 0 : -labelSize - 3)
                .attr('width', w).attr('height', labelSize + 3);
            }
         }

         this.position = 0;

         if (calculate_position) {
            const node1 = axis_g.node(),
                  node2 = this.getPadPainter()?.getPadSvg().node();
            if (isFunc(node1?.getBoundingClientRect) && isFunc(node2?.getBoundingClientRect)) {
               const rect1 = node1.getBoundingClientRect(),
                     rect2 = node2.getBoundingClientRect();
               this.position = rect1.left - rect2.left; // use to control left position of Y scale
            }
            if (node1 && !node2)
               console.warn('Why PAD element missing when search for position');
         }

         if (!this.fTitle || disable_axis_drawing)
            return;

         title_g = axis_g.append('svg:g').attr('class', 'axis_title');

         return this.startTextDrawingAsync(this.titleFont, 'font', title_g);
      }).then(() => {
         if (!title_g)
            return;

         const rotate = this.isRotateTitle() ? -1 : 1,
               xor_reverse = swap_side ^ this.titleOpposite, myxor = (rotate < 0) ^ xor_reverse;

         let title_offest_k = side;

         this.title_align = this.titleCenter ? 'middle' : (myxor ? 'begin' : 'end');

         if (this.vertical) {
            title_offest_k *= -1.6;

            title_shift_x = Math.round(title_offest_k * this.titleOffset);

            title_shift_y = Math.round(this.titleCenter ? h / 2 : (xor_reverse ? h : 0));

            this.drawText({
               align: this.title_align + ';middle',
               rotate: (rotate < 0) ? 90 : 270,
               text: this.fTitle, color: this.titleFont.color, draw_g: title_g
            });
         } else {
            title_offest_k *= 1.6;

            title_shift_x = Math.round(this.titleCenter ? w / 2 : (xor_reverse ? 0 : w));
            title_shift_y = Math.round(title_offest_k * this.titleOffset);
            this.drawText({
               align: this.title_align + ';middle',
               rotate: (rotate < 0) ? 180 : 0,
               text: this.fTitle, color: this.titleFont.color, draw_g: title_g
            });
         }

         this.addTitleDrag(title_g, this.vertical, title_offest_k, swap_side, this.vertical ? h : w);

         return this.finishTextDrawing(title_g);
      }).then(() => {
         if (title_g) {
            if (!this.titleOffset && this.vertical)
               title_shift_x = Math.round(-side * ((labelsMaxWidth || labelSize) + 0.7 * this.offsetScaling * this.titleSize));
            makeTranslate(title_g, title_shift_x, title_shift_y);
            title_g.property('shift_x', title_shift_x)
                  .property('shift_y', title_shift_y);
         }

         return this;
      });
   }

} // class TAxisPainter


export { EAxisBits, chooseTimeFormat, AxisPainterMethods, TAxisPainter };
