
import sys
import time
try:
  import wx
except:
  print("ImportError: No module named wx\n\n"
        "wxPython is not installed. This program requires wxPython to run.\n"
        "See your package manager and/or http://wxpython.org/download.php.")
  time.sleep(10)
  sys.exit(-1)

import os.path

from configtool.data import reHelpText
from configtool.decoration import Decoration
from configtool.settings import Settings, SettingsDlg
from configtool.printerpanel import PrinterPanel
from configtool.boardpanel import BoardPanel
from configtool.build import Build, Upload
from configtool.data import reInclude

ID_LOAD_PRINTER = 1000
ID_SAVE_PRINTER = 1001
ID_SAVE_PRINTER_AS = 1002
ID_LOAD_BOARD = 1010
ID_SAVE_BOARD = 1011
ID_SAVE_BOARD_AS = 1012
ID_LOAD_CONFIG = 1020
ID_SAVE_CONFIG = 1021
ID_BUILD = 1030
ID_UPLOAD = 1031
ID_SETTINGS = 1040
ID_HELP = 1050
ID_REPORT = 1051
ID_ABOUT = 1052


class ConfigFrame(wx.Frame):
  def __init__(self, settings):
    wx.Frame.__init__(self, None, -1, "Teacup Configtool", size = (880, 550))
    self.Bind(wx.EVT_CLOSE, self.onClose)
    self.Bind(wx.EVT_SIZE, self.onResize)

    self.deco = Decoration()

    panel = wx.Panel(self, -1)
    panel.SetBackgroundColour(self.deco.getBackgroundColour())
    panel.Bind(wx.EVT_PAINT, self.deco.onPaintBackground)

    self.settings = settings
    self.settings.app = self
    self.settings.font = wx.Font(8, wx.FONTFAMILY_SWISS, wx.FONTSTYLE_NORMAL,
                                 wx.FONTWEIGHT_BOLD)

    self.heaters = []
    self.savePrtEna = False
    self.saveBrdEna = False
    self.protPrtFile = False
    self.protBrdFile = False

    sz = wx.BoxSizer(wx.HORIZONTAL)

    self.nb = wx.Notebook(panel, wx.ID_ANY, size = (880, 550),
                          style = wx.BK_DEFAULT)
    self.nb.SetBackgroundColour(self.deco.getBackgroundColour())
    self.nb.SetFont(self.settings.font)

    self.printerFileName = None
    self.printerTabDecor = ""
    self.printerBaseText = "Printer"
    self.pgPrinter = PrinterPanel(self, self.nb, self.settings)
    self.nb.AddPage(self.pgPrinter, self.printerBaseText)

    self.boardFileName = None
    self.boardTabDecor = ""
    self.boardBaseText = "Board"
    self.pgBoard = BoardPanel(self, self.nb, self.settings)
    self.nb.AddPage(self.pgBoard, self.boardBaseText)

    panel.Fit()
    self.panel = panel

    sz.Add(self.nb, 1, wx.EXPAND + wx.ALL, 5)
    self.SetSizer(sz)
    self.makeMenu()

  def onClose(self, evt):
    if not self.pgPrinter.confirmLoseChanges("exit"):
      return

    if not self.pgBoard.confirmLoseChanges("exit"):
      return

    self.Destroy()

  def onResize(self, evt):
    self.panel.SetSize(self.GetClientSize())
    self.Refresh()
    evt.Skip();

  def setPrinterTabFile(self, fn):
    self.printerFileName = fn
    self.updatePrinterTab()

  def setPrinterTabDecor(self, prefix):
    self.printerTabDecor = prefix
    self.updatePrinterTab()

  def updatePrinterTab(self):
    txt = self.printerTabDecor + self.printerBaseText
    if self.printerFileName:
      txt += " <%s>" % self.printerFileName
    self.nb.SetPageText(0, txt)

  def setBoardTabFile(self, fn):
    self.boardFileName = fn
    self.updateBoardTab()

  def setBoardTabDecor(self, prefix):
    self.boardTabDecor = prefix
    self.updateBoardTab()

  def updateBoardTab(self):
    txt = self.boardTabDecor + self.boardBaseText
    if self.boardFileName:
      txt += " <%s>" % self.boardFileName
    self.nb.SetPageText(1, txt)

  def setHeaters(self, ht):
    self.heaters = ht
    self.pgPrinter.setHeaters(ht)

  def makeMenu(self):
    file_menu = wx.Menu()

    file_menu.Append(ID_LOAD_CONFIG, "Load config.h",
                     "Load config.h and its named printer and board files.")
    self.Bind(wx.EVT_MENU, self.onLoadConfig, id = ID_LOAD_CONFIG)
    file_menu.Enable(ID_LOAD_CONFIG, False)

    file_menu.Append(ID_SAVE_CONFIG, "Save config.h", "Save config.h file.")
    self.Bind(wx.EVT_MENU, self.onSaveConfig, id = ID_SAVE_CONFIG)
    file_menu.Enable(ID_SAVE_CONFIG, False)

    file_menu.AppendSeparator()

    file_menu.Append(ID_LOAD_PRINTER, "Load printer",
                     "Load a printer configuration file.")
    self.Bind(wx.EVT_MENU, self.pgPrinter.onLoadConfig, id = ID_LOAD_PRINTER)

    file_menu.Append(ID_SAVE_PRINTER, "Save printer",
                     "Save printer configuration.")
    self.Bind(wx.EVT_MENU, self.onSavePrinterConfig, id = ID_SAVE_PRINTER)
    file_menu.Enable(ID_SAVE_PRINTER, False)

    file_menu.Append(ID_SAVE_PRINTER_AS, "Save printer as...",
                     "Save printer configuration to a new file.")
    self.Bind(wx.EVT_MENU, self.onSavePrinterConfigAs, id = ID_SAVE_PRINTER_AS)
    file_menu.Enable(ID_SAVE_PRINTER_AS, False)

    file_menu.AppendSeparator()

    file_menu.Append(ID_LOAD_BOARD, "Load board",
                     "Load a board configuration file.")
    self.Bind(wx.EVT_MENU, self.pgBoard.onLoadConfig, id = ID_LOAD_BOARD)

    file_menu.Append(ID_SAVE_BOARD, "Save board", "Save board configuration.")
    self.Bind(wx.EVT_MENU, self.onSaveBoardConfig, id = ID_SAVE_BOARD)
    file_menu.Enable(ID_SAVE_BOARD, False)

    file_menu.Append(ID_SAVE_BOARD_AS, "Save board as...",
                     "Save board configuration to a new file.")
    self.Bind(wx.EVT_MENU, self.onSaveBoardConfigAs, id = ID_SAVE_BOARD_AS)
    file_menu.Enable(ID_SAVE_BOARD_AS, False)

    file_menu.AppendSeparator()

    file_menu.Append(wx.ID_EXIT, "E&xit", "Exit the application.")
    self.Bind(wx.EVT_MENU, self.onClose, id = wx.ID_EXIT)

    self.fileMenu = file_menu

    menu_bar = wx.MenuBar()

    menu_bar.Append(file_menu, "&File")

    edit_menu = wx.Menu()

    edit_menu.Append(ID_SETTINGS, "Settings", "Change settings.")
    self.Bind(wx.EVT_MENU, self.onEditSettings, id = ID_SETTINGS)

    self.editMenu = edit_menu

    menu_bar.Append(edit_menu, "&Edit")

    build_menu = wx.Menu()

    build_menu.Append(ID_BUILD, "Build", "Build the executable.")
    self.Bind(wx.EVT_MENU, self.onBuild, id = ID_BUILD)

    build_menu.Append(ID_UPLOAD, "Upload", "Upload the executable.")
    self.Bind(wx.EVT_MENU, self.onUpload, id = ID_UPLOAD)

    self.buildMenu = build_menu

    menu_bar.Append(build_menu, "&Build")

    help_menu = wx.Menu()

    help_menu.Append(ID_HELP, "Help", "Find help.")
    self.Bind(wx.EVT_MENU, self.onHelp, id = ID_HELP)

    help_menu.Append(ID_REPORT, "Report problem",
                     "Report a problem to Teacup maintainers.")
    self.Bind(wx.EVT_MENU, self.onReportProblem, id = ID_REPORT)

    help_menu.AppendSeparator()

    help_menu.Append(ID_ABOUT, "About Teacup")
    self.Bind(wx.EVT_MENU, self.onAbout, id = ID_ABOUT)

    self.helpMenu = help_menu

    menu_bar.Append(help_menu, "&Help")

    self.SetMenuBar(menu_bar)
    loadFlag = self.checkEnableLoadConfig()
    self.checkEnableUpload()
    if loadFlag:
      self.loadConfigFile("config.h")

  def onSaveBoardConfig(self, evt):
    rc = self.pgBoard.onSaveConfig(evt)
    if rc:
      self.checkEnableLoadConfig()
    return rc

  def onSaveBoardConfigAs(self, evt):
    rc = self.pgBoard.onSaveConfigAs(evt)
    if rc:
      self.checkEnableLoadConfig()
    return rc

  def onSavePrinterConfig(self, evt):
    rc = self.pgPrinter.onSaveConfig(evt)
    if rc:
      self.checkEnableLoadConfig()
    return rc

  def onSavePrinterConfigAs(self, evt):
    rc = self.pgPrinter.onSaveConfigAs(evt)
    if rc:
      self.checkEnableLoadConfig()
    return rc

  def checkEnableLoadConfig(self):
    fn = os.path.join(self.settings.folder, "config.h")
    if os.path.isfile(fn):
      self.fileMenu.Enable(ID_LOAD_CONFIG, True)
      self.buildMenu.Enable(ID_BUILD, True)
      return True
    else:
      self.fileMenu.Enable(ID_LOAD_CONFIG, False)
      self.buildMenu.Enable(ID_BUILD, False)
      return False

  def checkEnableUpload(self):
    fn = os.path.join(self.settings.folder, "teacup.hex")
    if os.path.isfile(fn):
      self.buildMenu.Enable(ID_UPLOAD, True)
    else:
      self.buildMenu.Enable(ID_UPLOAD, False)

  def enableSavePrinter(self, saveFlag, saveAsFlag):
    self.fileMenu.Enable(ID_SAVE_PRINTER, saveFlag)
    self.fileMenu.Enable(ID_SAVE_PRINTER_AS, saveAsFlag)
    self.savePrtEna = saveAsFlag
    self.protPrtFile = not saveFlag
    if self.savePrtEna and self.saveBrdEna:
      self.enableSaveConfig(True)
    else:
      self.enableSaveConfig(False)

  def enableSaveBoard(self, saveFlag, saveAsFlag):
    self.fileMenu.Enable(ID_SAVE_BOARD, saveFlag)
    self.fileMenu.Enable(ID_SAVE_BOARD_AS, saveAsFlag)
    self.saveBrdEna = saveAsFlag
    self.protBrdFile = not saveFlag
    if self.savePrtEna and self.saveBrdEna:
      self.enableSaveConfig(True)
    else:
      self.enableSaveConfig(False)

  def enableSaveConfig(self, flag):
    self.fileMenu.Enable(ID_SAVE_CONFIG, flag)

  def onLoadConfig(self, evt):
    self.loadConfigFile("config.h")

  def loadConfigFile(self, fn):
    if not self.pgPrinter.confirmLoseChanges("load config"):
      return False

    if not self.pgBoard.confirmLoseChanges("load config"):
      return False

    pfile, bfile = self.getConfigFileNames(fn)

    if not pfile:
      self.message("Config file did not contain a printer file "
                   "include statement.", "Config error")
      return False
    else:
      if not self.pgPrinter.loadConfigFile(pfile):
        self.message("There was a problem loading the printer config file:\n%s"
                     % pfile, "Config error")
        return False

    if not bfile:
      self.message("Config file did not contain a board file "
                   "include statement.", "Config error")
      return False
    else:
      if not self.pgBoard.loadConfigFile(bfile):
        self.message("There was a problem loading the board config file:\n%s"
                     % bfile, "Config error")
        return False

    return True

  def getConfigFileNames(self, fn):
    pfile = None
    bfile = None
    path = os.path.join(self.settings.folder, fn)
    try:
      cfgBuffer = list(open(path))
    except:
      self.message("Unable to process config file %s." % fn, "File error")
      return None, None

    for ln in cfgBuffer:
      if not ln.lstrip().startswith("#include"):
        continue

      m = reInclude.search(ln)
      if m:
        t = m.groups()
        if len(t) == 1:
          if "printer." in t[0]:
            if pfile:
              self.message("Multiple printer file include statements.\n"
                           "Ignoring %s." % ln, "Config error",
                           wx.OK + wx.ICON_WARNING)
            else:
              pfile = os.path.join(self.settings.folder, t[0])
          elif "board." in t[0]:
            if bfile:
              self.message("Multiple board file include statements.\n"
                           "Ignoring %s." % ln, "Config error",
                           wx.OK + wx.ICON_WARNING)
            else:
              bfile = os.path.join(self.settings.folder, t[0])
          else:
            self.message("Unable to parse include statement:\n%s" % ln,
                         "Config error")

    return pfile, bfile

  def onSaveConfig(self, evt):
    fn = os.path.join(self.settings.folder, "config.h")
    try:
      fp = open(fn, 'w')
    except:
      self.message("Unable to open config.h for output.", "File error")
      return False

    bfn = self.pgBoard.getFileName()
    if self.pgBoard.isModified() and self.pgBoard.isValid():
      if not self.pgBoard.saveConfigFile(bfn):
        return False
    else:
      self.pgBoard.generateTempTables()

    pfn = self.pgPrinter.getFileName()
    if self.pgPrinter.isModified() and self.pgPrinter.isValid():
      if not self.pgPrinter.saveConfigFile(pfn):
        return False

    prefix = self.settings.folder + os.path.sep
    lpfx = len(prefix)

    if bfn.startswith(prefix):
      rbfn = bfn[lpfx:]
    else:
      rbfn = bfn

    if pfn.startswith(prefix):
      rpfn = pfn[lpfx:]
    else:
      rpfn = pfn

    fp.write("\n")
    fp.write("// Configuration for controller board.\n")
    fp.write("#include \"%s\"\n" % rbfn)
    fp.write("\n")
    fp.write("// Configuration for printer board.\n")
    fp.write("#include \"%s\"\n" % rpfn)

    fp.close()

    self.checkEnableLoadConfig()
    return True

  def onBuild(self, evt):
    self.onBuildorUpload(True)

  def onUpload(self, evt):
    self.onBuildorUpload(False)

  def onBuildorUpload(self, buildFlag):
    if not (self.pgPrinter.hasData() or self.pgBoard.hasData()):
      dlg = wx.MessageDialog(self, "Data needs to be loaded. "
                                   "Click Yes to load config.h.",
                             "Data missing",
                             wx.YES_NO | wx.NO_DEFAULT | wx.ICON_INFORMATION)
      rc = dlg.ShowModal()
      dlg.Destroy()
      if rc != wx.ID_YES:
        return

      self.loadConfigFile("config.h")
    else:
      if self.pgPrinter.isModified():
        dlg = wx.MessageDialog(self, "Printer data needs to be saved. Click "
                                     "Yes to save printer configuration.",
                               "Changes pending",
                               wx.YES_NO | wx.NO_DEFAULT | wx.ICON_INFORMATION)
        rc = dlg.ShowModal()
        dlg.Destroy()
        if rc != wx.ID_YES:
          return

        if self.protPrtFile:
          rc = self.onSavePrinterConfigAs(None)
        else:
          rc = self.onSavePrinterConfig(None)
        if not rc:
          return

      if self.pgBoard.isModified():
        dlg = wx.MessageDialog(self, "Board data needs to be saved. Click "
                                     "Yes to save board configuration.",
                               "Changes pending",
                               wx.YES_NO | wx.NO_DEFAULT | wx.ICON_INFORMATION)
        rc = dlg.ShowModal()
        dlg.Destroy()
        if rc != wx.ID_YES:
          return

        if self.protBrdFile:
          rc = self.onSaveBoardConfigAs(None)
        else:
          rc = self.onSaveBoardConfig(None)
        if not rc:
          return

    if not self.verifyConfigLoaded():
      dlg = wx.MessageDialog(self, "Loaded configuration does not match the "
                                   "config.h file. Click Yes to save config.h.",
                             "Configuration changed",
                             wx.YES_NO | wx.NO_DEFAULT | wx.ICON_INFORMATION)
      rc = dlg.ShowModal()
      dlg.Destroy()
      if rc != wx.ID_YES:
        return

      if not self.onSaveConfig(None):
        return

    f_cpu, cpu = self.pgBoard.getCPUInfo()
    if not cpu:
      dlg = wx.MessageDialog(self, "Unable to determine CPU type.",
                             "CPU type error", wx.OK | wx.ICON_ERROR)
      dlg.ShowModal()
      dlg.Destroy()
      return

    if not f_cpu:
      dlg = wx.MessageDialog(self, "Unable to determine CPU clock rate.",
                             "CPU clock rate error", wx.OK | wx.ICON_ERROR)
      dlg.ShowModal()
      dlg.Destroy()
      return

    if buildFlag:
      dlg = Build(self, self.settings, f_cpu, cpu)
      dlg.ShowModal()
      dlg.Destroy()
      self.checkEnableUpload()
    else:
      dlg = Upload(self, self.settings, f_cpu, cpu)
      dlg.ShowModal()
      dlg.Destroy()

  def verifyConfigLoaded(self):
    pfile, bfile = self.getConfigFileNames("config.h")
    lpfile = self.pgPrinter.getFileName()
    lbfile = self.pgBoard.getFileName()

    return ((pfile == lpfile) and (bfile == lbfile))

  def onEditSettings(self, evt):
    dlg = SettingsDlg(self, self.settings)
    rc = dlg.ShowModal()
    dlg.Destroy()

  def onHelp(self, evt):
    self.message("Find help by hovering slowly over the buttons and text "
                 "fields. Tooltip should appear, explaining things.",
                 "Find help", style = wx.OK)

  def onReportProblem(self, evt):
    import urllib
    import webbrowser
    import subprocess
    from sys import platform

    # Testing allowed URLs up to 32 kB in size. Longer URLs are simply chopped.
    mailRecipients ="reply+0004dc756da9f0641af0a3834c580ad5be469f4f6b" \
                    "5d4cfc92cf00000001118c958a92a169ce051faa8c@" \
                    "reply.github.com,mah@jump-ing.de"
    mailSubject = "Teacup problem report"
    mailBody = "Please answer these questions before hitting \"send\":\n\n" \
               "What did you try to do?\n\n\n" \
               "What did you expect to happen?\n\n\n" \
               "What happened instead?\n\n\n\n" \
               "To allow developers to help, configuration files are " \
               "attached, with help comments stripped:\n"

    for f in self.pgBoard.getFileName(), self.pgPrinter.getFileName():
      if not f:
        mailBody += "\n(no file loaded)\n"
        continue

      mailBody += "\n" + os.path.basename(f) + ":\n"
      mailBody += "----------------------------------------------\n"
      try:
        fc = open(f).read()
        fc = reHelpText.sub("", fc)
        mailBody += fc
      except:
        mailBody += "(could not read this file)\n"
      mailBody += "----------------------------------------------\n"

    url = "mailto:" + urllib.quote(mailRecipients) + \
          "?subject=" + urllib.quote(mailSubject) + \
          "&body=" + urllib.quote(mailBody)

    # This is a work around a bug in gvfs-open coming with (at least) Ubuntu
    # 15.04. gvfs-open would open mailto:///user@example.com instead of
    # the requested mailto:user@example.com.
    if platform.startswith("linux"):
      try:
        subprocess.check_output(["gvfs-open", "--help"])

        # Broken gvfs-open exists, so it might be used.
        # Try to open the URL directly.
        for urlOpener in "thunderbird", "evolution", "firefox", "mozilla", \
                         "epiphany", "konqueror", "chromium-browser", \
                         "google-chrome":
          try:
            subprocess.check_output([urlOpener, url], stderr=subprocess.STDOUT)
            return
          except:
            pass
      except:
        pass

    webbrowser.open_new(url)

  def onAbout(self, evt):
    # Get the contributors' top 10 with something like this:
    #   export B=experimental
    #   git log $B | grep "Author:" | sort | uniq | while \
    #     read A; do N=$(git log $B | grep "$A" | wc -l); echo "$N $A"; done | \
    #     sort -rn
    self.message("Teacup Firmware is a 3D Printer and CNC machine controlling "
                 "firmware with emphasis on performance, efficiency and "
                 "outstanding quality. What Teacup does, shall it do very well."
                 "\n\n\n"
                 "Lots of people hard at work! Top 10 contributors:\n\n"
                 "    Markus Hitter (542 commits)\n"
                 "    Michael Moon (322 commits)\n"
                 "    Phil Hord (55 commits)\n"
                 "    Jeff Bernardis (51 commits)\n"
                 "    Markus Amsler (47 commits)\n"
                 "    David Forrest (27 commits)\n"
                 "    Jim McGee (15 commits)\n"
                 "    Ben Jackson (12 commits)\n"
                 "    Bas Laarhoven (10 commits)\n"
                 "    Stephan Walter (9 commits)\n"
                 "    Roland Brochard (3 commits)\n"
                 "    Jens Ch. Restemeier (3 commits)\n",
                 "About Teacup", style = wx.OK)

  def message(self, text, title, style = wx.OK + wx.ICON_ERROR):
    dlg = wx.MessageDialog(self, text, title, style)
    dlg.ShowModal()
    dlg.Destroy()


def StartGui(settings):
  app = wx.App(False)
  frame = ConfigFrame(settings)
  frame.Show(True)
  app.MainLoop()
