## ------------------------------------------------------------------------
## 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
## ------------------------------------------------------------------------

''' QueryBuilder and related classes.  '''

import wx
import os.path, copy
import ws
import config.fgloboptions
import config.silk
from analysisresult import ProblemSet
from resultset import QueryResult
from tools.query import QueryTool
import wx.lib.dialogs
from constants import *
import textwrap
import silk
import tools, analysistool
import datetime
import pdb
from controls.previewcmdpanel import PreviewCommandPanel

# A few control panels this dialog depends on
from controls.fgloboptionspanel import FGlobOptionsPanel
from controls.filteroptionspanel import FilterOptionsPanel, \
    BasicFilterOptionsPanel, SizeFilterOptionsPanel, MoreFilterOptionsPanel, \
    PrefixMapFilterPanel
from controls.filteroptionspanel import ValidationError

class QueryBuilder(wx.Frame):
    
    ''' QueryBuilder is a toplevel frame used to construct and spawn a new
   query. Encapsulates a set of QueryBuilderPanels, each of which provides a
   different set of controls for selecting query options. The frame also
   contains controls for selecting the problem set to add the query to,
   buttons, and other controls that are common to each of the panels. '''
    
    def __init__(self, *args, **kargs):
        wx.Frame.__init__(self, *args, **kargs)
        #sz = wx.BoxSizer(wx.VERTICAL)
        # Current and available problem sets
        self._problemSet = None
        self._problemSets = []
        #self.SetBackgroundColour(wx.SystemSettings.GetColour(wx.SYS_COLOUR_APPWORKSPACE))
        # Create and populate notebook
        self.panel = wx.Panel(self)
        self.nb = wx.Notebook(self.panel)#, style=wx.BK_DEFAULT)
        self.bottomPanel = self._createBottomPanel(self.panel)
        self.p = [];
        self.page_size = []
        self.p.append(QueryBuilderPanel(self, self.nb, False))
        self.p.append(FilterDetailsPanel(self.nb))
        #self.p.append(QueryBuilderPanel(self.nb, True))
        self.nb.AddPage(self.p[0], "Basic Query Options")
        self.nb.AddPage(self.p[1], "More Filter Options")
        
        
        szPanel = wx.BoxSizer(wx.VERTICAL)
        szPanel.Add(self.nb, flag=wx.EXPAND|wx.LEFT|wx.RIGHT|wx.TOP, border=4)
        szPanel.Add(self.bottomPanel, flag=wx.EXPAND|wx.LEFT|wx.RIGHT|wx.BOTTOM, border=10)
        self.panel.SetSizerAndFit(szPanel)
        
        sz = wx.BoxSizer(wx.VERTICAL)
        sz.Add(self.panel, flag=wx.EXPAND|wx.LEFT|wx.RIGHT|wx.TOP, border=0)# border=16)
        #sz.Add(self.bottomPanel, flag=wx.EXPAND|wx.LEFT|wx.RIGHT|wx.BOTTOM, border=16)
        self.SetSizerAndFit(sz)
        
        # Evant handling
        self.Bind(wx.EVT_ACTIVATE, self.OnActivate)
        self.Bind(wx.EVT_CLOSE, self.OnClose)
        self.Bind(wx.EVT_BUTTON, self.OnCloseButton)
        self.Bind(wx.EVT_MENU, self.OnClose, id=ID_CLOSE_WINDOW)
        self.Bind(wx.EVT_NOTEBOOK_PAGE_CHANGED, self.OnPageChanged)
        self.Bind(wx.EVT_NOTEBOOK_PAGE_CHANGING, self.OnPageChanging)
        self.Bind(wx.EVT_BUTTON, self.OnQuery, self.btnCreate)
        self.Bind(wx.EVT_BUTTON, self.OnValidate, self.btnValidate)
        self.Bind(wx.EVT_COMBOBOX, self.OnProblemSet, self.cbProblemSet)
        # Handle any option change event for a control fglob or filter panel
        self.Bind(ws.EVT_OPTION_CHANGE, self.OnOptionChange) #
        self.Bind(wx.EVT_CHECKBOX, self.OnOptionChange, self.chkUseFail)
    
    def _createBottomPanel(self, parent):
        # --- Controls at bottom ---
        panel = wx.Panel(parent)
        self.previewPanel = PreviewCommandPanel(panel,  "Not yet initialized")
        # Name and problem set selector
        self.name = wx.TextCtrl(panel, -1, "Untitled Query",
            validator=ws.NameValidator(), size=(175,-1))
        self.lblUseProblemSet = ws.SmStaticText(panel, -1, "Add to")
        self.cbProblemSet = wx.ComboBox(panel, choices=[],
            style=wx.CB_DROPDOWN|wx.CB_READONLY)
        self.chkUseFail = wx.CheckBox(panel, -1,
                "  Return records that FAIL filter")
        self.chkUseFail.SetValue(wx.CHK_UNCHECKED)
        
        # Pack them
        szControls = wx.BoxSizer(wx.HORIZONTAL)
#        szControls.SetEmptyCellSize((0,0))
        szControls.Add(ws.SmStaticText(panel, -1, "Name"),
            flag=wx.RIGHT|wx.ALIGN_CENTER, border=6)
        
        szControls.Add(self.name, flag=wx.ALIGN_CENTER|wx.RIGHT, border=10)
        szControls.Add(self.lblUseProblemSet, flag=wx.RIGHT|wx.ALIGN_CENTER, border=6)
        szControls.Add(self.cbProblemSet, flag=wx.RIGHT|wx.ALIGN_CENTER|wx.RIGHT, border=6)
        szControls.Add(wx.StaticLine(panel, style=wx.VERTICAL), flag=wx.EXPAND|wx.LEFT|wx.RIGHT, border=4)
        szControls.Add(self.chkUseFail,
            flag=wx.LEFT|wx.ALIGN_LEFT|wx.ALIGN_CENTER_VERTICAL, border=6)
        # Two buttons at bottom
        self.btnClose = wx.Button(panel, -1, "Close")
        self.btnValidate = wx.Button(panel, -1, "Validate Options")
        self.btnCreate = wx.Button(panel, wx.ID_OK, "Run Remote Query",
            size=(-1, -1))
        self.btnCreate.SetDefault()
        # Layout
        sz = wx.GridBagSizer()
        sz.Add(self.previewPanel,     (0,0), (1,4), flag=wx.EXPAND, border=0)
        sz.Add(szControls,            (1,0), (1,4), flag=wx.ALL|wx.ALIGN_RIGHT, border=10)
        sz.Add(self.btnClose,         (2,1), (1,1), flag=wx.RIGHT|wx.ALIGN_RIGHT|wx.ALIGN_CENTER_VERTICAL, border=30)
        sz.Add(self.btnValidate,      (2,2), (1,1), flag=wx.ALL, border=10)
        sz.Add(self.btnCreate,        (2,3), (1,1), flag=wx.TOP|wx.BOTTOM, border=10)
        sz.AddGrowableRow(0)
        sz.AddGrowableCol(0)
        panel.SetSizerAndFit(sz)
        return panel
    
    def OnOptionChange(self, evt):
        self.RefreshCommandLine()
    
    def OnProblemSet(self, evt):
        ''' User has selected a different problem set. '''
        iSel = self.cbProblemSet.GetCurrentSelection()
        self._problemSet = self._problemSets[iSel]
        s = self._problemSet
        title = "Query Builder (%s)" % os.path.split(s.GetLocalDir())[-1]
        self.SetTitle(title)
        self.RefreshCommandLine()
    
    def OnActivate(self, evt):
        ''' Window reactivated; switch problem set to last problem set. '''
        self.SelectProblemSet(wx.GetApp().GetActiveProblemSet())
        evt.Skip()
    
    def OnClose(self, evt):
       ''' User closed the window.  Either hide it or quit. '''
       if wx.GetApp().quit:
           evt.Skip(False)
       else:
           self.Hide()
    
    def OnCloseButton(self, evt):
        self.Close()
    
    def OnQuery(self, evt):
        ''' User wants to start a query. '''
        # Are any queries already active?
        problemSet = wx.GetApp().GetActiveProblemSet()
        self.DoQuery()
    
    def _parseDates(self):
        opts = self.GetFilterOptions()
        start_date = silk.parse_fglob_date(opts["start-date"])
        if "end-date" in opts:
            end_date = silk.parse_fglob_date(opts["end-date"])
        else:
            if start_date[3] == None:
                start_date[3] = 23
            else:
                end_date = start_date
        return (start_date, end_date)
    
    def CheckAvailability(self):
        opts = copy.copy(self.GetFilterOptions())
        if "fail" in opts:
            del opts["fail"]
        (start_date, end_date) = self._parseDates()
        try:
            files = analysistool.RunFunction(runRemote=True, func=silk.fglob, **opts)
            date_list = silk.parse_fglob_result(files)
            missing = silk.find_missing_fglob_hours(date_list,
                  datetime.datetime(*start_date),
                  datetime.datetime(*end_date))
            return missing
        except silk.CommandError, e:
            if wx.GetApp():
                wx.GetApp().ShowError("Error running fglob - %s" %e.summary,
                    e.details)
            else:
                print "ERROR: %s\n\n%s" % (e.summary, e.details)
            raise e
        return []
    
    def DoQuery(self):
        class ConfirmDialog(wx.Dialog):
            def __init__(self, parent, message, title, warnings,
                    missing_hours, *args, **kargs):
                wx.Dialog.__init__(self, parent=parent, title=title,
                    *args, **kargs)
                bmp = wx.ArtProvider.GetBitmap(wx.ART_INFORMATION,
                    wx.ART_MESSAGE_BOX)
                msg = wx.StaticText(self, -1, message, size=(500,-1))
                msg.Wrap(500)
                choices = []
                for f in missing_hours:
                    choices.append("%d/%d/%d:%02d" % f)
                if len(missing_hours) > 0:
                    missing = wx.ListBox(self, -1, choices=choices, size=(400,-1))#, choices=missing_hours)
                szBtns = self.CreateButtonSizer(wx.YES|wx.NO)
                sz = wx.GridBagSizer() #(wx.VERTICAL)
                row = 0
                # Add message
                sz.Add(msg, (row,0), (1,2), flag=wx.ALL|wx.EXPAND, border=8)
                # Add warnings
                row += 1
                for t in warnings:
                    #bm = wx.StaticBitmap(self, -1, bmp)
                    #bm = wx.StaticText(self, -1, "test")
                    #print "best = %s, size=%s" % (bm.GetBestSize(), bm.GetSize())
                    #sz.Add(bm, (row, 1), (1,1), flag=wx.ALL|wx.ALIGN_CENTER_VERTICAL, border=8)
                    txt = wx.StaticText(self, -1, t)
                    txt.Wrap(400)
                    sz.Add(txt, (row, 0), (1,1), flag=wx.ALL|wx.ALIGN_LEFT|wx.ALIGN_CENTER_VERTICAL, border=8)
                    row += 1
                # Add missing files list
                if len(missing_hours) > 0:
                    sz.Add(missing, (row, 0), (1,2), flag=wx.ALL|wx.EXPAND, border=8)
                    row += 1
                # Add buttons
                sz.Add(szBtns, (row,0), (1,2), flag=wx.ALL|wx.ALIGN_RIGHT, border=8)
                self.SetEscapeId(wx.ID_NO)
                self.SetSizerAndFit(sz)
        # Validate before allowing query
        if not self.ValidateOptions(False):
            return
        # MUSTFIXME: Warn the user about excessively large query. For
        # now, we simply enforce a cap on the number of records
        # returned.
        (start_date, end_date) = self._parseDates()
        (start_date, end_date) = (datetime.datetime(*start_date),
                datetime.datetime(*end_date))
        delta = end_date - start_date
        hours = delta.days*24 + delta.seconds/(60*60)
        warnings = []
        #print "hours = %d thresh = %d" % (hours, silk.config.getLargeQueryThreshold())
        #if hours > silk.config.getLargeQueryThreshold():
        #    print "WARNING -- LOTS OF DATA"
        #    warnings.append("WARNING: Querying %d hours of data.  This query may take a very long time." % hours)
        busy = wx.BusyInfo("Checking availability of data", self)
        #try:
        missing_hours = self.CheckAvailability()
        #except:
        #    pdb.set_trace()
        #    return
        del busy
        opts = self.GetFilterOptions()
        # Testing
        msg = "Please confirm that you're ready to query the repository.  "
        problemSet = self.GetSelectedProblemSet()
        if problemSet != None:
            msg += 'This will launch a new remote query process and add the the result to the selected problem set.'
        else:
            msg += 'This will launch a new remote query process and create a new problem set.'
        if len(missing_hours) > 0:
            if missing_hours >= hours:
                warnings.append("WARNING: This query will likely return no data: rwfglob wasn't able to find any data files.")
            warnings.append("No data is available for the following %d hours:" % len(missing_hours))
        d = ConfirmDialog(self, msg, "Proceed with query?",
                    warnings=warnings,
                    missing_hours=missing_hours)
        d.CenterOnParent()
        res = d.ShowModal()
        if res == wx.ID_YES:
           name = self.name.GetValue()
           self.GetTopLevelParent().Show(False)
           self.result = analysistool.RunTool(True, False,
                tools.query.QueryTool, problemSet,
                name=name,
                **self.GetFilterOptions())
        else:
           self.result = None
    
    def OnValidate(self, evt):
        self.ValidateOptions(True)
    
    def OnReset(self, evt):
        ''' User wants to reset options to defaults. '''
        self.p[0].ResetOptions()
        self.p[1].ResetOptions()
    
    def GetSelectedProblemSet(self):
        ''' Return the active problem set for the query builder. '''
        return self._problemSet
    
    def SetAvailableProblemSets(self, problemSets):
        ''' Update the list of available problem sets'''
        self._problemSets = problemSets
        # Problem set to use
        psets = wx.GetApp().GetProblemSets()
        problem_sets = [os.path.split(s.GetLocalDir())[-1] for s in psets]
        self._problemSet = self._problemSets[-1]
        self.cbProblemSet.Clear()
        for ps in problem_sets:
            self.cbProblemSet.Append(ps)
        self.cbProblemSet.SetSelection(0)
        self.RefreshCommandLine()
    
    def GetFilterOptions(self):
        opts = {}
        opts.update(self.p[0].GetFilterOptions())
        opts.update(self.p[1].GetFilterOptions())
        if self.chkUseFail.GetValue() == wx.CHK_CHECKED:
            opts["fail"] = True
        return opts
    
    def OnPageChanging(self, evt):
        ''' Validate before allowing a switch'''
        sel = self.nb.GetSelection()
        if not self.ValidateOptions(showValidDialog=False):
            evt.Veto()
    
    def OnPageChanged(self, evt):
        ''' User selected a different note book page (e.g., Advanced from
Basic).  Copy over state of last page to the new page. '''
        #self.DoLayout()
        return
        sel = self.nb.GetSelection()
        if sel == 0: last_sel = 1
        else: last_sel = 0
        self.DoLayout()
        self.p[sel].CopyState(self.p[last_sel])
    
    def SelectProblemSet(self, problemSet):
        ''' Select the active problem set. '''
        s = os.path.split(problemSet.GetLocalDir())[-1]
        self.cbProblemSet.SetStringSelection(s)
        s = self._problemSet = problemSet
        title = "Query Builder (%s)" % os.path.split(s.GetLocalDir())[-1]
        self.SetTitle(title)
    
    def ResetOptions(self):
        self.filterPanel.ResetOptions()
    
    def SetFilterOptions(self, opts):
        self.p[0].ResetFilterOptions()
        self.p[0].SetFilterOptions(opts)
    
    def RefreshRange(self):
        self.fglobPanel.RefreshRange()
    
    def ValidateOptions(self, showValidDialog=True):
         # Validate before allowing query
         try:
             for panel in self.p:
                  panel.ValidateOptions()
             #print "ValidateOptions succeeded"
             if showValidDialog:
                 d = wx.MessageDialog(self, "Query filter options appear to be valid.",
                        "Validation successful",
                        wx.ICON_INFORMATION)
                 d.ShowModal()
         except ValidationError, e:
             #print "Showing dialog"
             e.ShowDialog()
             return False
         return True
    
    def RefreshCommandLine(self):
        basic_opts = self.p[0].GetFilterOptions()
        more_opts = self.p[1].GetFilterOptions()
        cmd = self.GetCommandLine()
        self.previewPanel.SetContent(cmd)
    
    def GetCommandLine(self):
        opts = copy.copy(self.GetFilterOptions())
        if self._problemSet:
            dir = self._problemSet.GetRemoteDir()
        else:
            dir = "$dir"
        return silk.build_query_command(True, "$output", opts, cmdDir = dir)


class FilterDetailsPanel(wx.Panel):
    def __init__(self, parent, *args, **kargs):
        wx.Panel.__init__(self, parent, *args, **kargs)
        self.filterPanel = MoreFilterOptionsPanel(self, True)
        self.sizeFilterPanel = SizeFilterOptionsPanel(self)
        self.pmapPanel = PrefixMapFilterPanel(self)
         # Layout everything
        self.gbs = wx.GridBagSizer(0,0)
        self.gbs.Add(self.filterPanel, (0,1), (1,1),
                    wx.ALL|wx.EXPAND, border=16)
        self.gbs.Add(self.sizeFilterPanel, (1,1), (1,1),
                    wx.ALL|wx.EXPAND, border=16)
        self.gbs.Add(self.pmapPanel, (0,0), (1,1),
                    wx.ALL|wx.EXPAND, border=16)
        self.SetSizerAndFit(self.gbs)
        self.Layout()
    
    def GetFilterOptions(self):
        opts = {}
        opts.update(self.filterPanel.GetOptions())
        opts.update(self.sizeFilterPanel.GetOptions())
        opts.update(self.pmapPanel.GetOptions())
        return opts
    
    def SetFilterOptions(self, opts):
        self.filterPanel.SetOptions(opts)
        self.sizeFilterPanel.SetOptions(opts)
        self.pmapPanel.SetOptions(opts)
    
    def ResetOptions(self):
        self.filterPanel.ResetOptions()
        self.sizeFilterPanel.ResetOptions()
        self.pmapPanel.ResetOptions()
    
    def ValidateOptions(self):
        self.filterPanel.ValidateOptions()
        self.sizeFilterPanel.ValidateOptions()
        self.pmapPanel.ValidateOptions()        
        return True

class QueryBuilderPanel(wx.Panel):
    ''' Panel with controls for constructing a query. '''
    def __init__(self, queryBuilder, parent, showDetails, *args, **kargs):
        wx.Panel.__init__(self, parent, *args, **kargs)
        self.queryBuilder = queryBuilder
        self.showDetails = showDetails
        self.showQueryButton = False
        # Create two panels (fglob and filter details)
        self.fglobPanel = FGlobOptionsPanel(self, True) #showDetails)
        self.filterPanel = BasicFilterOptionsPanel(self, False)
        self.RefreshRange()
        # Layout everything
        self.gbs = wx.GridBagSizer(0,0)
        row = 0
        # fglob panel on the left side
        self.gbs.Add(self.fglobPanel, (row,0), (1,1),
                    wx.LEFT|wx.RIGHT|wx.EXPAND, border=8)
        # filter panel on top right
        self.gbs.Add(self.filterPanel,(row,1), (1,1),
                    wx.LEFT|wx.RIGHT|wx.EXPAND, border=8)
        self.SetSizerAndFit(self.gbs)
        self.Layout()
    
    def GetSelectedProblemSet(self):
        return self.queryBuilder.GetSelectedProblemSet()
    
    # Copy any state shared by both simple and advanced panels
    def CopyState(self, other):
        # Fglob control state
        self.fglobPanel.CopyState(other.fglobPanel)
        # Filter options
        self.filterPanel.CopyState(other.filterPanel)
        self.RefreshCommandLine()
    
    def SetFilterOptions(self, opts):
        self.fglobPanel.SetOptions(opts)
        self.filterPanel.SetOptions(opts)
    
    def ResetFilterOptions(self):
        self.filterPanel.ResetOptions()
    
    def RefreshRange(self):
        self.fglobPanel.RefreshRange()
    
    # Generate dictionary of options for rwfilter
    def GetFilterOptions(self):
       # Build options list
       opts = {}
       if False:
           opts = self.fglobPanel.trPanel.GetOptions()
           if self.showDetails:
               sensorGroupSel = self.fglobPanel.cbSensorGroup.GetValue()
           classTypeSel = self.fglobPanel.cbDataPool.GetValue()
           opts.update(config.fgloboptions.opts["CLASS_TYPE_OPTIONS"][classTypeSel])
           if self.showDetails:
               opts.update(config.fgloboptions.opts["SENSOR_OPTIONS"][sensorGroupSel])
       glob_opts = self.fglobPanel.GetOptions()
       filter_opts = self.filterPanel.GetOptions()
       opts.update(glob_opts)
       opts.update(filter_opts)
       return opts
    
    def ValidateOptions(self):
        self.fglobPanel.ValidateOptions()
        return self.filterPanel.ValidateOptions()           
