import { settings, create, parse, toJSON, loadScript, registerMethods, isBatchMode, isFunc, isStr, nsREX } from '../core.mjs';
import { select as d3_select, rgb as d3_rgb } from '../d3.mjs';
import { closeCurrentWindow, showProgress, loadOpenui5, ToolbarIcons, getColorExec } from '../gui/utils.mjs';
import { GridDisplay, getHPainter } from '../gui/display.mjs';
import { makeTranslate } from '../base/BasePainter.mjs';
import { convertColor } from '../base/colors.mjs';
import { selectActivePad, cleanup, resize, EAxisBits } from '../base/ObjectPainter.mjs';
import { RObjectPainter } from '../base/RObjectPainter.mjs';
import { RAxisPainter } from './RAxisPainter.mjs';
import { RFramePainter } from './RFramePainter.mjs';
import { RPadPainter } from './RPadPainter.mjs';
import { addDragHandler } from './TFramePainter.mjs';


/**
 * @summary Painter class for RCanvas
 *
 * @private
 */

class RCanvasPainter extends RPadPainter {

   #websocket; // WebWindow handle used for communication with server
   #changed_layout; // modified layout
   #submreq;  // submitted requests
   #nextreqid; // id of next request

   /** @summary constructor */
   constructor(dom, canvas, opt) {
      super(dom, canvas, opt, true);
      this.#websocket = null;
      this.#submreq = {};
      this.tooltip_allowed = settings.Tooltip;
      this.v7canvas = true;
   }

   /** @summary Cleanup canvas painter */
   cleanup() {
      this.#websocket = undefined;
      this.#submreq = {};

      if (this.#changed_layout)
         this.setLayoutKind('simple');
      this.#changed_layout = undefined;

      super.cleanup();
   }

   /** @summary Returns readonly flag */
   isReadonly() { return false; }

   /** @summary Returns canvas name */
   getCanvasName() {
      const title = this.getRootPad()?.fTitle;
      return (!title || !isStr(title)) ? 'rcanvas' : title.replace(/ /g, '_');
   }

   /** @summary Returns layout kind */
   getLayoutKind() {
      const origin = this.selectDom('origin'),
            layout = origin.empty() ? '' : origin.property('layout');
      return layout || 'simple';
   }

   /** @summary Set canvas layout kind */
   setLayoutKind(kind, main_selector) {
      const origin = this.selectDom('origin');
      if (!origin.empty()) {
         if (!kind)
            kind = 'simple';
         origin.property('layout', kind);
         origin.property('layout_selector', (kind !== 'simple') && main_selector ? main_selector : null);
         this.#changed_layout = (kind !== 'simple'); // use in cleanup
      }
   }

   /** @summary Changes layout
     * @return {Promise} indicating when finished */
   async changeLayout(layout_kind, mainid) {
      const current = this.getLayoutKind();
      if (current === layout_kind)
         return true;

      const origin = this.selectDom('origin'),
            sidebar2 = origin.select('.side_panel2'),
            lst = [];
      let sidebar = origin.select('.side_panel'),
          main = this.selectDom(), force;

      while (main.node().firstChild)
         lst.push(main.node().removeChild(main.node().firstChild));

      if (!sidebar.empty())
         cleanup(sidebar.node());
      if (!sidebar2.empty())
         cleanup(sidebar2.node());

      this.setLayoutKind('simple'); // restore defaults
      origin.html(''); // cleanup origin

      if (layout_kind === 'simple') {
         main = origin;
         for (let k = 0; k < lst.length; ++k)
            main.node().appendChild(lst[k]);
         this.setLayoutKind(layout_kind);
         force = true;
      } else {
         const grid = new GridDisplay(origin.node(), layout_kind);

         if (mainid === undefined)
            mainid = (layout_kind.indexOf('vert') === 0) ? 0 : 1;

         main = d3_select(grid.getGridFrame(mainid));
         main.classed('central_panel', true).style('position', 'relative');

         if (mainid === 2) {
            // left panel for Y
            sidebar = d3_select(grid.getGridFrame(0));
            sidebar.classed('side_panel2', true).style('position', 'relative');
            // bottom panel for X
            sidebar = d3_select(grid.getGridFrame(3));
            sidebar.classed('side_panel', true).style('position', 'relative');
         } else {
            sidebar = d3_select(grid.getGridFrame(1 - mainid));
            sidebar.classed('side_panel', true).style('position', 'relative');
         }

         // now append all childs to the new main
         for (let k = 0; k < lst.length; ++k)
            main.node().appendChild(lst[k]);

         this.setLayoutKind(layout_kind, '.central_panel');

         // remove reference to MDIDisplay, solves resize problem
         origin.property('mdi', null);
      }

      // resize main drawing and let draw extras
      resize(main.node(), force);
      return true;
   }

   /** @summary Toggle projection
     * @return {Promise} indicating when ready
     * @private */
   async toggleProjection(kind) {
      delete this.proj_painter;

      if (kind)
         this.proj_painter = { X: false, Y: false }; // just indicator that drawing can be preformed

      if (isFunc(this.showUI5ProjectionArea))
         return this.showUI5ProjectionArea(kind);

      let layout = 'simple', mainid;

      switch (kind) {
         case 'XY':
            layout = 'projxy';
            mainid = 2;
            break;
         case 'X':
         case 'bottom':
            layout = 'vert2_31';
            mainid = 0;
            break;
         case 'Y':
         case 'left':
            layout = 'horiz2_13';
            mainid = 1;
            break;
         case 'top':
            layout = 'vert2_13';
            mainid = 1;
            break;
         case 'right':
            layout = 'horiz2_31';
            mainid = 0;
            break;
      }

      return this.changeLayout(layout, mainid);
   }

   /** @summary Draw projection for specified histogram
     * @private */
   async drawProjection(/* kind, hist, hopt */) {
      // dummy for the moment
      return false;
   }

   /** @summary Draw in side panel
     * @private */
   async drawInSidePanel(canv, opt, kind) {
      const sel = ((this.getLayoutKind() === 'projxy') && (kind === 'Y')) ? '.side_panel2' : '.side_panel',
            side = this.selectDom('origin').select(sel);
      return side.empty() ? null : this.drawObject(side.node(), canv, opt);
   }

   /** @summary Checks if canvas shown inside ui5 widget
     * @desc Function should be used only from the func which supposed to be replaced by ui5
     * @private */
   testUI5() {
      return this.use_openui ?? false;
   }

   /** @summary Show message
     * @desc Used normally with web-based canvas and handled in ui5
     * @private */
   showMessage(msg) {
      if (!this.testUI5())
         showProgress(msg, 7000);
   }

   /** @summary Function called when canvas menu item Save is called */
   saveCanvasAsFile(fname) {
      const pnt = fname.indexOf('.');
      this.createImage(fname.slice(pnt + 1))
          .then(res => this.sendWebsocket(`SAVE:${fname}:${res}`));
   }

   /** @summary Send command to server to save canvas with specified name
     * @desc Should be only used in web-based canvas
     * @private */
   sendSaveCommand(fname) { this.sendWebsocket('PRODUCE:' + fname); }

   /** @summary Return assigned web socket
    * @private */
   getWebsocket() { return this.#websocket; }

   /** @summary Return true if message can be send via web socket
    * @private */
   canSendWebsocket(noper = 1) { return this.#websocket?.canSend(noper); }

   /** @summary Send message via web socket
     * @private */
   sendWebsocket(msg) {
      if (this.#websocket?.canSend()) {
         this.#websocket.send(msg);
         return true;
      }

      return false;
   }

   /** @summary Close websocket connection to canvas
     * @private */
   closeWebsocket(force) {
      if (this.#websocket) {
         this.#websocket.close(force);
         this.#websocket.cleanup();
         this.#websocket = undefined;
      }
   }

   /** @summary Use provided connection for the web canvas
     * @private */
   useWebsocket(handle) {
      this.closeWebsocket();

      this.#websocket = handle;
      this.#websocket.setReceiver(this);
      this.#websocket.connect();
   }

   /** @summary set, test or reset timeout of specified name
     * @desc Used to prevent overloading of websocket for specific function */
   websocketTimeout(name, tm) {
      if (!this.#websocket)
         return;
      if (!this.#websocket._tmouts)
         this.#websocket._tmouts = {};

      const handle = this.#websocket._tmouts[name];
      if (tm === undefined)
         return handle !== undefined;

      if (tm === 'reset') {
         if (handle) {
            clearTimeout(handle);
            delete this.#websocket._tmouts[name];
         }
      } else if (!handle && Number.isInteger(tm))
         this.#websocket._tmouts[name] = setTimeout(() => { delete this.#websocket._tmouts[name]; }, tm);
   }

   /** @summary Handler for websocket open event
     * @private */
   onWebsocketOpened(/* handle */) {
   }

   /** @summary Handler for websocket close event
     * @private */
   onWebsocketClosed(/* handle */) {
      if (!this.embed_canvas)
         closeCurrentWindow();
   }

   /** @summary Handler for websocket message
     * @private */
   onWebsocketMsg(handle, msg) {
      // console.log('GET_MSG ' + msg.slice(0,30));

      if (msg === 'CLOSE') {
         this.onWebsocketClosed();
         this.closeWebsocket(true);
      } else if (msg.slice(0, 5) === 'SNAP:') {
         msg = msg.slice(5);
         const p1 = msg.indexOf(':'),
               snapid = msg.slice(0, p1),
               snap = parse(msg.slice(p1 + 1));
         this.syncDraw(true)
             .then(() => {
                if (!this.getSnapId() && snap?.fWinSize)
                   this.resizeBrowser(snap.fWinSize[0], snap.fWinSize[1]);
             }).then(() => this.redrawPadSnap(snap))
             .then(() => {
                this.addPadInteractive();
                handle.send(`SNAPDONE:${snapid}`); // send ready message back when drawing completed
                this.confirmDraw();
             }).catch(err => {
                if (isFunc(this.showConsoleError))
                   this.showConsoleError(err);
                else
                   console.log(err);
             });
      } else if (msg.slice(0, 4) === 'JSON') {
         const obj = parse(msg.slice(4));
         this.redrawObject(obj);
      } else if (msg.slice(0, 9) === 'REPL_REQ:')
         this.processDrawableReply(msg.slice(9));
      else if (msg.slice(0, 4) === 'CMD:') {
         msg = msg.slice(4);
         const p1 = msg.indexOf(':'),
               cmdid = msg.slice(0, p1),
               cmd = msg.slice(p1 + 1),
               reply = `REPLY:${cmdid}:`;
         if ((cmd === 'SVG') || (cmd === 'PNG') || (cmd === 'JPEG') || (cmd === 'WEBP') || (cmd === 'PDF')) {
            this.createImage(cmd.toLowerCase())
                .then(res => handle.send(reply + res));
         } else if (cmd.indexOf('ADDPANEL:') === 0) {
            if (!isFunc(this.showUI5Panel))
               handle.send(reply + 'false');
            else {
               const window_path = cmd.slice(9),
                     conn = handle.createNewInstance(window_path);

               // set interim receiver until first message arrives
               conn.setReceiver({
                  cpainter: this,

                  onWebsocketOpened() {
                  },

                  onWebsocketMsg(panel_handle, msg2) {
                     const panel_name = (msg2.indexOf('SHOWPANEL:') === 0) ? msg2.slice(10) : '';
                     this.cpainter.showUI5Panel(panel_name, panel_handle)
                                  .then(res => handle.send(reply + (res ? 'true' : 'false')));
                  },

                  onWebsocketClosed() {
                     // if connection failed,
                     handle.send(reply + 'false');
                  },

                  onWebsocketError() {
                     // if connection failed,
                     handle.send(reply + 'false');
                  }

               });

               // only when connection established, panel will be activated
               conn.connect();
            }
         } else {
            console.log('Unrecognized command ' + cmd);
            handle.send(reply);
         }
      } else if ((msg.slice(0, 7) === 'DXPROJ:') || (msg.slice(0, 7) === 'DYPROJ:')) {
         const kind = msg[1],
               hist = parse(msg.slice(7));
         this.drawProjection(kind, hist);
      } else if (msg.slice(0, 5) === 'SHOW:') {
         const that = msg.slice(5),
               on = that.at(-1) === '1';
         this.showSection(that.slice(0, that.length - 2), on);
      } else
         console.log(`unrecognized msg len: ${msg.length} msg: ${msg.slice(0, 30)}`);
   }

   /** @summary Submit request to RDrawable object on server side */
   submitDrawableRequest(kind, req, painter, method) {
      if (!this.getWebsocket() || !req?._typename || !painter.getSnapId())
         return null;

      if (kind && method) {
         // if kind specified - check if such request already was submitted
         if (!painter._requests)
            painter._requests = {};

         const prevreq = painter._requests[kind];

         if (prevreq) {
            const tm = new Date().getTime();
            if (!prevreq._tm || (tm - prevreq._tm < 5000)) {
               prevreq._nextreq = req; // submit when got reply
               return false;
            }
            delete painter._requests[kind]; // let submit new request after timeout
         }

         painter._requests[kind] = req; // keep reference on the request
      }

      req.id = painter.getSnapId();

      if (method) {
         if (!this.#nextreqid)
            this.#nextreqid = 1;
         req.reqid = this.#nextreqid++;
      } else
         req.reqid = 0; // request will not be replied


      const msg = JSON.stringify(req);

      if (req.reqid) {
         req._kind = kind;
         req._painter = painter;
         req._method = method;
         req._tm = new Date().getTime();

         this.#submreq[req.reqid] = req; // fast access to submitted requests
      }

      this.sendWebsocket('REQ:' + msg);
      return req;
   }

   /** @summary Submit menu request
     * @private */
   async submitMenuRequest(painter, menukind, reqid) {
      return new Promise(resolveFunc => {
         this.submitDrawableRequest('', {
            _typename: `${nsREX}RDrawableMenuRequest`,
            menukind: menukind || '',
            menureqid: reqid // used to identify menu request
         }, painter, resolveFunc);
      });
   }

   /** @summary Submit executable command for given painter */
   submitExec(painter, exec, subelem) {
      if (subelem && isStr(subelem)) {
         const len = subelem.length;
         if ((len > 2) && (subelem.indexOf('#x') === len - 2))
            subelem = 'x';
         else if ((len > 2) && (subelem.indexOf('#y') === len - 2))
            subelem = 'y';
         else if ((len > 2) && (subelem.indexOf('#z') === len - 2))
            subelem = 'z';

         if ((subelem === 'x') || (subelem === 'y') || (subelem === 'z'))
            exec = subelem + 'axis#' + exec;
         else
            return console.log(`not recoginzed subelem ${subelem} in submitExec`);
      }

      this.submitDrawableRequest('', { _typename: `${nsREX}RDrawableExecRequest`, exec }, painter);
   }

   /** @summary Process reply from request to RDrawable */
   processDrawableReply(msg) {
      const reply = parse(msg);
      if (!reply?.reqid)
         return false;

      const req = this.#submreq[reply.reqid];
      if (!req)
         return false;

      // remove reference first
      this.#submreq[reply.reqid] = undefined;

      // remove blocking reference for that kind
      if (req._kind && req._painter?._requests) {
         if (req._painter._requests[req._kind] === req)
            delete req._painter._requests[req._kind];
      }

      if (req._method)
         req._method(reply, req);

      // resubmit last request of that kind
      if (req._nextreq && !req._painter._requests[req._kind])
         this.submitDrawableRequest(req._kind, req._nextreq, req._painter, req._method);
   }

   /** @summary Show specified section in canvas */
   async showSection(that, on) {
      switch (that) {
         case 'Menu': break;
         case 'StatusBar': break;
         case 'Editor': break;
         case 'ToolBar': break;
         case 'ToolTips': this.setTooltipAllowed(on); break;
      }
      return true;
   }

   /** @summary Method informs that something was changed in the canvas
     * @desc used to update information on the server (when used with web6gui)
     * @private */
   processChanges(kind, painter, subelem) {
      // check if we could send at least one message more - for some meaningful actions
      if (!this.canSendWebsocket(2) || !isStr(kind))
         return;

      const msg = '';
      if (!painter)
         painter = this;
      switch (kind) {
         case 'sbits':
            console.log('Status bits in RCanvas are changed - that to do?');
            break;
         case 'frame': // when moving frame
         case 'zoom':  // when changing zoom inside frame
            console.log('Frame moved or zoom is changed - that to do?');
            break;
         case 'pave_moved':
            console.log('TPave is moved inside RCanvas - that to do?');
            break;
         default:
            if ((kind.slice(0, 5) === 'exec:') && painter?.getSnapId())
               this.submitExec(painter, kind.slice(5), subelem);
            else
               console.log('UNPROCESSED CHANGES', kind);
      }

      if (msg)
         console.log('RCanvas::processChanges want to send  ' + msg.length + '  ' + msg.slice(0, 40));
   }

   /** @summary Handle pad button click event
     * @private */
   clickPadButton(funcname, evnt) {
      if (funcname === 'ToggleGed')
         return this.activateGed(this, null, 'toggle');
      if (funcname === 'ToggleStatus')
         return this.activateStatusBar('toggle');
      return super.clickPadButton(funcname, evnt);
   }

   /** @summary returns true when event status area exist for the canvas */
   hasEventStatus() {
      if (this.testUI5())
         return false;
      if (this.brlayout)
         return this.brlayout.hasStatus();
      const hp = getHPainter();
      return hp ? hp.hasStatusLine() : false;
   }

   /** @summary Check if status bar can be toggled
     * @private */
   canStatusBar() {
      return this.testUI5() || this.brlayout || getHPainter();
   }

   /** @summary Show/toggle event status bar
     * @private */
   activateStatusBar(state) {
      if (this.testUI5())
         return;
      if (this.brlayout)
         this.brlayout.createStatusLine(23, state);
      else
         getHPainter()?.createStatusLine(23, state);

      this.processChanges('sbits', this);
   }

   /** @summary Show online canvas status
     * @private */
   showCanvasStatus(...msgs) {
      if (this.testUI5())
         return;

      const br = this.brlayout || getHPainter()?.brlayout;
      br?.showStatus(...msgs);
   }

   /** @summary Returns true if GED is present on the canvas */
   hasGed() {
      if (this.testUI5())
         return false;
      return this.brlayout?.hasContent() ?? false;
   }

   /** @summary Function used to de-activate GED
     * @private */
   removeGed() {
      if (this.testUI5())
         return;

      this.registerForPadEvents(null);

      if (this.ged_view) {
         this.ged_view.getController().cleanupGed();
         this.ged_view.destroy();
         delete this.ged_view;
      }
      this.brlayout?.deleteContent(true);
      this.processChanges('sbits', this);
   }

   /** @summary Get view data for ui5 panel
     * @private */
   getUi5PanelData(/* panel_name */) {
      return { jsroot: { settings, create, parse, toJSON, loadScript, EAxisBits, getColorExec } };
   }

   /** @summary Function used to activate GED
     * @return {Promise} when GED is there
     * @private */
   async activateGed(objpainter, kind, mode) {
      if (this.testUI5() || !this.brlayout)
         return false;

      if (this.brlayout.hasContent()) {
         if ((mode === 'toggle') || (mode === false))
            this.removeGed();
         else
            objpainter?.getPadPainter()?.selectObjectPainter(objpainter);

         return true;
      }

      if (mode === false)
         return false;

      const btns = this.brlayout.createBrowserBtns();

      ToolbarIcons.createSVG(btns, ToolbarIcons.diamand, 15, 'toggle fix-pos mode', 'browser')
                  .style('margin', '3px').on('click', () => this.brlayout.toggleKind('fix'));

      ToolbarIcons.createSVG(btns, ToolbarIcons.circle, 15, 'toggle float mode', 'browser')
                  .style('margin', '3px').on('click', () => this.brlayout.toggleKind('float'));

      ToolbarIcons.createSVG(btns, ToolbarIcons.cross, 15, 'delete GED', 'browser')
                  .style('margin', '3px').on('click', () => this.removeGed());

      // be aware, that jsroot_browser_hierarchy required for flexible layout that element use full browser area
      this.brlayout.setBrowserContent('<div class=\'jsroot_browser_hierarchy\' id=\'ged_placeholder\'>Loading GED ...</div>');
      this.brlayout.setBrowserTitle('GED');
      this.brlayout.toggleBrowserKind(kind || 'float');

      return new Promise(resolveFunc => {
         loadOpenui5.then(sap => {
            d3_select('#ged_placeholder').text('');

            sap.ui.require(['sap/ui/model/json/JSONModel', 'sap/ui/core/mvc/XMLView'], (JSONModel, XMLView) => {
               const oModel = new JSONModel({ handle: null });

               XMLView.create({
                  viewName: 'rootui5.canv.view.Ged',
                  viewData: this.getUi5PanelData('Ged')
               }).then(oGed => {
                  oGed.setModel(oModel);

                  oGed.placeAt('ged_placeholder');

                  this.ged_view = oGed;

                  // TODO: should be moved into Ged controller - it must be able to detect canvas painter itself
                  this.registerForPadEvents(oGed.getController().padEventsReceiver.bind(oGed.getController()));

                  objpainter?.getPadPainter()?.selectObjectPainter(objpainter);

                  this.processChanges('sbits', this);

                  resolveFunc(true);
               });
            });
         });
      });
   }

   /** @summary produce JSON for RCanvas, which can be used to display canvas once again
     * @private */
   produceJSON() {
      console.error('RCanvasPainter.produceJSON not yet implemented');
      return '';
   }

   /** @summary resize browser window to get requested canvas sizes */
   resizeBrowser(fullW, fullH) {
      if (!fullW || !fullH || this.isBatchMode() || this.embed_canvas || this.batch_mode)
         return;
      this.getWebsocket()?.resizeWindow(fullW, fullH);
   }

   /** @summary draw RCanvas object */
   static async draw(dom, can, opt) {
      const nocanvas = !can;
      if (nocanvas)
         can = create(`${nsREX}RCanvas`);

      const painter = new RCanvasPainter(dom, can, opt);
      painter.createCanvasSvg(0);

      selectActivePad({ pp: painter, active: false });

      return painter.drawPrimitives().then(() => {
         painter.addPadInteractive();
         painter.addPadButtons();
         painter.showPadButtons();
         return painter;
      });
   }

} // class RCanvasPainter


/** @summary draw RPadSnapshot object
  * @private */
function drawRPadSnapshot(dom, snap, opt) {
   const painter = new RCanvasPainter(dom, null, opt);
   painter.batch_mode = isBatchMode();
   return painter.syncDraw(true).then(() => painter.redrawPadSnap(snap)).then(() => {
      painter.confirmDraw();
      painter.showPadButtons();
      return painter;
   });
}

/** @summary Ensure RCanvas and RFrame for the painter object
  * @param {Object} painter  - painter object to process
  * @param {string|boolean} frame_kind  - false for no frame or '3d' for special 3D mode
  * @desc Assigns DOM, creates and draw RCanvas and RFrame if necessary, add painter to pad list of painters
  * @return {Promise} for ready
  * @private */
async function ensureRCanvas(painter /* , frame_kind */) {
   if (!painter)
      return Promise.reject(Error('Painter not provided in ensureRCanvas'));

   // simple check - if canvas there, can use painter
   const pad_painter = painter.getPadPainter(),
         pr = pad_painter ? Promise.resolve(pad_painter) :
              RCanvasPainter.draw(painter.getDom(), null /* noframe */);

   return pr.then(pp => {
      // if ((frame_kind !== false) && pp.getFrameSvg().selectChild('.main_layer').empty())
      //   return RFramePainter.draw(painter.getDom(), null, isStr(frame_kind) ? frame_kind : '');
      painter.addToPadPrimitives(pp);
      return painter;
   });
}


/** @summary Function used for direct draw of RFrameTitle
  * @private */
function drawRFrameTitle(reason, drag) {
   const fp = this.getFramePainter();
   if (!fp)
      return console.log('no frame painter - no title');

   const rect = fp.getFrameRect(),
         fx = rect.x,
         fy = rect.y,
         fw = rect.width,
         // fh           = rect.height,
         ph = this.getPadPainter().getPadHeight(),
         title = this.getObject(),
         title_width = fw,
         textFont = this.v7EvalFont('text', { size: 0.07, color: 'black', align: 22 });
   let title_margin = this.v7EvalLength('margin', ph, 0.02),
       title_height = this.v7EvalLength('height', ph, 0.05);

   if (reason === 'drag') {
      title_height = drag.height;
      title_margin = fy - drag.y - drag.height;
      const changes = {};
      this.v7AttrChange(changes, 'margin', title_margin / ph);
      this.v7AttrChange(changes, 'height', title_height / ph);
      this.v7SendAttrChanges(changes, false); // do not invoke canvas update on the server
   }

   const g = this.createG();

   makeTranslate(g, fx, Math.round(fy - title_margin - title_height));

   return this.startTextDrawingAsync(textFont, 'font').then(() => {
      this.drawText({ x: title_width / 2, y: title_height / 2, text: title.fText, latex: 1 });
      return this.finishTextDrawing();
   }).then(() => {
      addDragHandler(this, {
         x: fx, y: Math.round(fy - title_margin - title_height), width: title_width, height: title_height,
         minwidth: 20, minheight: 20, no_change_x: true, redraw: d => this.redraw('drag', d)
      });
   });
}

// ==========================================================

registerMethods(`${nsREX}RPalette`, {

   extractRColor(rcolor) {
      const col = rcolor.fColor || 'black';
      return convertColor(col);
   },

   getColor(indx) {
      return this.palette[indx];
   },

   getContourIndex(zc) {
      const cntr = this.fContour;
      let l = 0, r = cntr.length - 1;

      if (zc < cntr[0])
         return -1;
      if (zc >= cntr[r])
         return r - 1;

      if (this.fCustomContour) {
         while (l < r - 1) {
            const mid = Math.round((l + r) / 2);
            if (cntr[mid] > zc)
               r = mid;
            else
               l = mid;
         }
         return l;
      }

      // last color in palette starts from level cntr[r-1]
      return Math.floor((zc - cntr[0]) / (cntr[r - 1] - cntr[0]) * (r - 1));
   },

   getContourColor(zc) {
      const zindx = this.getContourIndex(zc);
      return (zindx < 0) ? '' : this.getColor(zindx);
   },

   getContour() {
      return this.fContour && (this.fContour.length > 1) ? this.fContour : null;
   },

   deleteContour() {
      this.fContour = undefined;
   },

   calcColor(value, entry1, entry2) {
      const dist = entry2.fOrdinal - entry1.fOrdinal,
            r1 = entry2.fOrdinal - value,
            r2 = value - entry1.fOrdinal;

      if (!this.fInterpolate || (dist <= 0))
         return convertColor((r1 < r2) ? entry2.fColor : entry1.fColor);

      // interpolate
      const col1 = d3_rgb(this.extractRColor(entry1.fColor)),
            col2 = d3_rgb(this.extractRColor(entry2.fColor)),
            color = d3_rgb(Math.round((col1.r * r1 + col2.r * r2) / dist),
                           Math.round((col1.g * r1 + col2.g * r2) / dist),
                           Math.round((col1.b * r1 + col2.b * r2) / dist));

      return color.formatRgb();
   },

   createPaletteColors(len) {
      const arr = [];
      let indx = 0;

      while (arr.length < len) {
         const value = arr.length / (len - 1),
               entry = this.fColors[indx];

         if ((Math.abs(entry.fOrdinal - value) < 0.0001) || (indx === this.fColors.length - 1)) {
            arr.push(this.extractRColor(entry.fColor));
            continue;
         }

         const next = this.fColors[indx + 1];
         if (next.fOrdinal <= value)
            indx++;
         else
            arr.push(this.calcColor(value, entry, next));
      }

      return arr;
   },

   getColorOrdinal(value) {
      // extract color with ordinal value between 0 and 1
      if (!this.fColors)
         return 'black';
      if ((typeof value !== 'number') || (value < 0))
         value = 0;
      else if (value > 1)
         value = 1;

      // TODO: implement better way to find index

      let entry, next = this.fColors[0];
      for (let indx = 0; indx < this.fColors.length - 1; ++indx) {
         entry = next;

         if (Math.abs(entry.fOrdinal - value) < 0.0001)
            return this.extractRColor(entry.fColor);

         next = this.fColors[indx + 1];
         if (next.fOrdinal > value)
            return this.calcColor(value, entry, next);
      }

      return this.extractRColor(next.fColor);
   },

   setFullRange(min, max) {
      // set full z scale range, used in zooming
      this.full_min = min;
      this.full_max = max;
   },

   createContour(logz, nlevels, zmin, zmax, zminpositive) {
      this.fContour = [];
      delete this.fCustomContour;
      this.colzmin = zmin;
      this.colzmax = zmax;

      if (logz) {
         if (this.colzmax <= 0)
            this.colzmax = 1.0;
         if (this.colzmin <= 0) {
            if ((zminpositive === undefined) || (zminpositive <= 0))
               this.colzmin = 0.0001 * this.colzmax;
            else
               this.colzmin = ((zminpositive < 3) || (zminpositive > 100)) ? 0.3 * zminpositive : 1;
         }
         if (this.colzmin >= this.colzmax)
            this.colzmin = 0.0001 * this.colzmax;

         const logmin = Math.log(this.colzmin) / Math.log(10),
               logmax = Math.log(this.colzmax) / Math.log(10),
               dz = (logmax - logmin) / nlevels;
         this.fContour.push(this.colzmin);
         for (let level = 1; level < nlevels; level++)
            this.fContour.push(Math.exp((logmin + dz * level) * Math.log(10)));
         this.fContour.push(this.colzmax);
         this.fCustomContour = true;
      } else {
         if ((this.colzmin === this.colzmax) && this.colzmin) {
            this.colzmax += 0.01 * Math.abs(this.colzmax);
            this.colzmin -= 0.01 * Math.abs(this.colzmin);
         }
         const dz = (this.colzmax - this.colzmin) / nlevels;
         for (let level = 0; level <= nlevels; level++)
            this.fContour.push(this.colzmin + dz * level);
      }

      if (!this.palette || (this.palette.length !== nlevels))
         this.palette = this.createPaletteColors(nlevels);
   }

});

/** @summary draw RFont object
  * @private */
function drawRFont() {
   const font = this.getObject(),
         svg = this.getCanvSvg(),
         clname = 'custom_font_' + font.fFamily + font.fWeight + font.fStyle;
   let defs = svg.selectChild('.canvas_defs');

   if (defs.empty())
      defs = svg.insert('svg:defs', ':first-child').attr('class', 'canvas_defs');

   let entry = defs.selectChild('.' + clname);
   if (entry.empty()) {
      entry = defs.append('style')
                  .attr('type', 'text/css')
                  .attr('class', clname)
                  .text(`@font-face { font-family: "${font.fFamily}"; font-weight: ${font.fWeight ? font.fWeight : 'normal'}; font-style: ${font.fStyle ? font.fStyle : 'normal'}; src: ${font.fSrc}; }`);
      const p1 = font.fSrc.indexOf('base64,'),
            p2 = font.fSrc.lastIndexOf(' format(');
      if (p1 > 0 && p2 > p1) {
         const base64 = font.fSrc.slice(p1 + 7, p2 - 2),
               is_ttf = font.fSrc.indexOf('data:application/font-ttf') > 0;
         // TODO: for the moment only ttf format supported by jsPDF
         if (is_ttf)
            entry.property('$fontcfg', { n: font.fFamily, base64 });
      }
   }

   if (font.fDefault)
      this.getPadPainter()._dfltRFont = font;

   return true;
}

/** @summary draw RAxis object
  * @private */
function drawRAxis(dom, obj, opt) {
   const painter = new RAxisPainter(dom, obj, opt);
   painter.disable_zooming = true;
   return ensureRCanvas(painter, false)
           .then(() => painter.redraw())
           .then(() => painter);
}

/** @summary draw RFrame object
  * @private */
function drawRFrame(dom, obj, opt) {
   const p = new RFramePainter(dom, obj);
   if (opt === '3d')
      p.mode3d = true;
   return ensureRCanvas(p, false).then(() => p.redraw());
}

export { ensureRCanvas, drawRPadSnapshot,
         drawRFrameTitle, drawRFont, drawRAxis, drawRFrame,
         RObjectPainter, RPadPainter, RCanvasPainter };
