var Tooltip = {
  # default delay (in seconds)
  DELAY: 4.0,
  # Constructor
  #
  # @param size ([width, height])
  new: func(size, id = nil)
  {
    var m = {
      parents: [Tooltip, PropertyElement.new(["/sim/gui/canvas", "window"], id)],
      _listener: nil,
      _property: nil,
      _mapping: "",
      _mappingFunc: nil,
      _width: 0,
      _height: 0,
      _tipId: nil,
      _slice: 17,
      _measureText: nil,
      _measureBB: nil,
      _hideTimer: nil,
      _hiding: nil
    };

    m.setInt("size[0]", size[0]);
    m.setInt("size[1]", size[1]);
    m.setBool("visible", 0);
    m.setInt("z-index", gui.STACK_INDEX["tooltip"]);

    m._hideTimer = maketimer(m.DELAY, m, Tooltip._hideTimeout);
    m._hideTimer.singleShot = 1;

    return m;
  },
  # Destructor
  del: func
  {
    me.parents[1].del();
    if( me["_canvas"] != nil )
      me._canvas.del();
  },
  # Create the canvas to be used for this Tooltip
  #
  # @return The new canvas
  createCanvas: func()
  {
    var size = [
      me.get("size[0]"),
      me.get("size[1]")
    ];

    me._canvas = new({
      size: [2 * size[0], 2 * size[1]],
      view: size,
      placement: {
        type: "window",
        index: me._node.getIndex()
      }
    });

    # don't do anything with mouse events ourselves
    me.set("capture-events", 0);
    me.set("fill", "rgba(255,255,255,0.8)");

    # transparent background
    me._canvas.setColorBackground(0.0, 0.0, 0.0, 0.0);

    var root = me._canvas.createGroup();
    me._root = root;

    me._frame =
      root.createChild("image", "background")
          .set("src", "gui/images/tooltip.png")
          .set("slice", me._slice ~ " fill")
          .setSize(size);

    me._text =
      root.createChild("text", "tooltip-caption")
          .setText("Aircraft Help")
          .setAlignment("left-top")
          .setFontSize(14)
          .setFont("LiberationFonts/LiberationSans-Bold.ttf")
          .setColor(1,1,1)
          .setDrawMode(Text.TEXT)
          .setTranslation(me._slice, me._slice)
          .setMaxWidth(size[0]);

    return me._canvas;
  },

  setLabel: func(msg)
  {
    me._label = msg;
    me._updateText();
  },

  setProperty: func(prop)
  {
    if (me._property != nil)
      removelistener(me._listener);

    me._property = prop;
    if (me._property != nil)
      me._listener = setlistener(me._property, func { me._updateText(); });

    me._updateText();
  },

  # specify a string used to compute the width of the tooltip
  setWidthText: func(txt)
  {
    me._measureBB = me._text.setText(txt)
                            .update()
                            .getBoundingBox();
    me._updateBounds();
  },

  _updateText: func
  {
    var msg = me._label;
    if (me._property != nil) {
      var val = me._property.getValue() or 0;

      # https://code.google.com/p/flightgear-bugs/issues/detail?id=1454
      # wrap mapping in 'call' to catch conversion errors
      var val_mapped = call(me._remapValue, [val], me, nil, var err = []);
      if( size(err) )
        printlog(
          "warn",
          "Tooltip: failed to remap " ~ debug.string(me._property, 0) ~ ":\n"
          ~ debug.string(err, 0)
        );

      msg = sprintf(me._label, val_mapped or val);
    }

    me._text.setText(msg);
    me._updateBounds();
  },

  _updateBounds: func
  {
    var max_width = me.get("size[0]") - 2 * me._slice;
    me._text.setMaxWidth(max_width);

    # compute the bounds
    var text_bb = me._text.update().getBoundingBox();
    var width = text_bb[2];
    var height = text_bb[3];

    if( me._measureBB != nil )
    {
      width = math.max(width, me._measureBB[2]);
      height = math.max(height, me._measureBB[3]);
    }

    if( width > max_width )
      width = max_width;

    me._width = width + 2 * me._slice;
    me._height = height + 2 * me._slice;
    me._frame.setSize(me._width, me._height)
             .update();
  },

  _remapValue: func(val)
  {
    if (me._mapping == "") return val;
    if (me._mapping == "percent") return math.round(val * 100);

    # TODO - translate me!
    if (me._mapping == "on-off") return (val == 1) ? "ON" : "OFF";
    if (me._mapping == "arm-disarm") return (val == 1) ? "ARMED" : "DISARMED";

    # provide both 'senses' of the flag here
    if (me._mapping == "up-down") return (val == 1) ? "UP" : "DOWN";
    if (me._mapping == "down-up") return (val == 1) ? "DOWN" : "UP";
    if (me._mapping == "open-close") return (val == 1) ? "OPEN" : "CLOSED";
    if (me._mapping == "close-open") return (val == 1) ? "CLOSED" : "OPEN";

    if (me._mapping == "heading") return geo.normdeg(val);
    if (me._mapping == "nasal") return me._mappingFunc(val);

    return val;
  },

  setMapping: func(mapping, f = nil)
  {
    me._mapping = mapping;
    me._mappingFunc = f;
    me._updateText();
  },

  setTooltipId: func(tipId)
  {
    me._tipId = tipId;
    if ((tipId != nil) and me._hiding) {
      me._hideTimer.stop();
      me._hiding = 0;
    }
  },

  getTooltipId: func { me._tipId; },

  # Get the displayed canvas
  getCanvas: func()
  {
    return me['_canvas'];
  },

  setPosition: func(x, y)
  {
    me.setInt("x", x + 10);
    me.setInt("y", y + 10);
  },

  show: func()
  {
    # don't show if undefined
    if (me._tipId == nil) return;

    if (me._hiding) {
      me._hideTimer.stop();
      me._hiding = 0;
    }

    if (!me.isVisible()) {
      me.setBool("visible", 1);
    }
  },

  showMessage: func(timeout = nil, node = nil)
  {
    if(var y = me._haveNode(node, 'y') != nil ) {
      me.setInt("y", y);
    } else {
      me.setInt("y", getprop('/sim/startup/ysize') * 0.2);
    }
    if(var x = me._haveNode(node, 'x')  != nil) {
      me.setInt("x", x);
    } else {
      var screenW = getprop('/sim/startup/xsize');
      me.setInt("x", (screenW - me._width) * 0.5);
    }
    me.show();
    # https://code.google.com/p/flightgear-bugs/issues/detail?id=1273
    # when tooltip is shown for some other reason, ensure it stays for
    # the full delay (unless replaced). Don't allow the update-hover
    # code path to hide() with the shorter delay.
    me._hiding = 1;
    me._hideTimer.restart(timeout or me.DELAY);
  },

  _haveNode: func(node, key) {
    if(node == nil ) return nil;
    var value = num(node.getValue(key) );
    return value;
  },

  hide: func()
  {
    # this gets run repeatedly during mouse-moves
    if (me._hiding) return;
    me._hiding = 1;

    me._hideTimer.restart(0.5);
  },

  hideNow: func()
  {
    me._hideTimer.stop();
    me._hideTimeout();
  },

  isVisible: func
  {
    return me.getBool("visible");
  },

  fadeIn: func()
  {
    me.show();
  },

  fadeOut: func()
  {
    me.hide();
  },

# private:
  _hideTimeout: func()
  {
    me.setBool("visible", 0);
    me._hiding = 0;
  }
};

var tooltip = canvas.Tooltip.new([300, 100]);
tooltip.createCanvas();

var innerSetTooltip = func(node)
{
   tooltip.setLabel(cmdarg().getNode('label').getValue());
   var measure = cmdarg().getNode('measure-text');
   if (measure != nil) {
       tooltip.setWidthText(measure.getValue());
   } else {
       tooltip.setWidthText(nil);
   }

   var propPath = cmdarg().getNode('property');
   if (propPath != nil) {
     var n = props.globals.getNode(propPath.getValue());
     tooltip.setProperty(n);
   } else {
      tooltip.setProperty(nil);
   }

   var mapping = cmdarg().getNode('mapping');
   if (mapping != nil) {
     var m = mapping.getValue();
     var f = nil;
     if (m == 'nasal') {
       f = compile(cmdarg().getNode('script').getValue());
     }

     tooltip.setMapping(m, f);
   } else {
     tooltip.setMapping(nil);
  }
}

var setTooltip = func(node)
{
  var tipId = cmdarg().getNode('tooltip-id').getValue();
  if (tooltip.getTooltipId() == tipId) {
    return; # nothing more to do
  }

  var x = cmdarg().getNode('x').getValue();
  var y = cmdarg().getNode('y').getValue();

  var screenHeight = getprop('/sim/startup/ysize');
  tooltip.setPosition(x, screenHeight - y);
  tooltip.setTooltipId(tipId);
  innerSetTooltip(node);

  # don't actually show here, we do that response to tooltip-timeout
  # so this is just getting ready
}

var showTooltip = func(node)
{
  var r = node.getNode("reason");
  if ((r != nil) and (r.getValue() == "click")) {
    # click triggering tooltip, show immediately
    tooltip.show();
  } else {
    tooltip.fadeIn();
  }
}

var updateHover = func(node)
{
  tooltip.setTooltipId(nil);

  # if not shown, nothing to do here
  if (!tooltip.isVisible()) return;

  # reset cursor to standard
  tooltip.fadeOut();

}

var showMessage = func(node)
{
  var msgId = node.getNode("id");
  tooltip.setTooltipId((msgId == nil) ? 'msg' : msgId.getValue());
  innerSetTooltip(node);

  var timeout = node.getNode("delay");
  tooltip.showMessage( timeout != nil ? timeout.getValue() : nil, node);
}

var clearMessage = func(node)
{
  var msgId = node.getNode("id");
  if (tooltip.getTooltipId() != msgId.getValue()) return;
  tooltip.hideNow();
}

addcommand("update-hover", updateHover);
addcommand("set-tooltip", setTooltip);
addcommand("tooltip-timeout", showTooltip);
addcommand("show-message", showMessage);
addcommand("clear-message", clearMessage);