import { isStr, isFunc, nsREX, settings, isNodeJs, isBatchMode } from '../core.mjs';
import { FontHandler } from './FontHandler.mjs';
import { ObjectPainter } from './ObjectPainter.mjs';
import { color as d3_color } from '../d3.mjs';


const kNormal = 1, /* kLessTraffic = 2, */ kOffline = 3;

class RObjectPainter extends ObjectPainter {

   #pending_request;
   #auto_colors; // handle for auto colors

   constructor(dom, obj, opt, csstype) {
      super(dom, obj, opt);
      this.csstype = csstype;
   }

   /** @summary Add painter to pad list of painters
    * @desc For RCanvas also handles common style
    * @protected */
   addToPadPrimitives(pad_painter) {
      const pp = super.addToPadPrimitives(pad_painter);

      if (pp && !this.rstyle && pp.next_rstyle)
         this.rstyle = pp.next_rstyle;

      return pp;
   }

   /** @summary Evaluate v7 attributes using fAttr storage and configured RStyle */
   v7EvalAttr(name, dflt) {
      const obj = this.getObject();
      if (!obj)
         return dflt;
      if (this.cssprefix)
         name = this.cssprefix + name;

      const type_check = res => {
         if (dflt === undefined)
            return res;
         const typ1 = typeof dflt, typ2 = typeof res;
         if (typ1 === typ2)
            return res;
         if (typ1 === 'boolean') {
            if (typ2 === 'string')
               return (res !== '') && (res !== '0') && (res !== 'no') && (res !== 'off');
            return Boolean(res);
         }
         if ((typ1 === 'number') && (typ2 === 'string'))
            return parseFloat(res);
         return res;
      };

      if (obj.fAttr?.m) {
         const value = obj.fAttr.m[name];
         if (value)
            return type_check(value.v); // found value direct in attributes
      }

      if (this.rstyle?.fBlocks) {
         const blks = this.rstyle.fBlocks;
         for (let k = 0; k < blks.length; ++k) {
            const block = blks[k],
                  match = (this.csstype && (block.selector === this.csstype)) ||
                          (obj.fId && (block.selector === ('#' + obj.fId))) ||
                          (obj.fCssClass && (block.selector === ('.' + obj.fCssClass)));

            if (match && block.map?.m) {
               const value = block.map.m[name.toLowerCase()];
               if (value)
                  return type_check(value.v);
            }
         }
      }

      return dflt;
   }

   /** @summary Set v7 attributes value */
   v7SetAttr(name, value) {
      const obj = this.getObject();
      if (this.cssprefix)
         name = this.cssprefix + name;

      if (obj?.fAttr?.m)
         obj.fAttr.m[name] = { v: value };
   }

   /** @summary Decode pad length from string, return pixel value */
   v7EvalLength(name, sizepx, dflt) {
      if (sizepx <= 0)
         sizepx = 1;

      const value = this.v7EvalAttr(name);

      if (value === undefined)
         return Math.round(dflt * sizepx);

      if (typeof value === 'number')
         return Math.round(value * sizepx);

      if (value === null)
         return 0;

      let norm = 0, px = 0, val = value, operand = 0, pos = 0;

      while (val) {
         // skip empty spaces
         while ((pos < val.length) && ((val[pos] === ' ') || (val[pos] === '\t')))
            ++pos;

         if (pos >= val.length)
            break;

         if ((val[pos] === '-') || (val[pos] === '+')) {
            if (operand) {
               console.log(`Fail to parse RPadLength ${value}`);
               return dflt;
            }
            operand = (val[pos] === '-') ? -1 : 1;
            pos++;
            continue;
         }

         if (pos > 0) {
            val = val.slice(pos);
            pos = 0;
         }

         while ((pos < val.length) && (((val[pos] >= '0') && (val[pos] <= '9')) || (val[pos] === '.')))
            pos++;

         const v = parseFloat(val.slice(0, pos));
         if (!Number.isFinite(v)) {
            console.log(`Fail to parse RPadLength ${value}`);
            return Math.round(dflt * sizepx);
         }

         val = val.slice(pos);
         pos = 0;
         if (!operand)
            operand = 1;
         if (val && (val[0] === '%')) {
            val = val.slice(1);
            norm += operand * v * 0.01;
         } else if ((val.length > 1) && (val[0] === 'p') && (val[1] === 'x')) {
            val = val.slice(2);
            px += operand * v;
         } else
            norm += operand * v;

         operand = 0;
      }

      return Math.round(norm * sizepx + px);
   }

   /** @summary Evaluate RColor using attribute storage and configured RStyle */
   v7EvalColor(name, dflt) {
      let val = this.v7EvalAttr(name, '');
      if (!val || !isStr(val))
         return dflt;

      if (val === 'auto') {
         const pp = this.getPadPainter();
         if (pp) {
            val = pp.getAutoColor();
            if (!this.#auto_colors)
               this.#auto_colors = {};
            this.#auto_colors[name] = val;
         } else if (this.#auto_colors && this.#auto_colors[name])
            val = this.#auto_colors[name];
         else {
            console.error(`Autocolor ${name} not defined yet - please check code`);
            val = '';
         }
      } else if (val[0] === '[') {
         const ordinal = parseFloat(val.slice(1, val.length - 1));
         val = 'black';
         if (Number.isFinite(ordinal)) {
            const pal = this.getPadPainter()?.getHistPalette();
            if (pal)
               val = pal.getColorOrdinal(ordinal);
         }
      }

      // to make colors similar in node and in pupperteer
      if ((val[0] === '#') && (isNodeJs() || (isBatchMode() && settings.ApproxTextSize))) {
         const col = d3_color(val);
         if (col.opacity !== 1)
            col.opacity = col.opacity.toFixed(2);
         return col.formatRgb();
      }

      return val;
   }

   /** @summary Evaluate RAttrText properties
     * @return {Object} FontHandler, can be used directly for the text drawing */
   v7EvalFont(name, dflts, fontScale) {
      if (!dflts)
         dflts = {};
      else if (typeof dflts === 'number')
         dflts = { size: dflts };

      const pp = this.getPadPainter(),
            rfont = pp?._dfltRFont || { fFamily: 'Arial', fStyle: '', fWeight: '' },
            text_angle = this.v7EvalAttr(name + '_angle', 0),
            text_align = this.v7EvalAttr(name + '_align', dflts.align || 'none'),
            text_color = this.v7EvalColor(name + '_color', dflts.color || 'none'),
            font_family = this.v7EvalAttr(name + '_font_family', rfont.fFamily || 'Arial'),
            font_style = this.v7EvalAttr(name + '_font_style', rfont.fStyle || ''),
            font_weight = this.v7EvalAttr(name + '_font_weight', rfont.fWeight || '');
      let text_size = this.v7EvalAttr(name + '_size', dflts.size || 12);

      if (isStr(text_size))
         text_size = parseFloat(text_size);
      if (!Number.isFinite(text_size) || (text_size <= 0))
         text_size = 12;
      if (!fontScale)
         fontScale = pp?.getPadHeight() || 100;

      const handler = new FontHandler(null, text_size, fontScale);
      handler.setNameStyleWeight(font_family, font_style, font_weight);

      if (text_angle)
         handler.setAngle(360 - text_angle);
      if (text_align !== 'none')
         handler.setAlign(text_align);
      if (text_color !== 'none')
         handler.setColor(text_color);

      return handler;
   }

   /** @summary Create this.fillatt object based on v7 fill attributes */
   createv7AttFill(prefix) {
      if (!prefix || !isStr(prefix))
         prefix = 'fill_';

      const color = this.v7EvalColor(prefix + 'color', ''),
            pattern = this.v7EvalAttr(prefix + 'style', 0);

      this.createAttFill({ pattern, color, color_as_svg: true });
   }

   /** @summary Create this.lineatt object based on v7 line attributes */
   createv7AttLine(prefix) {
      if (!prefix || !isStr(prefix))
         prefix = 'line_';

      const color = this.v7EvalColor(prefix + 'color', 'black'),
            width = this.v7EvalAttr(prefix + 'width', 1),
            style = this.v7EvalAttr(prefix + 'style', 1);
      let pattern = this.v7EvalAttr(prefix + 'pattern');
      if (pattern && isNodeJs())
         pattern = pattern.split(',').join(', ');

      this.createAttLine({ color, width, style, pattern });

      if (prefix === 'border_')
         this.lineatt.setBorder(this.v7EvalAttr(prefix + 'rx', 0), this.v7EvalAttr(prefix + 'ry', 0));
   }

   /** @summary Create this.markeratt object based on v7 attributes */
   createv7AttMarker(prefix) {
      if (!prefix || !isStr(prefix))
         prefix = 'marker_';

      const color = this.v7EvalColor(prefix + 'color', 'black'),
            size = this.v7EvalAttr(prefix + 'size', 0.01),
            style = this.v7EvalAttr(prefix + 'style', 1),
            refsize = (size >= 1) ? 1 : (this.getPadPainter()?.getPadHeight() || 100);

      this.createAttMarker({ color, size, style, refsize });
   }

   /** @summary Create RChangeAttr, which can be applied on the server side
     * @private */
   v7AttrChange(req, name, value, kind) {
      if (!this.getSnapId())
         return false;

      if (!req._typename) {
         req._typename = `${nsREX}RChangeAttrRequest`;
         req.ids = [];
         req.names = [];
         req.values = [];
         req.update = true;
      }

      if (this.cssprefix)
         name = this.cssprefix + name;
      req.ids.push(this.getSnapId());
      req.names.push(name);

      if ((value === null) || (value === undefined)) {
         if (!kind)
            kind = 'none';
         if (kind !== 'none')
            console.error(`Trying to set ${kind} for none value`);
      }

      if (!kind) {
         switch (typeof value) {
            case 'number': kind = 'double'; break;
            case 'boolean': kind = 'boolean'; break;
         }
      }

      const obj = { _typename: `${nsREX}RAttrMap::` };
      switch (kind) {
         case 'none':
            obj._typename += 'NoValue_t';
            break;
         case 'boolean':
            obj._typename += 'BoolValue_t';
            obj.v = Boolean(value);
            break;
         case 'int':
            obj._typename += 'IntValue_t';
            obj.v = parseInt(value);
            break;
         case 'double':
            obj._typename += 'DoubleValue_t';
            obj.v = parseFloat(value);
            break;
         default:
            obj._typename += 'StringValue_t';
            obj.v = isStr(value) ? value : JSON.stringify(value);
            break;
      }

      req.values.push(obj);
      return true;
   }

   /** @summary Sends accumulated attribute changes to server */
   v7SendAttrChanges(req, do_update) {
      const canp = this.getCanvPainter();
      if (canp && req?._typename) {
         if (do_update !== undefined)
            req.update = Boolean(do_update);
         canp.v7SubmitRequest('', req);
      }
   }

   /** @summary Submit request to server-side drawable
    * @param kind defines request kind, only single request a time can be submitted
    * @param req is object derived from DrawableRequest, including correct _typename
    * @param method is method of painter object which will be called when getting reply */
   v7SubmitRequest(kind, req, method) {
      const canp = this.getCanvPainter();
      if (!isFunc(canp?.submitDrawableRequest))
         return null;

      // special situation when snapid not yet assigned - just keep ref until snapid is there
      // maybe keep full list - for now not clear if really needed
      if (!this.getSnapId()) {
         this.#pending_request = { kind, req, method };
         return req;
      }

      return canp.submitDrawableRequest(kind, req, this, method);
   }

   /** @summary Assign snapid to the painter
     * @desc Overwrite default method */
   assignSnapId(id) {
      super.assignSnapId(id);
      if (this.getSnapId() && this.#pending_request) {
         const p = this.#pending_request;
         this.#pending_request = undefined;
         this.v7SubmitRequest(p.kind, p.req, p.method);
      }
   }

   /** @summary Return communication mode with the server
     * @desc
     * kOffline means no server there,
     * kLessTraffic advise not to send commands if offline functionality available
     * kNormal is standard functionality with RCanvas on server side */
   v7CommMode() {
      const canp = this.getCanvPainter();
      if (!canp || !canp.submitDrawableRequest || !canp.getWebsocket())
         return kOffline;

      return kNormal;
   }

   v7NormalMode() { return this.v7CommMode() === kNormal; }

   v7OfflineMode() { return this.v7CommMode() === kOffline; }

} // class RObjectPainter

export { RObjectPainter };
