## ------------------------------------------------------------------------
## iSiLK - A python-based graphical user interface for SiLK
## ------------------------------------------------------------------------
## Copyright (c) 2007-2008 Carnegie Mellon University. All Rights Reserved.
## ------------------------------------------------------------------------
## Authors: Andrew Kompanek (ajk@cert.org)
## ------------------------------------------------------------------------
## GNU General Public License (GPL) Rights pursuant to Version 2, June 1991
## Government Purpose License Rights (GPLR) pursuant to DFARS 252.225-7013
## ------------------------------------------------------------------------
## See the LICENSE file included in this distribution
## ------------------------------------------------------------------------

""" Module encapsulating silk tools and data files """
import wx
import ws
import constants
import pdb
import datetime
import os, os.path, sys, traceback, tempfile, shutil
import re, string, time, copy, threading
import logging
import config
import config.viewoptions
import xmlutils
# Use a simple backend for random access to records
from datacache import SimpleDataCache
# Commands that do the actual work
import silk, remote
import random
import socket

# FIXME: better way to check this?
POPEN_SUCCESS_CODE=1

# FIXME: This is probably not quite correct
_hostname_cache = {}

def change_event_trigger(func):
    ''' Decorator for generating a StatusChangeEvent when a method 
        is invoked.  Used by subclasses of Change Event Source. '''
    def wrapper(self, *args, **kargs):
        result = func(self, *args, **kargs)
        if result:
            ChangeEventSource._postUpdate(self)
        return result
    return wrapper

def getHostName(ipaddr):
    global _hostname_cache
    if ipaddr in _hostname_cache:
        #print "Using cached value: %s" % _hostname_cache[ipaddr]
        return _hostname_cache[ipaddr]
    else:
        return ipaddr

def hostnameIsCached(ipaddr):

    return _hostname_cache.has_key(ipaddr)

def generateUniqueId():
    choices = list("0123456789abcdefghijklmnopqrstuvwxyz")
    uniqueId = ""
    for i in range(0,4):
        uniqueId += random.choice(choices)
    return uniqueId
    
def _buildFileName(name, uniqueId, ext):
    name = name.replace(" ", "_")
    r = re.compile(r'^[\w\s\-\_\,\.]*$')
    if not r.match(name):
        raise Exception, "Invalid name: '%s'" % name
    filename = "%s-%s.%s" % (name, uniqueId, ext)
    return filename
    
def _extractNameFromFileName(filename, ignoreUniqueId=False):
    if ignoreUniqueId:
        r = re.compile(r'^([\w\_\-]*)\.(\w*)')        
    else:
        r = re.compile(r'^([\w\_\-]*)-(\w*)\.(\w*)')
    m = r.match(filename)
    if not m:
        raise Exception, "Invalid data file name: '%s'" % filename
    if ignoreUniqueId:
        (name, ext) = m.groups()        
    else:
        (name, unique_id, ext) = m.groups()
    name = name.replace("_", " ")
    #print "Extracted name %s ext %s from %s" % (name, ext, filename)
    return name

def modifier(func):
    ''' Decorator that indicates that after invoking a method the
        problem set should be marked as having been modified. '''
    def wrapper(self, *args, **kargs):
        result = func(self, *args, **kargs)
        if self.GetProblemSet():
            self.GetProblemSet().SetModified(True)
        return result
    return wrapper
   

class ResultRunContext(object):
    def __init__(self, result, runRemote=False, runDir=None):
        self.result = result
        self.sshOptions = self.result.GetSSHOptions()
        self.runRemote = runRemote
        if not runDir:
            if runRemote:
                self.runDir = self.result.GetRemoteDir()
            else:
                self.runDir = self.result.GetLocalDir()
        else:
            self.runDir = runDir
    def GetSSHOptions(self):
        return self.sshOptions
    def _UpdateStatus(self, loc=None, status=None, percentComplete=None):
        self.result._UpdateStatus(loc, status, percentComplete)
    def _LogCommand(self, cmd):
        self.result._LogCommand(cmd)
    def _ResultFileAvailable(self, label, filename):
        pass


class ChangeEventSource(object):
     ''' Mixin class for classes that generate StatusChangeEvents when
         certain methods are invoked.  The _postUpdate method can also be
         invoked within long-running methods that alter state. '''
     class StatusChangeEvent (wx.PyCommandEvent):
        def __init__(self, dataset, id):
            wx.PyCommandEvent.__init__(self, ws.EVTTYPE_UPDATE_STATUS, id)
            self.data = dataset
        def GetData(self):
            return self.data

     class CommandLogEvent (wx.PyCommandEvent):
        def __init__(self, dataset, id, cmd):
            wx.PyCommandEvent.__init__(self, ws.EVTTYPE_LOG_COMMAND, id)
            self.data = dataset
            self.cmd = cmd

     class ResultChangeEvent (wx.PyCommandEvent):
         def __init__(self, dataset, id):
             wx.PyCommandEvent.__init__(self, ws.EVTTYPE_RESULT_CHANGED, id)
             self.data = dataset

     def __init__(self):
        self._changeListeners = []
        self._logListeners = []

     def _postEvent(self, evt):
          global eventCount
          if len(self._changeListeners) == 0:
              return
          eventCount += 1
          for l in self._changeListeners:
              wx.PostEvent(l, evt)
              
     def _postLogEvent(self, evt):
          global eventCount
          if len(self._logListeners) == 0:
              return
          eventCount += 1
          for l in self._logListeners:
              wx.PostEvent(l, evt)

     def _postLogMessage(self, cmd):
          evt = ChangeEventSource.CommandLogEvent(self, -1, cmd)
          self._postLogEvent(evt)

     def _postUpdate(self):
          evt = ChangeEventSource.StatusChangeEvent(self, -1)
          self._postEvent(evt)
          #print "***Posted status change event ***"

     def _postResultChanged(self):
          evt = ChangeEventSource.ResultChangeEvent(self, -1)
          self._postEvent(evt)

     # FIXME: Use the wxPython bind mechanism somehow?
     def AddChangeListener(self, win):
         if not win in self._changeListeners:
             self._changeListeners.append(win)
             
     def AddLogListener(self, win):
         if not win in self._logListeners:
              self._logListeners.append(win)

# FIXME: problem set should be an event source too
# FIXME: base directory name on problem set name?
class ProblemSet(ChangeEventSource):
    # FIXME: don't make this global
    _xml_filename = None
    
    def __init__(self, createDirectory=True, uniqueId=None):
        # ProblemSet can source change events
        ChangeEventSource.__init__(self)
        self._xml_children = ["uniqueId",
                              "name",
                            #"filename", 
                              #"localOutputDir",
                              #"remoteOutputDir",
                              "sshOptions",
                              "children", "commandLineLog", "notes"]
        self._modified = True
        self.notes = ""
        self.children = []
        self.fileinfo = {}
        self._directoryExists = False
        self.name = "Untitled Problem Set"
        self.uniqueId = uniqueId
        if not self.uniqueId:
            self.uniqueId = generateUniqueId()
        self.labels = ["none"]
        dirName = _buildFileName(self.name, self.uniqueId, "isilk")
        
        # Initialize SSH configration based on defaults
        self.sshOptions = config.silk.getSSHDefaults()
        # If currently loading, then set the local directory to where
        # we're loading from
        if ProblemSet._xml_filename:
            (self.localOutputDir, f) = \
                os.path.split(ProblemSet._xml_filename)
        else:
            self.localOutputDir = \
                os.path.join(config.silk.opts["LOCAL_OUTPUT_DIR"], dirName)
        print "self.localOutputDir = %s" % self.localOutputDir
        #self.remoteOutputDir = remote.join(self.sshOptions["REMOTE_OUTPUT_DIR"], dirName)
        #print "self.remoteOutputDir = %s" % self.remoteOutputDir
        self.filename = os.path.join(self.localOutputDir, "index.xml")
        self.commandLineLog = []
        # Create local directory if it doesn't exist
        if not os.path.exists(self.GetLocalDir()):
            os.makedirs(self.GetLocalDir())
        # Create remote directories if they don't exist yet
        #if createDirectory:
        #    self.CreateDirectory()
            #self._createScratchProblemSet()

        self.currentCommand = ""

    def GetSSHOptions(self):
        return self.sshOptions

    def DirectoryExists(self):
        return self._directoryExists
        
    def CreateDirectory(self):
        if not self._directoryExists:
            self._createDirectories()
            
    def _createDirectories(self):
        tempDir = os.path.join(self.GetLocalDir(), "scratch")
        if not os.path.exists(self.GetLocalDir()):
            os.makedirs(self.GetLocalDir())
        #if not os.path.exists(tempDir):
        #    os.makedirs(tempDir)
        context = ResultRunContext(self, True, runDir=".")
        silk.create_remote_directory(context, self.GetRemoteDir())
        self._directoryExists = True
    
    def RenameDirectory(self, newDirName):
        # TO DO NOW: don't let the user do this if processes are running
        
        # TO DO NOW: Verify if it exists already -- if so prompt
        # Rename it
        (path, ps_dir) = os.path.split(self.localOutputDir)
        newDirName = os.path.join(path, "Test_" + ps_dir)
        print "Renaming %s as %s" % (self.localOutputDir, newDirName)
        os.rename(self.localOutputDir, newDirName)
        # Change the local output directory
        self.localOutputDir = newDirName
        # Make sure everything is saed
        self.Save()
    
    def Save(self):
        ''' Save a problem set to disk. '''
        x = xmlutils.xmlPickler().toxml(self)
        f = open(self.GetFileName(), "w")
        f.write(x.toxml())
        f.close()
        self._modified = False
    
    ''' Load a problem set from disk.  Returns a ProblemSet instance.
        How can I use a constructor to do this? '''
    def Load(filename):
        if not os.path.isfile(filename):
            ProblemSet._xml_filename = os.path.join(filename,
                "index.xml")
        else:
            ProblemSet._xml_filename = filename
        print "Opening file: %s" % ProblemSet._xml_filename
        f = open(ProblemSet._xml_filename, "r")
        s = f.read()
        f.close()
        # Classes expected within a document
        import resultset, graphresult
        classDict = {
            "ProblemSet"   : ProblemSet,
            "QueryResult"  : resultset.QueryResult,
            "UniqResult"  : resultset.UniqResult,
            "CountGraph"   : graphresult.CountGraph,
            "CountResult"  : resultset.CountResult,
            "FilterResult" : resultset.FilterResult,
            "SetResult"    : resultset.SetResult,
            "SimpleSplitResult" : resultset.SimpleSplitResult,
            "ViewOptions" : config.ViewOptions,
            "OptionsGroup" : config.OptionsGroup
        }
        problemSet = xmlutils.xmlPickler(classDict).parseString(s)
        print "Post-processing problem set"
        (ps_dir, xml_file) = os.path.split(ProblemSet._xml_filename)
        print "Setting directory to %s" % ps_dir
        problemSet.localOutputDir = ps_dir
        ProblemSet._xml_filename = None
        print "Checking to see if remote directory exists"
        context = ResultRunContext(problemSet, True, runDir=".")
        if silk.remote_directory_exists(context, problemSet.GetRemoteDir()):
            problemSet._directoryExists = True
        return problemSet
    Load = staticmethod(Load)
    
    def _xml_children_(self):
        return self._xml_children
    
    def _xml_preprocess_(self, parent):
        pass
        
    def _xml_postprocess_(self):
        pass
        
    def GetName(self):
        return self.name
        return self.uniqueId + ".isilk"
        
    @change_event_trigger
    def SetName(self, name):
        return self._setName(name)
        
    def _setName(self, name):
        if name == self.name:
            return False
        name = name.replace("_", " ")
        remoteDocDir = self.sshOptions["REMOTE_OUTPUT_DIR"]
        localDocDir = config.silk.opts["LOCAL_OUTPUT_DIR"]
        dirName = _buildFileName(name, self.uniqueId, "isilk")
        newLocalDirName  = os.path.join(localDocDir, dirName)
        newRemoteDirName = remote.join(remoteDocDir, dirName)
        print "Renaming local directory: %s as %s " % (self.GetLocalDir(), newLocalDirName)
        os.rename(self.localOutputDir, newLocalDirName)
        # TO DO NOW: If remote directory exists
        if self.DirectoryExists():
            print "Renaming remote directory: %s as %s " % (self.GetRemoteDir(), newRemoteDirName)
            context = ResultRunContext(self, True, runDir=self.GetRemoteDir())
            silk.rename_remote_file(context, self.GetRemoteHost(), ".", 
                                    self.GetRemoteDir(), 
                                    newRemoteDirName)
        self.name = name
        self.localOutputDir = newLocalDirName
        self.Save()
        return True

    
    def GetParent(self): 
        return None
    
    def GetProblemSet(self):
        return self
    
    def GetInfo(self):
        return self.fileinfo
        
    def GetFileInfo(self, attr):
        if attr in self.fileinfo:
            return self.fileinfo[attr]
        else:
            return "(unknown)"
        
    def SetNotes(self, notes):
        self.notes = notes
        return True

    def GetNotes(self):
        return self.notes
        
    def GetLabels(self):
        return self.labels
    
    def GetFileName(self):
        return os.path.join(self.localOutputDir, "index.xml")
        
    def GetLocalPath(self):
        return self.GetLocalDir()
    
    def GetLocalDir(self):
        return self.localOutputDir
        
    def _LogCommand(self, cmd):
        self._postLogMessage(cmd)
        self.AddCommandToLog(cmd + "\n")
        self.currentCommand = cmd
        
    def _UpdateStatus(self, loc=None, status=None, percentComplete=None):
        pass    
    
    def GetRemoteHost(self):
        return self.sshOptions["REMOTE_HOST"]
        
    def GetRemoteUser(self):
        return self.sshOptions["REMOTE_USER"]
    
    def GetRemoteDir(self):
        dirName = _buildFileName(self.name, self.uniqueId, "isilk")
        return remote.join(self.sshOptions["REMOTE_OUTPUT_DIR"],
            dirName)
        
        #return self.remoteOutputDir
    
    def GetDataSets(self):
        return self.children
        
    def GetChildren(self):
        return self.children
    
    def AddChild(self, dataset):
        self.children.append(dataset)
    
    @modifier
    # FIXME:  rethink the return value here
    def DeleteChild(self, dataset):
      for c in self.children:
          if c == dataset:
              self.children.remove(c)
              c.Delete()
      return False
      
    def HasActiveQueries(self):
        for c in self.children:
            if c.IsProcessing():
                return True
        return False
        
    def GetActiveQuery(self):
        for c in self.children:
            if c.IsProcessing():
                return c
        return None
    
    # some child is processing
    def HasActiveChildren(self):
        for c in self.children:
            if c.IsProcessing():
                return True
            if c.HasActiveChildren():
                return True                
        return False
        
    def IsDeleted(self):
        return False
    
    def IsFilterResult(self):
        return False
        
    def IsToolResult(self):
        return False
    
    def IsSetResult(self):
        return False
        
    def IsGraphResult(self):
        return False
    
    def IsFileAvailable(self):
        return True
        
    def GetRemotePath(self):
        return self.GetRemoteDir()
    
    def IsLocalFileAvailable(self):
        return True
    
    def IsRemoteFileAvailable(self):
        return True
    
    def IsModified(self):
        return self._modified
        
    def SetModified(self, isModified):
        self._modified = isModified
        
    def AddCommandToLog(self, msg):
        #self._postLogMessage(cmd)
        msg = silk.format_log_message(self.GetRemoteUser(),
            self.GetRemoteHost(), msg)
        self.commandLineLog.append(msg)
        
    def GetCommandLog(self):
        return self.commandLineLog

    # FIXME: should my DataInfoPanel rely on this full interface?
    def GetCommandLines(self):
        return []
        
    def GetStatus(self):
        return ""
        
    def IsCopying(self):
        return False
        
    def IsLoading(self):
        return False
        
    def IsProcessing(self):
        return False

    def GetCurrentCommand(self):
        return self.currentCommand
        
    def SetCurrentCommand(self, cmd):
        self.currentCommand = cmd
        

eventCount = 0


    
def analysis_job(func):
    ''' Decorator for running a method in a thread as a first-class 
        analysis job.'''
    def wrapper(self, *args, **kargs):
        class _JobThread (threading.Thread):
           def __init__(self, dataset):
               threading.Thread.__init__(self)
               self.dataset = dataset
           def run(self):
               try:
                   result = func(self.dataset, *args, **kargs)
               except silk.CommandError, e:
                   self.dataset._UpdateStatus(loc=constants.DELETED,
                        status="Tool invocation failed")
                   if wx.GetApp():
                       wx.GetApp().ShowError(e.details, e.summary)
                   else:
                       print "(analysis_job) ERROR: %s\n\n%s" % (e.summary, e.details)
               self.dataset.workerThread = None
        self.workerThread = _JobThread(self)
        self.workerThread.start()
    return wrapper


class AnalysisResult(ChangeEventSource, SimpleDataCache):
                       
    ( UNKNOWN,
      FILTER_RESULT,
      COUNT_RESULT,
      TREND_RESULT,
      SET_RESULT,
      GRAPH_RESULT,
      WEB_RESULT
    ) = range(0,7)
    
    def __init__(self, resultType, namePrefix, fileExtension, parent, opts):
        print "  opts = %s" % opts
        # All results can source change events
        ChangeEventSource.__init__(self)
        SimpleDataCache.__init__(self)
        # Initialize name and identification
        self.resultType = resultType
        self.uniqueId = generateUniqueId()
        print "in AnalysisResult.__init__: %s" % self
        print "  opts = %s" % opts
        if namePrefix == None:
            namePrefix = "Analysis Result"
        namePrefix = namePrefix.replace("_", " ")
        self.name = namePrefix
        self.fileExtension = fileExtension
        self.outputFileName =  _buildFileName(self.name, self.uniqueId, fileExtension)
        self.fileinfo = {}
        self.fields = []
        if parent == None:
            self.labels = []
        else:
            self.labels = parent.labels
        self.tool = None
        self.notes = ""
        if self.name == None: self.name = self.uniqueId
        self.parentResult  = parent
        self.opts = opts
        self.viewOptions = config.ViewOptions(copy.copy(config.viewoptions.opts))
        # Initialize other state
        self.availableRemote = False
        self.availableLocal = False
        self.location = constants.UNINITIALIZED
        self.percentComplete = None
        self.status = "Uninitialized"
        self.byteCount = -1
        self.children = []
        if self.parentResult:
            self.parentResult.AddChild(self)

        self.currentCommand = ""
        return
    
    # Methods implemented in subclass
    def _xml_children_(self):
        return self._xmlChildren

    def _xml_preprocess_(self, parentResult):
        pass
    
    def _xml_postprocess_(self):
        if self.availableLocal:
            print "Initializing cache: %s is available locally"
            self._InitLocalCache()
        
    def SetAscFileSize(self, size):
        self.asc_file_size = size
        
    def GetAscFileSize(self):
        return self.asc_file_size
        
    @analysis_job
    def DownloadFile(self):
        # Wait for analysis to complete if running
        if self.GetTool():
            self.GetTool()._waitToComplete()
        print "Analysis complete"
        
        self._UpdateStatus(constants.COPYING, status="Copying file", percentComplete=10)
        # FIXME: abstraction for this?   
        logging.info("Waiting for processing to complete before copying")
        print "Waiting for processing to complete before copying"
        while self.tool and self.tool.workerThread:
            self.tool.workerThread.join(5)
        logging.info("Analyis complete -- copying result")
        print "Analyis complete -- copying result"
        self._startUpdateThread()
        
        try:
            # Only copy the actual binary file if we're not working remotely
            if not config.silk.workRemotely() and (self.IsFilterResult() or self.IsSetResult()):
                silk.copy_file(self, self.GetRemoteHost(),
                    self.GetRemotePath(), self.GetLocalPath())
            # Copy the ascii file
            if self.IsToolResult():
                silk.copy_file(self, self.GetRemoteHost(),
                    self.GetRemotePath(), self.GetLocalPath())        
            if self.IsFilterResult():
                silk.copy_file(self, self.GetRemoteHost(),
                    self.GetRemotePath()+".asc", self.GetLocalPath()+".asc")
            if self.IsSetResult():
                silk.copy_file(self, self.GetRemoteHost(),
                    self.GetRemotePath()+".asc", self.GetLocalPath()+".asc")
        except silk.CommandError, e:
            if wx.GetApp():
                wx.GetApp().ShowError(e.details, e.summary)
            # Reset as if we didn't do the copy
            self._UpdateStatus(constants.AVAILABLE_REMOTE,
                "Fatal error downloading data")
            return
        except Exception, e:
            print "***DOWNLOAD ERROR***"
            dlg = wx.MessageDialog(None, str(e),
                "Download Error", wx.ICON_EXCLAMATION)
            dlg.ShowModal()
            # Reset as if we didn't do the copy            
            self._UpdateStatus(constants.AVAILABLE_REMOTE,
                "(Fatal error downloading data)")
            return
        self._UpdateStatus(constants.LOADING, status="Processing record data",
            percentComplete=99)
        self._InitLocalCache()
        self._UpdateStatus(constants.AVAILABLE_LOCAL,
            percentComplete=100)
        
    def UploadFile(self):
        logging.info("Waiting for processing to complete before copying")
        print "Waiting for processing to complete before copying"
        while self.tool.workerThread:
            self.tool.workerThread.join(5)
        logging.info("Analyis complete -- copying result")
        print "Analyis complete -- copying result"
        self._startUpdateThread()
        silk.upload_file(self, self.GetLocalPath(), self.GetRemoteHost(),
            self.GetRemotePath())
        self._UpdateStatus(constants.AVAILABLE_REMOTE, percentComplete=100)
       
    def BindFile(self, sourcefilename, copyFile=False):
        if copyFile:
            filename = self.uniqueId + "_" + os.path.split(sourcefilename)[-1]
            destfilename = os.path.join(self.GetLocalDir(), filename)
            shutil.copyfile(sourcefilename, destfilename)
        else:
            filename = sourcefilename
        self.outputFileName = filename
        self._InitLocalCache()
        
    def _InitLocalCache(self):
        if self.IsFilterResult():
            runCut = True
            cacheFileName = self.GetLocalPath() + ".asc"
        elif self.IsToolResult():
            runCut = False
            cacheFileName = self.GetLocalPath()
        elif self.IsSetResult():
            runCut = True
            cacheFileName = self.GetLocalPath() + ".asc"
        elif self.resultType == AnalysisResult.GRAPH_RESULT:
            return
        else:
            raise Exception, "INTERNAL ERROR - Can't initialize cache"

        # Prepare the cache/storage for the data itself
        if self.IsSetResult():
            # Cache is based on rwcut output
            if runCut and not os.path.exists(self.GetLocalPath() + ".asc"):
                context = ResultRunContext(self, False)
                silk.setcat(context, self.GetFileName(), self.GetFileName() + ".asc")
            # Initialize the cache
            SimpleDataCache._cacheData(self, cacheFileName) #, self.recCount)
            self._UpdateStatus(constants.AVAILABLE_LOCAL, status="Loaded")
        else:
            print "Seeing if file exists: %s" % self.GetLocalPath() + ".asc"
            # Cache is based on rwcut output
            if runCut and not os.path.exists(self.GetLocalPath() + ".asc"):
                opts = {"fields" : ",".join(self.fields)}
                context = ResultRunContext(self, False)
                silk.cut(context, self.GetFileName(), self.GetFileName() + ".asc", **opts)
            # Initialize the cache
            print "Initializing the cache"
            cached = SimpleDataCache._cacheData(self, cacheFileName) #, self.recCount)
            if not cached:
                return
            self._UpdateStatus(constants.AVAILABLE_LOCAL, status="Loaded")
            # Initialize fileinfo
            # FIXME: better place for this?
            if self.resultType != AnalysisResult.FILTER_RESULT:
                self.fileinfo["count-records"] = self.GetRecordCount()
                self.fileinfo["file-size"] = os.stat(self.GetLocalPath()).st_size
    
    def GetFileRecordCount(self):
        if "count-records" in self.fileinfo:
            return self.fileinfo["count-records"]
        return 0
    
    # Properties - basic attributes
    def GetParent(self):
        return self.parentResult
        
    def GetParentQuery(self):
        print "In GetParentQuery on %s" % self
        if self.IsQueryResult():
            print "I'm a query"
            return self
        elif self.GetParent() != None:
            print "Get parent's parent query"
            return self.GetParent().GetParentQuery()
        else:
            print "No parent"
            return None
    
    def GetProblemSet(self):
        if self.GetParent():
            return self.GetParent().GetProblemSet()
        return None
        
    def GetSSHOptions(self):
        return self.GetProblemSet().GetSSHOptions()
        
    def IsFilterResult(self): 
        return self.resultType == AnalysisResult.FILTER_RESULT
        
    def IsQueryResult(self):
        return False
        
    def IsSetResult(self):
        return self.resultType == AnalysisResult.SET_RESULT

    def IsToolResult(self):
        return self.IsUniqResult() or self.IsCountResult()
        
    def IsUniqResult(self):
        return self.resultType == AnalysisResult.COUNT_RESULT
        
    def IsCountResult(self):
        return self.resultType == AnalysisResult.TREND_RESULT
        
    def IsGraphResult(self):
        return self.resultType == AnalysisResult.GRAPH_RESULT

    def GetName(self): 
        return self.name
        
    # Get the tool used to generate this data
    def GetTool(self):
        return self.tool
        
    def GetOptions(self):
        return self.opts
    
    def _setName(self, name):
        if name == self.name:
            return False
        name = name.replace("_", " ")
        self.name = name
        filename = _buildFileName(name, self.uniqueId, self.fileExtension)
        print "Renaming %s as %s" % (self.outputFileName, filename)
        # Renaame remote files
        if self.IsRemoteFileAvailable():
            context = ResultRunContext(self, True, runDir=self.GetRemoteDir())
            silk.rename_remote_file(context, self.GetRemoteHost(), self.GetRemoteDir(), 
                                    self.outputFileName, filename)
            if self.IsFilterResult() or self.IsSetResult():
                silk.rename_remote_file(context, self.GetRemoteHost(), self.GetRemoteDir(), 
                                        self.outputFileName + ".asc", filename + ".asc")
        if self.IsLocalFileAvailable():
            if os.path.exists(os.path.join(self.GetLocalDir(), self.outputFileName)):
                os.rename(os.path.join(self.GetLocalDir(), self.outputFileName), 
                      os.path.join(self.GetLocalDir(), filename))
            if os.path.exists(os.path.join(self.GetLocalDir(), self.outputFileName+ ".asc")):
                os.rename(os.path.join(self.GetLocalDir(), self.outputFileName+".asc"), 
                          os.path.join(self.GetLocalDir(), filename+".asc"))
        self.outputFileName = filename
        return True
        
    @change_event_trigger
    def SetName(self, name):
        return self._setName(name)
        
    @change_event_trigger
    def SetNotes(self, notes):
        self.notes = notes
        return True
        
    def GetNotes(self):
        return self.notes
    
    def GetFileName(self):
        return self.outputFileName
    
    def GetLocalDir(self):
        return self.GetParent().GetProblemSet().GetLocalDir()
    
    def GetRemoteDir(self):
        return self.GetParent().GetProblemSet().GetRemoteDir()
    
    def GetRemoteHost(self):
        return self.GetParent().GetProblemSet().GetRemoteHost()
    
    def GetRemoteAscPath(self):
         if self.IsFilterResult() or self.IsSetResult():
             return self.GetRemotePath() + ".asc"
         else:
             return self.GetRemotePath()

    def GetLocalAscPath(self):
        if self.IsFilterResult() or self.IsSetResult():
            return self.GetLocalPath() + ".asc"
        else:
            return self.GetLocalPath()
        
    def GetBytesCopied(self):
        # FIXME: this is not a good design
        localPath = self.GetLocalPath()
        # If working remotely look at the ascii file
        if config.silk.workRemotely():
            if self.IsFilterResult() or self.IsSetResult():
                localPath = localPath + ".asc" 
        # Return zero if we're not copying yet or the file isn't present
        if not os.path.exists(localPath):
            return 0
        if self.location < constants.COPYING: return 0
        size = os.stat(localPath).st_size
        return size
    
    def GetStatus(self): 
        return self.status
    
    def GetRemotePath(self):
        return remote.join(self.GetRemoteDir(), self.GetFileName())
    
    def GetLocalPath(self):
        return os.path.join(self.GetLocalDir(), self.GetFileName())
    
    '''Get the full local path name of the data if it has been downloaded,
        otherwise the path to the file on the remote host prefixed with
        <hostname>:'''
    def GetPath(self, addHostPrefix=False):
        if self.location == constants.UNINITIALIZED:
           return None
        elif self.IsLocalFileAvailable():
           return self.GetLocalPath()
        else:
           remote_dir = self.GetRemotePath()
           if addHostPrefix:
                remote_dir = "%s:%s" % (self.GetRemoteHost(), remote_dir)
           return remote_dir
           
    def GetViewOptions(self):
        return self.viewOptions

    def GetViewOption(self, option, def_val=None):
        return self.viewOptions.get(option, def_val)
    
    def GetInfo(self): return self.fileinfo
    
    def GetFields(self): return copy.copy(self.fields)
    
    def GetLabels(self): return self.labels
    
    def GetFileInfo(self, attr):
        if attr in self.fileinfo:
            return self.fileinfo[attr]
        else:
            return "(unknown)"
    
    # FIXME: cancel this thread if copy fails
    def _startUpdateThread(self):
        class _UpdateThread (threading.Thread):
            def __init__(self, dataset):
                threading.Thread.__init__(self)
                self.dataset = dataset
            def run(self):
                # Only monitor while copying
                while self.dataset.location == constants.COPYING or self.dataset.location == constants.LOADING:
                    self.dataset._postUpdate()
                    time.sleep(0.2)
                self.updateThread = None
        self.updateThread = _UpdateThread(self)
        self.updateThread.start()
    
    # Current processing state for the data
    @change_event_trigger
    @modifier
    def _UpdateStatus(self, loc=None, status=None, percentComplete=None):
        if loc != None:
            self.location = loc
        if status != None:
            self.status = status
            print "Set status to '%s'" % self.status
        if percentComplete != None:
            self.percentComplete = percentComplete
        if self.location == constants.AVAILABLE_REMOTE:
            self.availableRemote = True
        if self.location == constants.AVAILABLE_LOCAL:
            self.availableLocal = True
        return True
    
    def _LogCommand(self, cmd):
        self.GetProblemSet()._LogCommand(cmd)
        #self._postLogMessage(cmd)
        #self.GetProblemSet().AddCommandToLog(cmd)
        return
        
    def GetAscFileSize(self):
        return self.asc_file_size
    
    def GetLocationString(self):
        return constants.locations[self.location]
        
    def IsDeleted(self):
        return self.location == constants.DELETED
    
    def IsCopying(self):
        return self.location == constants.COPYING
    
    def IsLoading(self):
        return self.location == constants.LOADING
    
    def IsFiltering(self):
        return self.location == constants.RUNNING
        
    def IsProcessing(self):
        return not self.IsFileAvailable()
    
    def IsFileAvailable(self):
        if self.IsRemoteFileAvailable() or self.IsLocalFileAvailable():
            return True
        else: return False
    
    def IsRemoteFileAvailable(self):
        return self.availableRemote
        return self.location >= constants.AVAILABLE_REMOTE
    
    def IsLocalFileAvailable(self):
        return self.availableLocal
        return self.location >= constants.AVAILABLE_LOCAL
    
    # Children
    
    def GetChildren(self):
        return self.children
    
    @modifier
    def AddChild(self, dataset): 
        self.children.append(dataset)
        return True
    
    @modifier
    def Delete(self):
      # Delete local file associated with this instance
      if os.path.exists(self.GetLocalPath()):
        os.remove(self.GetLocalPath())
      # FIXME - hack; should be consistent
      if self in self.parentResult.children:
          self.parent.children.remove(self)
      for c in self.children:
          self.children.remove(c)
          c.Delete()
          return True
      return False

    @modifier
    def DeleteChild(self, dataset):
      for c in self.children:
          if c == dataset:
              self.children.remove(c)
              c.Delete()
              return True
      raise "Can't delete: dataset is not among children"
    
    def HasActiveChildren(self):
        for c in self.children:
            if c.IsProcessing():
                return True
            if c.HasActiveChildren():
                return True
        return False
        
    # Convenience methods
    def GetCommandLines(self):
        if self.fileinfo and "command-lines" in self.fileinfo:
            return self.fileinfo["command-lines"]
        else:
            return []
            
    def GetLastCommandLine(self):
        if self.fileinfo and "command-lines" in self.fileinfo:
            lines = self.fileinfo["command-lines"]
            if len(lines) != 0:
                return lines[len(lines)-1]
            else:
                return ""
        else:
            return ""
            
    def GetRecFieldAsString(self, index, field,
        lookupPortAndProtocol=True, lookupHostName=True):
        if field == "nhip" and self.labels:
            val = SimpleDataCache.GetRecFieldAsString(self, index, field)
            val = val.split(".")
            val = int(val[-1])
            return self.labels[val]
        val = SimpleDataCache.GetRecFieldAsString(self, index, field)
        if lookupHostName and (field == "sip" or field == "dip"):
            val = getHostName(val)
        if not lookupPortAndProtocol:
            return val
        if field == "sport" or field == "dport":
            val = config.viewoptions.getPortName(val, val)
        if field == "proto":
            val = config.viewoptions.getProtoName(val, val)
        return val
        
    def cacheHostName(self, ipaddrs):
        _thread = threading.Thread(target = self._run_hostnames, args = [ipaddrs])
        _thread.start()

    def legacyCacheHostName(self, ipaddrs):

        for ip in ipaddrs:

            if hostnameIsCached(ip):
                print "skipping lookup on %s since it is cached" % (ip)
                continue

            basetime = time.time()
            try:
                print "sending request for %s" % ip
                hostname = socket.gethostbyaddr(ip)[0] + (" (%s)" % ip)
                print "processing response"
            except:
                # Use the ip address itself if we can't resolve it
                hostname = ip + " (can't resolve)"

            # Cache result and return it
            _hostname_cache[ip] = hostname

            newtime = time.time()
            if newtime - basetime > 2:
                self._postResultChanged()

            basetime = newtime

        self._postResultChanged()
        return hostname

    def _run_hostnames(self, ipaddrs):
        #if adns is not installed, default to the old way
        try:
            import adns
            print "using adns for hostname lookup"
        except ImportError:
            print "adns module not found, using socket method"
            self.legacyCacheHostName(ipaddrs)
            return
        state = adns.init()
        rs = {}
        for ip in ipaddrs:
            basetime = time.time()

            if hostnameIsCached(ip):
                print "skipping lookup on %s since it is cached" % (ip)
                continue

            rs[ip] = state.submit_reverse(ip, adns.rr.PTR)

            newtime = time.time()
            if newtime - basetime > 2:
                self.processAdnsResponse(rs)
                rs = {}
                self._postResultChanged()

            basetime = newtime

        self.processAdnsResponse(rs)
        self._postResultChanged()

    def processAdnsResponse(self, rs):

        for k, v in rs.items():

            try:
                hostname = v.wait()[3][0]
            except IndexError:
                hostname = k + " (can't resolve)"

            _hostname_cache[k] = hostname

    def GetCurrentCommand(self):
        return self.currentCommand
        
    def SetCurrentCommand(self, cmd):
        self.currentCommand = cmd
