import wx.lib.newevent

try:
    import thread as _thread
except ImportError:
    import _thread
import shlex
import subprocess
import os
import 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).decode("utf-8", "ignore")
                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.Add((10, 10))

        sz = wx.BoxSizer(wx.VERTICAL)
        sz.Add((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.Add((10, 10))
        hsz.Add(sz, 1, wx.EXPAND)
        hsz.Add((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.Add((10, 10))

        sz = wx.BoxSizer(wx.VERTICAL)
        sz.Add((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.Add((10, 10))
        hsz.Add(sz, 1, wx.EXPAND)
        hsz.Add((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)
