#!/usr/bin/python3

import os
import re
import sys
import webbrowser
import idlelib.colorizer as colorizer
from idlelib.percolator import Percolator as percolator
from tkinter import *
from tkinter import ttk
from tkinter import scrolledtext, filedialog, messagebox, font
from testapi import Definition as df
from testapi import testAPI as api

class Application:
    def __init__(self, master=None, filename=None):
        # Set up the main application window
        self.toplevel = Tk() if master is None else Toplevel(master)
        # Configure rows and columns for resizing
        self.toplevel.rowconfigure(0, weight=1)
        self.toplevel.columnconfigure(0, weight=1)
        # Do not tear off menus.
        self.toplevel.option_add('*tearOff', FALSE)
        self.toplevel.configure(takefocus=False)
        self.toplevel.protocol("WM_DELETE_WINDOW", self.close_application)
        # Set the appname and update the application title
        self.appname = "Chancery - an openQA script editor"
        self.toplevel.title(self.appname)
        # Configure the main application frame
        self.mainframe = ttk.Frame(self.toplevel)
        self.mainframe.configure(borderwidth='2', relief='ridge', takefocus=True)
        self.mainframe.rowconfigure(0, weight=1)
        self.mainframe.columnconfigure(0, weight=1)
        self.mainframe.columnconfigure(1, weight=5)
        self.mainframe.grid(column=0, row=0, sticky=(N, W, E, S))
        # Set up status variables
        self.db = api.show_definitions()
        self.filetosave = None
        self.is_saved = True
        self.comments_on = IntVar()

        # Set up the Syntax Highlighting
        self.scheme = colorizer.ColorDelegator()
        self.pattern = colorizer.make_pat().pattern
        self.scheme.tagdefs['COMMANDS'] = {'foreground': 'blue', 'background': 'white'}
        self.scheme.tagdefs['COMMENT'] = {'foreground': 'grey', 'background': 'white'}
        self.scheme.tagdefs['KEYWORD'] = {'foreground': 'red', 'background': 'white'}
        self.scheme.tagdefs['PERL'] = {'foreground': 'red', 'background': 'white'}
        self.scheme.tagdefs['BUILTIN'] = {'foreground': 'orange', 'background': 'white'}
        self.scheme.tagdefs['STRING'] = {'foreground': 'green', 'background': 'white'}
        self.scheme.tagdefs['DEFINITION'] = {'foreground': 'yellow', 'background': 'white'}

        # Run the construction methods to build the UI
        self.create_menu_bar()
        self.create_quick_menu()
        self.create_text()
        self.bind_accelerators()

        if filename:
            self.open_file(filename)

    def create_menu_bar(self):
        """ Creates the application menu """
        self.menubar = Menu(self.toplevel)
        self.menu_file = Menu(self.menubar)
        # Create the File submenu.
        self.menubar.add_cascade(menu=self.menu_file, label='File')
        self.menu_file.add_command(label='New', accelerator='Ctrl-N', command=self.new_file)
        self.menu_file.add_command(label='Open', accelerator='Ctrl-O', command=self.open_file)
        self.menu_file.add_separator()
        self.menu_file.add_command(label='Save', accelerator='Ctrl-S', command=self.save_file)
        self.menu_file.add_command(label='Save As', command=self.save_as_file)
        self.menu_file.add_separator()
        self.menu_file.add_command(label='Exit', accelerator='Ctrl-Q', command=self.close_application)
        # Create the Video submenu
        self.menu_video = Menu(self.menubar)
        self.menubar.add_cascade(menu=self.menu_video, label='Video')
        self.menu_video.add_command(label="Assert and click match", accelerator="Alt-V-C", command=lambda: self.database('video', 'Assert and click match'))
        self.menu_video.add_command(label="Assert and doubleclick match", accelerator="Alt-V-D", command=lambda: self.database('video', 'Assert and doubleclick match'))
        self.menu_video.add_command(label="Click last match", accelerator="Alt-V-L", command=lambda: self.database('video', 'Click last match'))
        self.menu_video.add_separator()
        self.menu_video.add_command(label="Assert screen for match", accelerator="Alt-V-M", command=lambda: self.database('video', 'Assert screen for match'))
        self.menu_video.add_command(label="Check screen for match", accelerator="Alt-V-H", command=lambda: self.database('video', 'Check screen for match'))
        self.menu_video.add_command(label="Check if match has tag", accelerator="Alt-V-G", command=lambda: self.database('video', 'Check if match has tag'))
        self.menu_video.add_command(label="Assert that screen is still", accelerator="Alt-V-Q", command=lambda: self.database('video', 'Assert that screen is still'))
        self.menu_video.add_command(label="Assert that screen changes", accelerator="Alt-V-A", command=lambda: self.database('video', 'Assert that screen changes'))
        self.menu_video.add_command(label="Wait if screen changes", accelerator="Alt-V-E", command=lambda: self.database('video', 'Wait if screen changes'))
        self.menu_video.add_command(label="Wait until screen is still", accelerator="Alt-V-W", command=lambda: self.database('video','Wait until screen is still'))
        self.menu_video.add_separator()
        self.menu_video.add_command(label="Force soft failure", accelerator="Alt-V-U", command=lambda: self.database('video', 'Force soft failure'))
        self.menu_video.add_command(label="Record soft failure", accelerator="Alt-V-F", command=lambda: self.database('video','Record soft failure'))
        self.menu_video.add_command(label="Record info", accelerator="Alt-V-I", command=lambda: self.database('video','Record info'))
        self.menu_video.add_command(label="Take screenshot", accelerator="Alt-V-T", command=lambda: self.database('video', 'Take screenshot'))
        # Create the Audio submenu
        self.menu_audio = Menu(self.menubar)
        self.menubar.add_cascade(menu=self.menu_audio,label='Audio')
        self.menu_audio.add_command(label="Start audio recording", accelerator="Alt-A-R", command=lambda: self.database('audio', 'Start audio recording'))
        self.menu_audio.add_command(label="Assert recorded sound", accelerator="Alt-A-S", command=lambda: self.database('audio', 'Assert recorded sound'))
        self.menu_audio.add_command(label="Check recorded sound", accelerator="Alt-A-C", command=lambda: self.database('audio', 'Check recorded sound'))
        # Create the Keyboard submenu
        self.menu_keyboard = Menu(self.menubar)
        self.menubar.add_cascade(menu=self.menu_keyboard,label='Keyboard')
        self.menu_keyboard.add_command(label="Press key (combination)", accelerator="Alt-K-P", command=lambda: self.database('keyboard', 'Press key'))
        self.menu_keyboard.add_command(label="Hold key", accelerator="Alt-K-H", command=lambda: self.database('keyboard', 'Hold key'))
        self.menu_keyboard.add_command(label="Release key", accelerator="Alt-K-R", command=lambda: self.database('keyboard', 'Release key'))
        self.menu_keyboard.add_command(label="Press key until event", accelerator="Alt-K-E", command=lambda: self.database('keyboard', 'Press key until event'))
        self.menu_keyboard.add_separator()
        self.menu_keyboard.add_command(label="Type text", accelerator="Alt-K-T", command=lambda: self.database('keyboard', 'Type text'))
        self.menu_keyboard.add_command(label="Type text safely (Fedora)", accelerator="Alt-K-S", command=lambda: self.database('keyboard', 'Type text safely'))
        self.menu_keyboard.add_command(label="Type password", accelerator="Alt-K-W", command=lambda: self.database('keyboard', 'Type password'))
        self.menu_keyboard.add_command(label="Run command", accelerator="Alt-K-C", command=lambda: self.database('keyboard', 'Run command'))

        # Create the Mouse submenu
        self.menu_mouse = Menu(self.menubar)
        self.menubar.add_cascade(menu=self.menu_mouse,label='Mouse')
        self.menu_mouse.add_command(label="Click mouse at location", accelerator="Alt-M-C", command=lambda: self.database('mouse', 'Click mouse'))
        self.menu_mouse.add_command(label="Doubleclick mouse at location", accelerator="Alt-M-D", command=lambda: self.database('mouse', 'Doubleclick mouse'))
        self.menu_mouse.add_command(label="Tripleclick mouse at location", accelerator="Alt-M-T", command=lambda: self.database('mouse', 'Tripleclick mouse'))
        self.menu_mouse.add_command(label="Drag with mouse", accelerator="Alt-M-R", command=lambda: self.database('mouse', 'Drag mouse'))
        self.menu_mouse.add_separator()
        self.menu_mouse.add_command(label="Set mouse location", accelerator="Alt-M-P", command=lambda: self.database('mouse', 'Set mouse location'))
        self.menu_mouse.add_command(label="Hide mouse cursor", accelerator="Alt-M-H", command=lambda: self.database('mouse', 'Hide mouse'))
        # Create the Variable submenu
        self.menu_variable = Menu(self.menubar)
        self.menubar.add_cascade(menu=self.menu_variable,label='Variables')
        self.menu_variable.add_command(label="Read variable", accelerator="Alt-R-G", command=lambda: self.database('variable', 'Read variable'))
        self.menu_variable.add_command(label="Read variable (strict)", accelerator="Alt-R-D", command=lambda: self.database('variable', 'Read strict variable'))
        self.menu_variable.add_command(label="Set variable", accelerator="Alt-R-S", command=lambda: self.database('variable', 'Set variable'))
        self.menu_variable.add_command(label="Check variable value", accelerator="Alt-R-C", command=lambda: self.database('variable', 'Check variable'))
        self.menu_variable.add_command(label="Read variable as list", accelerator="Alt-R-L", command=lambda: self.database('variable', 'Read variable as list'))
        self.menu_variable.add_command(label="Check value in list of variables", accelerator="Alt-R-O", command=lambda: self.database('variable', 'Check value in list of variables'))
        # Create the Script submenu
        self.menu_script = Menu(self.menubar)
        self.menubar.add_cascade(menu=self.menu_script,label='Scripts')
        self.menu_script.add_command(label="Run script and assert", accelerator="Alt-S-A", command=lambda: self.database('script', 'Run script and assert'))
        self.menu_script.add_command(label="Run script", accelerator="Alt-S-R", command=lambda: self.database('script', 'Run script'))
        self.menu_script.add_command(label="Run script in background", accelerator="Alt-S-B", command=lambda: self.database('script', 'Run script in background'))
        self.menu_script.add_command(label="Run sudo script and assert", accelerator="Alt-S-U", command=lambda: self.database('script', 'Run sudo script and assert'))
        self.menu_script.add_command(label="Run sudo script", accelerator="Alt-S-D", command=lambda: self.database('script', 'Run sudo script'))
        self.menu_script.add_command(label="Collect script output", accelerator="Alt-S-C", command=lambda: self.database('script', 'Collect script output'))
        self.menu_script.add_command(label="Collect script output and validate", accelerator="Alt-S-V", command=lambda: self.database('script', 'Collect script output and validate'))
        self.menu_script.add_command(label="Check if terminal is serial", accelerator="Alt-S-T", command=lambda: self.database('script', 'Check if terminal is serial'))
        self.menu_script.add_command(label="Wait for serial output", accelerator="Alt-S-W", command=lambda: self.database('script', 'Wait for serial output'))
        self.menu_script.add_separator()
        self.menu_script.add_command(label="Read test data from file", accelerator="Alt-S-F", command=lambda: self.database('script', 'Read test data from file'))
        self.menu_script.add_command(label="Save temp file", accelerator="Alt-S-E", command=lambda: self.database('script', 'Save temp file'))
        self.menu_script.add_command(label="Become root", accelerator="Alt-S-O", command=lambda: self.database('script', 'Become root'))
        self.menu_script.add_command(label="Make sure package is installed", accelerator="Alt-S-I", command=lambda: self.database('script', 'Make sure package is installed'))
        self.menu_script.add_command(label="Hash the string with MD5", accelerator="Alt-S-M", command=lambda: self.database('script', 'Hash the string with MD5'))
        self.menu_script.add_separator()
        self.menu_script.add_command(label="Start GUI application", accelerator="Alt-S-X", command=lambda: self.database('script', 'Start GUI application'))
        # Create the Log submenu
        self.menu_log = Menu(self.menubar)
        self.menubar.add_cascade(menu=self.menu_log,label='Logs')
        self.menu_log.add_command(label="Log a diagnostic message", accelerator="Alt-L-M", command=lambda: self.database('logs', 'Log a message'))
        self.menu_log.add_command(label="Upload logs", accelerator="Alt-L-U", command=lambda: self.database('logs', 'Upload logs'))
        self.menu_log.add_command(label="Upload asset", accelerator="Alt-L-A", command=lambda: self.database('logs', 'Upload asset'))
        # Create the Helpers submenu
        self.menu_helpers = Menu(self.menubar)
        self.menubar.add_cascade(menu=self.menu_helpers,label='Helpers')
        self.menu_helpers.add_command(label="Get host's IP address", accelerator="Alt-H-I", command=lambda: self.database('helper', 'Get host ip'))
        self.menu_helpers.add_command(label="Get the base URL of local os-autoinst", accelerator="Alt-H-U", command=lambda: self.database('helper', 'Get base url'))
        self.menu_helpers.add_command(label="Get the data asset URL", accelerator="Alt-H-D", command=lambda: self.database('helper', 'Get data asset url'))
        self.menu_helpers.add_command(label="Make arguments compatible", accelerator="Alt-H-C", command=lambda: self.database('helper', 'Make arguments compatible'))
        self.menu_helpers.add_command(label="Show Curl progress meter", accelerator="Alt-H-P", command=lambda: self.database('helper', 'Show curl progress'))
        # Create the Misc submenu
        self.menu_misc = Menu(self.menubar)
        self.menubar.add_cascade(menu=self.menu_misc,label='Misc')
        self.menu_misc.add_command(label="Send power signal to machine", accelerator="Alt-M-P", command=lambda: self.database('misc', 'Send power signal'))
        self.menu_misc.add_command(label="Check machine shut down", accelerator="Alt-M-C", command=lambda: self.database('misc', 'Check machine shut down'))
        self.menu_misc.add_command(label="Assert machine shut down", accelerator="Alt-M-A", command=lambda: self.database('misc', 'Assert machine shut down'))
        self.menu_misc.add_command(label="Eject machine CD", accelerator="Alt-M-E", command=lambda: self.database('misc', 'Eject machine CD'))
        self.menu_misc.add_command(label="Save memory dump", accelerator="Alt-M-D", command=lambda: self.database('misc', 'Save memory dump'))
        self.menu_misc.add_command(label="Save storage drives", accelerator="Alt-M-S", command=lambda: self.database('misc', 'Save storage drives'))
        self.menu_misc.add_command(label="Freeze the VM", accelerator="Alt-M-F", command=lambda: self.database('misc', 'Freeze the VM'))
        self.menu_misc.add_command(label="Resume the VM", accelerator="Alt-M-R", command=lambda: self.database('misc', 'Resume the VM'))
        self.menu_misc.add_command(label="Parse jUnit log", accelerator="Alt-M-J", command=lambda: self.database('misc', 'Parse junit log'))
        self.menu_misc.add_command(label="Parse extra log", accelerator="Alt-M-L", command=lambda: self.database('misc', 'Parse extra log'))
        # Create the Perl submenu
        self.menu_perl = Menu(self.menubar)
        self.menubar.add_cascade(menu=self.menu_perl,label='Perl')
        self.menu_perl.add_command(label="IF clause", accelerator="Alt-P-I", command=lambda: self.perl_snippets('if'))
        self.menu_perl.add_command(label="IF ELSE clause", accelerator="Alt-P-E", command=lambda: self.perl_snippets('if_else'))
        self.menu_perl.add_command(label="IF ELSIF ELSE clause", accelerator="Alt-P-L", command=lambda: self.perl_snippets('if_elsif'))
        self.menu_perl.add_command(label="FOR clause", accelerator="Alt-P-F", command=lambda: self.perl_snippets('for'))
        self.menu_perl.add_command(label="FOREACH clause", accelerator="Alt-P-C", command=lambda: self.perl_snippets('foreach'))
        self.menu_perl.add_command(label="Assign VAR", accelerator="Alt-P-V", command=lambda: self.perl_snippets('var'))
        self.menu_perl.add_command(label="SUB take single argument", accelerator="Alt-P-R", command=lambda: self.perl_snippets('arg'))
        self.menu_perl.add_command(label="SUB take multiple arguments", accelerator="Alt-P-M", command=lambda: self.perl_snippets('args'))
        self.menu_perl.add_command(label="UNLESS clause", accelerator="Alt-P-U", command=lambda: self.perl_snippets('unless'))
        # Create the Help
        self.menu_help = Menu(self.menubar)
        self.menubar.add_cascade(menu=self.menu_help,label='Help')
        self.menu_help.add_command(label="Help", accelerator="F1", command=lambda: self.show_docs('help'))
        self.menu_help.add_command(label="openQA TestApi Documentation", command=lambda: self.show_docs('testapi'))
        self.menu_help.add_command(label="openQA Documentation", command=lambda: self.show_docs('docs'))
        self.menu_help.add_separator()
        self.menu_help.add_command(label="About", command=self.show_about)
         
        # Register the menubar with the main application
        self.toplevel['menu'] = self.menubar

    def create_quick_menu(self):
        """ Create widgets for the Quick menu. """
        self.buttons = ttk.Labelframe(self.mainframe, text="Quick actions")
        self.buttons.grid(column=0, row=0, sticky=(N,S,E,W), ipady=5)
        # Configure rows for the application to scale.
        self.buttons.rowconfigure(0, weight=2)
        self.buttons.rowconfigure(1, weight=1)
        self.buttons.rowconfigure(2, weight=1)
        self.buttons.rowconfigure(3, weight=1)
        self.buttons.rowconfigure(4, weight=1)
        self.buttons.rowconfigure(5, weight=1)
        self.buttons.rowconfigure(6, weight=1)
        self.buttons.rowconfigure(7, weight=1)
        self.buttons.rowconfigure(8, weight=1)
        self.buttons.rowconfigure(9, weight=1)
        self.buttons.rowconfigure(10, weight=1)
        self.buttons.rowconfigure(11, weight=1)
        self.buttons.columnconfigure(0, weight=1)

        # Define buttons
        self.skel_button = ttk.Button(self.buttons, text='Create file layout', command=self.create_layout)
        self.skel_button.grid(column=0, row=1, sticky=(N,S,E,W), pady='1')

        self.aclick_button = ttk.Button(self.buttons, text='Assert and click match', command=lambda: self.database('video', 'Assert and click match'))
        self.aclick_button.grid(column=0, row=2, sticky=(N,S,E,W), pady='1')

        self.ascreen_button = ttk.Button(self.buttons, text='Assert screen for match', command=lambda: self.database('video', 'Assert screen for match'))
        self.ascreen_button.grid(column=0, row=3, sticky=(N,S,E,W), pady='1')

        self.check_button = ttk.Button(self.buttons, text='Check screen for match', command=lambda: self.database('video', 'Check screen for match'))
        self.check_button.grid(column=0, row=4, sticky=(N,S,E,W), pady='1')

        self.type_button = ttk.Button(self.buttons, text='Type text', command=lambda: self.database('keyboard', 'Type text'))
        self.type_button.grid(column=0, row=5, sticky=(N,S,E,W), pady='1')

        self.stype_button = ttk.Button(self.buttons, text='Type text safely (Fedora)', command=lambda: self.database('keyboard', 'Type text safely'))
        self.stype_button.grid(column=0, row=6, sticky=(N,S,E,W), pady='1')

        self.press_button = ttk.Button(self.buttons, text='Press key (combination)', command=lambda: self.database('keyboard', 'Press key'))
        self.press_button.grid(column=0, row=7, sticky=(N,S,E,W), pady='1')

        self.wait_button = ttk.Button(self.buttons, text='Wait until screen is still', command=lambda: self.database('video', 'Wait until screen is still'))
        self.wait_button.grid(column=0, row=8, sticky=(N,S,E,W), pady='1')

        self.if_button = ttk.Button(self.buttons, text='IF statement layout', command=lambda: self.perl_snippets('if'))
        self.if_button.grid(column=0, row=9, sticky=(N,S,E,W), pady='1')

        self.for_button = ttk.Button(self.buttons, text='FOR statement layout', command=lambda: self.perl_snippets('for'))
        self.for_button.grid(column=0, row=10, sticky=(N,S,E,W), pady='1')

        self.flags_button = ttk.Button(self.buttons, text='Set test flags', command=self.set_test_flags)
        self.flags_button.grid(column=0, row=11, sticky=(N,S,E,W), pady='1')

        self.comments = ttk.Checkbutton(self.buttons, text="Include comments", variable=self.comments_on, onvalue=1, offvalue=0)
        self.comments.grid(column=0, row=12, sticky=(N,S,E,W), pady='1')
        self.comments_on.set(True)

    def regex_api_commands(self):
        """ Walk over the database and pick commands to list
        the into the tag database for syntax highlighting. """
        # Iterate over all openqa commands in the test api 
        # and add them to the lexer.
        commands = []
        for family in self.db:
            for name in self.db[family]:
                commands.append(name.command)
        begin = r'\b(?P<COMMANDS>'
        end = r')\b|'
        mid = '|'.join(commands)
        # Also add selected Perl keywords because the lexer is based
        # on Python and therefore lacks the Perl stuff.
        # If more Perl keyboards should be recognized, they can be added here.
        # Alternatively, there might be some external tool to cover in one 
        # of the future versions.
        perl = r'\b(?P<PERL>sub|use|my|run|unless|until|qx|qw|qr|qq|no|ne|foreach|elsif)\b|'
        return begin + mid + end + perl
            

    def colorize(self):
        """ Update the lexer database with new items. """
        # Take the regex expression needed to be added and add it to the scheme.
        openqa_keywords = self.regex_api_commands()
        self.scheme.prog = re.compile(openqa_keywords + self.pattern, re.S)
        self.scheme.idprog = re.compile(r'\s+(\w+)', re.S)
        
    def create_text(self):
        """ Create the text widget. """
        self.text = scrolledtext.ScrolledText(self.mainframe)
        fnt = font.Font(font=self.text['font'])
        tab_size = fnt.measure('    ')
        self.text.configure(height='20', maxundo='100', width='120', tabs=tab_size)
        self.text.grid(column='1', row='0', sticky='nsew')
        # Add Syntax highlighting to the text widget
        self.colorize()
        percolator(self.text).insertfilter(self.scheme)

    def bind_accelerators(self):
        """ Bind accelerators (shortcuts) to the main application. """
        # Accelerators for the Video menu items.
        self.toplevel.bind("<Alt-v><c>", lambda x: self.database('video', 'Assert and click match'))
        self.toplevel.bind("<Alt-v><d>", lambda x: self.database('video', 'Assert and doubleclick match'))
        self.toplevel.bind("<Alt-v><m>", lambda x: self.database('video', 'Assert screen for match'))
        self.toplevel.bind("<Alt-v><h>", lambda x: self.database('video', 'Check screen for match'))
        self.toplevel.bind("<Alt-v><w>", lambda x: self.database('video', 'Wait until screen is still'))
        self.toplevel.bind("<Alt-v><f>", lambda x: self.database('video','Record soft failure'))
        self.toplevel.bind("<Alt-v><l>", lambda x: self.database('video','Click last match'))
        self.toplevel.bind("<Alt-v><g>", lambda x: self.database('video','Check if match has tag'))
        self.toplevel.bind("<Alt-v><q>", lambda x: self.database('video','Assert that screen is still'))
        self.toplevel.bind("<Alt-v><a>", lambda x: self.database('video','Assert that screen changes'))
        self.toplevel.bind("<Alt-v><e>", lambda x: self.database('video','Wait if screen changes'))
        self.toplevel.bind("<Alt-v><u>", lambda x: self.database('video','Force soft failure'))
        self.toplevel.bind("<Alt-v><i>", lambda x: self.database('video','Record info'))
        self.toplevel.bind("<Alt-v><t>", lambda x: self.database('video','Take screenshot'))
        # Accelerators for the Audio menu items.
        self.toplevel.bind("<Alt-a><r>", lambda x: self.database('audio', 'Start audio recording'))
        self.toplevel.bind("<Alt-a><s>", lambda x: self.database('audio', 'Assert recorded sound'))
        self.toplevel.bind("<Alt-a><c>", lambda x: self.database('audio', 'Check recorded sound'))
        # Accelerators for the Keyboard menu items.
        self.toplevel.bind("<Alt-k><p>", lambda x: self.database('keyboard','Press key'))
        self.toplevel.bind("<Alt-k><h>", lambda x: self.database('keyboard','Hold key'))
        self.toplevel.bind("<Alt-k><r>", lambda x: self.database('keyboard','Release key'))
        self.toplevel.bind("<Alt-k><t>", lambda x: self.database('keyboard','Type text'))
        self.toplevel.bind("<Alt-k><s>", lambda x: self.database('keyboard','Type text safely'))
        self.toplevel.bind("<Alt-k><w>", lambda x: self.database('keyboard','Type password'))
        self.toplevel.bind("<Alt-k><c>", lambda x: self.database('keyboard','Run command'))
        self.toplevel.bind("<Alt-k><e>", lambda x: self.database('keyboard','Press key until event'))
        # Accelerators for the Mouse menu items.
        self.toplevel.bind("<Alt-m><c>", lambda x: self.database('mouse','Click mouse'))
        self.toplevel.bind("<Alt-m><d>", lambda x: self.database('mouse','Doubleclick mouse'))
        self.toplevel.bind("<Alt-m><t>", lambda x: self.database('mouse','Tripleclick mouse'))
        self.toplevel.bind("<Alt-m><r>", lambda x: self.database('mouse','Drag mouse'))
        self.toplevel.bind("<Alt-m><p>", lambda x: self.database('mouse','Set mouse location'))
        self.toplevel.bind("<Alt-m><h>", lambda x: self.database('mouse','Hide mouse'))
        # Accelerators for the Variables menu items.
        self.toplevel.bind("<Alt-r><g>", lambda x: self.database('variable','Read variable'))
        self.toplevel.bind("<Alt-r><d>", lambda x: self.database('variable','Read strict variable'))
        self.toplevel.bind("<Alt-r><s>", lambda x: self.database('variable','Set variable'))
        self.toplevel.bind("<Alt-r><c>", lambda x: self.database('variable','Check variable'))
        self.toplevel.bind("<Alt-r><l>", lambda x: self.database('variable','Read variable as list'))
        self.toplevel.bind("<Alt-r><o>", lambda x: self.database('variable','Check value in list of variables'))
        # Accelerators for the Script menu items.
        self.toplevel.bind("<Alt-s><a>", lambda x: self.database('script','Run script and assert'))
        self.toplevel.bind("<Alt-s><r>", lambda x: self.database('script','Run script'))
        self.toplevel.bind("<Alt-s><b>", lambda x: self.database('script','Run script in background'))
        self.toplevel.bind("<Alt-s><u>", lambda x: self.database('script','Run sudo script and assert'))
        self.toplevel.bind("<Alt-s><d>", lambda x: self.database('script','Run sudo script'))
        self.toplevel.bind("<Alt-s><c>", lambda x: self.database('script','Collect script output'))
        self.toplevel.bind("<Alt-s><v>", lambda x: self.database('script','Collect script output and validate'))
        self.toplevel.bind("<Alt-s><t>", lambda x: self.database('script','Check if terminal is serial'))
        self.toplevel.bind("<Alt-s><w>", lambda x: self.database('script','Wait for serial output'))
        self.toplevel.bind("<Alt-s><f>", lambda x: self.database('script','Read test data from file'))
        self.toplevel.bind("<Alt-s><e>", lambda x: self.database('script','Save temp file'))
        self.toplevel.bind("<Alt-s><o>", lambda x: self.database('script','Become root'))
        self.toplevel.bind("<Alt-s><i>", lambda x: self.database('script','Make sure package is installed'))
        self.toplevel.bind("<Alt-s><m>", lambda x: self.database('script','Hash the string with MD5'))
        self.toplevel.bind("<Alt-s><x>", lambda x: self.database('script','Start GUI application'))
        # Accelerators for the Log menu items.
        self.toplevel.bind("<Alt-l><m>", lambda x: self.database('logs','Log a message'))
        self.toplevel.bind("<Alt-l><u>", lambda x: self.database('logs','Upload logs'))
        self.toplevel.bind("<Alt-l><a>", lambda x: self.database('logs','Upload asset'))
        # Accelerators for the Helpers menu items.
        self.toplevel.bind("<Alt-h><i>", lambda x: self.database('helper','Get host ip'))
        self.toplevel.bind("<Alt-h><u>", lambda x: self.database('helper','Get base url'))
        self.toplevel.bind("<Alt-h><d>", lambda x: self.database('helper','Get data asset url'))
        self.toplevel.bind("<Alt-h><c>", lambda x: self.database('helper','Make arguments compatible'))
        self.toplevel.bind("<Alt-h><p>", lambda x: self.database('helper','Show curl progress'))
        # Accelerators for the Misc menu
        self.toplevel.bind("<Alt-m><p>", lambda x: self.database('misc','Send power signal'))
        self.toplevel.bind("<Alt-m><c>", lambda x: self.database('misc','Check machine shut down'))
        self.toplevel.bind("<Alt-m><a>", lambda x: self.database('misc','Assert machine shut down'))
        self.toplevel.bind("<Alt-m><e>", lambda x: self.database('misc','Eject machine CD'))
        self.toplevel.bind("<Alt-m><d>", lambda x: self.database('misc','Save memory dump'))
        self.toplevel.bind("<Alt-m><s>", lambda x: self.database('misc','Save storage drives'))
        self.toplevel.bind("<Alt-m><f>", lambda x: self.database('misc','Freeze the VM'))
        self.toplevel.bind("<Alt-m><r>", lambda x: self.database('misc','Resume the VM'))
        self.toplevel.bind("<Alt-m><j>", lambda x: self.database('misc','Parse junit log'))
        self.toplevel.bind("<Alt-m><l>", lambda x: self.database('misc','Parse extra log'))
        # Accelerators for the Perl menu items.
        self.toplevel.bind("<Alt-p><i>", lambda x: self.perl_snippets('if'))
        self.toplevel.bind("<Alt-p><l>", lambda x: self.perl_snippets('if_elsif'))
        self.toplevel.bind("<Alt-p><e>", lambda x: self.perl_snippets('if_else'))
        self.toplevel.bind("<Alt-p><f>", lambda x: self.perl_snippets('for'))
        self.toplevel.bind("<Alt-p><c>", lambda x: self.perl_snippets('foreach'))
        self.toplevel.bind("<Alt-p><v>", lambda x: self.perl_snippets('var'))
        self.toplevel.bind("<Alt-p><r>", lambda x: self.perl_snippets('arg'))
        self.toplevel.bind("<Alt-p><m>", lambda x: self.perl_snippets('args'))
        self.toplevel.bind("<Alt-p><u>", lambda x: self.perl_snippets('unless'))
        # Accelerators for the About menu
        self.toplevel.bind("<F1>", lambda x: self.show_docs('help'))
        # Accelerators for usual application stuff
        self.toplevel.bind("<Control-q>", lambda x: self.close_application())
        self.toplevel.bind("<Control-s>", lambda x: self.save_file())
        self.toplevel.bind("<Control-n>", lambda x: self.new_file())
        self.toplevel.bind("<Control-o>", lambda x: self.open_file())
        self.toplevel.bind("<Control-a>", lambda x: self.select_all())
        # Binds some keys in the text widget
        self.text.bind('<Tab>', lambda x: self.insert_tab())
        self.text.bind('<Key>', self.text_edited)

    def run(self):
        """ Run the application """
        self.toplevel.mainloop()

    def close_application(self):
        """ Work around the destroy method to close the application properly. """
        # If the content is not saved ask to save or close anyway.
        if self.is_saved == False:
            closefile = messagebox.askyesno('File not saved', 'The current file has not been saved yet. Do you still want to close the file?')
        # If the content is saved, then we act as if Yes was chosen in the dialogue.
        else:
            closefile = True
        # If closing was confirmed, then destroy the application or do nothing.
        if closefile:
            self.toplevel.destroy()
        else:
            pass

###############################  Below this line, we will define other useful methods for the application.
    
    def update_title(self, status):
        """ Prepare a title string to reflect if file is saved or not 
        and show it in the title bar. """
        # If not saved, then produce * before the file name.
        if status == 'unsave':
            s = "*"
        else:
            s = ""
        # If we have the opened file, then let's get its base name and
        # put it in the title string as well.
        if not self.filetosave:
            base = ""
        else:
            base = os.path.basename(self.filetosave)
        # Update the application title
        self.toplevel.title(f"{s} {base} - {self.appname}")

    def put_into_text(self, text):
        """ Puts a string into the text widget and set the status to 'no saved'. """
        # Read the cursor position
        idx = self.text.index('insert')
        # Place the text on that position
        self.text.insert(idx, text)
        # Set the document to unsaved and update the title string.
        self.is_saved = False
        self.update_title('unsave')

    def database(self, family, record):
        """ Get a snippet to from a database of snippets located in the testapi.py module. """
        # The database is a dictionary where keys are "families" and values are lists of definitions.
        # So we pick up the family of definitions
        chapter = self.db[family]
        data = None
        # Iterate over the definitions and find the one that matches the selection.
        for c in chapter:
            if c.name == record:
                data = c
        # When the data is found it is placed into the text widget.
        if data and self.comments_on.get() == 0:
            self.put_into_text(data.display_syntax())
        elif data:
            self.put_into_text(data.all())
        # This should actually never happen, unless there is an error in the code.
        else:
            messagebox.showerror('Unknown request','It seems that a menu item or a keyboard shortcut require a non-existing snippet. Please, report a bug.')

    def perl_snippets(self, snippet):
        """ Return a perl snippet. """
        # The perl snippets have a different structure and they are not as numerous as openqa snippets. 
        # Therefore we only have them in a separate routine. 
        if snippet == 'if':
            self.put_into_text("if (condition) {\n   # Put some code here;\n}\n")
        elif snippet == 'unless':
            self.put_into_text("unless (condition) {\n   # Put some code here;\n}\n")
        elif snippet == 'if_else':
            self.put_into_text("if (condition) {\n   # Put some code here;\n}\nelse {\n    # Some alternative code;\n}\n")
        elif snippet == 'if_elsif':
            self.put_into_text("if (condition) {\n   # Put some code here;\n}\nelsif (other condition) {\n    # Some alternative code;\n}\nelse {\n    # Fallback code;\n}\n")
        elif snippet == 'for':
            self.put_into_text("for my $i (@array) {\n    # Put some code here;\n}\n")
        elif snippet == 'foreach':
            self.put_into_text("foreach (@array) {\n    # Put some code here;\n}\n")
        elif snippet == 'var':
            self.put_into_text('my $variable = value;\n')
        elif snippet == 'arg':
            self.put_into_text("my $variable = shift;\n")
        elif snippet == 'args':
            self.put_into_text("my ($var1, $var2, ...) = @_;\n")
        else:
            messagebox.showerror('Unknown request','It seems that a menu item or a keyboard shortcut calls for a non-existing snippet. Please, report a bug.')

    def set_test_flags(self):
        """ Insert test flag snippet into the text field. """
        lines = [
            'sub test_flags {',
            '    return {fatal => 0, ignore_failure => 0, milestone => 0, no_rollback => 0, always_rollback => 0};',
            '}'
        ]
        snippet = '\n'.join(lines)
        self.put_into_text(snippet)

    def create_layout(self):
        """ Insert the file layout into the text field. """
        lines = [
            'use base "installedtest";',
            'use strict;',
            'use testapi;',
            'use utils;',
            '',
            'sub run {',
            '',
            '',
            '}'
        ]
        snippet = '\n'.join(lines)
        self.put_into_text(snippet)
            
    def open_file(self, filename=None):
        """ Opens a file. """
        # First, let's make sure the document is saved, or the user does not want to save it. The dialog sets the $closefile
        # to True if the user does not want to save the current buffer.
        if self.is_saved == False:
            closefile = messagebox.askyesno('File not saved', 'The file has not been saved yet, do you want to open another file?')
        # Else it means, that the document is saved so we can proceed with closing the current buffer.
        else:
            closefile = True
        # If closefile is True from the decision above, we will throw away the unsaved buffer
        if closefile:
            # We will use a dialogue to open the file, so only existing files will be visible.
            if not filename:
                self.filetosave = filedialog.askopenfilename(filetypes=[('openQA script','*.pm')])
            else:
                self.filetosave = os.path.abspath(filename)
            # Get the filename
            base = os.path.basename(self.filetosave)
            # Read the data from the file
            with open(self.filetosave, 'r') as inputfile:
                data = inputfile.readlines()
            # Delete the content of the text widget.
            self.text.delete('1.0', 'end')
            # Fill the text widget with data
            for line in data:
                self.put_into_text(line)
            # Update the status to "saved"
            self.update_title('save')

    def save_file(self):
        """ Save the file """
        # If there is no file opened to save the buffer, use the "Save as" routine.
        if not self.filetosave:
            self.save_as_file()
        else:
            # Get the content of the text widget
            data = self.text.get('1.0','end')
            # Write it into the file.
            with open(self.filetosave, 'w') as outputfile:
                outputfile.write(data)
            # Get the file name and update the title bar.
            base = os.path.basename(self.filetosave)
            self.update_title('save')
            self.is_saved = True

    def save_as_file(self):
        # Use the dialogue to save as into a file.
        self.filetosave = filedialog.asksaveasfilename(filetypes=[('openQA script','*.pm')])
        # Get the content of the text widget
        data = self.text.get('1.0', 'end')
        # Save it into the file
        with open(self.filetosave, 'w') as outputfile:
            outputfile.write(data)
        # Get the file name and update the title bar.    
        base = os.path.basename(self.filetosave)
        self.update_title('save')

    def new_file(self):
        """ Create a new file. """
        # If the current buffer is not saved, ask if user wants to save it, for details see the open_file routine.
        if self.is_saved == False:
            closefile = messagebox.askyesno('File not saved', 'The file has not been saved yet, do you still want to open new file?')
        else:
            closefile = True
        # Discard the content of the text widget and set the status variables as they were in the beginning.
        if closefile:
            self.text.delete('1.0','end')
            self.filetosave = None
            self.toplevel.title(self.appname)
        else:
            pass

    def text_edited(self, e):
        """ Trigger the status to unsave whenever an keyboard event happens in the test widget. """
        if e.state == 20:
            pass
        else:
            self.is_saved = False
            self.update_title('unsave')

    def select_all(self):
        """ Select the content of the text widget. """
        # This methods supplies the Ctrl-A combination that is not supported by the Text widget per se.
        self.text.tag_add('sel', '1.0', 'end')

    def show_about(self):
        """ Shows the About window. """
        lines = [
            'openQA Scripting Tool',
            'version 0.9',
            '',
            'a basic text editor with pre-created openQA snippets that enables developing openQA test scripts more rapidly.',
            '',
            'This tool is open software developed under the GPL license.',
            '',
            'Created by Lukáš Růžička (lruzicka@redhat.com)',
            'Fedora QA Team (2021)'
        ]
        text = '\n'.join(lines)
        about = Toplevel()
        about.wm_title('About')
        about.columnconfigure(0, weight=1)
        about.rowconfigure(0, weight=1)
        ttk.Label(about,text=text, wraplength=180, justify="center").grid(column=0, row=0, sticky='nsew', pady=20, padx=10)
        ttk.Button(about, text="Close", command=about.destroy).grid(column=0,row=1)

    def show_docs(self, doctype):
        """ Open external web resources to cover for Help and Documentation. """
        if doctype == 'testapi':
            url = 'http://open.qa/api/testapi/'
        elif doctype == 'docs':
            url = 'http://open.qa/documentation/'
        elif doctype == 'help':
            url = 'https://pagure.io/fedora-qa/openQA_scripting_tool'
        webbrowser.open_new(url)

    def insert_tab(self):
        """ Inserts four spaces into the text, when tab is pressed. """
        self.put_into_text("   ")

################################################################################################
    
def main():
    # Read the arguments if there is a file name on the CLI.
    try:
        filename = sys.argv[1]
    except IndexError:
        filename = None
    # Start the application and open the file, if there is one.
    app = Application(filename=filename)
    app.run()
        
################################################################################################


if __name__ == '__main__':
    main()


