##===-- sourcewin.py -----------------------------------------*- Python -*-===##
##
##                     The LLVM Compiler Infrastructure
##
## This file is distributed under the University of Illinois Open Source
## License. See LICENSE.TXT for details.
##
##===----------------------------------------------------------------------===##

import cui
import curses
import lldb, lldbutil
import re
import os

class SourceWin(cui.TitledWin):
  def __init__(self, driver, x, y, w, h):
    super(SourceWin, self).__init__(x, y, w, h, "Source")
    self.sourceman = driver.getSourceManager()
    self.sources = {}

    self.filename= None
    self.pc_line = None
    self.viewline = 0

    self.breakpoints = { }

    self.win.scrollok(1)

    self.markerPC = ":) "
    self.markerBP = "B> "
    self.markerNone  = "   "

    try:
      from pygments.formatters import TerminalFormatter
      self.formatter = TerminalFormatter()
    except ImportError:
      #self.win.addstr("\nWarning: no 'pygments' library found. Syntax highlighting is disabled.")
      self.lexer = None
      self.formatter = None
      pass

    # FIXME: syntax highlight broken
    self.formatter = None
    self.lexer = None

  def handleEvent(self, event):
    if isinstance(event, int):
      self.handleKey(event)
      return

    if isinstance(event, lldb.SBEvent):
      if lldb.SBBreakpoint.EventIsBreakpointEvent(event):
        self.handleBPEvent(event)

      if lldb.SBProcess.EventIsProcessEvent(event) and \
          not lldb.SBProcess.GetRestartedFromEvent(event):
        process = lldb.SBProcess.GetProcessFromEvent(event)
        if not process.IsValid():
          return
        if process.GetState() == lldb.eStateStopped:
          self.refreshSource(process)
        elif process.GetState() == lldb.eStateExited:
          self.notifyExited(process)

  def notifyExited(self, process):
    self.win.erase()
    target = lldbutil.get_description(process.GetTarget())
    pid = process.GetProcessID()
    ec = process.GetExitStatus()
    self.win.addstr("\nProcess %s [%d] has exited with exit-code %d" % (target, pid, ec))

  def pageUp(self):
    if self.viewline > 0:
      self.viewline = self.viewline - 1
      self.refreshSource()

  def pageDown(self):
    if self.viewline < len(self.content) - self.height + 1:
      self.viewline = self.viewline + 1
      self.refreshSource()
    pass

  def handleKey(self, key):
    if key == curses.KEY_DOWN:
      self.pageDown()
    elif key == curses.KEY_UP:
      self.pageUp()

  def updateViewline(self):
    half = self.height / 2
    if self.pc_line < half:
      self.viewline = 0
    else:
      self.viewline = self.pc_line - half + 1

    if self.viewline < 0:
      raise Exception("negative viewline: pc=%d viewline=%d" % (self.pc_line, self.viewline))

  def refreshSource(self, process = None):
    (self.height, self.width) = self.win.getmaxyx()

    if process is not None:
      loc = process.GetSelectedThread().GetSelectedFrame().GetLineEntry()
      f = loc.GetFileSpec()
      self.pc_line = loc.GetLine()

      if not f.IsValid():
        self.win.addstr(0, 0, "Invalid source file")
        return

      self.filename = f.GetFilename()
      path = os.path.join(f.GetDirectory(), self.filename)
      self.setTitle(path)
      self.content = self.getContent(path)
      self.updateViewline()

    if self.filename is None:
      return
   
    if self.formatter is not None:
      from pygments.lexers import get_lexer_for_filename
      self.lexer = get_lexer_for_filename(self.filename)

    bps = [] if not self.filename in self.breakpoints else self.breakpoints[self.filename]
    self.win.erase()
    if self.content:
      self.formatContent(self.content, self.pc_line, bps)

  def getContent(self, path):
    content = []
    if path in self.sources:
      content = self.sources[path]
    else:
      if os.path.exists(path): 
        with open(path) as x:
          content = x.readlines()
        self.sources[path] = content
    return content

  def formatContent(self, content, pc_line, breakpoints):
    source = ""
    count = 1
    self.win.erase()
    end = min(len(content), self.viewline + self.height)
    for i in range(self.viewline, end):
      line_num = i + 1
      marker = self.markerNone
      attr = curses.A_NORMAL
      if line_num == pc_line:
        attr = curses.A_REVERSE
      if line_num in breakpoints:
        marker = self.markerBP
      line = "%s%3d %s" % (marker, line_num, self.highlight(content[i]))
      if len(line) >= self.width:
        line = line[0:self.width-1] + "\n"
      self.win.addstr(line, attr)
      source += line
      count = count + 1
    return source

  def highlight(self, source):
    if self.lexer and self.formatter:
      from pygments import highlight
      return highlight(source, self.lexer, self.formatter)
    else:
      return source

  def addBPLocations(self, locations):
    for path in locations:
      lines = locations[path]
      if path in self.breakpoints:
        self.breakpoints[path].update(lines)
      else:
        self.breakpoints[path] = lines

  def removeBPLocations(self, locations):
    for path in locations:
      lines = locations[path]
      if path in self.breakpoints:
        self.breakpoints[path].difference_update(lines)
      else:
        raise "Removing locations that were never added...no good"

  def handleBPEvent(self, event):
    def getLocations(event):
      locs = {}

      bp = lldb.SBBreakpoint.GetBreakpointFromEvent(event)

      if bp.IsInternal():
        # don't show anything for internal breakpoints
        return

      for location in bp:
        # hack! getting the LineEntry via SBBreakpointLocation.GetAddress.GetLineEntry does not work good for
        # inlined frames, so we get the description (which does take into account inlined functions) and parse it.
        desc = lldbutil.get_description(location, lldb.eDescriptionLevelFull)
        match = re.search('at\ ([^:]+):([\d]+)', desc)
        try:
          path = match.group(1)
          line = int(match.group(2).strip())
        except ValueError as e:
          # bp loc unparsable
          continue

        if path in locs:
          locs[path].add(line)
        else:
          locs[path] = set([line])
      return locs

    event_type = lldb.SBBreakpoint.GetBreakpointEventTypeFromEvent(event)
    if event_type == lldb.eBreakpointEventTypeEnabled \
        or event_type == lldb.eBreakpointEventTypeAdded \
        or event_type == lldb.eBreakpointEventTypeLocationsResolved \
        or event_type == lldb.eBreakpointEventTypeLocationsAdded:
      self.addBPLocations(getLocations(event))
    elif event_type == lldb.eBreakpointEventTypeRemoved \
        or event_type == lldb.eBreakpointEventTypeLocationsRemoved \
        or event_type == lldb.eBreakpointEventTypeDisabled:
      self.removeBPLocations(getLocations(event))
    elif event_type == lldb.eBreakpointEventTypeCommandChanged \
        or event_type == lldb.eBreakpointEventTypeConditionChanged \
        or event_type == lldb.eBreakpointEventTypeIgnoreChanged \
        or event_type == lldb.eBreakpointEventTypeThreadChanged \
        or event_type == lldb.eBreakpointEventTypeInvalidType:
      # no-op
      pass
    self.refreshSource()


