
import wx.lib.newevent
import thread, shlex, subprocess
import os, re
from os.path import isfile, join
from sys import platform

if platform.startswith("win"):
  from _subprocess import STARTF_USESHOWWINDOW

(scriptEvent, EVT_SCRIPT_UPDATE) = wx.lib.newevent.NewEvent()
SCRIPT_RUNNING = 1
SCRIPT_FINISHED = 2
SCRIPT_CANCELLED = 3

TOOLPATHS_INSIDE_ARDUINO = [
  "hardware/tools/avr/bin/",
  "hardware/tools/" # avrdude in Arduino 1.0.x
]
if platform.startswith("darwin"):
  # That's an OS property, the Applicaton Bundle hierarchy.
  pathsCopy = TOOLPATHS_INSIDE_ARDUINO
  TOOLPATHS_INSIDE_ARDUINO = []
  for path in pathsCopy:
    TOOLPATHS_INSIDE_ARDUINO.append("Contents/Resources/Java/" + path)
    TOOLPATHS_INSIDE_ARDUINO.append("Contents/Java/" + path)


class ScriptTools:
  def __init__(self, settings):
    self.settings = settings

  def figureCommandPath(self, baseCommand):
    findConf = False
    if baseCommand == "avrdude":
      findConf = True

    if platform.startswith("win"):
      baseCommand += ".exe"

    if self.settings.arduinodir:
      cmdpath = self.settings.arduinodir

      for pathOption in TOOLPATHS_INSIDE_ARDUINO:
        cmdpathTry = cmdpath
        for dir in pathOption.split("/"):
          cmdpathTry = os.path.join(cmdpathTry, dir)
        cmdpathTry = os.path.join(cmdpathTry, baseCommand)
        if os.path.exists(cmdpathTry):
          cmdpath = "\"" + cmdpathTry + "\""
          break

      if findConf:
        confpath = cmdpath.strip("\"")
        exepos = confpath.rfind(".exe")
        if exepos >= 0:
          confpath = confpath[0:exepos]
        confpath += ".conf"
        if not os.path.exists(confpath):
          confpath = os.path.split(confpath)[0]
          confpath = os.path.split(confpath)[0]
          confpath = os.path.join(confpath, "etc")
          confpath = os.path.join(confpath, "avrdude.conf")
        if os.path.exists(confpath):
          cmdpath += " -C \"" + confpath + "\""

    else:
      cmdpath = baseCommand
      # No need to search avrdude.conf in this case.

    return cmdpath


class ScriptThread:
  def __init__(self, win, script):
    self.win = win
    self.running = False
    self.cancelled = False
    self.script = script

  def Start(self):
    self.running = True
    self.cancelled = False
    thread.start_new_thread(self.Run, ())

  def Stop(self):
    self.cancelled = True

  def IsRunning(self):
    return self.running

  def Run(self):
    if platform.startswith("win"):
      startupinfo = subprocess.STARTUPINFO()
      startupinfo.dwFlags |= STARTF_USESHOWWINDOW

    for cmd in self.script:
      evt = scriptEvent(msg = cmd, state = SCRIPT_RUNNING)
      wx.PostEvent(self.win, evt)
      args = shlex.split(str(cmd))
      try:
        if platform.startswith("win"):
          p = subprocess.Popen(args, stderr = subprocess.STDOUT,
                               stdout = subprocess.PIPE,
                               startupinfo = startupinfo)
        else:
          p = subprocess.Popen(args, stderr = subprocess.STDOUT,
                               stdout = subprocess.PIPE)
      except:
        evt = scriptEvent(msg = "Exception occurred trying to run\n\n%s" % cmd,
                          state = SCRIPT_CANCELLED)
        wx.PostEvent(self.win, evt)
        self.running = False
        return
      obuf = ''
      while not self.cancelled:
        o = p.stdout.read(1)
        if o == '': break
        if o == '\r' or o == '\n':
          if obuf.strip() != "":
            evt = scriptEvent(msg = obuf, state = SCRIPT_RUNNING)
            wx.PostEvent(self.win, evt)
          obuf = ''
        elif ord(o) < 32:
          pass
        else:
          obuf += o

      if self.cancelled:
        evt = scriptEvent(msg = None, state = SCRIPT_CANCELLED)
        wx.PostEvent(self.win, evt)
        p.kill()
        self.running = False
        p.wait()
        return

      rc = p.wait()
      if rc != 0:
        msg = "RC = " + str(rc) + " - Build terminated"
        evt = scriptEvent(msg = msg, state = SCRIPT_CANCELLED)
        wx.PostEvent(self.win, evt)
        self.running = False
        return

      evt = scriptEvent(msg = "", state = SCRIPT_RUNNING)
      wx.PostEvent(self.win, evt)

    evt = scriptEvent(msg = None, state = SCRIPT_FINISHED)
    wx.PostEvent(self.win, evt)

    self.running = False


class Build(wx.Dialog):
  def __init__(self, parent, settings, f_cpu, cpu):
    wx.Dialog.__init__(self, parent, wx.ID_ANY, "Build teacup",
                       style = wx.RESIZE_BORDER + wx.DEFAULT_DIALOG_STYLE)
    self.settings = settings
    self.SetFont(self.settings.font)
    self.root = self.settings.folder
    self.f_cpu = f_cpu
    self.cpu = cpu
    self.Bind(wx.EVT_CLOSE, self.onExit)
    self.cancelPending = False

    hsz = wx.BoxSizer(wx.HORIZONTAL)
    hsz.AddSpacer((10, 10))

    sz = wx.BoxSizer(wx.VERTICAL)
    sz.AddSpacer((10, 10))

    tc = wx.TextCtrl(self, wx.ID_ANY, size = (900, 300),
                     style = wx.TE_READONLY + wx.TE_MULTILINE)
    sz.Add(tc, 1, wx.EXPAND)
    f = wx.Font(8, wx.FONTFAMILY_MODERN, wx.FONTSTYLE_NORMAL,
                wx.FONTWEIGHT_BOLD)
    tc.SetFont(f)
    self.log = tc

    sz.AddSpacer((10, 10))
    hsz.Add(sz, 1, wx.EXPAND)
    hsz.AddSpacer((10, 10))

    self.SetSizer(hsz)

    self.Fit()
    builddir = join(self.root, "build")
    if not os.path.exists(builddir):
      os.makedirs(builddir)
      self.log.AppendText("Directory %s created.\n\n" % builddir)

    self.compile()

  def compile(self):
    self.generateCompileScript()
    if len(self.script) == 0:
      self.log.AppendText("Nothing to compile!\n")
      self.active = False
    else:
      self.Bind(EVT_SCRIPT_UPDATE, self.compileUpdate)
      self.t = ScriptThread(self, self.script)
      self.active = True
      self.t.Start()

  def link(self):
    self.generateLinkScript()
    if len(self.script) == 0:
      self.log.AppendText("Nothing to link!\n")
      self.active = False
    else:
      self.Bind(EVT_SCRIPT_UPDATE, self.linkUpdate)
      t = ScriptThread(self, self.script)
      self.active = True
      t.Start()

  def report(self):
    self.script = []
    self.reportLines = []
    cmdpath = ScriptTools(self.settings).figureCommandPath("avr-objdump")
    elfpath = "\"" + join(self.root, "build", "teacup.elf") + "\""
    cmd = cmdpath + " -h " + elfpath
    self.script.append(cmd)
    self.Bind(EVT_SCRIPT_UPDATE, self.reportUpdate)
    t = ScriptThread(self, self.script)
    self.active = True
    t.Start()

  def generateCompileScript(self):
    self.script = []
    cmdpath = ScriptTools(self.settings).figureCommandPath("avr-gcc")

    cfiles = [f for f in os.listdir(self.root)
                if isfile(join(self.root,f)) and f.endswith(".c")]
    for f in cfiles:
      basename = f[:-2]
      ofile = basename + ".o"
      alfile = basename + ".al"
      opath = "\"" + join(self.root, "build", ofile) + "\""
      alpath = "\"" + join(self.root, "build", alfile) + "\""
      cpath = "\"" + join(self.root, f) + "\""

      opts = self.settings.cflags
      opts = opts.replace("%ALNAME%", alpath)
      opts = opts.replace("%F_CPU%", self.f_cpu)
      opts = opts.replace("%CPU%", self.cpu)

      cmd = cmdpath + " -c " + opts + " -o " + opath + " " + cpath
      self.script.append(cmd)

  def generateLinkScript(self):
    self.script = []
    cmdpath = ScriptTools(self.settings).figureCommandPath("avr-gcc")

    # This is ugly:
    # Work around a problem of avr-ld.exe coming with Arduino 1.6.4 for
    # Windows. Without this it always drops this error message:
    #   collect2.exe: error: ld returned 5 exit status 255
    # Just enabling verbose messages allows ld.exe to complete without failure.
    if platform.startswith("win"):
      cmdpath += " -Wl,-V"

    ofiles = ["\"" + join(self.root, "build", f) + "\""
              for f in os.listdir(join(self.root, "build"))
                if isfile(join(self.root, "build", f)) and f.endswith(".o")]
    opath = " ".join(ofiles)
    elfpath = "\"" + join(self.root, "build", "teacup.elf") + "\""
    hexpath = "\"" + join(self.root, "teacup.hex") + "\""
    opts = self.settings.cflags
    opts = opts.replace("%ALNAME%", "teacup.elf")
    opts = opts.replace("%F_CPU%", self.f_cpu)
    opts = opts.replace("%CPU%", self.cpu)
    cmd = cmdpath + " " + self.settings.ldflags + " " + opts + " -o " + \
          elfpath + " " + opath + " -lm"
    self.script.append(cmd)

    cmdpath = ScriptTools(self.settings).figureCommandPath("avr-objcopy")
    cmd = cmdpath + " " + self.settings.objcopyflags + " " +  elfpath + \
          " " + hexpath
    self.script.append(cmd)

  def compileUpdate(self, evt):
    if evt.msg is not None:
      self.log.AppendText(evt.msg + "\n")

    if evt.state == SCRIPT_RUNNING:
      pass

    if evt.state == SCRIPT_CANCELLED:
      self.active = False

      if self.cancelPending:
        self.EndModal(wx.ID_OK)

      self.log.AppendText("Build terminated abnormally.\n")

    if evt.state == SCRIPT_FINISHED:
      self.log.AppendText("Compile completed normally.\n\n")
      self.link()

  def linkUpdate(self, evt):
    if evt.msg is not None:
      self.log.AppendText(evt.msg + "\n")

    if evt.state == SCRIPT_RUNNING:
      pass
    if evt.state == SCRIPT_CANCELLED:
      self.log.AppendText("Link terminated abnormally.\n")
      self.active = False
    if evt.state == SCRIPT_FINISHED:
      self.log.AppendText("Link completed normally.\n")
      self.report()

  def reportUpdate(self, evt):
    if evt.state == SCRIPT_RUNNING:
      if evt.msg is not None:
        self.reportLines.append(evt.msg)
    if evt.state == SCRIPT_CANCELLED:
      self.log.AppendText(evt.msg + "\n")
      self.log.AppendText("Report terminated abnormally.\n")
      self.active = False
    if evt.state == SCRIPT_FINISHED:
      self.formatReport()
      self.log.AppendText("\nBuild completed normally.\n")
      self.active = False

  def formatReportLine(self, m, name, v168, v328, v644, v1280):
    t = m.groups()
    v = int(t[0], 16)
    self.log.AppendText(("%12s:  %6d bytes   %6.2f%%   %6.2f%%"
                         "   %6.2f%%   %6.2f%%\n") %
                        (name, v, v / float(v168 * 1024) * 100.0,
                         v / float(v328 * 1024) * 100.0,
                         v / float(v644 * 1024) * 100.0,
                         v / float(v1280 * 1024) * 100.0))

  def formatReport(self):
    reText = re.compile("\.text\s+([0-9a-f]+)")
    reBss = re.compile("\.bss\s+([0-9a-f]+)")
    reEEProm = re.compile("\.eeprom\s+([0-9a-f]+)")

    self.log.AppendText("\n                   ATmega...     '168   '328(P)"
                        "   '644(P)     '1280\n")
    for l in self.reportLines:
      m = reText.search(l)
      if m:
        self.formatReportLine(m, "FLASH", 14, 30, 62, 126)
      else:
        m = reBss.search(l)
        if m:
          self.formatReportLine(m, "RAM", 1, 2, 4, 8)
        else:
          m = reEEProm.search(l)
          if m:
            self.formatReportLine(m, "EEPROM", 1, 2, 2, 4)

  def onExit(self, evt):
    if self.active:
      dlg = wx.MessageDialog(self, "Are you sure you want to cancel building?",
                             "Build active",
                             wx.YES_NO | wx.NO_DEFAULT | wx.ICON_INFORMATION)
      rc = dlg.ShowModal()
      dlg.Destroy()

      if rc == wx.ID_YES:
        self.cancelPending = True
        self.log.AppendText("Cancelling...\n")
        self.t.Stop()
      return

    self.EndModal(wx.ID_OK)


class Upload(wx.Dialog):
  def __init__(self, parent, settings, f_cpu, cpu):
    wx.Dialog.__init__(self, parent, wx.ID_ANY, "Upload teacup",
                       style = wx.RESIZE_BORDER + wx.DEFAULT_DIALOG_STYLE)
    self.settings = settings
    self.SetFont(self.settings.font)
    self.root = self.settings.folder
    self.f_cpu = f_cpu
    self.cpu = cpu
    self.baud = self.settings.uploadspeed
    self.Bind(wx.EVT_CLOSE, self.onExit)
    self.cancelPending = False

    hsz = wx.BoxSizer(wx.HORIZONTAL)
    hsz.AddSpacer((10, 10))

    sz = wx.BoxSizer(wx.VERTICAL)
    sz.AddSpacer((10, 10))

    tc = wx.TextCtrl(self, wx.ID_ANY, size = (900, 300),
                     style = wx.TE_READONLY + wx.TE_MULTILINE)
    sz.Add(tc, 1, wx.EXPAND)
    f = wx.Font(8, wx.FONTFAMILY_MODERN, wx.FONTSTYLE_NORMAL,
                wx.FONTWEIGHT_BOLD)
    tc.SetFont(f)
    self.log = tc

    sz.AddSpacer((10, 10))
    hsz.Add(sz, 1, wx.EXPAND)
    hsz.AddSpacer((10, 10))

    self.SetSizer(hsz)

    self.Fit()
    self.generateUploadScript()
    if len(self.script) == 0:
      self.log.AppendText("Nothing to upload!\n")
      self.active = False
    else:
      self.Bind(EVT_SCRIPT_UPDATE, self.uploadUpdate)
      self.t = ScriptThread(self, self.script)
      self.active = True
      self.t.Start()

  def generateUploadScript(self):
    self.script = []
    cmdpath = ScriptTools(self.settings).figureCommandPath("avrdude")
    hexpath = "\"" + join(self.root, "teacup.hex") + "\""

    cmd = cmdpath + " -c %s %s -b %s -p %s -P %s -U flash:w:%s:i" % \
          (self.settings.programmer, self.settings.programflags, self.baud,
           self.cpu, self.settings.port, hexpath)
    self.script.append(cmd)

  def uploadUpdate(self, evt):
    if evt.msg is not None:
      self.log.AppendText(evt.msg + "\n")

    if evt.state == SCRIPT_RUNNING:
      pass

    if evt.state == SCRIPT_CANCELLED:
      self.active = False

      if self.cancelPending:
        self.EndModal(wx.ID_OK)

      self.log.AppendText("Upload terminated abnormally.\n")

    if evt.state == SCRIPT_FINISHED:
      self.log.AppendText("Upload completed normally.\n")
      self.active = False

  def onExit(self, evt):
    if self.active:
      dlg = wx.MessageDialog(self, "Are you sure you want to cancel upload?",
                             "Upload active",
                             wx.YES_NO | wx.NO_DEFAULT | wx.ICON_INFORMATION)
      rc = dlg.ShowModal()
      dlg.Destroy()

      if rc == wx.ID_YES:
        self.cancelPending = True
        self.log.AppendText("Cancelling...\n")
        self.t.Stop()
      return

    self.EndModal(wx.ID_OK)
