# Part of the A-A-P GUI IDE: Tool class and associated stuff

# Copyright (C) 2002-2003 Stichting NLnet Labs
# Permission to copy and use this file is specified in the file COPYING.
# If this file is missing you can find it here: http://www.a-a-p.org/COPYING

import os.path
from glob import glob

import Util

# TODO: do this properly with gettext().
def _(x):
    return x

class Tool:
    """Generic Tool, to be derived by actual tools."""

    def __init__(self, name, item, action, lnum = None, col = None, off = None):
        self.name = name                # name of the tool
        self.topmodel = item.acty.topmodel # toplevel model: AIDE object
        self.itemlist = [ item ]        # Items this tool is working on
        self.currentnode = None         # Set after tool is editing the item.
        self.action = action            # action we were started for

    def setName(self, name):
        """Set the name of the tool.  Used for a tool that knows more about
           what it's doing after it started."""
        self.name = name
        self.topmodel.toollist.updateTool(self)

    def addItem(self, item):
        """Add an ActyItem that this tool is working on.  An item can be added
           multiple times."""
        if not item in self.itemlist:
            self.itemlist.append(item)

    def setCurrentItem(self, item):
        """Set the item currently edited by this Tool.  Normally called from
           foreground()."""
        if not item in self.itemlist:
            self.itemlist.append(item)
        self.currentnode = item.node

    def delItem(self, item):
        """Remove an Item from the list of items this tool is working on."""
        if item in self.itemlist:
            self.itemlist.remove(item)

    def allClosed(self):
        """Return non-zero if the tool does not have items."""
        return len(self.itemlist) == 0

    def getProperties(self):
        """Properties that this tools supports.
           To be overruled by the derived class."""
        return {}

    def close(self, shutdown):
        """Close the tool.
           Return non-zero if closing is OK.
           Should be overruled by the derived class."""
        self.topmodel.toollist.delTool(self)
        return 1        # can close

    def foreground(self, item, node = None, lnum = None):
        """Move this activity to the foreground.
           Use the specified "item", unless it's None.
           Use the specified "node", unless it's None.
           Should be overruled by the derived class."""
        if item:
            self.setCurrentItem(item)
        else:
            self.currentnode = node


class ToolList:
    """List of Tool objects."""

    def __init__(self):
        self.list = []
        self.view = None        # view for the ToolList, if any

    def __len__(self):
        return len(self.list)

    def addTool(self, tool):
        """Add a tool to the list of open tools."""
        if not tool in self.list:       # Add it only once.
            self.list.append(tool)
            if self.view:
                self.view.addTool(tool)

    def updateTool(self, tool):
        """Update the way "tool" is displayed.  Used when its name was
           changed."""
        if self.view:
            i = self.list.index(tool)
            self.view.setTool(i, tool)

    def getList(self):
        """Return the list of tools."""
        return self.list

    def getTool(self, idx):
        return self.list[idx]

    def delTool(self, tool):
        """Remove a tool from the list."""
        try:
            i = self.list.index(tool)
        except ValueError, e:
            # print "Tool does not exist in list: " + str(e)
            return
        self.list.remove(tool)
        if self.view:
            self.view.delTool(i)


def availableTools(topmodel):
    """Return the list of available Tool class names."""
    cwd = os.getcwd()
    os.chdir(topmodel.rootdir)
    l = map(lambda x: x[6:-3], glob("Tools/*.py"))
    l.sort()
    os.chdir(cwd)
    return l


def availableActions(item):
    """Return a list of action names that are possible for "item".
       Used for the context menu in navigators."""
    # Consult all available tools to find out which actions are possible
    # for this node.
    if item.node:
        type = item.node.getFiletype()
    else:
        type = ''
    alist = []
    for toolname in availableTools(item.acty.topmodel):
        exec "import " + toolname
        dict = eval(toolname + ".canDoActions(item, type)")
        for a in dict.keys():
            if not a in alist:
                alist.append(a)
    return alist


def runTool(item, action = None):
    """Run the default tool for an item.
       Perform "action" if specified.
       Return the tool."""
    node = item.node
    tool = None
    # Consult all available tools to find out which one is the best at
    # editing this node.
    if node:
        type = node.getFiletype()
    else:
        type = ''
    foundpri = 0
    for toolname in availableTools(item.acty.topmodel):
        exec "import " + toolname
        dict = eval(toolname + ".canDoActions(item, type)")
        # Prefer a tool that can do debugging, editing or viewing.
        pri = 0
        if action:
            if dict.has_key(action):
                pri = dict[action]
                act = action
        else:
            if dict.has_key("debug"):
                pri = dict["debug"] * 3
                act = "debug"
            elif dict.has_key("edit"):
                pri = dict["edit"] * 2
                act = "edit"
            elif dict.has_key("view"):
                pri = dict["view"]
                act = "view"
        if pri > foundpri:
            foundtoolname = toolname
            foundpri = pri
            foundaction = act

    if foundpri > 0:
        # Open the node in the found Tool.
        tool = runToolByName(foundtoolname, item, foundaction)
    else:
        print "Can't find a tool for this item"

    return tool

def runToolByName(toolname, item, action):
    """Run a tool with name "toolname" for ActyItem "item"."""
    exec "import " + toolname
    props = eval(toolname + ".getProperties(item.acty.topmodel)")
    toollist = eval(toolname + ".toollist")
    if not toollist or not "mdi" in props.keys():
        # Instantiate a new tool for this item.
        tool = eval(toolname + ".openItem(item, action)")
        if not tool:
            # Failed to run the tool or the user cancelled a dialog.
            return None
        toollist.append(tool)
    else:
        # Re-use an existing tool for this item.  Just take the first one
        # (normally there is only one anyway).
        tool = toollist[0]
        tool.openItem(item, action)
    item.acty.topmodel.toollist.addTool(tool)
    node = item.node
    if node:
        node.topmodel.nodelist.addNode(node)
    item.addTool(tool, action)
    item.foreground()
    return tool


# Activity for items that are not related to an activity that the user started.
no_activity = None

def set_no_activity(topmodel):
    """Make sure no_activity is set."""
    global no_activity
    if not no_activity:
        from Activity import Activity
        from Navigator import ActyItem, Navigator
        no_activity = Activity("Hidden", topmodel)
        no_activity.addNav(Navigator("Files",
                         ActyItem("top", ".", no_activity, None, addnode = 0)))

def shutdown_no_acty():
    """Shutdown, close no_activity.  Return non-zero when closed OK."""
    if no_activity:
        # Reset the modified flag, don't ask the user whether it should be
        # saved.
        no_activity.modified = 0
        return no_activity.close(1)
    return 1


def gotoFile(topmodel, acty, fname, lnum = None, col = None, off = None):
    """Show file "fname" and position at the mentioned lnum/col or off.
       Finds an exisiting file in Activity "acty" or creates one.
       Finds an existing view or edit action or opens a new one."""

    if not acty:
        # No specific activity given, use no_activity.
        set_no_activity(topmodel)
        acty = no_activity

    item = findOrCreateItem(topmodel, acty, fname)
    gotoItem(topmodel, item, lnum, col, off)


def gotoItem(topmodel, item, lnum = None, col = None, off = None):
    """Show file "item" and position at the mentioned lnum/col or off.
       Finds an existing view or edit action or opens a new one."""
    tool = findOrRunViewTool(item)
    if not tool:
        print "Cannot go to item."
    elif tool.getProperties().get("set_position"):
        tool.goto(item, lnum = lnum, col = col, off = off)

def toolOpenedFile(tool, name):
    """Called when a tool opened a file.  May add a node to the list of nodes
       that the tool is using.
       Returns the node."""
    from Navigator import findNodeByName
    node = findNodeByName(name)
    if not node or not node.items:
        # Not an existing node or node is not used in an actyitem.
        # Create a node and/or actyitem now.
        set_no_activity(tool.topmodel)
        item = no_activity.getTopItem().addChildByName(name)
        node = item.node
    else:
        item = node.items[0]    # XXX use another one?
    tool.addItem(item)
    item.addTool(tool, "edit")

    return node

def showPCFile(topmodel, acty, fname, lnum = None, col = None, off = None,
                                                                     show = 1):
    """Show PC in file "fname" at the mentioned lnum/col or off.
       Finds an exisiting file in Activity "acty" or creates one.
       Finds an existing view or edit action or opens a new one."""
    item = findOrCreateItem(topmodel, acty, fname)
    showPCItem(topmodel, item, lnum, col, off, show)


def showPCItem(topmodel, item, lnum = None, col = None, off = None, show = 1):
    """Show PC in "item" at the mentioned lnum/col or off.
       Finds an existing view or edit action or opens a new one."""
    tool = findOrRunViewTool(item)
    if not tool:
        print "Cannot go to item."
    elif tool.getProperties().get("show_pc"):
        tool.showPC(item, lnum = lnum, col = col, off = off, show = show)


class Breakpoint:
    """Class used to store info about a breakpoint."""
    def __init__(self, tool, ID, enable, dir, fname,
                                          lnum = None, col = None, off = None):
        self.ID = ID            # breakpoint number or name
        self.enable = enable    # true or false
        if dir and not os.path.isabs(fname):
            self.fname = os.path.join(dir, fname)       # dir + file name
        else:
            self.fname = fname                          # file name
        self.lnum = lnum        # line number in fname (one based)
        self.col = col          # column number (zero based)
        self.off = off          # character offset (zero based)
        self.item = findOrCreateItem(tool.topmodel,
                                             tool.itemlist[0].acty, self.fname)

def displayBreakpoint(tool, what, bp):
    """Update breakpoint "bp" with tool "tool" with action "what".  Check if
       the tool supports this."""
    if tool.getProperties().get("display_breakpoint"):
        tool.displayBreakpoint(what, bp)

def updateBreakpoints(tool, node):
    """Called by an editor that opened a node.  Check what breakpoints
       exist for this node and call back the editor to set them."""
    for bp in getBreakpoints(node):
        tool.displayBreakpoint("new", bp)

def getBreakpoints(node):
    """Get a list of all breakpoints for "node".  Goes through the list of
       running tools and gets breakpoints from the ones that have the
       "get_breakpoints" property."""
    list = []
    for tool in node.topmodel.toollist.getList():
        if tool.getProperties().get("get_breakpoints"):
            list.extend(tool.getBreakpoints(node))
    return list

def setBreakpoint(what, node, enable, lnum = None, col = None, off = None):
    """Change a breakpoint (add/remove/enable/disable).
       Finds all tools that can set breakpoints and invokes them."""
    for tool in node.topmodel.toollist.getList():
        if tool.getProperties().get("set_breakpoint"):
            tool.setBreakpoint(what, node, enable, lnum, col, off)

def debugCmd(what, node, enable, lnum = None, col = None, off = None):
    """Pass on a debugger command (run/cont/step/next/finish).
       Finds all debuggers that can take commands and invokes them."""
    for tool in node.topmodel.toollist.getList():
        if tool.getProperties().get("debug_command"):
            tool.debugCmd(what, node, enable, lnum, col, off)


def debugEvalText(tool, text):
    """Evaluate "text" in a debugger and call back "tool" to display the result
       (if any).  Used by an editor when the mouse pointer is resting on a word
       and can display the value in a balloon."""
    for t in tool.topmodel.toollist.getList():
        if t.getProperties().get("eval_text"):
            t.evalText(tool.showBalloon, text)
            break


def findOrCreateItem(topmodel, acty, fname):
    """Find an item for "fname".  If it doesn't exist yet, add it.
       To be used by a debugger.
       Return the item."""
    item = acty.findItemByName(Util.full_fname(fname))

    if not item:
        # Use a "Debug" Navigator.  XXX: what about non-debuggers?
        mod = acty.modified
        nav = acty.findNavByName("Debug")
        if not nav:
            import Navigator
            item = Navigator.ActyItem("debug files", '.', acty, None)
            nav = Navigator.Navigator("Debug", item)
            acty.addNav(nav)
            if acty.name != "Hidden":
                acty.foreground()       # will show the new Navigator
        item = nav.getTopItem().addChildByName(fname)

        # Restore the "modified" flag on the Activity, these items will not be
        # stored in the recipe.
        acty.modified = mod

    return item

def findOrRunViewTool(item):
    """Find an existing tool for viewing "item".  If it doesn't exist, start
       one."""
    if not item.node:
        return None
    tool = item.findViewTool()
    if not tool:
        tool = runTool(item, "view")
    return tool


#
# Spawning a process for any OS (theoretically).
#
def spawn(cmd):
    """Execute "cmd" in a shell and return the process ID, which can be given
       to stillRunning() to find out the process is still running."""
    # Use a dictionary for the result, so we can add an "exited" flag.
    res = {"pid" : 0, "exited" : 0}
    if os.name == "posix":
        # XXX Forking is quite expensive...
        pid = os.fork()
        if pid == 0:
            # child process
            os.system(cmd)
            os._exit(0)
        # parent process
        res["pid"] = pid
    elif os.name in [ 'dos', 'os2', 'nt', 'win32' ]:
        # Need to get cmd.exe or command.exe name.
        shell = Util.get_shell_name()

        # XXX this doesn't work for "sh", "bash", etc...
        # This actually gives us the process handle, not a process ID.
        # Can't use os.P_DETACH, this always returns zero.
        res["pid"] = os.spawnv(os.P_NOWAIT, shell, ["/c", cmd])
    else:
        # TODO!
        os.system(cmd)
    return res

def stillRunning(proc):
    """Check process "proc", returned by spawn() above, is still running."""
    if proc["exited"]:
        return 0
    if os.name == "posix":
        pid, status = os.waitpid(proc["pid"], os.WNOHANG)
        if pid != 0 and os.WIFEXITED(status):
            proc["exited"] = 1
            return 0
    elif os.name in [ 'dos', 'os2', 'nt', 'win32' ]:
        try:
            import win32event
            res = win32event.WaitForSingleObject(proc["pid"], 0)
            if res == win32event.WAIT_OBJECT_0:
                proc["exited"] = 1
                return 0
        except ImportError:
            # cannot wait for process because win32event is missing
            return 0
    else:
        # TODO!
        return 0

    return 1


# gettext: message translation

gettextobj = None

def init_gettext(topmodel):
    if gettextobj:
        gettextobj.bindtextdomain("agide", topmodel.rootdir + "/lang")
        gettextobj.textdomain("agide")

def _null_gettext(x):
    """Dummy gettext() function."""
    return x

# Try importing the gettext library.  If it works use its gettext() function.
try:
    import gettext
    gettextobj = gettext
    _gettext = gettext.gettext
except ImportError:
    _gettext = _null_gettext

# vim: set sw=4 et sts=4 tw=79 fo+=l: