#!/usr/bin/env python3
# -*- encoding: utf-8 -*-
#    Gcode display / edit widget for QT_VCP
#    Copyright 2016 Chris Morley
#
#    This program is free software; you can redistribute it and/or modify
#    it under the terms of the GNU General Public License as published by
#    the Free Software Foundation; either version 2 of the License, or
#    (at your option) any later version.
#
#    This program is distributed in the hope that it will be useful,
#    but WITHOUT ANY WARRANTY; without even the implied warranty of
#    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
#    GNU General Public License for more details.
#
#    You should have received a copy of the GNU General Public License
#    along with this program; if not, write to the Free Software
#    Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA

# This was based on
# QScintilla sample with PyQt
# Eli Bendersky (eliben@gmail.com)
# Which is code in the public domain
#
# See also:
# http://pyqt.sourceforge.net/Docs/QScintilla2/index.html
# https://qscintilla.com/
# https://qscintilla.com/simple-example/

import sys
import os
import re

from qtpy.QtCore import Property, Signal, QSize
from qtpy.QtGui import QFont, QFontMetrics, QColor, QIcon
from qtpy.QtWidgets import QWidget, QAction,\
        QVBoxLayout, QToolBar, QLineEdit, QHBoxLayout, QMessageBox, \
        QFrame, QLabel, QStyle

from qtvcp.widgets.widget_baseclass import _HalWidgetBase
from qtvcp.core import Status, Info, Action
from qtvcp import logger

# Instantiate the libraries with global reference
# INFO holds INI file details
# STATUS gives us status messages from linuxcnc
# LOG is for running code logging
STATUS = Status()
INFO = Info()
ACTION = Action()
LOG = logger.getLogger(__name__)

# Force the log level for this module
# LOG.setLevel(logger.DEBUG) # One of DEBUG, INFO, WARNING, ERROR, CRITICAL

# load this after Logging set up so we get a nice dialog.
try:
    from qtpy.Qsci import QsciScintilla, QsciLexerCustom, QsciLexerPython
except ImportError:
    try:
        from PyQt5.Qsci import QsciScintilla, QsciLexerCustom, QsciLexerPython
    except ImportError:
        try:
            from PyQt6.Qsci import QsciScintilla, QsciLexerCustom, QsciLexerPython
        except ImportError as e:
            LOG.critical("Can't import QsciScintilla - is package python3-pyqt5.qsci or python3-pyqt6.qsci installed?", exc_info=e)
            sys.exit(1)


class GcodeLexer(QsciLexerCustom):
    """ QSciLexer for parsing and highlighting G-code """

    def __init__(self, parent):
        super(GcodeLexer, self).__init__(parent)

        # Style/Token Types (Used for Styling & Regex)
        self._styles = {
            0: 'Default',
            1: 'Comment',
            2: 'Gcode',
            3: 'Mcode',
            4: 'Axis',
            5: 'Other',
            6: 'AxisValue',
            7: 'OtherValue',
        }

        for key, value in self._styles.items():
            setattr(self, value, key)

    def language(self):
        return "G-code"

    def description(self, style):
        return self._styles.get(style, "")

    def styleText(self, start, end):
        editor = self.editor()
        if editor is None:
            return

        self.startStyling(start)

        # scintilla works with encoded bytes, not decoded characters.
        # this matters if the source contains non-ascii characters and
        # a multi-byte encoding is used (e.g. utf-8)
        source = ''
        if end > editor.length():
            end = editor.length()
        if end > start:
            if sys.hexversion >= 0x02060000:
                # faster when styling big files, but needs python 2.6
                source = bytearray(end - start)
                editor.SendScintilla(
                    editor.SCI_GETTEXTRANGE, start, end, source)
                source = source.decode("utf-8", "ignore")
            else:
                source = str(editor.text())[start:end]
        if not source:
            return

        re_tokens = {
            1: r"(?:[N]\d+|\(.*?\)|;.*)",                                       # LineNo and Comment
            2: r"[G]\s*\d{1,2}\.\d\s|[G]\s*\d{1,2}",                            # Gcode
            3: r"[M]\s*\d{1,3}",                                                # Mcode
            4: r"[XYZABCUVW]{1}(?:[+-]?[\d\.\s\][+-]+|\#\<.*\>|\[.*\]|\#\d+)",  # Axis
            5: r"[EFHIJKDQLRPST$]{1}(?:[+-]?[\d\.\s]+|\#\<.*\>|\[.*\]|\#\d+)",  # Other (feed,rpm,radius,etc)
            0: r"\s+|\w+|\W",                                                   # Default (fallback)
        }

        re_comment_cmd = r"(?:\(\s*(?:print,|debug,|msg,|logopen,|logappend,|logclose|log,|pyrun,|pyreload|abort,|probeopen|probeclose)|^\s*\;py,)"

        re_string = "|".join(re_tokens.values())
        p = re.compile(re_string, re.IGNORECASE)

        for line in source.splitlines(True):
            token_list = [(token, len(bytearray(token, "utf-8"))) for token in p.findall(line)]
            num_comment_cmds = len(re.findall(re_comment_cmd, line, re.IGNORECASE))

            for token in token_list:
                if re.match(re_tokens[self.Comment], token[0], re.IGNORECASE):
                    m = re.search(re_comment_cmd, token[0], re.IGNORECASE)
                    if m:
                        num_comment_cmds -= 1

                    if m and num_comment_cmds == 0:
                        # Only highlight last comment_cmd on line
                        self.setStyling(1, self.Comment)
                        self.setStyling(m.span()[1] - 1, self.Other)
                        self.setStyling(len(token[0]) - m.span()[1], self.Comment)
                    else:
                        self.setStyling(token[1], self.Comment)
                elif re.match(re_tokens[self.Gcode], token[0], re.IGNORECASE):
                    self.setStyling(token[1], self.Gcode)
                elif re.match(re_tokens[self.Mcode], token[0], re.IGNORECASE):
                    self.setStyling(token[1], self.Mcode)
                elif re.match(re_tokens[self.Axis], token[0], re.IGNORECASE):
                    self.setStyling(1, self.Axis)
                    self.setStyling(token[1] - 1, self.AxisValue)
                elif re.match(re_tokens[self.Other], token[0], re.IGNORECASE):
                    self.setStyling(1, self.Other)
                    self.setStyling(token[1] - 1, self.OtherValue)
                else:
                    self.setStyling(token[1], self.Default)


##########################################################
# Base editor class
##########################################################
class EditorBase(QsciScintilla):
    CURRENT_MARKER_NUM = 0
    USER_MARKER_NUM = 1

    # Default Styles
    # get/set function for font and colors styles will return
    # styleFont[0] and _styleColor[0] as defaults when they are not
    # set in the dict manually, or by CSS properties.
    _styleFont = {
        0: QFont("Courier", 11),                    # Default Font
#        2: QFont("Courier", 20, weight=QFont.Bold), # Gcode
#        5: QFont("Courier", 11, weight=QFont.Bold), # Other
#        "Margins": QFont("Courier", 9),            # Margins
    }

    _styleColor = {
        0: QColor("#000000"),               # Color0 (Default)
        1: QColor("#434d3f"),               # Color1 (Lexer Comment)
        2: QColor("#ba220b"),               # Color2 (Lexer Gcode)
        3: QColor("#f56b1b"),               # Color3 (Lexer Mcode)
        4: QColor("#1883c9"),               # Color4 (Lexer Axis)
        5: QColor("#dd30f0"),               # Color5 (Lexer Other)
        6: QColor("#0e5482"),               # Color6 (Lexer Axis Value)
        7: QColor("#a420b3"),               # Color7 (Lexer Other Value)
        "Margins":  QColor("#666769"),      # Margins
    }

    # keep track of the property value
    _styleBackgroundColor = QColor("#c0c0c0")
    _styleCursorColor = QColor("white")
    _styleMarginsBackgroundColor = QColor("#cccccc")
    _styleMarkerBackgroundColor = QColor("#a5a526")
    _styleSelectionBackgroundColor = QColor("#001111")
    _styleSelectionForegroundColor = QColor("#ffffff")
    _styleSyntaxHighlightEnabled = True

    def __init__(self, parent=None):
        super(EditorBase, self).__init__(parent)

        self.lexer = None
        self.lexer_num_styles = 0   # updated when lexer is loaded

        self._lastUserLine = 0
        self.setReadOnly(True)      # don't allow editing by default

        self.set_lexer("g-code")

        self._marginWidth = '00000'

        # Margin 0 is used for line numbers
        self.setMarginWidth(0, self._marginWidth)
        self.linesChanged.connect(self.on_lines_changed)
        self.setMarginLineNumbers(0, True)

        # Clickable margin for showing markers
        self.marginClicked.connect(self.on_margin_clicked)
        self.setMarginMarkerMask(0, 0b1111)
        self.setMarginSensitivity(0, True)
        # setting _marker_ margin width
        self.setMarginWidth(1, 0)

        # Gcode highlight current line
        self.currentHandle = self.markerDefine(QsciScintilla.Background,
                          self.CURRENT_MARKER_NUM)
        self.setColorMarkerBackground(self.getColorMarkerBackground())

        # User Highlight line (when clicking margin)
        self.userHandle = self.markerDefine(QsciScintilla.Background,
                          self.USER_MARKER_NUM)
        self.setMarkerBackgroundColor(QColor("#ffc0c0"),
                                      self.USER_MARKER_NUM)

        # Brace matching: enable for a brace immediately before or after
        # the current position
        self.setBraceMatching(QsciScintilla.SloppyBraceMatch)

        # Current line visible with special background color
        self.setCaretLineVisible(False)
        self.SendScintilla(QsciScintilla.SCI_GETCARETLINEVISIBLEALWAYS, True)
        self.setCaretLineBackgroundColor(QColor("#ffe4e4"))
        self.ensureLineVisible(True)

        # Don't want to see the horizontal scrollbar at all
        # Use raw message to Scintilla here (all messages are documented
        # here: http://www.scintilla.org/ScintillaDoc.html)
        #self.SendScintilla(QsciScintilla.SCI_SETHSCROLLBAR, 0)
        self.SendScintilla(QsciScintilla.SCI_SETSCROLLWIDTH, 700)
        self.SendScintilla(QsciScintilla.SCI_SETSCROLLWIDTHTRACKING)

        # not too small
        self.setMinimumSize(200, 100)
        self.filepath = None

    def set_lexer(self, lexer_type=None):
        self.lexer = None
        self.lexer_num_styles = 0

        if lexer_type is None or not self._styleSyntaxHighlightEnabled:
            self.lexer = None
        elif lexer_type.lower() == "g-code":
            self.lexer = GcodeLexer(self)
        elif lexer_type.lower() == "python":
            self.lexer = QsciLexerPython(self)

        # Get Number of lexer styles
        if self.lexer is not None:
            while self.lexer.description(self.lexer_num_styles) != "":
                self.lexer_num_styles += 1

        self.setLexer(self.lexer)
        self.refresh_styles()
        #print("Loaded Lexer {} with {} Styles".format(lexer_type, self.lexer_num_styles))

    def refresh_styles(self):
        self.setDefaultFont(self.getDefaultFont())
        self.set_font_colors()
        self.setColorBackground(self.getColorBackground())
        self.setColorMarginsBackground(self.getColorMarginsBackground())
        self.setSelectionBackgroundColor(self.getColorSelectionBackground())
        self.setSelectionForegroundColor(self.getColorSelectionForeground())

    def set_font_colors(self):
        self.setColor(self.getColor0())
        self.setColorMarginsForeground(self.getColorMarginsForeground())

        if self.lexer is not None:
            for i in range(0, self.lexer_num_styles):
                self.lexer.setColor(self._styleColor.get(i, self._styleColor[0]), i)

    def set_margin_width(self):
        self.setMarginWidth(0, self._marginWidth)

    def set_margin_metric(self, width):
        fontmetrics = QFontMetrics(self.getFontMargins())
        self.setMarginWidth(0, fontmetrics.width("0" * width) + 6)

    # reset margin width when number of lines change
    def on_lines_changed(self):
        if len(str(self.lines())) < 3:
            self._marginWidth = '0000'
        else:
            self._marginWidth = str(self.lines())+'0'
        self.setMarginWidth(0, self._marginWidth)

    def on_margin_clicked(self, nmargin, nline, modifiers):
        # Toggle marker for the line the margin was clicked on
        # 2 means it's already there
        if self.markersAtLine(nline) != 2:
            self.markerDelete(self._lastUserLine, self.USER_MARKER_NUM)
            self.markerAdd(nline, self.USER_MARKER_NUM)
        elif self._lastUserLine != nline:
            self.markerAdd(nline, self.USER_MARKER_NUM)
            self.markerDelete(self._lastUserLine, self.USER_MARKER_NUM)
        else:
            self.markerDelete(self._lastUserLine, self.USER_MARKER_NUM)
            self._lastUserLine = 0
            return
        self._lastUserLine = nline

    def mouseDoubleClickEvent(self, event):
        pass

    def new_text(self):
        self.setText('')

    def load_text(self, filepath):
        self.filepath = filepath
        if filepath is None:
            return
        try:
            fp = os.path.expanduser(filepath)
            with open(fp) as f:
                self.setText(f.read())
        except OSError as e:
            LOG.error("load_text(): {}".format(e))
            self.setText('')
            return
        except Exception as e:
            LOG.error("load_text(): {}".format(e))
            self.setText('')
            return
        self.ensureCursorVisible()
        self.SendScintilla(QsciScintilla.SCI_VERTICALCENTRECARET)
        self.setModified(False)

    def save_text(self):
        try:
            with open(self.filepath + 'text', "w") as f:
                f.write(self.text())
        except OSError as e:
            LOG.error("save_text(): {}".format(e))

    def replace_text(self, text):
        self.replace(text)

    def search(self, text, re=False, case=False, word=False, wrap=False, fwd=True):
        self.findFirst(text, re, case, word, wrap, fwd)

    def search_Next(self):
        self.SendScintilla(QsciScintilla.SCI_SEARCHANCHOR)
        self.findNext()

    # Style get/set functions

    # Default Color
    def getColor0(self):
        return self._styleColor[0]
    def setColor0(self, value):
        self._styleColor[0] = value
        self.setColor(value)
        if self.lexer is not None:
            self.lexer.setColor(value, 0)
    styleColor0 = Property(QColor, getColor0, setColor0)

    # Lexer Colors
    def getColor1(self):
        return self._styleColor.get(1, self._styleColor[0])
    def setColor1(self, value):
        self._styleColor[1] = value
        if self.lexer is not None:
            self.lexer.setColor(value, 1)
    styleColor1 = Property(QColor, getColor1, setColor1)

    def getColor2(self):
        return self._styleColor.get(2, self._styleColor[0])
    def setColor2(self, value):
        self._styleColor[2] = value
        if self.lexer is not None:
            self.lexer.setColor(value, 2)
    styleColor2 = Property(QColor, getColor2, setColor2)

    def getColor3(self):
        return self._styleColor.get(3, self._styleColor[0])
    def setColor3(self, value):
        self._styleColor[3] = value
        if self.lexer is not None:
            self.lexer.setColor(value, 3)
    styleColor3 = Property(QColor, getColor3, setColor3)

    def getColor4(self):
        return self._styleColor.get(4, self._styleColor[0])
    def setColor4(self, value):
        self._styleColor[4] = value
        if self.lexer is not None:
            self.lexer.setColor(value, 4)
    styleColor4 = Property(QColor, getColor4, setColor4)

    def getColor5(self):
        return self._styleColor.get(5, self._styleColor[0])
    def setColor5(self, value):
        self._styleColor[5] = value
        if self.lexer is not None:
            self.lexer.setColor(value, 5)
    styleColor5 = Property(QColor, getColor5, setColor5)

    def getColor6(self):
        return self._styleColor.get(6, self._styleColor[0])
    def setColor6(self, value):
        self._styleColor[6] = value
        if self.lexer is not None:
            self.lexer.setColor(value, 6)
    styleColor6 = Property(QColor, getColor6, setColor6)

    def getColor7(self):
        return self._styleColor.get(7, self._styleColor[0])
    def setColor7(self, value):
        self._styleColor[7] = value
        if self.lexer is not None:
            self.lexer.setColor(value, 7)
    styleColor7 = Property(QColor, getColor7, setColor7)

    # Margins Text Color
    def getColorMarginsForeground(self):
        return self._styleColor.get("Margins", self._styleColor[0])
    def setColorMarginsForeground(self, value):
        super(EditorBase, self).setMarginsForegroundColor(value)
        self._styleColor["Margins"] = value
    styleColorMarginText = Property(QColor, getColorMarginsForeground, setColorMarginsForeground)

    # Cursor Color
    def getColorCursor(self):
        return self._styleCursorColor
    def setColorCursor(self, color):
        self._styleCursorColor = color
        super(EditorBase, self).setCaretForegroundColor(color)
    styleColorCursor = Property(QColor, getColorCursor, setColorCursor)

    # Backgrounds
    def getColorBackground(self):
        return self._styleBackgroundColor
    def setColorBackground(self, color):
        self._styleBackgroundColor = color
        #self.SendScintilla(QsciScintilla.SCI_STYLESETBACK, QsciScintilla.STYLE_DEFAULT, color)
        self.setPaper(color)
        if self.lexer is not None:
            self.lexer.setDefaultPaper(color)
            for i in range(0, self.lexer_num_styles):
                self.lexer.setPaper(color, i)
    styleColorBackground = Property(QColor, getColorBackground, setColorBackground)

    # Margins Background
    def setColorMarginsBackground(self, color):
        super(EditorBase, self).setMarginsBackgroundColor(color)
        self._styleMarginsBackgroundColor = color
    def getColorMarginsBackground(self):
        return self._styleMarginsBackgroundColor
    styleColorMarginBackground = Property(QColor, getColorMarginsBackground, setColorMarginsBackground)

    # Selection Highlight Background & Foreground
    def getColorSelectionBackground(self):
        return self._styleSelectionBackgroundColor
    def setColorSelectionBackground(self, value):
        self._styleSelectionBackgroundColor = value
        self.setSelectionBackgroundColor(value)
    styleColorSelectionBackground = Property(QColor, getColorSelectionBackground, setColorSelectionBackground)

    def getColorSelectionForeground(self):
        return self._styleSelectionForegroundColor
    def setColorSelectionForeground(self, value):
        self._styleSelectionForegroundColor = value
        self.setSelectionForegroundColor(value)
    styleColorSelectionText = Property(QColor, getColorSelectionForeground, setColorSelectionForeground)

    # Current Line Marker Background
    def getColorMarkerBackground(self):
        return self._styleMarkerBackgroundColor
    def setColorMarkerBackground(self, value):
        self._styleMarkerBackgroundColor = value
        self.setMarkerBackgroundColor(value, self.CURRENT_MARKER_NUM)
    styleColorMarkerBackground = Property(QColor, getColorMarkerBackground, setColorMarkerBackground)

    # Fonts
    def setDefaultFont(self, value):
        self._styleFont[0] = value
        self.setFont(value)
        self.setFontMargins(self.getFontMargins())

        if self.lexer is not None:
            self.lexer.setFont(value)
            for i in range(0, self.lexer_num_styles):
                self.lexer.setFont(self._styleFont.get(i, self._styleFont[0]), i)
    def getDefaultFont(self):
        return self._styleFont[0]
    styleFont = Property(QFont, getDefaultFont, setDefaultFont)

    def getFont0(self):
        return self._styleFont[0]
    def setFont0(self, value):
        self._styleFont[0] = value
        self.setFont(value)
        if self.lexer is not None:
            self.lexer.setFont(value, 0)
    styleFont0 = Property(QFont, getFont0, setFont0)

    def getFont1(self):
        return self._styleFont.get(1, self._styleFont[0])
    def setFont1(self, value):
        self._styleFont[1] = value
        if self.lexer is not None:
            self.lexer.setFont(value, 1)
    styleFont1 = Property(QFont, getFont1, setFont1)

    def getFont2(self):
        return self._styleFont.get(2, self._styleFont[0])
    def setFont2(self, value):
        self._styleFont[2] = value
        if self.lexer is not None:
            self.lexer.setFont(value, 2)
    styleFont2 = Property(QFont, getFont2, setFont2)

    def getFont3(self):
        return self._styleFont.get(3, self._styleFont[0])
    def setFont3(self, value):
        self._styleFont[3] = value
        if self.lexer is not None:
            self.lexer.setFont(value, 3)
    styleFont3 = Property(QFont, getFont3, setFont3)

    def getFont4(self):
        return self._styleFont.get(4, self._styleFont[0])
    def setFont4(self, value):
        self._styleFont[4] = value
        if self.lexer is not None:
            self.lexer.setFont(value, 4)
    styleFont4 = Property(QFont, getFont4, setFont4)

    def getFont5(self):
        return self._styleFont.get(5, self._styleFont[0])
    def setFont5(self, value):
        self._styleFont[5] = value
        if self.lexer is not None:
            self.lexer.setFont(value, 5)
    styleFont5 = Property(QFont, getFont5, setFont5)

    def getFont6(self):
        return self._styleFont.get(6, self._styleFont[0])
    def setFont6(self, value):
        self._styleFont[6] = value
        if self.lexer is not None:
            self.lexer.setFont(value, 6)
    styleFont6 = Property(QFont, getFont6, setFont6)

    def getFont7(self):
        return self._styleFont.get(7, self._styleFont[0])
    def setFont7(self, value):
        self._styleFont[7] = value
        if self.lexer is not None:
            self.lexer.setFont(value, 7)
    styleFont7 = Property(QFont, getFont7, setFont7)

    def getFontMargins(self):
        return self._styleFont.get("Margins", self._styleFont[0])
    def setFontMargins(self, value):
        self._styleFont["Margins"] = value
        self.setMarginsFont(value)
    styleFontMargin = Property(QFont, getFontMargins, setFontMargins)

    # Syntax Highlighting Bool
    def getSyntaxHighlightEnabled(self):
        return self._styleSyntaxHighlightEnabled
    def setSyntaxHighlightEnabled(self, value):
        if value is not self._styleSyntaxHighlightEnabled:
            self._styleSyntaxHighlightEnabled = value
            if not value:
                self.set_lexer(None)
            else:
                self.set_lexer("g-code")
    styleSyntaxHighlightEnabled = Property(bool, getSyntaxHighlightEnabled, setSyntaxHighlightEnabled)


##########################################################
# Gcode display widget (intended read-only)
##########################################################
class GcodeDisplay(EditorBase, _HalWidgetBase):
    CURRENT_MARKER_NUM = 0
    USER_MARKER_NUM = 1

    def __init__(self, parent=None):
        super(GcodeDisplay, self).__init__(parent)
        # linuxcnc defaults
        self.idle_line_reset = False
        self._last_filename = None
        self.auto_show_mdi = True
        self.auto_show_manual = False
        self.auto_show_preference = True
        self.last_line = 0
        # keep track of vertical scroll setting in auto mode
        self._last_auto_scroll = 0

    def _hal_init(self):
        self.cursorPositionChanged.connect(self.line_changed)
        if self.auto_show_mdi:
            STATUS.connect('mode-mdi', self.load_mdi)
            STATUS.connect('mdi-history-changed', self.load_mdi)
            STATUS.connect('mode-auto', self.reload_last)
            STATUS.connect('move-text-lineup', self.select_lineup)
            STATUS.connect('move-text-linedown', self.select_linedown)
            STATUS.connect('mode-manual', self.load_manual)
        if self.auto_show_manual:
            STATUS.connect('mode-manual', self.load_manual)
            STATUS.connect('machine-log-changed', self.load_manual)
        if self.auto_show_preference:
            STATUS.connect('show-preference', self.load_preference)
        STATUS.connect('file-loaded', self.load_program)
        STATUS.connect('reload-display', self.load_program)
        STATUS.connect('line-changed', self.external_highlight_request)
        STATUS.connect('graphics-line-selected', self.external_highlight_request)
        STATUS.connect('command-stopped', lambda w: self.run_stopped())

        if self.idle_line_reset:
            STATUS.connect('interp_idle', lambda w: self.set_line_number(0))
        self.markerDeleteHandle(self.currentHandle)

    def load_program(self, w, filename=None):
        if filename is None:
            filename = self._last_filename
        elif 'file_clear.ngc' in filename:
            self._last_filename = None
        else:
            self._last_filename = filename
        self.load_text(filename)
        #self.zoomTo(6)
        self.setCursorPosition(0, 0)
        self.markerDeleteHandle(self.currentHandle)
        self.setModified(False)
        self._lastUserLine = 0

    # when switching from MDI to AUTO we need to reload the
    # last (linuxcnc loaded) program.
    def reload_last(self, w):
        self.load_text(STATUS.old['file'])
        self.setCursorPosition(0, 0)
        # keep track of vertical scroll bar setting
        self.verticalScrollBar().setValue(self._last_auto_scroll)
        #  and margin marker if it is showing
        if self._lastUserLine >0:
            self.markerAdd(self._lastUserLine, self.USER_MARKER_NUM)

    # With the auto_show__mdi option, MDI history is shown
    def load_mdi(self, w):
        # record scroll position in auto mode's gcode
        if STATUS.get_previous_mode() == STATUS.AUTO: 
            self._last_auto_scroll = self.verticalScrollBar().value()

        self.load_text(INFO.MDI_HISTORY_PATH)
        self._last_filename = INFO.MDI_HISTORY_PATH
        self.setCursorPosition(self.lines()-1, 0)
        self.moveMarker(self.lines()-1)

    # With the auto_show__mdi option, MDI history is shown
    def load_manual(self, w):
        # record scroll position in auto mode's gcode
        if STATUS.get_previous_mode() == STATUS.AUTO: 
            self._last_auto_scroll = self.verticalScrollBar().value()

        if self.auto_show_manual and STATUS.is_man_mode():
            self.load_text(INFO.MACHINE_LOG_HISTORY_PATH)
            self.setCursorPosition(self.lines()-1, 0)

    def load_preference(self, w):
        self.load_text(self.PATHS_.PREFS_FILENAME)
        self.setCursorPosition(self.lines()-1, 0)

    # external line numbers start at 1 - convert that to start at 0
    def external_highlight_request(self, w, line):
        if line in (-1, None):
            return
        if STATUS.is_auto_running():
            self.highlight_line(None, line-1)
            return
        LOG.verbose('editor: got external highlight {}'.format(line))
        #self.highlight_line(None, line-1)
        self.ensureLineVisible(line-1)
        #self.setSelection(line-1,0,line-1,self.lineLength(line-1)-1)
        self.moveMarker(line-1)
        self.selectAll(False)

    def moveMarker(self, line):
        if STATUS.is_auto_running() and STATUS.stat.file == '':
            self.last_line = 0
            return
        self.markerDeleteHandle(self.currentHandle)
        self.currentHandle = self.markerAdd(line, self.CURRENT_MARKER_NUM)
        self.last_line = line

    def highlight_line(self, w, line):
        LOG.verbose('editor: highlight line {}'.format(line))
        if STATUS.is_auto_running():
            if not STATUS.old['file'] == self._last_filename:
                LOG.debug('should reload the display')
                self.load_text(STATUS.old['file'])
                self._last_filename = STATUS.old['file']
            self.emit_percent(round(line*100/self.lines()))
        self.moveMarker(line)
        self.setCursorPosition(line, 0)
        self.ensureCursorVisible()
        self.SendScintilla(QsciScintilla.SCI_VERTICALCENTRECARET)

    def emit_percent(self, percent):
        pass

    def run_stopped(self):
        self.emit_percent(-1)

    def set_line_number(self, line):
        STATUS.emit('gcode-line-selected', line+1)

    def line_changed(self, line, index):
        LOG.verbose('Line changed: {}'.format(line))
        if STATUS.is_auto_running() is False:
            if STATUS.is_mdi_mode():
                line_text = str(self.text(line)).strip()
                STATUS.emit('mdi-line-selected', line_text, self._last_filename)
            else:
                self.moveMarker(line)
                self.set_line_number(line)

    def select_lineup(self, w):
        line, col = self.getCursorPosition()
        LOG.verbose(line)
        self.setCursorPosition(line-1, 0)
        self.highlight_line(None, line-1)

    def select_linedown(self, w):
        line, col = self.getCursorPosition()
        LOG.verbose(line)
        self.setCursorPosition(line+1, 0)
        self.highlight_line(None, line+1)

    def jump_line(self, jump):
        line, col = self.getCursorPosition()
        line = line + jump
        LOG.verbose(line)
        if line <0:
            line = 0
        elif line > self.lines()-1:
            line = self.lines()-1
        self.setCursorPosition(line, 0)
        self.highlight_line(None, line)

    # overridden functions               #
    #####################################
    def zoomIn(self):
        super().zoomIn()
        self.set_margin_width()
    def zoomOut(self):
        super().zoomOut()
        self.set_margin_width()
    #####################################

    # designer recognized getter/setters
    # auto_show_mdi status
    def set_auto_show_mdi(self, data):
        self.auto_show_mdi = data
    def get_auto_show_mdi(self):
        return self.auto_show_mdi
    def reset_auto_show_mdi(self):
        self.auto_show_mdi = True
    auto_show_mdi_status = Property(bool, get_auto_show_mdi, set_auto_show_mdi, reset_auto_show_mdi)

    # designer recognized getter/setters
    # auto_show_manual status
    def set_auto_show_manual(self, data):
        self.auto_show_manual = data
    def get_auto_show_manual(self):
        return self.auto_show_manual
    def reset_auto_show_manual(self):
        self.auto_show_manual = True
    auto_show_manual_status = Property(bool, get_auto_show_manual, set_auto_show_manual, reset_auto_show_manual)

#############################################
# For Editing Gcode
#############################################

class GcodeEditor(QWidget, _HalWidgetBase):
    percentDone = Signal(int)

    def __init__(self, parent=None):
        super(GcodeEditor, self).__init__(parent)
        self.load_dialog_code = 'LOAD'
        self.save_dialog_code = 'SAVE'
        STATUS.connect('general',self.returnFromDialog)

        self.isCaseSensitive = 0

        self.setMinimumSize(QSize(300, 200))
        self.setWindowTitle("PyQt5 editor test example")

        lay = QVBoxLayout()
        lay.setContentsMargins(0,0,0,0)
        self.setLayout(lay)

        # make editor
        self.editor = GcodeDisplay(self)

        # class patch editor's function to ours
        # so we get the lines percent update
        self.editor.emit_percent = self.emit_percent

        self.editor.setReadOnly(True)
        self.editor.setModified(False)

        ################################
        # add menubar actions
        ################################

        # Create new action
        icon = self.style().standardIcon( QStyle.SP_FileIcon)
        self.newAction = QAction(icon, 'New', self)
        self.newAction.setShortcut('Ctrl+N')
        self.newAction.setStatusTip('New document')
        self.newAction.triggered.connect(self.newCall)

        # Create open action
        icon = self.style().standardIcon( QStyle.SP_DirOpenIcon)
        self.openAction = QAction(icon, '&Open', self)
        self.openAction.setShortcut('Ctrl+O')
        self.openAction.setStatusTip('Open document')
        self.openAction.triggered.connect(self.openCall)

        # Create save action
        icon = self.style().standardIcon( QStyle.SP_DialogSaveButton)
        self.saveAction = QAction(icon, '&Save', self)
        self.saveAction.setShortcut('Ctrl+S')
        self.saveAction.setStatusTip('Save document')
        self.saveAction.triggered.connect(self.saveCall)

        # Create exit action
        self.exitAction = QAction(QIcon.fromTheme('application-exit'), '&Exit', self)
        self.exitAction.setShortcut('Ctrl+Q')
        self.exitAction.setStatusTip('Exit application')
        self.exitAction.triggered.connect(self.exitCall)

        # Create gcode lexer action
        self.gCodeLexerAction = QAction(QIcon.fromTheme('lexer.png'), '&Gcode\nLexer', self)
        self.gCodeLexerAction.setCheckable(1)
        self.gCodeLexerAction.setShortcut('Ctrl+G')
        self.gCodeLexerAction.setStatusTip('Set Gcode highlighting')
        self.gCodeLexerAction.triggered.connect(self.gcodeLexerCall)

        # Create gcode lexer action
        self.pythonLexerAction = QAction(QIcon.fromTheme('lexer.png'), '&Python\nLexer', self)
        self.pythonLexerAction.setShortcut('Ctrl+P')
        self.pythonLexerAction.setStatusTip('Set Python highlighting')
        self.pythonLexerAction.triggered.connect(self.pythonLexerCall)

        # Create toolbar and add action
        self.toolBar = QToolBar('File')
        self.toolBar.addAction(self.newAction)
        self.toolBar.addAction(self.openAction)
        self.toolBar.addAction(self.saveAction)
        self.toolBar.addAction(self.exitAction)

        self.toolBar.addSeparator()

        # add lexer actions
        self.toolBar.addAction(self.gCodeLexerAction)
        self.toolBar.addAction(self.pythonLexerAction)

        self.toolBar.addSeparator()
        self.label = QLabel('''<html><head/><body><p><span style=" font-size:20pt;
                         font-weight:600;">Edit Mode</span></p></body></html>''')
        self.toolBar.addWidget(self.label)

        # create a frame for buttons
        self.topBox = QHBoxLayout()
        self.topBox.addWidget(self.toolBar)

        self.topMenu = QFrame()
        self.topMenu.setLayout(self.topBox)

        # add widgets
        lay.addWidget(self.topMenu)
        lay.addWidget(self.editor)
        lay.addWidget(self.createGroup())

        self.readOnlyMode()

    def createGroup(self):
        self.bottomMenu = QFrame()

        self.searchText = QLineEdit(self)
        self.searchText.setStatusTip('Text to search for')
        self.replaceText = QLineEdit(self)
        self.replaceText.setStatusTip('Replace search text with this text')
        toolBar = QToolBar()
        # Create new action
        undoAction = QAction(QIcon.fromTheme('edit-undo'), 'Undo', self)
        undoAction.setStatusTip('Undo')
        undoAction.triggered.connect(self.undoCall)
        toolBar.addAction(undoAction)

        # create redo action
        redoAction = QAction(QIcon.fromTheme('edit-redo'), 'Redo', self)
        redoAction.setStatusTip('Redo')
        redoAction.triggered.connect(self.redoCall)
        toolBar.addAction(redoAction)

        toolBar.addSeparator()

        # create replace action
        replaceAction = QAction(QIcon.fromTheme('edit-find-replace'), 'Replace', self)
        replaceAction.setStatusTip('Replace text')
        replaceAction.triggered.connect(self.replaceCall)
        toolBar.addAction(replaceAction)

        # create find action
        findAction = QAction(QIcon.fromTheme('edit-find'), 'Find', self)
        findAction.setStatusTip('Find next occurrence of text')
        findAction.triggered.connect(self.findCall)
        toolBar.addAction(findAction)

        # create next action
        previousAction = QAction(QIcon.fromTheme('go-previous'), 'Find Previous', self)
        previousAction.setStatusTip('Find previous occurrence of text')
        previousAction.triggered.connect(self.previousCall)
        toolBar.addAction(previousAction)

        toolBar.addSeparator()

        # create case action
        caseAction = QAction(QIcon.fromTheme('edit-case'), 'Aa', self)
        caseAction.setStatusTip('Toggle between any case and match case')
        caseAction.setCheckable(1)
        caseAction.triggered.connect(self.caseCall)
        toolBar.addAction(caseAction)

        self.bottomBox = QHBoxLayout()
        self.bottomBox.addWidget(toolBar)
        self.bottomBox.addWidget(self.searchText)
        self.bottomBox.addWidget(self.replaceText)
        self.bottomBox.addStretch(1)
        self.bottomMenu.setLayout(self.bottomBox)

        return self.bottomMenu

    # callback functions built for easy class patching ##########
    # want to refrain from renaming these functions as it will break
    # any class patch user's use
    # we split them like this so a user can intercept the callback
    # but still call the original functionality

    def caseCall(self):
        self.case()
    def case(self):
        self.isCaseSensitive -=1
        self.isCaseSensitive *=-1

    def exitCall(self):
        self.exit()
    def exit(self):
        if self.editor.isModified():
            result = self.killCheck()
            if result:
                self.readOnlyMode()

    def findCall(self):
        self.find()
    def find(self):
        self.editor.search(str(self.searchText.text()),
                             re=False, case=self.isCaseSensitive,
                             word=False, wrap= True, fwd=True)

    def previousCall(self):
        self.previous()
    def previous(self):
        self.editor.setCursorPosition(self.editor.getSelection()[0],
                                      self.editor.getSelection()[1])
        self.editor.search(str(self.searchText.text()),
                           re=False, case=self.isCaseSensitive,
                           word=False, wrap=True, fwd=False)

    def gcodeLexerCall(self):
        self.gcodeLexer()
    def gcodeLexer(self):
        self.editor.set_lexer("g-code")

    def nextCall(self):
        self.next()
    def next(self):
        self.editor.search(str(self.searchText.text()),
                             re=False, case=self.isCaseSensitive,
                             word=False, wrap=True, fwd=False)
        self.editor.search_Next()

    def newCall(self):
        self.new()
    def new(self):
        if self.editor.isModified():
            result = self.killCheck()
            if result:
                self.editor.new_text()
        else:
            self.editor.new_text()

    def openCall(self):
        self.open()
    def open(self):
        self.getFileName()
    def openReturn(self,f):
        ACTION.OPEN_PROGRAM(f)
        self.editor.setModified(False)

    def pythonLexerCall(self):
        self.pythonLexer()
    def pythonLexer(self):
        self.editor.set_lexer("python")

    def redoCall(self):
        self.redo()
    def redo(self):
        self.editor.redo()

    def replaceCall(self):
        self.replace()
    def replace(self):
        self.editor.replace_text(str(self.replaceText.text()))
        self.editor.search(str(self.searchText.text()),
                             re=False, case=self.isCaseSensitive,
                             word=False, wrap=True, fwd=True)

    def saveCall(self):
        self.save()
    def save(self):
        self.getSaveFileName()
    def saveReturn(self, fname):
        saved = ACTION.SAVE_PROGRAM(self.editor.text(), fname)
        if saved is not None:
            self.editor.setModified(False)
            ACTION.OPEN_PROGRAM(saved)

    def undoCall(self):
        self.undo()
    def undo(self):
        self.editor.undo()

    # helper functions ############################################

    def _hal_init(self):
        # name the top and bottom frames so it's easier to style
        self.bottomMenu.setObjectName('%sBottomButtonFrame'% self.objectName())
        self.topMenu.setObjectName('%sTopButtonFrame'% self.objectName())
        self.editor.setObjectName('{}_display'.format( self.objectName()))

    def editMode(self):
        self.topMenu.show()
        self.bottomMenu.show()
        self.editor.setReadOnly(False)

    def readOnlyMode(self):
        self.topMenu.hide()
        self.bottomMenu.hide()
        self.editor.setReadOnly(True)

    def getFileName(self):
        mess = {'NAME':self.load_dialog_code,'ID':'%s__' % self.objectName(),
            'TITLE':'Load Editor'}
        STATUS.emit('dialog-request', mess)

    def getSaveFileName(self):
        mess = {'NAME':self.save_dialog_code,'ID':'%s__' % self.objectName(),
            'TITLE':'Save Editor', 'FILENAME':self.editor._last_filename}
        STATUS.emit('dialog-request', mess)

    # process the STATUS return message
    def returnFromDialog(self, w, message):
        if message.get('NAME') == self.load_dialog_code:
            path = message.get('RETURN')
            code = bool(message.get('ID') == '%s__'% self.objectName())
            if path and code:
                self.openReturn(path)
        elif message.get('NAME') == self.save_dialog_code:
            path = message.get('RETURN')
            code = bool(message.get('ID') == '%s__'% self.objectName())
            if path and code:
                self.saveReturn(path)

    def killCheck(self):
        choice = QMessageBox.question(self, 'Warning!!',
                                            "This file has changed since loading...Still want to proceed?",
                                            QMessageBox.Yes | QMessageBox.No)
        if choice == QMessageBox.Yes:
            return True
        else:
            return False

    def emit_percent(self, percent):
        self.percentDone.emit(int(percent))

    def select_lineup(self):
        self.editor.select_lineup(None)

    def select_linedown(self):
        self.editor.select_linedown(None)

    def select_line(self, line):
        self.editor.highlight_line(None, line)

    def jump_line(self, jump):
        self.editor.jump_line(jump)

    def get_line(self):
        return self.editor.getCursorPosition()[0] +1

    def set_margin_metric(self,width):
        self.editor.set_margin_metric(width)

    def set_font(self, font):
        self.editor.setDefaultFont(font)

    def isReadOnly(self):
        return self.editor.isReadOnly()

    # designer recognized getter/setters
    # auto_show_mdi status
    # These adjust the self.editor instance
    def set_auto_show_mdi(self, data):
        self.editor.auto_show_mdi = data
    def get_auto_show_mdi(self):
        return self.editor.auto_show_mdi
    def reset_auto_show_mdi(self):
        self.editor.auto_show_mdi = True
    auto_show_mdi_status = Property(bool, get_auto_show_mdi, set_auto_show_mdi, reset_auto_show_mdi)

    # designer recognized getter/setters
    # auto_show_manual status
    def set_auto_show_manual(self, data):
        self.editor.auto_show_manual = data
    def get_auto_show_manual(self):
        return self.editor.auto_show_manual
    def reset_auto_show_manual(self):
        self.editor.auto_show_manual = True
    auto_show_manual_status = Property(bool, get_auto_show_manual, set_auto_show_manual, reset_auto_show_manual)

# for direct testing
if __name__ == "__main__":
    from qtpy.QtWidgets import *
    from qtpy.QtCore import *
    from qtpy.QtGui import *

    sample_text = r"""( ----- Simple G-Code -----)
G17 G20 G40 G49 G54 G80 G90 G94
M7

G0 Z10
F400
G1 Z1


G3 X50 Y50 Z1 I-7.5 J0
G0 Z30


G5.2 X3.53   Y-1.50   P2
     X5.33   Y-11.01  P1
     X3.52   Y-24.00  P1
     X0.0    Y-29.56  P1
G5.3

G0 Z2
G1 Z-5 F100
F600

( ----- Line Numbers ----- )
N1
N20
N300
N030 (insert numbers in place of .)
N000   G1 X20
N98765 G0 Z30"""

    app = QApplication(sys.argv)
    w = GcodeEditor()
    w.editMode()

    if len(sys.argv) > 1:
        w.editor.load_text(sys.argv[1])
    else:
        w.editor.setText(sample_text)

    if 0:
        w.toolBar.hide()
    if 1:
        w.pythonLexerAction.setVisible(True)
        w.gCodeLexerAction.setVisible(True)
    if 1:
        w.openAction.setVisible(False)
        w.newAction.setVisible(False)
    if 0:
        w.saveAction.setVisible(False)
        w.exitAction.setVisible(False)
    if 1:
        w.label.setText('<b>Edit mode title label</b>')
    w.show()
    sys.exit( app.exec_() )

