import { gStyle, BIT, settings, create, createHistogram, setHistogramTitle, isFunc, isStr,
         clTPaveStats, clTCutG, clTH1F, clTH2F, clTF1, clTF2, clTPad, kNoZoom, kNoStats } from '../core.mjs';
import { select as d3_select } from '../d3.mjs';
import { DrawOptions, buildSvgCurve, makeTranslate, addHighlightStyle } from '../base/BasePainter.mjs';
import { ObjectPainter, kAxisNormal } from '../base/ObjectPainter.mjs';
import { FunctionsHandler } from './THistPainter.mjs';
import { TH1Painter, PadDrawOptions } from './TH1Painter.mjs';
import { kBlack, kWhite } from '../base/colors.mjs';
import { addMoveHandler } from '../gui/utils.mjs';
import { assignContextMenu, kNoReorder } from '../gui/menu.mjs';


const kNotEditable = BIT(18),   // bit set if graph is non editable
      clTGraphErrors = 'TGraphErrors',
      clTGraphAsymmErrors = 'TGraphAsymmErrors',
      clTGraphBentErrors = 'TGraphBentErrors',
      clTGraphMultiErrors = 'TGraphMultiErrors';

/**
 * @summary Painter for TGraph object.
 *
 * @private
 */

class TGraphPainter extends ObjectPainter {

   #bins;          // extracted graph bins
   #barwidth;      // width of each bar
   #baroffset;     // offset of each bar
   #redraw_hist;   // indicate that histogram need to be redrawn
   #auto_exec;     // can be reused when sending option back to server
   #funcs_handler; // special instance for functions drawing
   #frame_layer;   // frame layer used for drawing
   #cutg;          // is cutg object
   #cutg_lastsame; // indicate that last point is same as first
   #own_histogram; // if histogram created by TGraphPainter
   #marker_size;   // used marker size
   #move_binindx;  // index of moving bin
   #move_funcs;    // moving functions
   #move_bin;      // moving bin
   #move_x0;       // initial x position
   #move_y0;       // initial y position
   #pos_dx;        // accumulated x change
   #pos_dy;        // accumulated y change
   #has_errors;    // if has errors
   #is_bent;       // if graph has bent errors
   #draw_kind;     // way how graph is drawn

   constructor(dom, graph) {
      super(dom, graph);
      this.axes_draw = false; // indicate if graph histogram was drawn for axes
      this.xmin = this.ymin = this.xmax = this.ymax = 0;
      this.#is_bent = (graph._typename === clTGraphBentErrors);
      this.#has_errors = (graph._typename === clTGraphErrors) ||
                         (graph._typename === clTGraphMultiErrors) ||
                         (graph._typename === clTGraphAsymmErrors) ||
                          this.#is_bent || graph._typename.match(/^RooHist/);
      this.#draw_kind = '';
   }

   /** @summary Use in frame painter to check zoom Y is allowed
    * @protected */
   get _wheel_zoomy() { return true; }

   /** @summary Return drawn graph object */
   getGraph() { return this.getObject(); }

   /** @summary Return histogram object used for axis drawings */
   getHistogram() { return this.getObject()?.fHistogram; }

   /** @summary Return true if histogram not present or has dummy ranges (for requested axis) */
   isDummyHistogram(check_axis) {
      const histo = this.getHistogram();
      if (!histo)
         return true;

      let is_normal = false;
      if (check_axis !== 'y') {
         is_normal ||= ((histo.fXaxis.fXmin !== 0.0011) && (histo.fXaxis.fXmin !== 0)) ||
                       ((histo.fXaxis.fXmax !== 1.1) && (histo.fXaxis.fXmax !== 1));
      }

      if (check_axis !== 'x') {
         is_normal ||= ((histo.fYaxis.fXmin !== 0.0011) && (histo.fYaxis.fXmin !== 0)) ||
                       ((histo.fYaxis.fXmax !== 1.1) && (histo.fYaxis.fXmax !== 1)) ||
                       (histo.fMinimum !== 0.0011) || (histo.fMaximum !== 1.1);
      }

      return !is_normal;
   }

   /** @summary Set histogram object to graph */
   setHistogram(histo) {
      const obj = this.getObject();
      if (obj)
         obj.fHistogram = histo;
   }

   /** @summary Is TScatter object */
   isScatter() { return false; }

   /** @summary Redraw graph
     * @desc may redraw histogram which was used to draw axes
     * @return {Promise} for ready */
   async redraw() {
      let promise = Promise.resolve(true);

      if (this.#redraw_hist) {
         this.#redraw_hist = undefined;
         const hist_painter = this.getMainPainter();
         if (hist_painter?.isSecondary(this) && this.axes_draw)
            promise = hist_painter.redraw();
      }

      return promise.then(() => this.drawGraph()).then(() => {
         const res = this.#funcs_handler?.drawNext(0) ?? this;
         this.#funcs_handler = undefined;
         return res;
      });
   }

   /** @summary Cleanup graph painter */
   cleanup() {
      this.#bins = undefined;
      this.#own_histogram = undefined;
      super.cleanup();
   }

   /** @summary Returns object if this drawing TGraphMultiErrors object */
   get_gme() {
      const graph = this.getGraph();
      return graph?._typename === clTGraphMultiErrors ? graph : null;
   }

   /** @summary Decode options */
   decodeOptions(opt, first_time) {
      if (isStr(opt) && (opt.indexOf('same ') === 0))
         opt = opt.slice(5);

      const graph = this.getGraph(),
            is_gme = Boolean(this.get_gme()),
            has_main = first_time ? Boolean(this.getMainPainter()) : !this.axes_draw;

      function decodeBlock(d, res) {
         Object.assign(res, { Line: 0, Curve: 0, Rect: 0, Mark: 0, Bar: 0, OutRange: 0, EF: 0, Fill: 0, MainError: 1, Ends: 1, ScaleErrX: 1 });

         if (is_gme && d.check('S=', true))
            res.ScaleErrX = d.partAsFloat();

         if (d.check('L'))
            res.Line = 1;
         if (d.check('F'))
            res.Fill = 1;
         if (d.check('CC'))
            res.Curve = 2; // draw all points without reduction
         if (d.check('C'))
            res.Curve = 1;
         if (d.check('*'))
            res.Mark = 103;
         if (d.check('P0'))
            res.Mark = 104;
         if (d.check('P'))
            res.Mark = 1;
         if (d.check('B')) {
            res.Bar = 1;
            res.Errors = 0;
         }
         if (d.check('Z')) {
            res.Errors = 1;
            res.Ends = 0;
         }
         if (d.check('||')) {
            res.Errors = 1;
            res.MainError = 0;
            res.Ends = 1;
         }
         if (d.check('[]')) {
            res.Errors = 1;
            res.MainError = 0;
            res.Ends = 2;
         }
         if (d.check('|>')) {
            res.Errors = 1;
            res.Ends = 3;
         }
         if (d.check('>')) {
            res.Errors = 1;
            res.Ends = 4;
         }
         if (d.check('0')) {
            res.Mark = 1;
            res.Errors = 1;
            res.OutRange = 1;
         }
         if (d.check('1') && (res.Bar === 1))
            res.Bar = 2;
         if (d.check('2')) {
            res.Rect = 1;
            res.Errors = 0;
         }
         if (d.check('3')) {
            res.EF = 1;
            res.Errors = 0;
         }
         if (d.check('4')) {
            res.EF = 2;
            res.Errors = 0;
         }
         if (d.check('5')) {
            res.Rect = 2;
            res.Errors = 0;
         }
         if (d.check('X'))
            res.Errors = 0;
      }

      const res = this.setOptions({
         Axis: '', NoOpt: 0, PadStats: false, PadPalette: false, original: opt,
         second_x: false, second_y: false, individual_styles: false
      });

      let blocks_gme = [];

      if (is_gme && opt) {
         if (opt.indexOf(';') > 0) {
            blocks_gme = opt.split(';');
            opt = blocks_gme.shift();
         } else if (opt.indexOf('_') > 0) {
            blocks_gme = opt.split('_');
            opt = blocks_gme.shift();
         }
      }

      let d = new DrawOptions(opt), hopt = '';

      PadDrawOptions.forEach(name => {
         if (d.check(name))
            hopt += ';' + name;
      });
      if (d.check('XAXIS_', true))
         hopt += ';XAXIS_' + d.part;
      if (d.check('YAXIS_', true))
         hopt += ';YAXIS_' + d.part;

      if (d.empty()) {
         res.original = has_main ? 'lp' : 'alp';
         d = new DrawOptions(res.original);
      }

      if (d.check('FILL_', 'color')) {
         res.graphFillColor = d.color;
         res.graphFillPattern = 1001;
      }

      if (d.check('FILLPAT_', true))
         res.graphFillPattern = d.partAsInt();

      if (d.check('LINE_', 'color'))
         res.graphLineColor = this.getColor(d.color);

      if (d.check('WIDTH_', true))
         res.graphLineWidth = d.partAsInt();

      if (d.check('NOOPT'))
         res.NoOpt = 1;

      if (d.check('POS3D_', true))
         res.pos3d = d.partAsInt() - 0.5;

      if (d.check('PFC') && !res._pfc)
         res._pfc = 2;
      if (d.check('PLC') && !res._plc)
         res._plc = 2;
      if (d.check('PMC') && !res._pmc)
         res._pmc = 2;

      if (d.check('A'))
         res.Axis = d.check('I') ? 'A;' : ' '; // I means invisible axis
      if (d.check('X+')) {
         res.Axis += 'X+';
         res.second_x = has_main;
      }
      if (d.check('Y+')) {
         res.Axis += 'Y+';
         res.second_y = has_main;
      }
      if (d.check('RX'))
         res.Axis += 'RX';
      if (d.check('RY'))
         res.Axis += 'RY';

      if (is_gme) {
         res.blocks = [];
         res.skip_errors_x0 = res.skip_errors_y0 = false;
         if (d.check('X0'))
            res.skip_errors_x0 = true;
         if (d.check('Y0'))
            res.skip_errors_y0 = true;
      }

      decodeBlock(d, res);

      if (is_gme && d.check('S'))
         res.individual_styles = true;

      if (res.Errors === undefined)
         res.Errors = this.#has_errors && (!is_gme || !blocks_gme.length) ? 1 : 0;

      // special case - one could use svg:path to draw many pixels (
      if ((res.Mark === 1) && (graph.fMarkerStyle === 1))
         res.Mark = 101;

      // if no drawing option is selected and if opt === '' nothing is done.
      if ((res.Line + res.Fill + res.Curve + res.Mark + res.Bar + res.EF + res.Rect + res.Errors === 0) && d.empty())
         res.Line = 1;

      if (this.matchObjectType(clTGraphErrors)) {
         const len = graph.fEX.length;
         let m = 0;
         for (let k = 0; k < len; ++k)
            m = Math.max(m, graph.fEX[k], graph.fEY[k]);
         if (m < 1e-100)
            res.Errors = 0;
      }

      this.#cutg = this.matchObjectType(clTCutG);
      this.#cutg_lastsame = this.#cutg && (graph.fNpoints > 3) &&
                            (graph.fX[0] === graph.fX[graph.fNpoints - 1]) && (graph.fY[0] === graph.fY[graph.fNpoints - 1]);

      if (!res.Axis) {
         // check if axis should be drawn
         // either graph drawn directly or
         // graph is first object in list of primitives
         const pad = this.getPadPainter()?.getRootPad(true);
         if (!pad || (pad?.fPrimitives?.arr[0] === this.getObject()))
            res.Axis = ' ';
      }

      res.Axis += hopt;

      for (let bl = 0; bl < blocks_gme.length; ++bl) {
         const subd = new DrawOptions(blocks_gme[bl]), subres = {};
         decodeBlock(subd, subres);
         subres.skip_errors_x0 = res.skip_errors_x0;
         subres.skip_errors_y0 = res.skip_errors_y0;
         res.blocks.push(subres);
      }
   }

   /** @summary Return prepared graph bins
    * @protected */
   _getBins() { return this.#bins; }

   /** @summary Create bins for TF1 drawing */
   createBins() {
      const gr = this.getGraph(),
            o = this.getOptions();
      if (!gr)
         return;

      let kind = 0, npoints = gr.fNpoints;
      if (this.#cutg && this.#cutg_lastsame)
         npoints--;

      if (gr._typename === clTGraphErrors)
         kind = 1;
      else if (gr._typename === clTGraphMultiErrors)
         kind = 2;
      else if (gr._typename === clTGraphAsymmErrors || gr._typename === clTGraphBentErrors || gr._typename.match(/^RooHist/))
         kind = 3;

      this.#bins = new Array(npoints);

      for (let p = 0; p < npoints; ++p) {
         const bin = this.#bins[p] = { x: gr.fX[p], y: gr.fY[p], indx: p };
         switch (kind) {
            case 1:
               bin.exlow = bin.exhigh = gr.fEX[p];
               bin.eylow = bin.eyhigh = gr.fEY[p];
               break;
            case 2:
               bin.exlow = gr.fExL[p];
               bin.exhigh = gr.fExH[p];
               bin.eylow = gr.fEyL[0][p];
               bin.eyhigh = gr.fEyH[0][p];
               break;
            case 3:
               bin.exlow = gr.fEXlow[p];
               bin.exhigh = gr.fEXhigh[p];
               bin.eylow = gr.fEYlow[p];
               bin.eyhigh = gr.fEYhigh[p];
               break;
         }

         if (p === 0) {
            this.xmin = this.xmax = bin.x;
            this.ymin = this.ymax = bin.y;
         }

         if (kind > 0) {
            this.xmin = Math.min(this.xmin, bin.x - bin.exlow, bin.x + bin.exhigh);
            this.xmax = Math.max(this.xmax, bin.x - bin.exlow, bin.x + bin.exhigh);
            this.ymin = Math.min(this.ymin, bin.y - bin.eylow, bin.y + bin.eyhigh);
            this.ymax = Math.max(this.ymax, bin.y - bin.eylow, bin.y + bin.eyhigh);
         } else {
            this.xmin = Math.min(this.xmin, bin.x);
            this.xmax = Math.max(this.xmax, bin.x);
            this.ymin = Math.min(this.ymin, bin.y);
            this.ymax = Math.max(this.ymax, bin.y);
         }
      }

      // workaround, are there better way to show marker at 0,0 on the top of the frame?
      this.#frame_layer = true;
      if ((this.xmin === 0) && (this.ymin === 0) && (npoints > 0) && (this.#bins[0].x === 0) && (this.#bins[0].y === 0) &&
          o.Mark && !o.Line && !o.Curve && !o.Fill)
         this.#frame_layer = 'upper_layer';
   }

   /** @summary Return margins for histogram ranges */
   getHistRangeMargin() { return 0.1; }

   /** @summary Create histogram for graph
     * @desc graph bins should be created when calling this function
     * @param {boolean} [set_x] - set X axis range
     * @param {boolean} [set_y] - set Y axis range */
   createHistogram(set_x = true, set_y = true) {
      const graph = this.getGraph(),
            xmin = this.xmin,
            margin = this.getHistRangeMargin();
      let xmax = this.xmax, ymin = this.ymin, ymax = this.ymax;

      if (xmin >= xmax)
         xmax = xmin + 1;
      if (ymin >= ymax)
         ymax = ymin + 1;
      const dx = (xmax - xmin) * margin, dy = (ymax - ymin) * margin;
      let uxmin = xmin - dx, uxmax = xmax + dx,
          minimum = ymin - dy, maximum = ymax + dy;

      if ((ymin > 0) && (minimum <= 0))
         minimum = (1 - margin) * ymin;
      if ((ymax < 0) && (maximum >= 0))
         maximum = (1 - margin) * ymax;

      const minimum0 = minimum, maximum0 = maximum;
      let histo = this.getHistogram();

      if (!this.isScatter() && !histo?.fXaxis.fTimeDisplay) {
         const pad_logx = this.getPadPainter()?.getPadLog('x');

         if ((uxmin < 0) && (xmin >= 0))
            uxmin = pad_logx ? xmin * (1 - margin) : 0;
         if ((uxmax > 0) && (xmax <= 0))
            uxmax = pad_logx ? (1 + margin) * xmax : 0;
      }

      if (!histo) {
         histo = this.isScatter() ? createHistogram(clTH2F, 30, 30) : createHistogram(clTH1F, 100);
         histo.fName = graph.fName + '_h';
         histo.fBits |= kNoStats;
         this.#own_histogram = true;
         this.setHistogram(histo);
      } else if ((histo.fMaximum !== kNoZoom) && (histo.fMinimum !== kNoZoom) && !this.isDummyHistogram('y')) {
         minimum = histo.fMinimum;
         maximum = histo.fMaximum;
      }

      if (graph.fMinimum !== kNoZoom)
         minimum = ymin = graph.fMinimum;
      if (graph.fMaximum !== kNoZoom)
         maximum = graph.fMaximum;
      if ((minimum < 0) && (ymin >= 0))
         minimum = (1 - margin) * ymin;
      if ((ymax < 0) && (maximum >= 0))
         maximum = (1 - margin) * ymax;

      setHistogramTitle(histo, this.getObject().fTitle);

      if (set_x && !histo.fXaxis.fLabels) {
         histo.fXaxis.fXmin = uxmin;
         histo.fXaxis.fXmax = uxmax;
      }

      if (set_y && !histo.fYaxis.fLabels) {
         histo.fYaxis.fXmin = Math.min(minimum0, minimum);
         histo.fYaxis.fXmax = Math.max(maximum0, maximum);
         if (!this.isScatter()) {
            histo.fMinimum = minimum;
            histo.fMaximum = maximum;
         }
      }

      histo.$xmin_nz = xmin > 0 ? xmin : undefined;
      histo.$ymin_nz = ymin > 0 ? ymin : undefined;

      return histo;
   }

   /** @summary Check if user range can be un-zommed
     * @desc Used when graph points covers larger range than provided histogram */
   unzoomUserRange(dox, doy /* , doz */) {
      const graph = this.getGraph();
      if (this.#own_histogram || !graph)
         return false;

      const histo = this.getHistogram();

      dox = dox && histo && ((histo.fXaxis.fXmin > this.xmin) || (histo.fXaxis.fXmax < this.xmax));
      doy = doy && histo && ((histo.fYaxis.fXmin > this.ymin) || (histo.fYaxis.fXmax < this.ymax));
      if (!dox && !doy)
         return false;

      this.createHistogram(dox, doy);
      this.getMainPainter()?.extractAxesProperties(1); // just to enforce ranges extraction

      return true;
   }

   /** @summary Returns true if graph drawing can be optimize */
   canOptimize() {
      return (settings.OptimizeDraw > 0) && !this.getOptions().NoOpt;
   }

   /** @summary Returns optimized bins - if optimization enabled */
   optimizeBins(maxpnt, filter_func) {
      if ((this.#bins.length < 30) && !filter_func)
         return this.#bins;

      let selbins = null;
      if (isFunc(filter_func)) {
         for (let n = 0; n < this.#bins.length; ++n) {
            if (filter_func(this.#bins[n], n)) {
               if (!selbins)
                  selbins = (n === 0) ? [] : this.#bins.slice(0, n);
            } else if (selbins)
               selbins.push(this.#bins[n]);
         }
      }
      if (!selbins)
         selbins = this.#bins;

      if (!maxpnt)
         maxpnt = 500000;

      if ((selbins.length < maxpnt) || !this.canOptimize())
         return selbins;
      const optbins = [], step = Math.max(2, Math.floor(selbins.length / maxpnt));
      for (let n = 0; n < selbins.length; n += step)
         optbins.push(selbins[n]);

      return optbins;
   }

   /** @summary Check if such function should be drawn directly */
   needDrawFunc(graph, func) {
      if (func._typename === clTPaveStats)
         return (func.fName !== 'stats') || !graph.TestBit(kNoStats); // kNoStats is same for graph and histogram

      if ((func._typename === clTF1) || (func._typename === clTF2))
         return !func.TestBit(BIT(9)); // TF1::kNotDraw

      return true;
   }

   /** @summary Returns tooltip for specified bin */
   getTooltips(d) {
      const fp = this.get_fp(), lines = [], o = this.getOptions(),
            funcs = fp.getGrFuncs(o.second_x, o.second_y),
            gme = this.get_gme();

      lines.push(this.getObjectHint());

      if (d && funcs) {
         if (d.indx !== undefined)
            lines.push('p = ' + d.indx);
         lines.push('x = ' + funcs.axisAsText('x', d.x), 'y = ' + funcs.axisAsText('y', d.y));
         if (gme)
            lines.push('error x = -' + funcs.axisAsText('x', gme.fExL[d.indx]) + '/+' + funcs.axisAsText('x', gme.fExH[d.indx]));
         else if (o.Errors && (funcs.x_handle.kind === kAxisNormal) && (d.exlow || d.exhigh))
            lines.push('error x = -' + funcs.axisAsText('x', d.exlow) + '/+' + funcs.axisAsText('x', d.exhigh));

         if (gme) {
            for (let ny = 0; ny < gme.fNYErrors; ++ny)
               lines.push(`error y${ny} = -${funcs.axisAsText('y', gme.fEyL[ny][d.indx])}/+${funcs.axisAsText('y', gme.fEyH[ny][d.indx])}`);
         } else if ((o.Errors || (o.EF > 0)) && (funcs.y_handle.kind === kAxisNormal) && (d.eylow || d.eyhigh))
            lines.push('error y = -' + funcs.axisAsText('y', d.eylow) + '/+' + funcs.axisAsText('y', d.eyhigh));
      }
      return lines;
   }

   /** @summary Provide frame painter for graph
     * @desc If not exists, emulate its behavior */
   get_fp() {
      let fp = this.getFramePainter();

      if (fp?.grx && fp?.gry)
         return fp;

      // FIXME: check if needed, can be removed easily
      const pp = this.getPadPainter(),
            rect = pp?.getPadRect() || { width: 800, height: 600 };

      fp = {
         pad_layer: true,
         pad: pp?.getRootPad(true) ?? create(clTPad),
         pw: rect.width,
         ph: rect.height,
         fX1NDC: 0.1, fX2NDC: 0.9, fY1NDC: 0.1, fY2NDC: 0.9,
         getFrameWidth() { return this.pw; },
         getFrameHeight() { return this.ph; },
         grx(value) {
            if (this.pad.fLogx)
               value = (value > 0) ? Math.log10(value) : this.pad.fUxmin;
            else
               value = (value - this.pad.fX1) / (this.pad.fX2 - this.pad.fX1);
            return value * this.pw;
         },
         gry(value) {
            if (this.pad.fLogv ?? this.pad.fLogy)
               value = (value > 0) ? Math.log10(value) : this.pad.fUymin;
            else
               value = (value - this.pad.fY1) / (this.pad.fY2 - this.pad.fY1);
            return (1 - value) * this.ph;
         },
         revertAxis(name, v) {
            if (name === 'x')
               return v / this.pw * (this.pad.fX2 - this.pad.fX1) + this.pad.fX1;
            if (name === 'y')
               return (1 - v / this.ph) * (this.pad.fY2 - this.pad.fY1) + this.pad.fY1;
            return v;
         },
         getGrFuncs() { return this; }
      };

      return fp;
   }

   /** @summary append exclusion area to created path */
   appendExclusion(is_curve, path, drawbins, excl_width) {
      const extrabins = [];
      for (let n = drawbins.length - 1; n >= 0; --n) {
         const bin = drawbins[n],
               dlen = Math.sqrt(bin.dgrx ** 2 + bin.dgry ** 2);
         if (dlen > 1e-10) {
            // shift point
            bin.grx += excl_width * bin.dgry / dlen;
            bin.gry -= excl_width * bin.dgrx / dlen;
         }
         extrabins.push(bin);
      }

      const path2 = buildSvgCurve(extrabins, { cmd: 'L', line: !is_curve });

      this.appendPath(path + path2 + 'Z')
          .call(this.fillatt.func)
          .style('opacity', 0.75);
   }

   /** @summary draw TGraph bins with specified options
     * @desc Can be called several times */
   drawBins(funcs, options, draw_g, w, h, lineatt, fillatt, main_block) {
      const graph = this.getGraph();
      if (!graph?.fNpoints)
         return;

      let excl_width = 0, drawbins = null;
      // if markers or errors drawn - no need handle events for line drawing
      // this improves interactivity like zooming around graph points
      const line_events_handling = !this.isBatchMode() && (options.Line || options.Errors) ? 'none' : null;

      if (main_block && lineatt.excl_side) {
         excl_width = lineatt.excl_width;
         if ((lineatt.width > 0) && !options.Line && !options.Curve)
            options.Line = 1;
      }

      if (options.EF) {
         drawbins = this.optimizeBins((options.EF > 1) ? 20000 : 0);

         // build lower part
         for (let n = 0; n < drawbins.length; ++n) {
            const bin = drawbins[n];
            bin.grx = funcs.grx(bin.x);
            bin.gry = funcs.gry(bin.y - bin.eylow);
         }

         const path1 = buildSvgCurve(drawbins, { line: options.EF < 2, qubic: true }),
               bins2 = [];

         for (let n = drawbins.length - 1; n >= 0; --n) {
            const bin = drawbins[n];
            bin.gry = funcs.gry(bin.y + bin.eyhigh);
            bins2.push(bin);
         }

         // build upper part (in reverse direction)
         const path2 = buildSvgCurve(bins2, { line: options.EF < 2, cmd: 'L', qubic: true }),
               area = draw_g.append('svg:path')
                            .attr('d', path1 + path2 + 'Z')
                            .call(fillatt.func);

         // Let behaves as ROOT - see JIRA ROOT-8131
         if (fillatt.empty() && fillatt.colorindx)
            area.style('stroke', this.getColor(fillatt.colorindx));
         if (main_block)
            this.#draw_kind = 'lines';
      }

      if (options.Line || options.Fill) {
         let close_symbol = '';
         if (this.#cutg) {
            close_symbol = 'Z';
            if (!options.original)
               options.Fill = 1;
         }

         if (options.Fill) {
            close_symbol = 'Z'; // always close area if we want to fill it
            excl_width = 0;
         }

         if (!drawbins)
            drawbins = this.optimizeBins(0);

         for (let n = 0; n < drawbins.length; ++n) {
            const bin = drawbins[n];
            bin.grx = funcs.grx(bin.x);
            bin.gry = funcs.gry(bin.y);
         }

         const path = buildSvgCurve(drawbins, { line: true, calc: excl_width });

         if (excl_width)
            this.appendExclusion(false, path, drawbins, excl_width);

         const elem = draw_g.append('svg:path')
                            .attr('d', path + close_symbol)
                            .style('pointer-events', line_events_handling);
         if (options.Line)
            elem.call(lineatt.func);

         if (options.Fill)
            elem.call(fillatt.func);
         else
            elem.style('fill', 'none');

         if (main_block)
            this.#draw_kind = 'lines';
      }

      if (options.Curve) {
         let curvebins = drawbins;
         if ((this.#draw_kind !== 'lines') || !curvebins || ((options.Curve === 1) && (curvebins.length > 20000))) {
            curvebins = this.optimizeBins((options.Curve === 1) ? 20000 : 0);
            for (let n = 0; n < curvebins.length; ++n) {
               const bin = curvebins[n];
               bin.grx = funcs.grx(bin.x);
               bin.gry = funcs.gry(bin.y);
            }
         }

         const path = buildSvgCurve(curvebins, { qubic: !excl_width });
         if (excl_width)
            this.appendExclusion(true, path, curvebins, excl_width);

         draw_g.append('svg:path')
               .attr('d', path)
               .call(lineatt.func)
               .style('fill', 'none')
               .style('pointer-events', line_events_handling);
         if (main_block)
            this.#draw_kind = 'lines'; // handled same way as lines
      }

      let nodes = null;

      if (options.Errors || options.Rect || options.Bar) {
         drawbins = this.optimizeBins(5000, (pnt, i) => {
            const grx = funcs.grx(pnt.x);

            // when drawing bars, take all points
            if (!options.Bar && ((grx < 0) || (grx > w)))
               return true;

            const gry = funcs.gry(pnt.y);

            if (!options.Bar && !options.OutRange && ((gry < 0) || (gry > h)))
               return true;

            pnt.grx1 = Math.round(grx);
            pnt.gry1 = Math.round(gry);

            if (this.#has_errors) {
               pnt.grx0 = Math.round(funcs.grx(pnt.x - options.ScaleErrX * pnt.exlow) - grx);
               pnt.grx2 = Math.round(funcs.grx(pnt.x + options.ScaleErrX * pnt.exhigh) - grx);
               pnt.gry0 = Math.round(funcs.gry(pnt.y - pnt.eylow) - gry);
               pnt.gry2 = Math.round(funcs.gry(pnt.y + pnt.eyhigh) - gry);

               if (this.#is_bent) {
                  pnt.grdx0 = Math.round(funcs.gry(pnt.y + graph.fEXlowd[i]) - gry);
                  pnt.grdx2 = Math.round(funcs.gry(pnt.y + graph.fEXhighd[i]) - gry);
                  pnt.grdy0 = Math.round(funcs.grx(pnt.x + graph.fEYlowd[i]) - grx);
                  pnt.grdy2 = Math.round(funcs.grx(pnt.x + graph.fEYhighd[i]) - grx);
               } else
                  pnt.grdx0 = pnt.grdx2 = pnt.grdy0 = pnt.grdy2 = 0;
            }

            return false;
         });

         if (main_block)
            this.#draw_kind = 'nodes';

         nodes = draw_g.selectAll('.grpoint')
                       .data(drawbins)
                       .enter()
                       .append('svg:g')
                       .attr('class', 'grpoint')
                       .attr('transform', d => makeTranslate(d.grx1, d.gry1));
      }

      if (options.Bar) {
         // calculate bar width

         let xmin = 0, xmax = 0;
         for (let i = 0; i < drawbins.length; ++i) {
            if (i === 0)
               xmin = xmax = drawbins[i].grx1;
            else {
               xmin = Math.min(xmin, drawbins[i].grx1);
               xmax = Math.max(xmax, drawbins[i].grx1);
            }
         }

         const sz0 = drawbins.length < 2 ? w / 4 : (xmax - xmin) / drawbins.length,
               bw = sz0 * gStyle.fBarWidth,
               boff = sz0 * gStyle.fBarOffset,
               yy0 = Math.round(funcs.gry(0));
         let usefill = fillatt;

         if (main_block) {
            const fp = this.getFramePainter(),
                  fpcol = !fp?.fillatt?.empty() ? fp.fillatt.getFillColor() : -1;

            if (fpcol === fillatt.getFillColor())
               usefill = this.createAttFill({ color: fpcol === 'white' ? kBlack : kWhite, pattern: 1001, std: false });
         }

         nodes.append('svg:path')
              .attr('d', d => {
                 d.bar = true; // element drawn as bar
                 const dx = bw > 1 ? Math.round(boff - bw / 2) : 0,
                       dw = bw > 1 ? Math.round(bw) : 1,
                       dy = (options.Bar !== 1) ? 0 : ((d.gry1 > yy0) ? yy0 - d.gry1 : 0),
                       dh = (options.Bar !== 1) ? (h > d.gry1 ? h - d.gry1 : 0) : Math.abs(yy0 - d.gry1);
                 return `M${dx},${dy}h${dw}v${dh}h${-dw}z`;
              })
            .call(usefill.func);

         this.#barwidth = bw;
         this.#baroffset = boff;
      }

      if (options.Rect) {
         nodes.filter(d => (d.exlow > 0) && (d.exhigh > 0) && (d.eylow > 0) && (d.eyhigh > 0))
           .append('svg:path')
           .attr('d', d => {
              d.rect = true;
              return `M${d.grx0},${d.gry0}H${d.grx2}V${d.gry2}H${d.grx0}Z`;
           })
           .call(fillatt.func)
           .call(options.Rect === 2 ? lineatt.func : () => {});
      }

      this.error_size = 0;

      if (options.Errors) {
         // to show end of error markers, use line width attribute
         let lw = lineatt.width + gStyle.fEndErrorSize;
         const vv = options.Ends ? `m0,${lw}v${-2 * lw}` : '',
               hh = options.Ends ? `m${lw},0h${-2 * lw}` : '';
         let vleft = vv, vright = vv, htop = hh, hbottom = hh, bb;

         const mainLine = (dx, dy) => {
            if (!options.MainError)
               return `M${dx},${dy}`;
            const res = 'M0,0';
            if (dx)
               return res + (dy ? `L${dx},${dy}` : `H${dx}`);
            return dy ? res + `V${dy}` : res;
         };

         switch (options.Ends) {
            case 2:  // option []
               bb = Math.max(lineatt.width + 1, Math.round(lw * 0.66));
               vleft = `m${bb},${lw}h${-bb}v${-2 * lw}h${bb}`;
               vright = `m${-bb},${lw}h${bb}v${-2 * lw}h${-bb}`;
               htop = `m${-lw},${bb}v${-bb}h${2 * lw}v${bb}`;
               hbottom = `m${-lw},${-bb}v${bb}h${2 * lw}v${-bb}`;
               break;
            case 3: // option |>
               lw = Math.max(lw, Math.round(graph.fMarkerSize * 8 * 0.66));
               bb = Math.max(lineatt.width + 1, Math.round(lw * 0.66));
               vleft = `l${bb},${lw}v${-2 * lw}l${-bb},${lw}`;
               vright = `l${-bb},${lw}v${-2 * lw}l${bb},${lw}`;
               htop = `l${-lw},${bb}h${2 * lw}l${-lw},${-bb}`;
               hbottom = `l${-lw},${-bb}h${2 * lw}l${-lw},${bb}`;
               break;
            case 4: // option >
               lw = Math.max(lw, Math.round(graph.fMarkerSize * 8 * 0.66));
               bb = Math.max(lineatt.width + 1, Math.round(lw * 0.66));
               vleft = `l${bb},${lw}m0,${-2 * lw}l${-bb},${lw}`;
               vright = `l${-bb},${lw}m0,${-2 * lw}l${bb},${lw}`;
               htop = `l${-lw},${bb}m${2 * lw},0l${-lw},${-bb}`;
               hbottom = `l${-lw},${-bb}m${2 * lw},0l${-lw},${bb}`;
               break;
         }

         this.error_size = lw;

         lw = Math.floor((lineatt.width - 1) / 2); // one should take into account half of end-cup line width

         let visible = nodes.filter(d => (d.exlow > 0) || (d.exhigh > 0) || (d.eylow > 0) || (d.eyhigh > 0));
         if (options.skip_errors_x0 || options.skip_errors_y0)
            visible = visible.filter(d => (d.x || !options.skip_errors_x0) && (d.y || !options.skip_errors_y0));

         if (!this.isBatchMode() && settings.Tooltip && main_block) {
            visible.append('svg:path')
                   .attr('d', d => `M${d.grx0},${d.gry0}h${d.grx2 - d.grx0}v${d.gry2 - d.gry0}h${d.grx0 - d.grx2}z`)
                   .style('fill', 'none')
                   .style('pointer-events', 'visibleFill');
         }

         visible.append('svg:path')
                .attr('d', d => {
                   d.error = true;
                   return ((d.exlow > 0) ? mainLine(d.grx0 + lw, d.grdx0) + vleft : '') +
                          ((d.exhigh > 0) ? mainLine(d.grx2 - lw, d.grdx2) + vright : '') +
                          ((d.eylow > 0) ? mainLine(d.grdy0, d.gry0 - lw) + hbottom : '') +
                          ((d.eyhigh > 0) ? mainLine(d.grdy2, d.gry2 + lw) + htop : '');
                })
                .style('fill', 'none')
                .call(lineatt.func);
      }

      if (options.Mark) {
         // for tooltips use markers only if nodes were not created
         this.createAttMarker({ attr: graph, style: options.Mark - 100 });

         this.#marker_size = this.markeratt.getFullSize();

         this.markeratt.resetPos();

         const want_tooltip = !this.isBatchMode() && settings.Tooltip && (!this.markeratt.fill || (this.#marker_size < 7)) && !nodes && main_block,
               hsz = Math.max(5, Math.round(this.#marker_size * 0.7)),
               maxnummarker = 1000000 / (this.markeratt.getMarkerLength() + 7); // let produce SVG at maximum 1MB

         let path = '', pnt, grx, gry,
             hints_marker = '', step = 1;

         if (!drawbins)
            drawbins = this.optimizeBins(maxnummarker);
         else if (this.canOptimize() && (drawbins.length > 1.5 * maxnummarker))
            step = Math.min(2, Math.round(drawbins.length / maxnummarker));

         for (let n = 0; n < drawbins.length; n += step) {
            pnt = drawbins[n];
            grx = funcs.grx(pnt.x);
            if ((grx > -this.#marker_size) && (grx < w + this.#marker_size)) {
               gry = funcs.gry(pnt.y);
               if ((gry > -this.#marker_size) && (gry < h + this.#marker_size)) {
                  path += this.markeratt.create(grx, gry);
                  if (want_tooltip)
                     hints_marker += `M${grx - hsz},${gry - hsz}h${2 * hsz}v${2 * hsz}h${-2 * hsz}z`;
               }
            }
         }

         if (path) {
            draw_g.append('svg:path')
                  .attr('d', path)
                  .call(this.markeratt.func);
            if ((nodes === null) && (this.#draw_kind === 'none') && main_block)
               this.#draw_kind = (options.Mark === 101) ? 'path' : 'mark';
         }
         if (want_tooltip && hints_marker) {
            draw_g.append('svg:path')
                  .attr('d', hints_marker)
                  .style('fill', 'none')
                  .style('pointer-events', 'visibleFill');
         }
      }
   }

   /** @summary append TGraphQQ part */
   appendQQ(funcs, graph) {
      const xqmin = Math.max(funcs.scale_xmin, graph.fXq1),
            xqmax = Math.min(funcs.scale_xmax, graph.fXq2),
            yqmin = Math.max(funcs.scale_ymin, graph.fYq1),
            yqmax = Math.min(funcs.scale_ymax, graph.fYq2),
            makeLine = (x1, y1, x2, y2) => `M${funcs.grx(x1)},${funcs.gry(y1)}L${funcs.grx(x2)},${funcs.gry(y2)}`,
            yxmin = (graph.fYq2 - graph.fYq1) * (funcs.scale_xmin - graph.fXq1) / (graph.fXq2 - graph.fXq1) + graph.fYq1,
            yxmax = (graph.fYq2 - graph.fYq1) * (funcs.scale_xmax - graph.fXq1) / (graph.fXq2 - graph.fXq1) + graph.fYq1;

      let path2;
      if (yxmin < funcs.scale_ymin) {
         const xymin = (graph.fXq2 - graph.fXq1) * (funcs.scale_ymin - graph.fYq1) / (graph.fYq2 - graph.fYq1) + graph.fXq1;
         path2 = makeLine(xymin, funcs.scale_ymin, xqmin, yqmin);
      } else
         path2 = makeLine(funcs.scale_xmin, yxmin, xqmin, yqmin);

      if (yxmax > funcs.scale_ymax) {
         const xymax = (graph.fXq2 - graph.fXq1) * (funcs.scale_ymax - graph.fYq1) / (graph.fYq2 - graph.fYq1) + graph.fXq1;
         path2 += makeLine(xqmax, yqmax, xymax, funcs.scale_ymax);
      } else
         path2 += makeLine(xqmax, yqmax, funcs.scale_xmax, yxmax);

      const latt1 = this.createAttLine({ style: 1, width: 1, color: kBlack, std: false }),
            latt2 = this.createAttLine({ style: 2, width: 1, color: kBlack, std: false });

      this.appendPath(makeLine(xqmin, yqmin, xqmax, yqmax))
          .call(latt1.func)
          .style('fill', 'none');

      this.appendPath(path2)
          .call(latt2.func)
          .style('fill', 'none');
   }

   drawBins3D(/* fp, graph */) {
      console.log('Load ./hist/TGraphPainter.mjs to draw graph in 3D');
   }

   /** @summary Create necessary histogram draw attributes */
   createGraphDrawAttributes(only_check_auto) {
      const graph = this.getGraph(), o = this.getOptions();
      if (o._pfc > 1 || o._plc > 1 || o._pmc > 1) {
         const pp = this.getPadPainter();
         if (isFunc(pp?.getAutoColor)) {
            const icolor = pp.getAutoColor(graph.$num_graphs);
            this.#auto_exec = ''; // can be reused when sending option back to server
            if (o._pfc > 1) {
               o._pfc = 1;
               graph.fFillColor = icolor;
               this.#auto_exec += `SetFillColor(${icolor});;`;
               this.deleteAttr('fill');
            }
            if (o._plc > 1) {
               o._plc = 1;
               graph.fLineColor = icolor;
               this.#auto_exec += `SetLineColor(${icolor});;`;
               this.deleteAttr('line');
            }
            if (o._pmc > 1) {
               o._pmc = 1;
               graph.fMarkerColor = icolor;
               this.#auto_exec += `SetMarkerColor(${icolor});;`;
               this.deleteAttr('marker');
            }
         }
      }

      if (only_check_auto)
         this.deleteAttr();
      else {
         this.createAttLine({ attr: graph, can_excl: true, color0: o.graphLineColor, width: o.graphLineWidth });
         this.createAttFill({ attr: graph, color: o.graphFillColor, pattern: o.graphFillPattern, });
      }
   }

   /** @summary draw TGraph */
   drawGraph() {
      const fp = this.get_fp(),
            graph = this.getGraph(),
            o = this.getOptions();
      if (!fp)
         return;

      // special mode for TMultiGraph 3d drawing
      if (o.pos3d)
         return this.drawBins3D(fp, graph);

      const is_gme = Boolean(this.get_gme()),
            funcs = fp.getGrFuncs(o.second_x, o.second_y),
            w = funcs.getFrameWidth(),
            h = funcs.getFrameHeight(),
            g = this.createG(fp.pad_layer ? false : this.#frame_layer);

      this.createGraphDrawAttributes();

      this.fillatt.used = false; // mark used only when really used

      this.#draw_kind = 'none'; // indicate if special svg:g were created for each bin
      this.#marker_size = 0; // indicate if markers are drawn
      const draw_g = is_gme ? g.append('svg:g') : g;

      this.drawBins(funcs, o, draw_g, w, h, this.lineatt, this.fillatt, true);

      if (graph._typename === 'TGraphQQ')
         this.appendQQ(funcs, graph);

      if (is_gme) {
         const extract_gme_errors = nblock => {
            this.#bins?.forEach(bin => {
               bin.eylow = graph.fEyL[nblock][bin.indx];
               bin.eyhigh = graph.fEyH[nblock][bin.indx];
            });
         };

         for (let k = 0; k < graph.fNYErrors; ++k) {
            const lineatt = !o.individual_styles ? this.lineatt : this.createAttLine({ attr: graph.fAttLine[k], std: false }),
                  fillatt = !o.individual_styles ? this.fillatt : this.createAttFill({ attr: graph.fAttFill[k], std: false }),
                  sub_g = g.append('svg:g'),
                  options = k < o.blocks.length ? o.blocks[k] : o;
            extract_gme_errors(k);
            this.drawBins(funcs, options, sub_g, w, h, lineatt, fillatt);
         }
         extract_gme_errors(0); // ensure that first block kept at the end
      }

      if (!this.isBatchMode()) {
         addMoveHandler(this, this.testEditable());
         assignContextMenu(this, kNoReorder);
      }
   }

   /** @summary Provide tooltip at specified point */
   extractTooltip(pnt) {
      if (!pnt)
         return null;

      if ((this.#draw_kind === 'lines') || (this.#draw_kind === 'path') || (this.#draw_kind === 'mark'))
         return this.extractTooltipForPath(pnt);

      if (this.#draw_kind !== 'nodes')
         return null;

      const fp = this.get_fp(),
            o = this.getOptions(),
            height = fp.getFrameHeight(),
            bw = this.#barwidth,
            boff = this.#baroffset,
            esz = this.error_size,
            isbar1 = (o.Bar === 1),
            funcs = isbar1 ? fp.getGrFuncs(o.second_x, o.second_y) : null,
            msize = this.#marker_size ? Math.round(this.#marker_size / 2 + 1.5) : 0;
      let findbin = null, best_dist2 = 1e10, best = null;

      this.getG().selectAll('.grpoint').each(function() {
         const d = d3_select(this).datum();
         if (d === undefined)
            return;
         let dist2 = (pnt.x - d.grx1) ** 2;
         if (pnt.nproc === 1)
            dist2 += (pnt.y - d.gry1) ** 2;
         if (dist2 >= best_dist2)
            return;

         let rect;

         if (d.error || d.rect || d.marker) {
            rect = {
               x1: Math.min(-esz, d.grx0, -msize),
               x2: Math.max(esz, d.grx2, msize),
               y1: Math.min(-esz, d.gry2, -msize),
               y2: Math.max(esz, d.gry0, msize)
            };
         } else if (d.bar) {
            rect = {
               x1: boff - bw / 2,
               x2: boff + bw / 2,
               y1: 0,
               y2: height - d.gry1
            };

            if (isbar1) {
               const yy0 = funcs.gry(0);
               rect.y1 = (d.gry1 > yy0) ? yy0 - d.gry1 : 0;
               rect.y2 = (d.gry1 > yy0) ? 0 : yy0 - d.gry1;
            }
         } else
            rect = { x1: -5, x2: 5, y1: -5, y2: 5 };

         const matchx = (pnt.x >= d.grx1 + rect.x1) && (pnt.x <= d.grx1 + rect.x2),
               matchy = (pnt.y >= d.gry1 + rect.y1) && (pnt.y <= d.gry1 + rect.y2);

         if (matchx && (matchy || (pnt.nproc > 1))) {
            best_dist2 = dist2;
            findbin = this;
            best = rect;
            best.exact = /* matchx && */ matchy;
         }
      });

      if (findbin === null)
         return null;

      const d = d3_select(findbin).datum(),
            gr = this.getGraph(),
            res = {
               name: gr.fName, title: gr.fTitle,
               x: d.grx1, y: d.gry1,
               color1: this.lineatt.color,
               lines: this.getTooltips(d),
               rect: best, d3bin: findbin
            };

      res.user_info = { obj: gr, name: gr.fName, bin: d.indx, cont: d.y, grx: d.grx1, gry: d.gry1 };

      if (this.fillatt?.used && !this.fillatt?.empty())
         res.color2 = this.fillatt.getFillColor();

      if (best.exact)
         res.exact = true;
      res.menu = res.exact; // activate menu only when exactly locate bin
      res.menu_dist = 3; // distance always fixed
      res.bin = d;
      res.binindx = d.indx;

      return res;
   }

   /** @summary Show tooltip */
   showTooltip(hint) {
      let ttrect = this.getG()?.selectChild('.tooltip_bin');

      if (!hint || !this.getG()) {
         ttrect?.remove();
         return;
      }

      if (hint.usepath)
         return this.showTooltipForPath(hint);

      const d = d3_select(hint.d3bin).datum();

      if (ttrect.empty()) {
         ttrect = this.getG().append('svg:rect')
                             .attr('class', 'tooltip_bin')
                             .style('pointer-events', 'none')
                             .call(addHighlightStyle);
      }

      hint.changed = ttrect.property('current_bin') !== hint.d3bin;

      if (hint.changed) {
         ttrect.attr('x', d.grx1 + hint.rect.x1)
               .attr('width', hint.rect.x2 - hint.rect.x1)
               .attr('y', d.gry1 + hint.rect.y1)
               .attr('height', hint.rect.y2 - hint.rect.y1)
               .style('opacity', '0.3')
               .property('current_bin', hint.d3bin);
      }
   }

   /** @summary Process tooltip event */
   processTooltipEvent(pnt) {
      const hint = this.extractTooltip(pnt);
      if (!pnt || !pnt.disabled)
         this.showTooltip(hint);
      return hint;
   }

   /** @summary Find best bin index for specified point */
   findBestBin(pnt) {
      if (!this.#bins)
         return null;

      const islines = (this.#draw_kind === 'lines'),
            o = this.getOptions(),
            funcs = this.get_fp().getGrFuncs(o.second_x, o.second_y);
      let bestindx = -1,
          bestbin = null,
          bestdist = 1e10,
          dist, grx, gry, n, bin;

      for (n = 0; n < this.#bins.length; ++n) {
         bin = this.#bins[n];

         grx = funcs.grx(bin.x);
         gry = funcs.gry(bin.y);

         dist = (pnt.x - grx) ** 2 + (pnt.y - gry) ** 2;

         if (dist < bestdist) {
            bestdist = dist;
            bestbin = bin;
            bestindx = n;
         }
      }

      // check last point
      if ((bestdist > 100) && islines)
         bestbin = null;

      const radius = Math.max(this.lineatt.width + 3, 4, this.#marker_size);

      if (bestbin)
         bestdist = Math.sqrt((pnt.x - funcs.grx(bestbin.x)) ** 2 + (pnt.y - funcs.gry(bestbin.y)) ** 2);

      if (!islines && (bestdist > radius))
         bestbin = null;

      if (!bestbin)
         bestindx = -1;

      const res = { bin: bestbin, indx: bestindx, dist: bestdist, radius: Math.round(radius) };

      if (!bestbin && islines) {
         bestdist = 1e10;

         const is_inside = (x, x1, x2) => ((x1 >= x) && (x >= x2)) || ((x1 <= x) && (x <= x2));

         let bin0 = this.#bins[0], grx0 = funcs.grx(bin0.x), gry0, posy;
         for (n = 1; n < this.#bins.length; ++n) {
            bin = this.#bins[n];
            grx = funcs.grx(bin.x);

            if (is_inside(pnt.x, grx0, grx)) {
               // if inside interval, check Y distance
               gry0 = funcs.gry(bin0.y);
               gry = funcs.gry(bin.y);

               if (Math.abs(grx - grx0) < 1) {
                  // very close x - check only y
                  posy = pnt.y;
                  dist = is_inside(pnt.y, gry0, gry) ? 0 : Math.min(Math.abs(pnt.y - gry0), Math.abs(pnt.y - gry));
               } else {
                  posy = gry0 + (pnt.x - grx0) / (grx - grx0) * (gry - gry0);
                  dist = Math.abs(posy - pnt.y);
               }

               if (dist < bestdist) {
                  bestdist = dist;
                  res.linex = pnt.x;
                  res.liney = posy;
               }
            }

            bin0 = bin;
            grx0 = grx;
         }

         if (bestdist < radius * 0.5) {
            res.linedist = bestdist;
            res.closeline = true;
         }
      }

      return res;
   }

   /** @summary Check editable flag for TGraph
     * @desc if arg specified changes or toggles editable flag */
   testEditable(arg) {
      const obj = this.getGraph();
      if (!isFunc(obj?.TestBit))
         return false;
      if ((arg === 'toggle') || (arg !== undefined))
         obj.SetBit(kNotEditable, !arg);
      return !obj.TestBit(kNotEditable);
   }

   /** @summary Provide tooltip at specified point for path-based drawing */
   extractTooltipForPath(pnt) {
      if (!this.#bins)
         return null;

      const best = this.findBestBin(pnt);

      if (!best || (!best.bin && !best.closeline))
         return null;

      const islines = (this.#draw_kind === 'lines'),
            ismark = (this.#draw_kind === 'mark'),
            fp = this.get_fp(),
            o = this.getOptions(),
            funcs = fp.getGrFuncs(o.second_x, o.second_y),
            gr = this.getGraph(),
            res = {
               name: gr.fName, title: gr.fTitle,
               x: best.bin ? funcs.grx(best.bin.x) : best.linex,
               y: best.bin ? funcs.gry(best.bin.y) : best.liney,
               color1: this.lineatt.color,
               lines: this.getTooltips(best.bin),
               usepath: true, ismark, islines
            };

      res.user_info = { obj: gr, name: gr.fName, bin: 0, cont: 0, grx: res.x, gry: res.y };

      if (best.closeline) {
         res.menu = res.exact = true;
         res.menu_dist = best.linedist;
      } else if (best.bin) {
         if (o.EF && islines) {
            res.gry1 = funcs.gry(best.bin.y - best.bin.eylow);
            res.gry2 = funcs.gry(best.bin.y + best.bin.eyhigh);
         } else
            res.gry1 = res.gry2 = funcs.gry(best.bin.y);


         res.binindx = best.indx;
         res.bin = best.bin;
         res.radius = best.radius;
         res.user_info.bin = best.indx;
         res.user_info.cont = best.bin.y;

         res.exact = (Math.abs(pnt.x - res.x) <= best.radius) &&
            ((Math.abs(pnt.y - res.gry1) <= best.radius) || (Math.abs(pnt.y - res.gry2) <= best.radius));

         res.menu = res.exact;
         res.menu_dist = Math.sqrt((pnt.x - res.x) ** 2 + Math.min(Math.abs(pnt.y - res.gry1), Math.abs(pnt.y - res.gry2)) ** 2);
      }

      if (this.fillatt?.used && !this.fillatt?.empty())
         res.color2 = this.fillatt.getFillColor();

      if (!islines) {
         res.color1 = this.getColor(gr.fMarkerColor);
         if (!res.color2)
            res.color2 = res.color1;
      }

      return res;
   }

   /** @summary Show tooltip for path drawing */
   showTooltipForPath(hint) {
      let ttbin = this.getG()?.selectChild('.tooltip_bin');

      if (!hint?.bin || !this.getG()) {
         ttbin?.remove();
         return;
      }

      if (ttbin.empty())
         ttbin = this.getG().append('svg:g').attr('class', 'tooltip_bin');

      hint.changed = ttbin.property('current_bin') !== hint.bin;

      if (hint.changed) {
         ttbin.selectAll('*').remove(); // first delete all children
         ttbin.property('current_bin', hint.bin);

         if (hint.ismark) {
            ttbin.append('svg:rect')
                 .style('pointer-events', 'none')
                 .call(addHighlightStyle)
                 .style('opacity', '0.3')
                 .attr('x', Math.round(hint.x - hint.radius))
                 .attr('y', Math.round(hint.y - hint.radius))
                 .attr('width', 2 * hint.radius)
                 .attr('height', 2 * hint.radius);
         } else {
            ttbin.append('svg:circle').attr('cy', Math.round(hint.gry1));
            if (Math.abs(hint.gry1 - hint.gry2) > 1)
               ttbin.append('svg:circle').attr('cy', Math.round(hint.gry2));

            const o = this.getOptions(),
                  elem = ttbin.selectAll('circle')
                            .attr('r', hint.radius)
                            .attr('cx', Math.round(hint.x));

            if (!hint.islines)
               elem.style('stroke', hint.color1 === 'black' ? 'green' : 'black').style('fill', 'none');
            else {
               if (o.Line || o.Curve)
                  elem.call(this.lineatt.func);
               else
                  elem.style('stroke', 'black');
               if (o.Fill)
                  elem.call(this.fillatt.func);
               else
                  elem.style('fill', 'none');
            }
         }
      }
   }

   /** @summary Check if graph moving is enabled */
   moveEnabled() {
      return this.testEditable();
   }

   /** @summary Start moving of TGraph */
   moveStart(x, y, evnt) {
      const o = this.getOptions();
      this.#pos_dx = this.#pos_dy = 0;
      this.#move_funcs = this.get_fp().getGrFuncs(o.second_x, o.second_y);
      const hint = evnt?.shiftKey ? null : this.extractTooltip({ x, y });
      if (hint?.exact && (hint.binindx !== undefined)) {
         this.#move_binindx = hint.binindx;
         this.#move_bin = hint.bin;
         this.#move_x0 = this.#move_funcs.grx(this.#move_bin.x);
         this.#move_y0 = this.#move_funcs.gry(this.#move_bin.y);
      } else
         this.#move_binindx = undefined;
   }

   /** @summary Perform moving */
   moveDrag(dx, dy) {
      this.#pos_dx += dx;
      this.#pos_dy += dy;

      if (this.#move_binindx === undefined)
         makeTranslate(this.getG(), this.#pos_dx, this.#pos_dy);
      else if (this.#move_funcs && this.#move_bin) {
         this.#move_bin.x = this.#move_funcs.revertAxis('x', this.#move_x0 + this.#pos_dx);
         this.#move_bin.y = this.#move_funcs.revertAxis('y', this.#move_y0 + this.#pos_dy);
         this.drawGraph();
      }
   }

   /** @summary Complete moving */
   moveEnd(not_changed) {
      const graph = this.getGraph(), last = graph?.fNpoints - 1;
      let exec = '';

      const changeBin = bin => {
         exec += `SetPoint(${bin.indx},${bin.x},${bin.y});;`;
         graph.fX[bin.indx] = bin.x;
         graph.fY[bin.indx] = bin.y;
         if ((bin.indx === 0) && this.#cutg_lastsame) {
            exec += `SetPoint(${last},${bin.x},${bin.y});;`;
            graph.fX[last] = bin.x;
            graph.fY[last] = bin.y;
         }
      };

      if (this.#move_binindx === undefined) {
         this.getG().attr('transform', null);

         if (this.#move_funcs && this.#bins && !not_changed) {
            for (let k = 0; k < this.#bins.length; ++k) {
               const bin = this.#bins[k];
               bin.x = this.#move_funcs.revertAxis('x', this.#move_funcs.grx(bin.x) + this.#pos_dx);
               bin.y = this.#move_funcs.revertAxis('y', this.#move_funcs.gry(bin.y) + this.#pos_dy);
               changeBin(bin);
            }
            if (graph.$redraw_pad)
               this.redrawPad();
            else
               this.drawGraph();
         }
      } else {
         changeBin(this.#move_bin);
         this.#move_binindx = undefined;
         if (graph.$redraw_pad)
            this.redrawPad();
      }

      this.#move_funcs = undefined;

      if (exec && !not_changed)
         this.submitCanvExec(exec);
   }

   /** @summary Fill option object used in TWebCanvas */
   fillWebObjectOptions(res) {
      if (this.#auto_exec && res) {
         res.fcust = 'auto_exec:' + this.#auto_exec;
         this.#auto_exec = undefined;
      }
   }

   /** @summary Fill context menu */
   fillContextMenuItems(menu) {
      if (!this.hasSnapId()) {
         menu.addchk(this.testEditable(), 'Editable', () => {
            this.testEditable('toggle');
            this.drawGraph();
         });
         if (this.axes_draw) {
            menu.add('Title', () => menu.input('Enter graph title', this.getObject().fTitle).then(res => {
               this.getObject().fTitle = res;
               const hist_painter = this.getMainPainter();
               if (hist_painter?.isSecondary(this)) {
                  setHistogramTitle(hist_painter.getHisto(), res);
                  this.interactiveRedraw('pad');
               }
            }));
         }
         menu.addRedrawMenu(this.getPrimary());
      }
   }

   /** @summary Execute menu command
     * @private */
   executeMenuCommand(method, args) {
      if (super.executeMenuCommand(method, args))
         return true;

      const canp = this.getCanvPainter(), fp = this.get_fp();

      if ((method.fName === 'RemovePoint') || (method.fName === 'InsertPoint')) {
         if (!canp || canp.isReadonly())
            return true; // ignore function

         const pnt = isFunc(fp?.getLastEventPos) ? fp.getLastEventPos() : null,
               hint = this.extractTooltip(pnt);

         if (method.fName === 'InsertPoint') {
            if (pnt) {
               const o = this.getOptions(),
                     funcs = fp.getGrFuncs(o.second_x, o.second_y),
                     userx = funcs.revertAxis('x', pnt.x) ?? 0,
                     usery = funcs.revertAxis('y', pnt.y) ?? 0;
               this.submitCanvExec(`AddPoint(${userx.toFixed(3)}, ${usery.toFixed(3)})`, method.$execid);
            }
         } else if (method.$execid && (hint?.binindx !== undefined))
            this.submitCanvExec(`RemovePoint(${hint.binindx})`, method.$execid);

         return true; // call is processed
      }

      return false;
   }

   /** @summary Update TGraph object members
     * @private */
   _updateMembers(graph, obj) {
      graph.fBits = obj.fBits;
      graph.fTitle = obj.fTitle;
      graph.fX = obj.fX;
      graph.fY = obj.fY;
      ['fEX', 'fEY', 'fExL', 'fExH', 'fEXlow', 'fEXhigh', 'fEYlow', 'fEYhigh',
       'fEXlowd', 'fEXhighd', 'fEYlowd', 'fEYhighd'].forEach(member => {
         if (obj[member] !== undefined)
            graph[member] = obj[member];
      });
      graph.fNpoints = obj.fNpoints;
      graph.fMinimum = obj.fMinimum;
      graph.fMaximum = obj.fMaximum;

      const o = this.getOptions();

      if (this.hasSnapId())
         o._pfc = o._plc = o._pmc = 0; // auto colors should be processed in web canvas

      if (!o._pfc)
         graph.fFillColor = obj.fFillColor;
      graph.fFillStyle = obj.fFillStyle;
      if (!o._plc)
         graph.fLineColor = obj.fLineColor;
      graph.fLineStyle = obj.fLineStyle;
      graph.fLineWidth = obj.fLineWidth;
      if (!o._pmc)
         graph.fMarkerColor = obj.fMarkerColor;
      graph.fMarkerSize = obj.fMarkerSize;
      graph.fMarkerStyle = obj.fMarkerStyle;

      return obj.fFunctions;
   }

   /** @summary Update TGraph object */
   updateObject(obj, opt) {
      if (!this.matchObjectType(obj))
         return false;

      const o = this.getOptions();

      if (opt && (opt !== o.original))
         this.decodeOptions(opt);

      const new_funcs = this._updateMembers(this.getObject(), obj);

      this.createBins();

      this.#redraw_hist = undefined;

      // if our own histogram was used as axis drawing, we need update histogram as well
      if (this.axes_draw) {
         const histo = this.createHistogram(),
               hist_painter = this.getMainPainter();
         if (hist_painter?.isSecondary(this)) {
            hist_painter.updateObject(histo, o.Axis);
            this.#redraw_hist = true;
         }
      }

      this.#funcs_handler = new FunctionsHandler(this, this.getPadPainter(), new_funcs);

      return true;
   }

   /** @summary Checks if it makes sense to zoom inside specified axis range
     * @desc allow to zoom TGraph only when at least one point in the range */
   canZoomInside(axis, min, max) {
      const gr = this.getGraph();
      if (!gr || ((axis !== 'x') && (axis !== 'y')))
         return false;

      let arr = gr.fX;
      if (this.isScatter())
         arr = (axis === 'x') ? gr.fX : gr.fY;
      else if (axis !== (this.getOptions().pos3d ? 'y' : 'x'))
         return false;

      for (let n = 0; n < gr.fNpoints; ++n) {
         if ((min < arr[n]) && (arr[n] < max))
            return true;
      }

      return false;
   }

   /** @summary Process click on graph-defined buttons */
   clickButton(funcname) {
      if (funcname !== 'ToggleZoom')
         return false;

      if ((this.xmin === this.xmax) && (this.ymin === this.ymax))
         return false;

      return this.getFramePainter()?.zoom(this.xmin, this.xmax, this.ymin, this.ymax);
   }

   /** @summary Find TF1/TF2 in TGraph list of functions */
   findFunc() {
      return this.getGraph()?.fFunctions?.arr?.find(func => (func._typename === clTF1) || (func._typename === clTF2));
   }

   /** @summary Find stat box in TGraph list of functions */
   findStat() {
      return this.getGraph()?.fFunctions?.arr?.find(func => (func._typename === clTPaveStats) && (func.fName === 'stats'));
   }

   /** @summary Create stat box */
   createStat() {
      const func = this.findFunc();
      if (!func)
         return null;

      let stats = this.findStat();
      if (stats)
         return stats;

      const st = gStyle;

      // do not create stats box when drawing canvas
      if (!st.fOptFit || this.getCanvPainter()?.getRootPad(true)?.fPrimitives?.arr.length)
         return null;

      this.create_stats = true;

      stats = create(clTPaveStats);
      Object.assign(stats, {
         fName: 'stats', fOptStat: 0, fOptFit: st.fOptFit, fBorderSize: 1,
         fX1NDC: st.fStatX - st.fStatW, fY1NDC: st.fStatY - st.fStatH, fX2NDC: st.fStatX, fY2NDC: st.fStatY,
         fFillColor: st.fStatColor, fFillStyle: st.fStatStyle
      });

      stats.fTextAngle = 0;
      stats.fTextSize = st.fStatFontSize; // 9 ??
      stats.fTextAlign = 12;
      stats.fTextColor = st.fStatTextColor;
      stats.fTextFont = st.fStatFont;

      stats.AddText(func.fName);

      // while TF1 was found, one can be sure that stats is existing
      this.getGraph().fFunctions.Add(stats);

      return stats;
   }

   /** @summary Fill statistic */
   fillStatistic(stat, _dostat, dofit) {
      const func = this.findFunc();
      if (!func || !dofit)
         return false;

      stat.clearPave();
      stat.fillFunctionStat(func, (dofit === 1) ? 111 : dofit, 1);
      return true;
   }

   /** @summary Draw axis histogram
     * @private */
   async drawAxisHisto() {
      const set_x = this.isDummyHistogram('x'),
            set_y = this.isDummyHistogram('y'),
            histo = this.createHistogram(set_x, set_y);
      return TH1Painter.draw(this.getDrawDom(), histo, this.getOptions().Axis);
   }

   /** @summary Draw TGraph
     * @private */
   static async _drawGraph(painter, opt) {
      painter.decodeOptions(opt, true);
      painter.createBins();
      painter.createStat();
      const graph = painter.getGraph();
      if (!settings.DragGraphs)
         graph?.SetBit(kNotEditable, true);

      let promise = Promise.resolve();

      if ((!painter.getMainPainter() || painter.options.second_x || painter.options.second_y) && painter.options.Axis) {
         promise = painter.drawAxisHisto().then(hist_painter => {
            hist_painter?.setSecondaryId(painter, 'hist');
            painter.axes_draw = Boolean(hist_painter);
         });
      }

      return promise.then(() => {
         painter.addToPadPrimitives();
         return painter.drawGraph();
      }).then(() => {
         const handler = new FunctionsHandler(painter, painter.getPadPainter(), graph.fFunctions, true);
         return handler.drawNext(0); // returns painter
      });
   }

   /** @summary Draw TGraph in 2D only */
   static async draw(dom, graph, opt) {
      return TGraphPainter._drawGraph(new TGraphPainter(dom, graph), opt);
   }

} // class TGraphPainter

export { clTGraphAsymmErrors, TGraphPainter };
