# Authors: see git history
#
# Copyright (c) 2010 Authors
# Licensed under the GNU GPL version 3.0 or later.  See the file LICENSE for details.
import os
import sys
import time
from threading import Event, Thread

import wx
from wx.lib.intctrl import IntCtrl

from lib.debug import debug
from lib.utils import get_resource_dir
from lib.utils.settings import global_settings
from lib.utils.threading import ExitThread
from ..i18n import _
from ..stitch_plan import stitch_plan_from_file
from ..svg import PIXELS_PER_MM

# L10N command label at bottom of simulator window
COMMAND_NAMES = [_("STITCH"), _("JUMP"), _("TRIM"), _("STOP"), _("COLOR CHANGE")]

STITCH = 0
JUMP = 1
TRIM = 2
STOP = 3
COLOR_CHANGE = 4


class ControlPanel(wx.Panel):
    """"""

    @debug.time
    def __init__(self, parent, *args, **kwargs):
        """"""
        self.parent = parent
        self.stitch_plan = kwargs.pop('stitch_plan', None)
        self.detach_callback = kwargs.pop('detach_callback', None)
        self.target_stitches_per_second = kwargs.pop('stitches_per_second')
        self.target_duration = kwargs.pop('target_duration')
        kwargs['style'] = wx.BORDER_SUNKEN
        wx.Panel.__init__(self, parent, *args, **kwargs)

        self.drawing_panel = None
        self.num_stitches = 1
        self.current_stitch = 1
        self.speed = 1
        self.direction = 1
        self._last_color_block_end = 0

        self.icons_dir = get_resource_dir("icons")

        # Widgets
        self.button_size = self.GetTextExtent("M").y * 2
        self.button_style = wx.BU_EXACTFIT | wx.BU_NOTEXT
        self.btnMinus = wx.Button(self, -1, style=self.button_style)
        self.btnMinus.Bind(wx.EVT_BUTTON, self.animation_slow_down)
        self.btnMinus.SetBitmap(self.load_icon('slower'))
        self.btnMinus.SetToolTip(_('Slow down (arrow down)'))
        self.btnPlus = wx.Button(self, -1, style=self.button_style)
        self.btnPlus.Bind(wx.EVT_BUTTON, self.animation_speed_up)
        self.btnPlus.SetBitmap(self.load_icon('faster'))
        self.btnPlus.SetToolTip(_('Speed up (arrow up)'))
        self.btnBackwardStitch = wx.Button(self, -1, style=self.button_style)
        self.btnBackwardStitch.Bind(wx.EVT_BUTTON, self.animation_one_stitch_backward)
        self.btnBackwardStitch.SetBitmap(self.load_icon('backward_stitch'))
        self.btnBackwardStitch.SetToolTip(_('Go backward one stitch (-)'))
        self.btnForwardStitch = wx.Button(self, -1, style=self.button_style)
        self.btnForwardStitch.Bind(wx.EVT_BUTTON, self.animation_one_stitch_forward)
        self.btnForwardStitch.SetBitmap(self.load_icon('forward_stitch'))
        self.btnForwardStitch.SetToolTip(_('Go forward one stitch (+)'))
        self.btnBackwardCommand = wx.Button(self, -1, style=self.button_style)
        self.btnBackwardCommand.Bind(wx.EVT_BUTTON, self.animation_one_command_backward)
        self.btnBackwardCommand.SetBitmap(self.load_icon('backward_command'))
        self.btnBackwardCommand.SetToolTip(_('Go backward one command (page-down)'))
        self.btnForwardCommand = wx.Button(self, -1, style=self.button_style)
        self.btnForwardCommand.Bind(wx.EVT_BUTTON, self.animation_one_command_forward)
        self.btnForwardCommand.SetBitmap(self.load_icon('forward_command'))
        self.btnForwardCommand.SetToolTip(_('Go forward one command (page-up)'))
        self.btnDirection = wx.Button(self, -1, style=self.button_style)
        self.btnDirection.Bind(wx.EVT_BUTTON, self.on_direction_button)
        self.btnDirection.SetBitmap(self.load_icon('direction'))
        self.btnDirection.SetToolTip(_('Switch animation direction (arrow left, arrow right)'))
        self.btnPlay = wx.BitmapToggleButton(self, -1, style=self.button_style)
        self.btnPlay.Bind(wx.EVT_TOGGLEBUTTON, self.on_play_button)
        self.btnPlay.SetBitmap(self.load_icon('play'))
        self.btnPlay.SetToolTip(_('Play (P)'))
        self.btnRestart = wx.Button(self, -1, style=self.button_style)
        self.btnRestart.Bind(wx.EVT_BUTTON, self.animation_restart)
        self.btnRestart.SetBitmap(self.load_icon('restart'))
        self.btnRestart.SetToolTip(_('Restart (R)'))
        self.btnNpp = wx.BitmapToggleButton(self, -1, style=self.button_style)
        self.btnNpp.Bind(wx.EVT_TOGGLEBUTTON, self.toggle_npp)
        self.btnNpp.SetBitmap(self.load_icon('npp'))
        self.btnNpp.SetToolTip(_('Display needle penetration point (O)'))
        self.slider = SimulatorSlider(self, -1, value=1, minValue=1, maxValue=2)
        self.slider.Bind(wx.EVT_SLIDER, self.on_slider)
        self.stitchBox = IntCtrl(self, -1, value=1, min=1, max=2, limited=True, allow_none=True,
                                 size=((100, -1)), style=wx.TE_PROCESS_ENTER)
        self.stitchBox.Bind(wx.EVT_LEFT_DOWN, self.on_stitch_box_focus)
        self.stitchBox.Bind(wx.EVT_SET_FOCUS, self.on_stitch_box_focus)
        self.stitchBox.Bind(wx.EVT_TEXT_ENTER, self.on_stitch_box_focusout)
        self.stitchBox.Bind(wx.EVT_KILL_FOCUS, self.on_stitch_box_focusout)
        self.Bind(wx.EVT_LEFT_DOWN, self.on_stitch_box_focusout)
        self.totalstitchText = wx.StaticText(self, -1, label="/ ________")
        self.btnJump = wx.BitmapToggleButton(self, -1, style=self.button_style)
        self.btnJump.SetToolTip(_('Show jump stitches'))
        self.btnJump.SetBitmap(self.load_icon('jump'))
        self.btnJump.Bind(wx.EVT_TOGGLEBUTTON, lambda event: self.on_marker_button('jump', event))
        self.btnTrim = wx.BitmapToggleButton(self, -1, style=self.button_style)
        self.btnTrim.SetToolTip(_('Show trims'))
        self.btnTrim.SetBitmap(self.load_icon('trim'))
        self.btnTrim.Bind(wx.EVT_TOGGLEBUTTON, lambda event: self.on_marker_button('trim', event))
        self.btnStop = wx.BitmapToggleButton(self, -1, style=self.button_style)
        self.btnStop.SetToolTip(_('Show stops'))
        self.btnStop.SetBitmap(self.load_icon('stop'))
        self.btnStop.Bind(wx.EVT_TOGGLEBUTTON, lambda event: self.on_marker_button('stop', event))
        self.btnColorChange = wx.BitmapToggleButton(self, -1, style=self.button_style)
        self.btnColorChange.SetToolTip(_('Show color changes'))
        self.btnColorChange.SetBitmap(self.load_icon('color_change'))
        self.btnColorChange.Bind(wx.EVT_TOGGLEBUTTON, lambda event: self.on_marker_button('color_change', event))
        if self.detach_callback:
            self.btnDetachSimulator = wx.BitmapButton(self, -1, style=self.button_style)
            self.btnDetachSimulator.SetToolTip(_('Detach/attach simulator window'))
            self.btnDetachSimulator.SetBitmap(self.load_icon('detach_window'))
            self.btnDetachSimulator.Bind(wx.EVT_BUTTON, lambda event: self.detach_callback())

        # Layout
        self.hbSizer1 = wx.BoxSizer(wx.HORIZONTAL)
        self.hbSizer1.Add(self.slider, 1, wx.EXPAND | wx.RIGHT, 10)
        self.hbSizer1.Add(self.stitchBox, 0, wx.ALIGN_CENTER | wx.Right, 10)
        self.hbSizer1.Add(self.totalstitchText, 0, wx.ALIGN_CENTER | wx.LEFT, 10)

        self.controls_sizer = wx.StaticBoxSizer(wx.StaticBox(self, wx.ID_ANY, _("Controls")), wx.HORIZONTAL)
        self.controls_inner_sizer = wx.BoxSizer(wx.HORIZONTAL)
        self.controls_inner_sizer.Add(self.btnBackwardCommand, 0, wx.EXPAND | wx.ALL, 2)
        self.controls_inner_sizer.Add(self.btnBackwardStitch, 0, wx.EXPAND | wx.ALL, 2)
        self.controls_inner_sizer.Add(self.btnForwardStitch, 0, wx.EXPAND | wx.ALL, 2)
        self.controls_inner_sizer.Add(self.btnForwardCommand, 0, wx.EXPAND | wx.ALL, 2)
        self.controls_inner_sizer.Add(self.btnDirection, 0, wx.EXPAND | wx.ALL, 2)
        self.controls_inner_sizer.Add(self.btnPlay, 0, wx.EXPAND | wx.ALL, 2)
        self.controls_inner_sizer.Add(self.btnRestart, 0, wx.EXPAND | wx.ALL, 2)
        self.controls_sizer.Add((1, 1), 1)
        self.controls_sizer.Add(self.controls_inner_sizer, 0, wx.ALIGN_CENTER_VERTICAL | wx.ALL, 10)
        self.controls_sizer.Add((1, 1), 1)

        self.show_sizer = wx.StaticBoxSizer(wx.StaticBox(self, wx.ID_ANY, _("Show")), wx.HORIZONTAL)
        self.show_inner_sizer = wx.BoxSizer(wx.HORIZONTAL)
        self.show_inner_sizer.Add(self.btnNpp, 0, wx.EXPAND | wx.ALL, 2)
        self.show_inner_sizer.Add(self.btnJump, 0, wx.ALL, 2)
        self.show_inner_sizer.Add(self.btnTrim, 0, wx.ALL, 2)
        self.show_inner_sizer.Add(self.btnStop, 0, wx.ALL, 2)
        self.show_inner_sizer.Add(self.btnColorChange, 0, wx.ALL, 2)
        if self.detach_callback:
            self.show_inner_sizer.Add(self.btnDetachSimulator, 0, wx.ALL, 2)
        self.show_sizer.Add((1, 1), 1)
        self.show_sizer.Add(self.show_inner_sizer, 0, wx.ALIGN_CENTER_VERTICAL | wx.ALL, 10)
        self.show_sizer.Add((1, 1), 1)

        self.speed_sizer = wx.StaticBoxSizer(wx.StaticBox(self, wx.ID_ANY, _("Speed")), wx.VERTICAL)

        self.speed_buttons_sizer = wx.BoxSizer(wx.HORIZONTAL)
        self.speed_buttons_sizer.Add((1, 1), 1)
        self.speed_buttons_sizer.Add(self.btnMinus, 0, wx.ALL, 2)
        self.speed_buttons_sizer.Add(self.btnPlus, 0, wx.ALL, 2)
        self.speed_buttons_sizer.Add((1, 1), 1)
        self.speed_sizer.Add(self.speed_buttons_sizer, 0, wx.EXPAND | wx.ALL)
        self.speed_text = wx.StaticText(self, wx.ID_ANY, label="", style=wx.ALIGN_CENTRE_HORIZONTAL | wx.ST_NO_AUTORESIZE)
        self.speed_text.SetFont(wx.Font(wx.FontInfo(10).Bold()))
        extent = self.speed_text.GetTextExtent(self.format_speed_text(100000))
        self.speed_text.SetMinSize(extent)
        self.speed_sizer.Add(self.speed_text, 0, wx.EXPAND | wx.ALL, 5)

        # A normal BoxSizer can only make child components the same or
        # proportional size.  A FlexGridSizer can split up the available extra
        # space evenly among all growable columns.
        self.control_row2_sizer = wx.FlexGridSizer(cols=3, vgap=0, hgap=5)
        self.control_row2_sizer.AddGrowableCol(0)
        self.control_row2_sizer.AddGrowableCol(1)
        self.control_row2_sizer.AddGrowableCol(2)
        self.control_row2_sizer.Add(self.controls_sizer, 0, wx.EXPAND)
        self.control_row2_sizer.Add(self.speed_sizer, 0, wx.EXPAND)
        self.control_row2_sizer.Add(self.show_sizer, 0, wx.EXPAND)

        self.vbSizer = vbSizer = wx.BoxSizer(wx.VERTICAL)
        vbSizer.Add(self.hbSizer1, 1, wx.EXPAND | wx.ALL, 10)
        vbSizer.Add(self.control_row2_sizer, 0, wx.EXPAND | wx.LEFT | wx.RIGHT | wx.BOTTOM, 10)
        self.SetSizerAndFit(vbSizer)

        # Keyboard Shortcuts
        shortcut_keys = [
            (wx.ACCEL_NORMAL, wx.WXK_RIGHT, self.animation_forward),
            (wx.ACCEL_NORMAL, wx.WXK_NUMPAD_RIGHT, self.animation_forward),
            (wx.ACCEL_NORMAL, wx.WXK_LEFT, self.animation_reverse),
            (wx.ACCEL_NORMAL, wx.WXK_NUMPAD_LEFT, self.animation_reverse),
            (wx.ACCEL_NORMAL, wx.WXK_UP, self.animation_speed_up),
            (wx.ACCEL_NORMAL, wx.WXK_NUMPAD_UP, self.animation_speed_up),
            (wx.ACCEL_NORMAL, wx.WXK_DOWN, self.animation_slow_down),
            (wx.ACCEL_NORMAL, wx.WXK_NUMPAD_DOWN, self.animation_slow_down),
            (wx.ACCEL_NORMAL, ord('+'), self.animation_one_stitch_forward),
            (wx.ACCEL_NORMAL, ord('='), self.animation_one_stitch_forward),
            (wx.ACCEL_SHIFT, ord('='), self.animation_one_stitch_forward),
            (wx.ACCEL_NORMAL, wx.WXK_ADD, self.animation_one_stitch_forward),
            (wx.ACCEL_NORMAL, wx.WXK_NUMPAD_ADD, self.animation_one_stitch_forward),
            (wx.ACCEL_NORMAL, wx.WXK_NUMPAD_UP, self.animation_one_stitch_forward),
            (wx.ACCEL_NORMAL, ord('-'), self.animation_one_stitch_backward),
            (wx.ACCEL_NORMAL, ord('_'), self.animation_one_stitch_backward),
            (wx.ACCEL_NORMAL, wx.WXK_SUBTRACT, self.animation_one_stitch_backward),
            (wx.ACCEL_NORMAL, wx.WXK_NUMPAD_SUBTRACT, self.animation_one_stitch_backward),
            (wx.ACCEL_NORMAL, ord('r'), self.animation_restart),
            (wx.ACCEL_NORMAL, ord('o'), self.on_toggle_npp_shortcut),
            (wx.ACCEL_NORMAL, ord('p'), self.play_or_pause),
            (wx.ACCEL_NORMAL, wx.WXK_SPACE, self.play_or_pause),
            (wx.ACCEL_NORMAL, wx.WXK_PAGEDOWN, self.animation_one_command_backward),
            (wx.ACCEL_NORMAL, wx.WXK_PAGEUP, self.animation_one_command_forward),

        ]

        self.accel_entries = []

        for shortcut_key in shortcut_keys:
            eventId = wx.NewIdRef()
            self.accel_entries.append((shortcut_key[0], shortcut_key[1], eventId))
            self.Bind(wx.EVT_MENU, shortcut_key[2], id=eventId)

        self.accel_table = wx.AcceleratorTable(self.accel_entries)
        self.SetAcceleratorTable(self.accel_table)
        self.SetFocus()

        # wait for layouts so that panel size is set
        if self.stitch_plan:
            wx.CallLater(50, self.load, self.stitch_plan)

    def set_drawing_panel(self, drawing_panel):
        self.drawing_panel = drawing_panel
        self.drawing_panel.set_speed(self.speed)

    def _set_num_stitches(self, num_stitches):
        if num_stitches < 2:
            # otherwise the slider and intctrl get mad
            num_stitches = 2
        self.num_stitches = num_stitches
        self.stitchBox.SetMax(num_stitches)
        self.slider.SetMax(num_stitches)
        self.totalstitchText.SetLabel(f"/ { num_stitches }")
        self.choose_speed()

    def add_color(self, color, num_stitches):
        start = self._last_color_block_end + 1
        self.slider.add_color_section(ColorSection(color.rgb, start, start + num_stitches - 1))
        self._last_color_block_end = self._last_color_block_end + num_stitches

    def load(self, stitch_plan):
        self.stitches = []
        self._set_num_stitches(stitch_plan.num_stitches)

        stitch_num = 0
        for color_block in stitch_plan.color_blocks:
            self.stitches.extend(color_block.stitches)

            start = stitch_num + 1
            end = start + color_block.num_stitches
            self.slider.add_color_section(color_block.color.rgb, start, end)

            for stitch_num, stitch in enumerate(color_block.stitches, start):
                if stitch.trim:
                    self.slider.add_marker("trim", stitch_num)
                elif stitch.stop:
                    self.slider.add_marker("stop", stitch_num)
                elif stitch.jump:
                    self.slider.add_marker("jump", stitch_num)
                elif stitch.color_change:
                    self.slider.add_marker("color_change", stitch_num)

    def load_icon(self, icon_name):
        icon = wx.Image(os.path.join(self.icons_dir, f"{icon_name}.png"))
        icon.Rescale(self.button_size, self.button_size, wx.IMAGE_QUALITY_HIGH)
        return icon.ConvertToBitmap()

    def on_marker_button(self, marker_type, event):
        self.slider.enable_marker_list(marker_type, event.GetEventObject().GetValue())

    def choose_speed(self):
        if self.target_duration:
            self.set_speed(int(self.num_stitches / float(self.target_duration)))
        else:
            self.set_speed(self.target_stitches_per_second)

    def animation_forward(self, event=None):
        self.drawing_panel.forward()
        self.direction = 1
        self.update_speed_text()

    def animation_reverse(self, event=None):
        self.drawing_panel.reverse()
        self.direction = -1
        self.update_speed_text()

    def on_direction_button(self, event):
        if self.direction == -1:
            self.animation_forward()
        else:
            self.animation_reverse()

    def set_speed(self, speed):
        self.speed = int(max(speed, 1))
        self.update_speed_text()

        if self.drawing_panel:
            self.drawing_panel.set_speed(self.speed)

    def format_speed_text(self, speed):
        return _('%d stitches/sec') % speed

    def update_speed_text(self):
        self.speed_text.SetLabel(self.format_speed_text(self.speed * self.direction))

    def on_slider(self, event):
        self.animation_pause()
        stitch = event.GetEventObject().GetValue()
        self.stitchBox.SetValue(stitch)

        if self.drawing_panel:
            self.drawing_panel.set_current_stitch(stitch)

        self.parent.SetFocus()

    def on_current_stitch(self, stitch, command):
        if self.current_stitch != stitch:
            self.current_stitch = stitch
            self.slider.SetValue(stitch)
            self.stitchBox.SetValue(stitch)

    def on_stitch_box_focus(self, event):
        self.animation_pause()
        self.SetAcceleratorTable(wx.AcceleratorTable([]))
        event.Skip()

    def on_stitch_box_focusout(self, event):
        self.SetAcceleratorTable(self.accel_table)
        stitch = self.stitchBox.GetValue()
        # We now want to remove the focus from the stitchBox.
        # In Windows it won't work if we set focus to self.parent, while setting the focus to the
        # top level would work. This in turn would activate the trim button in Linux. So let's
        # set the focus on the slider instead where it doesn't cause any harm in any of the operating systems
        self.slider.SetFocus()

        if stitch is None:
            stitch = 1
            self.stitchBox.SetValue(1)

        self.slider.SetValue(stitch)

        if self.drawing_panel:
            self.drawing_panel.set_current_stitch(stitch)
        event.Skip()

    def animation_slow_down(self, event):
        """"""
        self.set_speed(self.speed / 2.0)

    def animation_speed_up(self, event):
        """"""
        self.set_speed(self.speed * 2.0)

    def animation_pause(self, event=None):
        self.drawing_panel.stop()

    def animation_start(self, event=None):
        self.drawing_panel.go()

    def on_start(self):
        self.btnPlay.SetValue(True)

    def on_stop(self):
        self.btnPlay.SetValue(False)

    def on_play_button(self, event):
        play = self.btnPlay.GetValue()
        if play:
            self.animation_start()
        else:
            self.animation_pause()

    def play_or_pause(self, event):
        if self.drawing_panel.animating:
            self.animation_pause()
        else:
            self.animation_start()

    def animation_one_stitch_forward(self, event):
        self.animation_pause()
        self.drawing_panel.one_stitch_forward()

    def animation_one_stitch_backward(self, event):
        self.animation_pause()
        self.drawing_panel.one_stitch_backward()

    def animation_one_command_backward(self, event):
        self.animation_pause()
        stitch_number = self.current_stitch - 1
        while stitch_number >= 1:
            # stitch number shown to the user starts at 1
            stitch = self.stitches[stitch_number - 1]
            if stitch.jump or stitch.trim or stitch.stop or stitch.color_change:
                break
            stitch_number -= 1
        self.drawing_panel.set_current_stitch(stitch_number)

    def animation_one_command_forward(self, event):
        self.animation_pause()
        stitch_number = self.current_stitch + 1
        while stitch_number <= self.num_stitches:
            # stitch number shown to the user starts at 1
            stitch = self.stitches[stitch_number - 1]
            if stitch.jump or stitch.trim or stitch.stop or stitch.color_change:
                break
            stitch_number += 1
        self.drawing_panel.set_current_stitch(stitch_number)

    def animation_restart(self, event):
        self.drawing_panel.restart()

    def on_toggle_npp_shortcut(self, event):
        self.btnNpp.SetValue(not self.btnNpp.GetValue())
        self.toggle_npp(event)

    def toggle_npp(self, event):
        self.drawing_panel.Refresh()


class DrawingPanel(wx.Panel):
    """"""

    # render no faster than this many frames per second
    TARGET_FPS = 30

    # It's not possible to specify a line thickness less than 1 pixel, even
    # though we're drawing anti-aliased lines.  To get around this we scale
    # the stitch positions up by this factor and then scale down by a
    # corresponding amount during rendering.
    PIXEL_DENSITY = 10

    # Line width in pixels.
    LINE_THICKNESS = 0.4

    def __init__(self, *args, **kwargs):
        """"""
        self.stitch_plan = kwargs.pop('stitch_plan', None)
        self.control_panel = kwargs.pop('control_panel')
        kwargs['style'] = wx.BORDER_SUNKEN
        wx.Panel.__init__(self, *args, **kwargs)

        # Drawing panel can really be any size, but without this wxpython likes
        # to allow the status bar and control panel to get squished.
        self.SetMinSize((100, 100))
        self.SetBackgroundColour('#FFFFFF')
        self.SetDoubleBuffered(True)

        self.animating = False
        self.target_frame_period = 1.0 / self.TARGET_FPS
        self.last_frame_duration = 0
        self.direction = 1
        self.current_stitch = 0
        self.black_pen = wx.Pen((128, 128, 128))
        self.width = 0
        self.height = 0
        self.loaded = False

        # desired simulation speed in stitches per second
        self.speed = 16

        self.Bind(wx.EVT_PAINT, self.OnPaint)
        self.Bind(wx.EVT_SIZE, self.choose_zoom_and_pan)
        self.Bind(wx.EVT_LEFT_DOWN, self.on_left_mouse_button_down)
        self.Bind(wx.EVT_MOUSEWHEEL, self.on_mouse_wheel)

        self.SetMinSize((400, 400))

        # wait for layouts so that panel size is set
        if self.stitch_plan:
            wx.CallLater(50, self.load, self.stitch_plan)

    def clamp_current_stitch(self):
        if self.current_stitch < 1:
            self.current_stitch = 1
        elif self.current_stitch > self.num_stitches:
            self.current_stitch = self.num_stitches

    def stop_if_at_end(self):
        if self.direction == -1 and self.current_stitch == 1:
            self.stop()
        elif self.direction == 1 and self.current_stitch == self.num_stitches:
            self.stop()

    def start_if_not_at_end(self):
        if self.direction == -1 and self.current_stitch > 1:
            self.go()
        elif self.direction == 1 and self.current_stitch < self.num_stitches:
            self.go()

    def animate(self):
        if not self.animating:
            return

        frame_time = max(self.target_frame_period, self.last_frame_duration)

        # No sense in rendering more frames per second than our desired stitches
        # per second.
        frame_time = max(frame_time, 1.0 / self.speed)

        stitch_increment = int(self.speed * frame_time)

        self.set_current_stitch(self.current_stitch + self.direction * stitch_increment)
        wx.CallLater(int(1000 * frame_time), self.animate)

    def OnPaint(self, e):
        dc = wx.PaintDC(self)

        if not self.loaded:
            dc.Clear()
            return

        canvas = wx.GraphicsContext.Create(dc)

        self.draw_stitches(canvas)
        self.draw_scale(canvas)

    def draw_stitches(self, canvas):
        canvas.BeginLayer(1)

        transform = canvas.GetTransform()
        transform.Translate(*self.pan)
        transform.Scale(self.zoom / self.PIXEL_DENSITY, self.zoom / self.PIXEL_DENSITY)
        canvas.SetTransform(transform)

        stitch = 0
        last_stitch = None

        start = time.time()
        for pen, stitches in zip(self.pens, self.stitch_blocks):
            canvas.SetPen(pen)
            if stitch + len(stitches) < self.current_stitch:
                stitch += len(stitches)
                if len(stitches) > 1:
                    canvas.StrokeLines(stitches)
                    self.draw_needle_penetration_points(canvas, pen, stitches)
                last_stitch = stitches[-1]
            else:
                stitches = stitches[:self.current_stitch - stitch]
                if len(stitches) > 1:
                    canvas.StrokeLines(stitches)
                    self.draw_needle_penetration_points(canvas, pen, stitches)
                last_stitch = stitches[-1]
                break
        self.last_frame_duration = time.time() - start

        if last_stitch:
            self.draw_crosshair(last_stitch[0], last_stitch[1], canvas, transform)

        canvas.EndLayer()

    def draw_crosshair(self, x, y, canvas, transform):
        x, y = transform.TransformPoint(float(x), float(y))
        canvas.SetTransform(canvas.CreateMatrix())
        crosshair_radius = 10
        canvas.SetPen(self.black_pen)
        canvas.StrokeLines(((x - crosshair_radius, y), (x + crosshair_radius, y)))
        canvas.StrokeLines(((x, y - crosshair_radius), (x, y + crosshair_radius)))

    def draw_scale(self, canvas):
        canvas.BeginLayer(1)

        canvas_width, canvas_height = self.GetClientSize()

        one_mm = PIXELS_PER_MM * self.zoom
        scale_width = one_mm
        max_width = min(canvas_width * 0.5, 300)

        while scale_width > max_width:
            scale_width /= 2.0

        while scale_width < 50:
            scale_width += one_mm

        scale_width_mm = int(scale_width / self.zoom / PIXELS_PER_MM)

        # The scale bar looks like this:
        #
        # |           |
        # |_____|_____|

        scale_lower_left_x = 20
        scale_lower_left_y = canvas_height - 30

        canvas.StrokeLines(((scale_lower_left_x, scale_lower_left_y - 6),
                            (scale_lower_left_x, scale_lower_left_y),
                            (scale_lower_left_x + scale_width / 2.0, scale_lower_left_y),
                            (scale_lower_left_x + scale_width / 2.0, scale_lower_left_y - 3),
                            (scale_lower_left_x + scale_width / 2.0, scale_lower_left_y),
                            (scale_lower_left_x + scale_width, scale_lower_left_y),
                            (scale_lower_left_x + scale_width, scale_lower_left_y - 6)))

        canvas.SetFont(wx.Font(12, wx.DEFAULT, wx.NORMAL, wx.NORMAL), wx.Colour((0, 0, 0)))
        canvas.DrawText("%s mm" % scale_width_mm, scale_lower_left_x, scale_lower_left_y + 5)

        canvas.EndLayer()

    def draw_needle_penetration_points(self, canvas, pen, stitches):
        if self.control_panel.btnNpp.GetValue():
            npp_pen = wx.Pen(pen.GetColour(), width=int(0.5 * PIXELS_PER_MM * self.PIXEL_DENSITY))
            canvas.SetPen(npp_pen)
            canvas.StrokeLineSegments(stitches, [(stitch[0] + 0.001, stitch[1]) for stitch in stitches])

    def clear(self):
        self.loaded = False
        self.Refresh()

    def load(self, stitch_plan):
        self.current_stitch = 1
        self.direction = 1
        self.last_frame_duration = 0
        self.minx, self.miny, self.maxx, self.maxy = stitch_plan.bounding_box
        self.width = self.maxx - self.minx
        self.height = self.maxy - self.miny
        self.num_stitches = stitch_plan.num_stitches
        self.parse_stitch_plan(stitch_plan)
        self.choose_zoom_and_pan()
        self.set_current_stitch(0)
        self.loaded = True
        self.go()

    def choose_zoom_and_pan(self, event=None):
        # ignore if EVT_SIZE fired before we load the stitch plan
        if not self.width and not self.height and event is not None:
            return

        panel_width, panel_height = self.GetClientSize()

        # add some padding to make stitches at the edge more visible
        width_ratio = panel_width / float(self.width + 10)
        height_ratio = panel_height / float(self.height + 10)
        self.zoom = min(width_ratio, height_ratio)

        # center the design
        self.pan = ((panel_width - self.zoom * self.width) / 2.0,
                    (panel_height - self.zoom * self.height) / 2.0)

    def stop(self):
        self.animating = False
        self.control_panel.on_stop()

    def go(self):
        if not self.loaded:
            return

        if not self.animating:
            self.animating = True
            self.animate()
            self.control_panel.on_start()

    def color_to_pen(self, color):
        # We draw the thread with a thickness of 0.1mm.  Real thread has a
        # thickness of ~0.4mm, but if we did that, we wouldn't be able to
        # see the individual stitches.
        return wx.Pen(list(map(int, color.visible_on_white.rgb)), int(0.1 * PIXELS_PER_MM * self.PIXEL_DENSITY))

    def parse_stitch_plan(self, stitch_plan):
        self.pens = []
        self.stitch_blocks = []

        # There is no 0th stitch, so add a place-holder.
        self.commands = [None]

        for color_block in stitch_plan:
            pen = self.color_to_pen(color_block.color)
            stitch_block = []

            for stitch in color_block:
                # trim any whitespace on the left and top and scale to the
                # pixel density
                stitch_block.append((self.PIXEL_DENSITY * (stitch.x - self.minx),
                                     self.PIXEL_DENSITY * (stitch.y - self.miny)))

                if stitch.trim:
                    self.commands.append(TRIM)
                elif stitch.jump:
                    self.commands.append(JUMP)
                elif stitch.stop:
                    self.commands.append(STOP)
                elif stitch.color_change:
                    self.commands.append(COLOR_CHANGE)
                else:
                    self.commands.append(STITCH)

                if stitch.trim or stitch.stop or stitch.color_change:
                    self.pens.append(pen)
                    self.stitch_blocks.append(stitch_block)
                    stitch_block = []

            if stitch_block:
                self.pens.append(pen)
                self.stitch_blocks.append(stitch_block)

    def set_speed(self, speed):
        self.speed = speed

    def forward(self):
        self.direction = 1
        self.start_if_not_at_end()

    def reverse(self):
        self.direction = -1
        self.start_if_not_at_end()

    def set_current_stitch(self, stitch):
        self.current_stitch = stitch
        self.clamp_current_stitch()
        command = self.commands[self.current_stitch]
        self.control_panel.on_current_stitch(self.current_stitch, command)
        statusbar = self.GetTopLevelParent().statusbar
        statusbar.SetStatusText(_("Command: %s") % COMMAND_NAMES[command], 1)
        self.stop_if_at_end()
        self.Refresh()

    def restart(self):
        if self.direction == 1:
            self.current_stitch = 1
        elif self.direction == -1:
            self.current_stitch = self.num_stitches

        self.go()

    def one_stitch_forward(self):
        self.set_current_stitch(self.current_stitch + 1)

    def one_stitch_backward(self):
        self.set_current_stitch(self.current_stitch - 1)

    def on_left_mouse_button_down(self, event):
        if self.loaded:
            self.CaptureMouse()
            self.drag_start = event.GetPosition()
            self.drag_original_pan = self.pan
            self.Bind(wx.EVT_MOTION, self.on_drag)
            self.Bind(wx.EVT_MOUSE_CAPTURE_LOST, self.on_drag_end)
            self.Bind(wx.EVT_LEFT_UP, self.on_drag_end)

    def on_drag(self, event):
        if self.HasCapture() and event.Dragging():
            delta = event.GetPosition()
            offset = (delta[0] - self.drag_start[0], delta[1] - self.drag_start[1])
            self.pan = (self.drag_original_pan[0] + offset[0], self.drag_original_pan[1] + offset[1])
            self.Refresh()

    def on_drag_end(self, event):
        if self.HasCapture():
            self.ReleaseMouse()

        self.Unbind(wx.EVT_MOTION)
        self.Unbind(wx.EVT_MOUSE_CAPTURE_LOST)
        self.Unbind(wx.EVT_LEFT_UP)

    def on_mouse_wheel(self, event):
        if event.GetWheelRotation() > 0:
            zoom_delta = 1.03
        else:
            zoom_delta = 0.97

        # If we just change the zoom, the design will appear to move on the
        # screen.  We have to adjust the pan to compensate.  We want to keep
        # the part of the design under the mouse pointer in the same spot
        # after we zoom, so that we appear to be zooming centered on the
        # mouse pointer.

        # This will create a matrix that takes a point in the design and
        # converts it to screen coordinates:
        matrix = wx.AffineMatrix2D()
        matrix.Translate(*self.pan)
        matrix.Scale(self.zoom, self.zoom)

        # First, figure out where the mouse pointer is in the coordinate system
        # of the design:
        pos = event.GetPosition()
        inverse_matrix = wx.AffineMatrix2D()
        inverse_matrix.Set(*matrix.Get())
        inverse_matrix.Invert()
        pos = inverse_matrix.TransformPoint(*pos)

        # Next, see how that point changes position on screen before and after
        # we apply the zoom change:
        x_old, y_old = matrix.TransformPoint(*pos)
        matrix.Scale(zoom_delta, zoom_delta)
        x_new, y_new = matrix.TransformPoint(*pos)
        x_delta = x_new - x_old
        y_delta = y_new - y_old

        # Finally, compensate for that change in position:
        self.pan = (self.pan[0] - x_delta, self.pan[1] - y_delta)

        self.zoom *= zoom_delta

        self.Refresh()


class MarkerList(list):
    def __init__(self, icon_name, stitch_numbers=()):
        super().__init__(self)
        icons_dir = get_resource_dir("icons")
        self.icon_name = icon_name
        self.icon = wx.Image(os.path.join(icons_dir, f"{icon_name}.png")).ConvertToBitmap()
        self.enabled = False
        self.extend(stitch_numbers)

    def __repr__(self):
        return f"MarkerList({self.icon_name})"


class ColorSection:
    def __init__(self, color, start, end):
        self.color = color
        self.start = start
        self.end = end
        self.brush = wx.Brush(wx.Colour(*color))


class SimulatorSlider(wx.Panel):
    PROXY_EVENTS = (wx.EVT_SLIDER,)

    def __init__(self, parent, id=wx.ID_ANY, minValue=0, maxValue=1, **kwargs):
        super().__init__(parent, id)

        kwargs['style'] = wx.SL_HORIZONTAL | wx.SL_VALUE_LABEL | wx.SL_TOP | wx.ALIGN_TOP

        self._height = self.GetTextExtent("M").y * 4
        self.SetMinSize((self._height, self._height))

        self.marker_lists = {
            "trim": MarkerList("trim"),
            "stop": MarkerList("stop"),
            "jump": MarkerList("jump"),
            "color_change": MarkerList("color_change"),
        }
        self.marker_pen = wx.Pen(wx.Colour(0, 0, 0))
        self.color_sections = []
        self.margin = 15
        self.tab_start = 0
        self.tab_width = 0.2
        self.tab_height = 0.2
        self.color_bar_start = 0.3
        self.color_bar_thickness = 0.25
        self.marker_start = self.color_bar_start
        self.marker_end = 0.75
        self.marker_icon_start = 0.75
        self.marker_icon_size = self._height // 4

        self._min = minValue
        self._max = maxValue
        self._value = 0
        self._tab_rect = None

        if sys.platform == "darwin":
            self.margin = 8

        self.Bind(wx.EVT_PAINT, self.on_paint)
        self.Bind(wx.EVT_ERASE_BACKGROUND, self.on_erase_background)
        self.Bind(wx.EVT_LEFT_DOWN, self.on_mouse_down)
        self.Bind(wx.EVT_LEFT_UP, self.on_mouse_up)
        self.Bind(wx.EVT_MOTION, self.on_mouse_motion)

    def SetMax(self, value):
        self._max = value
        self.Refresh()

    def SetMin(self, value):
        self._min = value
        self.Refresh()

    def SetValue(self, value):
        self._value = value
        self.Refresh()

    def GetValue(self):
        return self._value

    def add_color_section(self, color, start, end):
        self.color_sections.append(ColorSection(color, start, end))

    def add_marker(self, name, location):
        self.marker_lists[name].append(location)
        self.Refresh()

    def enable_marker_list(self, name, enabled=True):
        self.marker_lists[name].enabled = enabled
        self.Refresh()

    def disable_marker_list(self, name):
        self.marker_lists[name].enabled = False
        self.Refresh()

    def toggle_marker_list(self, name):
        self.marker_lists[name].enabled = not self.marker_lists[name].enabled
        self.Refresh()

    def on_paint(self, event):
        dc = wx.BufferedPaintDC(self)
        if not sys.platform.startswith("win"):
            # Without this, the background color will be white.
            background_brush = wx.Brush(self.GetTopLevelParent().GetBackgroundColour(), wx.SOLID)
            dc.SetBackground(background_brush)
        dc.Clear()
        gc = wx.GraphicsContext.Create(dc)

        width, height = self.GetSize()
        min_value = self._min
        max_value = self._max
        spread = max_value - min_value

        def _value_to_x(value):
            return (value - min_value) * (width - 2 * self.margin) / spread + self.margin

        gc.SetPen(wx.NullPen)
        for color_section in self.color_sections:
            gc.SetBrush(color_section.brush)

            start_x = _value_to_x(color_section.start)
            end_x = _value_to_x(color_section.end)
            gc.DrawRectangle(start_x, height * self.color_bar_start,
                             end_x - start_x, height * self.color_bar_thickness)

        gc.SetPen(wx.Pen(wx.Colour(255, 255, 255), 1))
        gc.SetBrush(wx.Brush(wx.Colour(0, 0, 0)))

        value_x = _value_to_x(self._value)
        tab_height = self.tab_height * height
        tab_width = self.tab_width * height
        tab_x = value_x - tab_width / 2
        tab_y = height * self.tab_start
        self._tab_rect = wx.Rect(round(tab_x), round(tab_y), round(tab_width), round(tab_height))
        gc.DrawRectangle(
            value_x - 1.5, 0,
            3, height * (self.color_bar_start + self.color_bar_thickness))
        gc.SetPen(wx.NullPen)
        gc.DrawRectangle(value_x - tab_width/2, height * self.tab_start,
                         tab_width, tab_height)

        gc.SetPen(self.marker_pen)
        for marker_list in self.marker_lists.values():
            if marker_list.enabled:
                for value in marker_list:
                    x = _value_to_x(value)
                    gc.StrokeLine(
                        x, height * self.marker_start,
                        x, height * self.marker_end
                    )
                    gc.DrawBitmap(
                        marker_list.icon,
                        x - self.marker_icon_size / 2, height * self.marker_icon_start,
                        self.marker_icon_size, self.marker_icon_size
                    )

    def on_erase_background(self, event):
        # supposedly this prevents flickering?
        pass

    def is_in_tab(self, point):
        return self._tab_rect and self._tab_rect.Inflate(2).Contains(point)

    def set_value_from_position(self, point):
        width, height = self.GetSize()
        min_value = self._min
        max_value = self._max
        spread = max_value - min_value
        value = round((point.x - self.margin) * spread / (width - 2 * self.margin))
        value = max(value, self._min)
        value = min(value, self._max)
        self.SetValue(round(value))

        event = wx.CommandEvent(wx.wxEVT_COMMAND_SLIDER_UPDATED, self.GetId())
        event.SetInt(value)
        event.SetEventObject(self)
        self.GetEventHandler().ProcessEvent(event)

    def on_mouse_down(self, event):
        click_pos = event.GetPosition()
        if self.is_in_tab(click_pos):
            debug.log("drag start")
            self.CaptureMouse()
            self.set_value_from_position(click_pos)
            self.Refresh()
        else:
            width, height = self.GetSize()
            relative_y = click_pos.y / height
            if relative_y > self.color_bar_start and relative_y - self.color_bar_start < self.color_bar_thickness:
                self.set_value_from_position(click_pos)
                self.Refresh()

    def on_mouse_motion(self, event):
        if self.HasCapture() and event.Dragging() and event.LeftIsDown():
            self.set_value_from_position(event.GetPosition())
            self.Refresh()

    def on_mouse_up(self, event):
        if self.HasCapture():
            self.ReleaseMouse()
            self.set_value_from_position(event.GetPosition())
            self.Refresh()


class SimulatorPanel(wx.Panel):
    """"""

    def __init__(self, parent, stitch_plan=None, target_duration=5, stitches_per_second=16, detach_callback=None):
        """"""
        super().__init__(parent, style=wx.BORDER_SUNKEN)

        self.cp = ControlPanel(self,
                               stitch_plan=stitch_plan,
                               stitches_per_second=stitches_per_second,
                               target_duration=target_duration,
                               detach_callback=detach_callback)
        self.dp = DrawingPanel(self, stitch_plan=stitch_plan, control_panel=self.cp)
        self.cp.set_drawing_panel(self.dp)

        vbSizer = wx.BoxSizer(wx.VERTICAL)
        vbSizer.Add(self.dp, 1, wx.EXPAND | wx.ALL, 2)
        vbSizer.Add(self.cp, 0, wx.EXPAND | wx.ALL, 2)
        self.SetSizerAndFit(vbSizer)

    def go(self):
        self.dp.go()

    def stop(self):
        self.dp.stop()

    def load(self, stitch_plan):
        self.dp.load(stitch_plan)
        self.cp.load(stitch_plan)

    def clear(self):
        self.dp.clear()


class SimulatorWindow(wx.Frame):
    def __init__(self, panel=None, parent=None, **kwargs):
        super().__init__(None, title=_("Embroidery Simulation"), **kwargs)

        self.SetWindowStyle(wx.FRAME_FLOAT_ON_PARENT | wx.DEFAULT_FRAME_STYLE)

        self.sizer = wx.BoxSizer(wx.VERTICAL)

        self.statusbar = self.CreateStatusBar(2)
        self.statusbar.SetStatusWidths((0, -1))

        if panel and parent:
            self.is_child = True
            self.panel = panel
            self.parent = parent
            self.panel.Reparent(self)
            self.sizer.Add(self.panel, 1, wx.EXPAND)
            self.panel.Show()
        else:
            self.is_child = False
            self.simulator_panel = SimulatorPanel(self)
            self.sizer.Add(self.simulator_panel, 1, wx.EXPAND)

        self.SetSizer(self.sizer)
        self.Layout()

        self.SetMinSize(self.sizer.CalcMin())

        if self.is_child:
            self.Bind(wx.EVT_CLOSE, self.on_close)

    def detach_simulator_panel(self):
        self.sizer.Detach(self.panel)

    def on_close(self, event):
        self.parent.attach_simulator()


class SplitSimulatorWindow(wx.Frame):
    def __init__(self, panel_class, title, target_duration=None, **kwargs):
        super().__init__(None, title=title)

        self.SetWindowStyle(wx.FRAME_FLOAT_ON_PARENT | wx.DEFAULT_FRAME_STYLE)

        self.detached_simulator_frame = None
        self.splitter = wx.SplitterWindow(self, style=wx.SP_LIVE_UPDATE)
        self.simulator_panel = SimulatorPanel(self.splitter, target_duration=target_duration, detach_callback=self.toggle_detach_simulator)
        self.settings_panel = panel_class(self.splitter, simulator=self.simulator_panel, **kwargs)

        self.splitter.SplitVertically(self.settings_panel, self.simulator_panel)
        self.splitter.SetMinimumPaneSize(100)

        icon = wx.Icon(os.path.join(get_resource_dir("icons"), "inkstitch256x256.png"))
        self.SetIcon(icon)

        self.statusbar = self.CreateStatusBar(2)

        self.sizer = wx.BoxSizer(wx.VERTICAL)
        self.sizer.Add(self.splitter, 1, wx.EXPAND)
        self.SetSizer(self.sizer)

        self.SetMinSize(self.sizer.CalcMin())

        self.Maximize()
        self.Show()
        wx.CallLater(100, self.set_sash_position)

        self.Bind(wx.EVT_SPLITTER_SASH_POS_CHANGING, self.splitter_resize)
        self.Bind(wx.EVT_CLOSE, self.on_close)

        if global_settings['pop_out_simulator']:
            self.detach_simulator()

    def splitter_resize(self, event):
        self.statusbar.SetStatusWidths((self.simulator_panel.GetScreenPosition()[0], -1))

    def set_sash_position(self):
        settings_panel_min_size = self.settings_panel.GetSizer().CalcMin()
        debug.log(f"{settings_panel_min_size=}")
        self.splitter.SetSashPosition(settings_panel_min_size.width)
        self.statusbar.SetStatusWidths((settings_panel_min_size.width, -1))

    def on_close(self, event):
        if self.detached_simulator_frame:
            self.detached_simulator_frame.Destroy()
        self.Destroy()

    def toggle_detach_simulator(self):
        if self.detached_simulator_frame:
            self.attach_simulator()
        else:
            self.detach_simulator()

    def attach_simulator(self):
        self.detached_simulator_frame.detach_simulator_panel()
        self.simulator_panel.Reparent(self.splitter)
        self.splitter.SplitVertically(self.settings_panel, self.simulator_panel)

        self.GetStatusBar().SetStatusText(self.detached_simulator_frame.GetStatusBar().GetStatusText(1), 1)

        self.detached_simulator_frame.Destroy()
        self.detached_simulator_frame = None
        self.Maximize()
        self.splitter.UpdateSize()
        self.SetFocus()
        self.Raise()
        wx.CallLater(100, self.set_sash_position)
        global_settings['pop_out_simulator'] = False

    def detach_simulator(self):
        self.splitter.Unsplit()
        self.detached_simulator_frame = SimulatorWindow(panel=self.simulator_panel, parent=self)
        self.splitter.SetMinimumPaneSize(100)

        current_screen = wx.Display.GetFromPoint(wx.GetMousePosition())
        display = wx.Display(current_screen)
        screen_rect = display.GetClientArea()
        settings_panel_size = self.settings_panel.GetSizer().CalcMin()
        self.SetMinSize(settings_panel_size)
        self.Maximize(False)
        self.SetSize((settings_panel_size.width, screen_rect.height))
        self.SetPosition((screen_rect.left, screen_rect.top))

        self.detached_simulator_frame.SetSize((screen_rect.width - settings_panel_size.width, screen_rect.height))
        self.detached_simulator_frame.SetPosition((settings_panel_size.width, screen_rect.top))

        self.detached_simulator_frame.GetStatusBar().SetStatusText(self.GetStatusBar().GetStatusText(1), 1)
        self.GetStatusBar().SetStatusText("", 1)

        self.detached_simulator_frame.Show()

        global_settings['pop_out_simulator'] = True


class PreviewRenderer(Thread):
    """Render stitch plan in a background thread."""

    def __init__(self, render_stitch_plan_hook, rendering_completed_hook):
        super(PreviewRenderer, self).__init__()
        self.daemon = True
        self.refresh_needed = Event()

        self.render_stitch_plan_hook = render_stitch_plan_hook
        self.rendering_completed_hook = rendering_completed_hook

        # This is read by utils.threading.check_stop_flag() to abort stitch plan
        # generation.
        self.stop = Event()

    def update(self):
        """Request to render a new stitch plan.

        self.render_stitch_plan_hook() will be called in a background thread, and then
        self.rendering_completed_hook() will be called with the resulting stitch plan.
        """

        if not self.is_alive():
            self.start()

        self.stop.set()
        self.refresh_needed.set()

    def run(self):
        while True:
            self.refresh_needed.wait()
            self.refresh_needed.clear()
            self.stop.clear()

            try:
                debug.log("update_patches")
                self.render_stitch_plan()
            except ExitThread:
                debug.log("ExitThread caught")
                self.stop.clear()

    def render_stitch_plan(self):
        try:
            stitch_plan = self.render_stitch_plan_hook()
            if stitch_plan:
                # rendering_completed() will be called in the main thread.
                wx.CallAfter(self.rendering_completed_hook, stitch_plan)
        except ExitThread:
            raise
        except:  # noqa: E722
            import traceback
            debug.log("unhandled exception in PreviewRenderer.render_stitch_plan(): " + traceback.format_exc())


def show_simulator(stitch_plan):
    app = wx.App()
    current_screen = wx.Display.GetFromPoint(wx.GetMousePosition())
    display = wx.Display(current_screen)
    screen_rect = display.GetClientArea()

    simulator_pos = (screen_rect[0], screen_rect[1])

    # subtract 1 because otherwise the window becomes maximized on Linux
    width = screen_rect[2] - 1
    height = screen_rect[3] - 1

    frame = SimulatorWindow(pos=simulator_pos, size=(width, height), stitch_plan=stitch_plan)
    app.SetTopWindow(frame)
    frame.Show()
    app.MainLoop()


if __name__ == "__main__":
    stitch_plan = stitch_plan_from_file(sys.argv[1])
    show_simulator(stitch_plan)
