//  Prototip 1.2.0_pre1 - 17-12-2007

//  Copyright (c) 2007 Nick Stakenburg (http://www.nickstakenburg.com)
//
//  Permission is hereby granted, free of charge, to any person obtaining
//  a copy of this software and associated documentation files (the
//  "Software"), to deal in the Software without restriction, including
//  without limitation the rights to use, copy, modify, merge, publish,
//  distribute, sublicense, and/or sell copies of the Software, and to
//  permit persons to whom the Software is furnished to do so, subject to
//  the following conditions:
//
//  The above copyright notice and this permission notice shall be
//  included in all copies or substantial portions of the Software.
//
//  THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
//  EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
//  MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
//  IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
//  CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
//  TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
//  SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

//  More information on this project:
//  http://www.nickstakenburg.com/projects/prototip/

var Prototip = {
  Version: '1.2.0_pre1',

  REQUIRED_Prototype: '1.6.0',
  REQUIRED_Scriptaculous: '1.8.0',

  start: function() {
    this.require('Prototype');
    Tips.initialize();
    Element.observe(window, 'unload', this.unload);
  },

  require: function(library) {
    if ((typeof window[library] == 'undefined') ||
      (this.convertVersionString(window[library].Version) < this.convertVersionString(this['REQUIRED_' + library])))
      throw('Prototip requires ' + library + ' >= ' + this['REQUIRED_' + library]);
  },

  convertVersionString: function(versionString) {
    var r = versionString.split('.');
    return parseInt(r[0])*100000 + parseInt(r[1])*1000 + parseInt(r[2]);
  },

  viewport: {
    getDimensions: function() {
      var dimensions = { };
      var B = Prototype.Browser;
      $w('width height').each(function(d) {
        var D = d.capitalize();
        dimensions[d] = (B.WebKit && !document.evaluate) ? self['inner' + D] :
          (B.Opera) ? document.body['client' + D] : document.documentElement['client' + D];
        });
      return dimensions;
    }
  },

  capture: function(func) {
    if (!Prototype.Browser.IE) {
      func = func.wrap(function(proceed, event) {
      var rel = event.relatedTarget, cur = event.currentTarget;
      if (rel && rel.nodeType == Node.TEXT_NODE) rel = rel.parentNode;
      if (rel && rel != cur && rel.descendantOf && !(rel.descendantOf(cur)))
        proceed(event);
      });
    }
    return func;
  },

  unload: function() { Tips.removeAll(); }
};

var Tips = {
  // Configuration
  closeButtons: false,
  zIndex: 1200,

  tips : [],
  visible : [],

  initialize: function() {
    this.zIndexTop = this.zIndex;
  },

  useEvent : (function(IE) { return {
    'mouseover': (IE ? 'mouseenter' : 'mouseover'),
    'mouseout': (IE ? 'mouseleave' : 'mouseout'),
    'mouseenter': (IE ? 'mouseenter' : 'mouseover'),
    'mouseleave': (IE ? 'mouseleave' : 'mouseout')
  };})(Prototype.Browser.IE),

  fixIE: (function(agent) {
    var version = new RegExp('MSIE ([\\d.]+)').exec(agent);
    return version ? (parseFloat(version[1]) <= 6) : false;
  })(navigator.userAgent),

  add: function(tip) {
    this.tips.push(tip);
  },

  remove: function(element) {
    var tip = this.tips.find(function(t){ return t.element == $(element); });
    if (tip) {
      tip.deactivate();
      if (tip.tooltip) {
        tip.wrapper.remove();
        if (Tips.fixIE) tip.iframeShim.remove();
      }
      this.tips = this.tips.without(tip);
    }
  },

  removeAll: function() {
    this.tips.each(function(tip) { this.remove(tip.element); }.bind(this));
  },

  raise: function(tip) {
    if (tip.highest) return;
    if (this.visible.length == 0) {
      this.zIndexTop = this.zIndex;
      for (var i=0;i<this.tips.length;i++) {
        this.tips[i].wrapper.style.zIndex = this.zIndex;
      }
    }
    tip.style.zIndex = this.zIndexTop++;
    for (var i=0;i<this.tips.length;i++) { this.tips[i].wrapper.highest = false; };
    tip.highest = true;
  },

  addVisibile: function(tip) {
    this.removeVisible(tip);
    this.visible.push(tip);
  },

  removeVisible: function(tip) {
    this.visible = this.visible.without(tip);
  }
};
Tips.initialize();

var Tip = Class.create({
    initialize: function(element, content) {
        this.element = $(element);
        Tips.remove(this.element);

        this.content = content;

        var isHooking = (arguments[2] && arguments[2].hook);
        var isShowOnClick = (arguments[2] && arguments[2].showOn == 'click');

        this.options = Object.extend({
            className: 'default',                 // see css, this will lead to .prototip .default
            closeButton: Tips.closeButtons,       // true, false
            delay: !isShowOnClick ? 0.2 : false,  // seconds before tooltip appears
            duration: 0.3,                        // duration of the effect
            effect: false,                        // false, 'appear' or 'blind'
            hideAfter: false,                     // second before hide after no hover/activity
            hideOn: 'mouseleave',                 // or any other event, false
            hook: false,                          // { element: topLeft|topRight|bottomLeft|bottomRight, tip: see element }
            offset: isHooking ? { x: 0, y: 0} : { x: 16, y: 16 },
            fixed: isHooking ? true : false,      // follow the mouse if false
            showOn: 'mousemove',
            target: this.element,                 // or another element
            title: false,
            viewport: isHooking ? false : true    // keep within viewport if mouse is followed
        }, arguments[2] || {});

        this.target = $(this.options.target);

        this.setup();

        if (this.options.effect) {
            //Prototip.require('Scriptaculous');
            this.queue = { position: 'end', limit: 1, scope: this.wrapper.identify() };
        }

        Tips.add(this);
        this.activate();
    },

    setup: function() {
        this.wrapper = new Element('div', { 'class': 'prototip' }).setStyle({
            display: 'none', zIndex: Tips.zIndex
        });
        this.wrapper.identify();

        if (Tips.fixIE) {
            this.iframeShim = new Element('iframe', { 'class': 'iframeShim', src: 'javascript:false;' }).setStyle({
                display: 'none', zIndex: Tips.zIndex - 1
            });
        }

        this.tip = new Element('div', { 'class': 'content' }).insert(this.content);
        this.tip.insert(new Element('div').setStyle({ clear: 'both' }));

        if (this.options.closeButton || (this.options.hideOn.element && this.options.hideOn.element == 'closeButton'))
            this.closeButton = new Element('a', { href: '#', 'class': 'close' });
    },

    build: function() {
        if (Tips.fixIE) document.body.appendChild(this.iframeShim).setOpacity(0);

        // effects go smooth with extra wrapper
        var wrapper = 'wrapper';
        if (this.options.effect) {
            this.effectWrapper = this.wrapper.appendChild(new Element('div', { 'class': 'effectWrapper' }));
            wrapper = 'effectWrapper';
        }

        this.tooltip = this[wrapper].appendChild(new Element('div', { 'class': 'tooltip ' + this.options.className }));

        if (this.options.title || this.options.closeButton) {
            this.toolbar = this.tooltip.appendChild(new Element('div', { 'class': 'toolbar' }));
            this.title = this.toolbar.appendChild(new Element('div', { 'class': 'title' }).update(this.options.title || ' '));
        }

        this.tooltip.insert(this.tip);
        document.body.appendChild(this.wrapper);

        // fixate elements for better positioning and effects
        var fixate = (this.options.effect) ? [this.wrapper, this.effectWrapper] : [this.wrapper];
        if (Tips.fixIE) fixate.push(this.iframeShim);

        // fix width
        var fixedWidth = this.wrapper.getWidth();
        fixate.invoke('setStyle', { width: fixedWidth + 'px' });

        // make toolbar width fixed
        if (this.toolbar) {
            this.wrapper.setStyle({ visibility: 'hidden' }).show();
            this.toolbar.setStyle({ width: this.toolbar.getWidth() + 'px' });
            this.wrapper.hide().setStyle({ visibility: 'visible' });
        }

        // add close button
        if (this.closeButton)
            this.title.insert({ top: this.closeButton }).insert(new Element('div').setStyle({ clear: 'both' }));

        var fixedHeight = this.wrapper.getHeight();
        fixate.invoke('setStyle', { width: fixedWidth + 'px', height: fixedHeight + 'px' });

        this[this.options.effect ? wrapper : 'tooltip'].hide();
    },

    activate: function() {
        this.eventShow = this.showDelayed.bindAsEventListener(this);
        this.eventHide = this.hide.bindAsEventListener(this);

        // if fixed use mouseover instead of mousemove for less event calls
        if (this.options.fixed && this.options.showOn == 'mousemove') this.options.showOn = 'mouseover';

        if (this.options.showOn == this.options.hideOn) {
            this.eventToggle = this.toggle.bindAsEventListener(this);
            this.element.observe(this.options.showOn, this.eventToggle);
        }

        var hideOptions = {
            'element': this.eventToggle ? [] : [this.element],
            'target': this.eventToggle ? [] : [this.target],
            'tip': this.eventToggle ? [] : [this.wrapper],
            'closeButton': [],
            'none': []
        };
        var el = this.options.hideOn.element;
        this.hideElement = el || (!this.options.hideOn ? 'none' : 'element');
        this.hideTargets = hideOptions[this.hideElement];
        if (!this.hideTargets && el && Object.isString(el)) this.hideTargets = this.tip.select(el);

        var realEvent = { 'mouseenter': 'mouseover', 'mouseleave': 'mouseout' };
        $w('show hide').each(function(e) {
            var E = e.capitalize();
            var event = (this.options[e + 'On'].event || this.options[e + 'On']);
            this[e + 'Action'] = event;
            if (['mouseenter', 'mouseleave', 'mouseover', 'mouseout'].include(event)) {
                this[e + 'Action'] = (Tips.useEvent[event] || event);
                this['event' + E] = Prototip.capture(this['event' + E]);
            }
        } .bind(this));

        if (!this.eventToggle) this.element.observe(this.options.showOn, this.eventShow);
        if (this.hideTargets) this.hideTargets.invoke('observe', this.hideAction, this.eventHide);

        // add postion observer to moving showOn click tips
        if (!this.options.fixed && this.options.showOn == 'click') {
            this.eventPosition = this.position.bindAsEventListener(this);
            this.element.observe('mousemove', this.eventPosition);
        }

        // close button
        this.buttonEvent = this.hide.wrap(function(proceed, event) {
            event.stop();
            proceed(event);
        }).bindAsEventListener(this);
        if (this.closeButton) this.closeButton.observe('click', this.buttonEvent);

        // delay timeout
        if (this.options.showOn != 'click' && (this.hideElement != 'element')) {
            this.eventCheckDelay = Prototip.capture(function() {
                this.clearTimer('show');
            }).bindAsEventListener(this);
            this.element.observe(Tips.useEvent['mouseout'], this.eventCheckDelay);
        }

        // activity (hideAfter, raise)
        var elements = [this.element, this.wrapper];
        this.activityEnter = Prototip.capture(function() {
            Tips.raise(this.wrapper);
            this.cancelHideAfter();
        }).bindAsEventListener(this);
        this.activityLeave = Prototip.capture(this.hideAfter).bindAsEventListener(this);
        elements.invoke('observe', Tips.useEvent['mouseover'], this.activityEnter);
        elements.invoke('observe', Tips.useEvent['mouseout'], this.activityLeave);
    },

    deactivate: function() {
        if (this.options.showOn == this.options.hideOn)
            this.element.stopObserving(this.options.showOn, this.eventToggle);
        else {
            this.element.stopObserving(this.options.showOn, this.eventShow);
            if (this.hideTargets) this.hideTargets.invoke('stopObserving');
        }

        if (this.eventPosition) this.element.stopObserving('mousemove', this.eventPosition);
        if (this.closeButton) this.closeButton.stopObserving();
        if (this.eventCheckDelay) this.element.stopObserving('mouseout', this.eventCheckDelay);
        this.wrapper.stopObserving();
        this.element.stopObserving(Tips.useEvent['mouseover'], this.activityEnter);
        this.element.stopObserving(Tips.useEvent['mouseout'], this.activityLeave);
    },

    showDelayed: function(event) {
        if (!this.tooltip) this.build();
        this.position(event); // follow mouse
        if (this.wrapper.visible()) return;

        this.clearTimer('show');
        this.showTimer = this.show.bind(this).delay(this.options.delay);
    },

    clearTimer: function(timer) {
        if (this[timer + 'Timer']) clearTimeout(this[timer + 'Timer']);
    },

    show: function() {
        if (this.wrapper.visible() && this.options.effect != 'appear') return;

        if (Tips.fixIE) this.iframeShim.show();
        Tips.addVisibile(this.wrapper);
        this.wrapper.show();

        if (!this.options.effect) this.tooltip.show();
        else {
            if (this.activeEffect) Effect.Queues.get(this.queue.scope).remove(this.activeEffect);
            this.activeEffect = Effect[Effect.PAIRS[this.options.effect][0]](this.effectWrapper,
        { duration: this.options.duration, queue: this.queue });
        }
    },

    hideAfter: function(event) {
        if (!this.options.hideAfter) return;
        this.cancelHideAfter();
        this.hideAfterTimer = this.hide.bind(this).delay(this.options.hideAfter);
    },

    cancelHideAfter: function() {
        if (this.options.hideAfter) this.clearTimer('hideAfter');
    },

    hide: function() {
        this.clearTimer('show');
        if (!this.wrapper.visible()) return;

        if (!this.options.effect) {
            if (Tips.fixIE) this.iframeShim.hide();
            this.tooltip.hide();
            this.wrapper.hide();
            Tips.removeVisible(this.wrapper);
        }
        else {
            if (this.activeEffect) Effect.Queues.get(this.queue.scope).remove(this.activeEffect);
            this.activeEffect = Effect[Effect.PAIRS[this.options.effect][1]](this.effectWrapper,
        { duration: this.options.duration, queue: this.queue, afterFinish: function() {
            if (Tips.fixIE) this.iframeShim.hide();
            this.wrapper.hide();
            Tips.removeVisible(this.wrapper);
        } .bind(this)
        });
        }
    },

    toggle: function(event) {
        if (this.wrapper && this.wrapper.visible()) this.hide(event);
        else this.showDelayed(event);
    },

    position: function(event) {
        Tips.raise(this.wrapper);

        var offset = { left: this.options.offset.x, top: this.options.offset.y };
        var targetPosition = Position.cumulativeOffset(this.target);
        var tipd = this.wrapper.getDimensions();
        var pos = { left: (this.options.fixed) ? targetPosition[0] : Event.pointerX(event),
            top: (this.options.fixed) ? targetPosition[1] : Event.pointerY(event)
        };

        // add offsets
        pos.left += offset.left;
        pos.top += offset.top;

        if (this.options.hook) {
            var dims = { target: this.target.getDimensions(), tip: tipd }
            var hooks = { target: Position.cumulativeOffset(this.target), tip: Position.cumulativeOffset(this.target) }

            for (var z in hooks) {
                switch (this.options.hook[z]) {
                    case 'topRight':
                        hooks[z][0] += dims[z].width;
                        break;
                    case 'topMiddle':
                        hooks[z][0] += (dims[z].width / 2);
                        break;
                    case 'rightMiddle':
                        hooks[z][0] += dims[z].width;
                        hooks[z][1] += (dims[z].height / 2);
                        break;
                    case 'bottomLeft':
                        hooks[z][1] += dims[z].height;
                        break;
                    case 'bottomRight':
                        hooks[z][0] += dims[z].width;
                        hooks[z][1] += dims[z].height;
                        break;
                    case 'bottomMiddle':
                        hooks[z][0] += (dims[z].width / 2);
                        hooks[z][1] += dims[z].height;
                        break;
                    case 'leftMiddle':
                        hooks[z][1] += (dims[z].height / 2);
                        break;
                }
            }

            // move based on hooks
            pos.left += -1 * (hooks.tip[0] - hooks.target[0]);
            pos.top += -1 * (hooks.tip[1] - hooks.target[1]);
        }

        // move tooltip when there is a different target
        if (!this.options.fixed && this.element !== this.target) {
            var elementPosition = Position.cumulativeOffset(this.element);
            pos.left += -1 * (elementPosition[0] - targetPosition[0]);
            pos.top += -1 * (elementPosition[1] - targetPosition[1]);
        }

        if (!this.options.fixed && this.options.viewport) {
            var scroll = document.viewport.getScrollOffsets();
            var viewport = Prototip.viewport.getDimensions();
            var pair = { left: 'width', top: 'height' };

            for (var z in pair) {
                if ((pos[z] + tipd[pair[z]] - scroll[z]) > viewport[pair[z]])
                    pos[z] = pos[z] - tipd[pair[z]] - 2 * offset[z];
            }
        }

        var setPos = { left: pos.left + 'px', top: pos.top + 'px' };
        this.wrapper.setStyle(setPos);
        if (Tips.fixIE) this.iframeShim.setStyle(setPos);
    }
});

Prototip.start();
