define(function(require, exports, module) {
"use strict";

var oop = require("ace/lib/oop");
var event = require("ace/lib/event");
var Range = require("ace/range").Range;
var EventEmitter = require("ace/lib/event_emitter").EventEmitter;

var HoverLink = function(editor) {
    if (editor.hoverLink)
        return;
    editor.hoverLink = this;
    this.editor = editor;

    this.update = this.update.bind(this);
    this.onMouseMove = this.onMouseMove.bind(this);
    this.onMouseOut = this.onMouseOut.bind(this);
    this.onClick = this.onClick.bind(this);
    this.onContextMenu = this.onContextMenu.bind(this);
    event.addListener(editor.renderer.scroller, "mousemove", this.onMouseMove);
    event.addListener(editor.renderer.content, "mouseout", this.onMouseOut);
    event.addListener(editor.renderer.content, "click", this.onClick);
    event.addListener(editor.renderer.container, "contextmenu", this.onContextMenu);
};

(function(){
    oop.implement(this, EventEmitter);
    
    this.token = {};
    this.range = new Range();

    this.update = function() {
        this.$timer = null;
        var editor = this.editor;
        var renderer = editor.renderer;
        
        var canvasPos = renderer.scroller.getBoundingClientRect();
        var offset = (this.x + renderer.scrollLeft - canvasPos.left - renderer.$padding) / renderer.characterWidth;
        var row = Math.floor((this.y + renderer.scrollTop - canvasPos.top) / renderer.lineHeight);
        var col = Math.round(offset);

        var screenPos = {row: row, column: col, side: offset - col > 0 ? 1 : -1};
        var session = editor.session;
        var docPos = session.screenToDocumentPosition(screenPos.row, screenPos.column);
        
        var selectionRange = editor.selection.getRange();
        if (!selectionRange.isEmpty()) {
            if (selectionRange.start.row <= row && selectionRange.end.row >= row)
                return this.clear();
        }
        
        var line = editor.session.getLine(docPos.row);
        if (docPos.column == line.length) {
            var clippedPos = editor.session.documentToScreenPosition(docPos.row, docPos.column);
            if (clippedPos.column != screenPos.column) {
                return this.clear();
            }
        }
        
        var token = this.findLink(docPos.row, docPos.column);
        // var isFocused = editor.isFocused();
        this.link = token;
        if (!token) {
            return this.clear();
        }
        if (!this.isOpen) {
            this.isOpen = true;
        }
        // token.isFocused = isFocused;
        editor.renderer.setCursorStyle("pointer");
        
        session.removeMarker(this.marker);
        
        this.range = this.getRange(token);
        this.marker = session.addMarker(this.range, "ace_link_marker", "text", true);
    };
    
    this.clear = function() {
        if (this.isOpen) {
            this.editor.session.removeMarker(this.marker);
            this.editor.renderer.setCursorStyle("");
            this.isOpen = false;
        }
    };
    
    this.getMatchAround = function(regExp, string, col) {
        var match;
        regExp.lastIndex = 0;
        string.replace(regExp, function(str) {
            var offset = arguments[arguments.length-2];
            var length = str.length;
            if (offset <= col && offset + length >= col)
                match = {
                    start: offset,
                    value: str
                };
        });
    
        return match;
    };
    
    this.onClick = function(e) {
        if (!this.editor.isFocused())
            return;
        if (e.detail != 1)
            return;
        if (this.link && this.isOpen) { // && this.link.isFocused
            if (this.editor.selection.isEmpty()) {
                this.editor.selection.setSelectionRange(this.range);
                this.lastRange = this.range;
            }
            
            this.link.editor = this.editor;
            this.link.x = e.clientX;
            this.link.y = e.clientY;
            this.link.metaKey = e.metaKey;
            this.link.ctrlKey = e.ctrlKey;
            this._signal("open", this.link);
        }
    };
    
    this.onContextMenu = function(e) {
        var range = this.editor.selection.getRange();
        if (!range.isEmpty()) {
            if (this.lastRange && range.isEqual(this.lastRange)) {
                this.editor.selection.clearSelection();
                this.update();
            }
        }
        if (this.link && this.isOpen) {
            if (this.editor.selection.isEmpty()) {
                this.editor.selection.setSelectionRange(this.range);
                this.lastRange = this.range;
            }
        }
    };
    
    this.getRange = function(token) {
        var session = this.editor.session;
        var startCol = token.start;
        var row = token.row;
        var line = session.getLine(row);
        while (line && line.length < startCol) {
            startCol -= line.length;
            row++;
            line = session.getLine(row);
        }
        var startRow = row;
        var endCol = startCol + token.value.length;
        while (line && line.length < endCol) {
            endCol -= line.length;
            row++;
            line = session.getLine(row);
        }
        var endRow = row;
        return new Range(startRow, startCol, endRow, endCol);
    };
    
    this.findLink = function(row, column) {
        var editor = this.editor;
        var session = editor.session;
        var lineData = session.getLineData(row);
        var prevLineData = session.getLineData(row - 1);
        var line = session.getLine(row);
        for (var i = 1; lineData && lineData.wrapped; i++) {
            line += session.getLine(row + i);
            lineData = session.getLineData(row + i);
        }
        while (prevLineData && prevLineData.wrapped) {
            row --;
            var prevLine = session.getLine(row);
            column += prevLine.length;
            line = prevLine + line;
            prevLineData = session.getLineData(row - 1);
        }
        lineData = session.getLineData(row);
        
        var match = this.getMatchAround(/https?:\/\/[^\s"']+|[~\/\w.]([:.~/\w-_]|\\.)+/g, line, column);
        if (!match)
            return;
        
        match.row = row;
        var value = match.value;
        if (/^https?:|\bc9.io\b|(\d{1,3}\b.?){4}|localhost/.test(value)) {
            match.type = "link";
            var m =  /((https?:\/\/)?(\d{1,3}\b.?){4}(:\d+[^\d\/]|[^:\d\/]))/.exec(value);
            if (m)
                value = m[0].slice(0, -1);
            value = value.replace(/[>)}\].,;:]+$/, "");
            match.value = value;
            return match;
        }
        var prompt = this.findPrompt(row);
        if (!prompt) {
            if (/^(\/|~|\/?\w:[\\/])/.test(value)) { // windows or unix absolute path
                match.type = "path";
                match.value = value.replace(/["'>)}\].,;:]+$/, "");
                return match;
            }
            return;
        }
        
        if (lineData.prompt && match.start <= prompt.index) {
            match.value = value = value.substr(prompt.index - match.start);
            match.start = prompt.index;
            if (match.start > column || !value)
                return;
            return;
        }
        
        match.command = prompt.command;
        match.args = prompt.args;
        match.basePath = prompt.path;
        
        if (prompt.command === "ls") {
            match.type = "path";
            if (lineData.prompt)
                return;
            // update basepath for ls /dir/name    
            if (prompt.args)
                match.basePath = this.findBasePath(prompt.args, prompt.path);
            if (prompt.args && /( |^)(--help|-h)\b/.test(prompt.args))
                return;
            var longStyle = /(^|\s)[ld\-][\-rwx]+\s/.test(line);
            if (longStyle) {
                if (/[*@|=>]$/.test(line))
                    line = line.slice(0, -1);
                if (match.start + value.length < line.length) {
                    var isLink = line[0] == "l" && line.substr(match.start + value.length, 4) == " -> ";
                    if (!isLink)
                        return;
                }
            } else {
                var before = line.substring(0, match.start).match(/[^\s\\\/@"']+ $/);
                var after = line.substr(match.start + value.length).match(/^ [^\s\\\/@"']+/);
                if (before && !/\d+/.test(before[0])) {
                    value = before[0] + value;
                    match.start -= before[0].length;
                }
                if (after) {
                    value += after[0];
                }
                match.value = value;
                return match;
            }
        }
        else if (prompt.command === "find") {
            if (match.start !== 0 || match.length < line.length)
                return;
            if (prompt.args)
                match.basePath = this.findBasePath(prompt.args, prompt.path);
            match.type = "path";
        }
        else if (prompt.command === "grep") {
            if (match.start !== 0)
                return;
            match.type = "path";
            match.value = value.replace(/:[^\d][^:]*$/, "");
            // match.basePath = "";
        }
        else if (prompt.command === "ack" || prompt.command === "ag" || prompt.command === "ack-grep") {
            match.type = "path";
            var fontColor = lineData[column] && lineData[column][0];
            if (match.start !== 0) {
                if (fontColor == session.term.defAttr)
                    return;
                
                var col = column;
                while (lineData[col] && lineData[col][0] == fontColor)
                    col--;
                match.start = col + 1;
                col = column;
                while (lineData[col] && lineData[col][0] == fontColor)
                    col++;
                match.value = line.substring(match.start, col);
            }
            
            var jumpLine = line.match(/^(\d*:)?/)[0];
            var jumpColumn = Math.max(match.start - jumpLine.length, 0);
            
            if (match.start == 0 && jumpLine)
                match.value = jumpLine;
            
            var pathLine = line;
            while (/^\d+/.test(pathLine) && row > prompt.row) {
                lineData = session.getLineData(row);
                if (!lineData.wrapped) {
                    pathLine = session.getLine(row);
                }
                row--;
            }
            
            match.path = pathLine;
            if (jumpLine)
                match.path += ":" + jumpLine + jumpColumn;
            
            if (match.start == 0 && jumpLine)
                match.action = "open";
        }
        else if (/^(~|\.\.?)?[\/\\]/.test(value) || /\w:[\\]/.test(value)) {
            match.type = "path";
            match.value = value.replace(/['">)}\].,;:]+$/, "");
        }
        else if (/^[ab]?\//.test(value) && /^([+\-]{3}|diff)/.test(line)) { // diff
            match.type = "path";
            match.basePath = "";
            match.start++;
            match.value = value.substr(2);
        }
        else if (prompt.command === "git") { // git status
            var prefix = line.substr(0, match.start);
            if (match.start + value.length == line.length
                && /^(#|[ MDRU?A]{2})\s+([\w\s]+:\s+)?$/.test(prefix)
            ) {
                match.type = "path";
            } else {
                value = value.replace(/[.:]$/, "");
                if (value.match(/[/][^/]*\.\w+[.:]$/) || prefix.match(/( in |merging )$/)) {
                    match.type = "path";
                    match.value = value;
                } else {
                    return;
                }
            }
        } else {
            return;
        }
        
        return match;
    };
    
    this.findBasePath = function(args, basePath) {
        var argList = args.split(" ").filter(function(x) {
            return x[0] != "-";
        });
        var pathArg = argList[argList.length - 1];
        if (pathArg) {
            if (pathArg[0] === "~" || pathArg[0] === "/")
                return pathArg;
            return basePath + "/" + pathArg;
        }
        return basePath;
    };

    this.findPrompt = function(row) {
        var promptRe = /([~\/](?:[^\\\s'"\(<]|\\.)*)[^$]*?\$/;
        var editor = this.editor;
        var session = editor.session;
        var prompt, args, command, promptRow;
        do {
            var lineData = session.getLineData(row);
            
            if (lineData.prompt)
                return lineData.prompt;
            
            var line = session.getLine(row);
            var m = promptRe.exec(line);
            if (m) {
                line = line.substr(m.index + m[0].length);
                var m2 = /^\s*([\w\-]+)/.exec(line);
                if (m2) {
                    command = m2[1];
                    args = line.substr(m2[0].length);
                }
                if (!prompt || lineData.isUserInput) {
                    prompt = {
                        path: m[1],
                        command: command,
                        index: m.index,
                        args: args,
                        lineData: lineData,
                        row: row
                    };
                }
                if (lineData.isUserInput)
                    break;
            }
        } while (row-- > 0);
        if (prompt && promptRow < session.term.ybase)
            prompt.lineData.prompt = prompt;
        return prompt;
    };

    this.onMouseMove = function(e) {
        if (e.shiftKey || e.ctrlKey || e.metaKey || !this.editor.isFocused())
            return this.clear();
        
        if (this.editor.$mouseHandler.isMousePressed) {
            if (!this.editor.selection.isEmpty())
                this.clear();
            return;
        }
        if (this.x == e.clientX && this.y == e.clientY)
            return;
        this.x = e.clientX;
        this.y = e.clientY;
        if (this.isOpen) {
            this.lastT = e.timeStamp;
        }
        this.update();
    };

    this.onMouseOut = function(e) {
        this.clear();
        this.$timer = clearTimeout(this.$timer);
    };

    this.destroy = function() {
        this.onMouseOut();
        event.removeListener(this.editor.renderer.scroller, "mousemove", this.onMouseMove);
        event.removeListener(this.editor.renderer.content, "mouseout", this.onMouseOut);
        delete this.editor.hoverLink;
    };

}).call(HoverLink.prototype);

exports.HoverLink = HoverLink;

});