#!/usr/bin/env python3

# This script is part of the WhiteboxTools geospatial analysis library.
# Authors: Dr. John Lindsay, Rachel Broders
# Created: 28/11/2017
# Last Modified: 05/11/2019
# License: MIT

import __future__
import sys
# if sys.version_info[0] < 3:
#     raise Exception("Must be using Python 3")
import json
import os
from os import path
# from __future__ import print_function
# from enum import Enum
import platform
import re    #Added by Rachel for snake_to_camel function
from pathlib import Path
import glob
from sys import platform as _platform
import shlex
import tkinter as tk
from tkinter import ttk
from tkinter.scrolledtext import ScrolledText
from tkinter import filedialog
from tkinter import messagebox
from tkinter import PhotoImage
import webbrowser
from whitebox_tools import WhiteboxTools, to_camelcase

wbt = WhiteboxTools()


class FileSelector(tk.Frame):
    def __init__(self, json_str, runner, master=None):
        # first make sure that the json data has the correct fields
        j = json.loads(json_str)
        self.name = j['name']
        self.description = j['description']
        self.flag = j['flags'][len(j['flags']) - 1]
        self.parameter_type = j['parameter_type']
        self.file_type = ""
        if "ExistingFile" in self.parameter_type:
            self.file_type = j['parameter_type']['ExistingFile']
        elif "NewFile" in self.parameter_type:
            self.file_type = j['parameter_type']['NewFile']
        self.optional = j['optional']
        default_value = j['default_value']

        self.runner = runner

        ttk.Frame.__init__(self, master, padding='0.02i')
        self.grid()

        self.label = ttk.Label(self, text=self.name, justify=tk.LEFT)
        self.label.grid(row=0, column=0, sticky=tk.W)
        self.label.columnconfigure(0, weight=1)

        if not self.optional:
            self.label['text'] = self.label['text'] + "*"

        fs_frame = ttk.Frame(self, padding='0.0i')
        self.value = tk.StringVar()
        self.entry = ttk.Entry(
            fs_frame, width=45, justify=tk.LEFT, textvariable=self.value)
        self.entry.grid(row=0, column=0, sticky=tk.NSEW)
        self.entry.columnconfigure(0, weight=1)
        if default_value:
            self.value.set(default_value)

        # dir_path = os.path.dirname(os.path.realpath(__file__))
        # print(dir_path)
        # self.open_file_icon = tk.PhotoImage(file =  dir_path + '//img//open.png')     #Added by Rachel to replace file selector "..." button with open file icon
        
        # self.open_button = ttk.Button(fs_frame, width=4, image = self.open_file_icon, command=self.select_file, padding = '0.02i')
        self.open_button = ttk.Button(fs_frame, width=4, text="...", command=self.select_file, padding = '0.02i')
        self.open_button.grid(row=0, column=1, sticky=tk.E)
        self.open_button.columnconfigure(0, weight=1)
        fs_frame.grid(row=1, column=0, sticky=tk.NSEW)
        fs_frame.columnconfigure(0, weight=10)
        fs_frame.columnconfigure(1, weight=1)
        # self.pack(fill=tk.BOTH, expand=1)
        self.columnconfigure(0, weight=1)
        self.rowconfigure(0, weight=1)
        self.rowconfigure(1, weight=1)

        # Add the bindings
        if _platform == "darwin":
            self.entry.bind("<Command-Key-a>", self.select_all)
        else:
            self.entry.bind("<Control-Key-a>", self.select_all)

    def select_file(self):
        try:
            result = self.value.get()
            if self.parameter_type == "Directory":
                result = filedialog.askdirectory(initialdir=self.runner.working_dir, title="Select directory")
            elif "ExistingFile" in self.parameter_type:
                ftypes = [('All files', '*.*')]
                if 'RasterAndVector' in self.file_type:
                    ftypes = [("Shapefiles", "*.shp"), ('Raster files', ('*.dep', '*.tif',
                                                '*.tiff', '*.flt',
                                                '*.sdat', '*.rdc',
                                                '*.asc'))]
                elif 'Raster' in self.file_type:
                    ftypes = [('Raster files', ('*.dep', '*.tif',
                                                '*.tiff', '*.flt',
                                                '*.sdat', '*.rdc',
                                                '*.asc'))]
                elif 'Lidar' in self.file_type:
                    ftypes = [("LiDAR files", ('*.las', '*.zlidar', '*.zip'))]
                elif 'Vector' in self.file_type:
                    ftypes = [("Shapefiles", "*.shp")]
                elif 'Text' in self.file_type:
                    ftypes = [("Text files", "*.txt"), ("all files", "*.*")]
                elif 'Csv' in self.file_type:
                    ftypes = [("CSC files", "*.csv"), ("all files", "*.*")]
                elif 'Html' in self.file_type:
                    ftypes = [("HTML files", "*.html")]

                result = filedialog.askopenfilename(
                    initialdir=self.runner.working_dir, title="Select file", filetypes=ftypes)

            elif "NewFile" in self.parameter_type:
                result = filedialog.asksaveasfilename()

            self.value.set(result)
            # update the working directory
            self.runner.working_dir = os.path.dirname(result)

        except:
            t = "file"
            if self.parameter_type == "Directory":
                t = "directory"
            messagebox.showinfo("Warning", "Could not find {}".format(t))

    def get_value(self):
        if self.value.get():
            v = self.value.get()
            # Do some quality assurance here.
            # Is there a directory included?
            if not path.dirname(v):
                v = path.join(self.runner.working_dir, v)

            # What about a file extension?
            ext = os.path.splitext(v)[-1].lower().strip()
            if not ext:
                ext = ""
                if 'RasterAndVector' in self.file_type:
                    ext = '.tif'
                elif 'Raster' in self.file_type:
                    ext = '.tif'
                elif 'Lidar' in self.file_type:
                    ext = '.las'
                elif 'Vector' in self.file_type:
                    ext = '.shp'
                elif 'Text' in self.file_type:
                    ext = '.txt'
                elif 'Csv' in self.file_type:
                    ext = '.csv'
                elif 'Html' in self.file_type:
                    ext = '.html'

                v += ext

            v = path.normpath(v)

            return "{}='{}'".format(self.flag, v)
        else:
            t = "file"
            if self.parameter_type == "Directory":
                t = "directory"
            if not self.optional:
                messagebox.showinfo(
                    "Error", "Unspecified {} parameter {}.".format(t, self.flag))

        return None

    def select_all(self, event):
        self.entry.select_range(0, tk.END)
        return 'break'


class FileOrFloat(tk.Frame):
    def __init__(self, json_str, runner, master=None):
        # first make sure that the json data has the correct fields
        j = json.loads(json_str)
        self.name = j['name']
        self.description = j['description']
        self.flag = j['flags'][len(j['flags']) - 1]
        self.parameter_type = j['parameter_type']
        self.file_type = j['parameter_type']['ExistingFileOrFloat']
        self.optional = j['optional']
        default_value = j['default_value']

        self.runner = runner

        ttk.Frame.__init__(self, master)
        self.grid()
        self['padding'] = '0.02i'

        self.label = ttk.Label(self, text=self.name, justify=tk.LEFT)
        self.label.grid(row=0, column=0, sticky=tk.W)
        self.label.columnconfigure(0, weight=1)

        if not self.optional:
            self.label['text'] = self.label['text'] + "*"

        fs_frame = ttk.Frame(self, padding='0.0i')
        self.value = tk.StringVar()
        self.entry = ttk.Entry(
            fs_frame, width=35, justify=tk.LEFT, textvariable=self.value)
        self.entry.grid(row=0, column=0, sticky=tk.NSEW)
        self.entry.columnconfigure(0, weight=1)
        if default_value:
            self.value.set(default_value)

        # self.img = tk.PhotoImage(file=script_dir + "/img/open.gif")
        # self.open_button = ttk.Button(fs_frame, width=55, image=self.img, command=self.select_dir)
        self.open_button = ttk.Button(
            fs_frame, width=4, text="...", command=self.select_file)
        self.open_button.grid(row=0, column=1, sticky=tk.E)
        # self.open_button.columnconfigure(0, weight=1)

        self.label = ttk.Label(fs_frame, text='OR', justify=tk.LEFT)
        self.label.grid(row=0, column=2, sticky=tk.W)
        # self.label.columnconfigure(0, weight=1)

        self.value2 = tk.StringVar()
        self.entry2 = ttk.Entry(
            fs_frame, width=10, justify=tk.LEFT, textvariable=self.value2)
        self.entry2.grid(row=0, column=3, sticky=tk.NSEW)
        self.entry2.columnconfigure(0, weight=1)
        self.entry2['justify'] = 'right'

        fs_frame.grid(row=1, column=0, sticky=tk.NSEW)
        fs_frame.columnconfigure(0, weight=10)
        fs_frame.columnconfigure(1, weight=1)
        # self.pack(fill=tk.BOTH, expand=1)
        self.columnconfigure(0, weight=1)
        self.rowconfigure(0, weight=1)
        self.rowconfigure(1, weight=1)

        # Add the bindings
        if _platform == "darwin":
            self.entry.bind("<Command-Key-a>", self.select_all)
        else:
            self.entry.bind("<Control-Key-a>", self.select_all)

    def select_file(self):
        try:
            result = self.value.get()
            ftypes = [('All files', '*.*')]
            if 'RasterAndVector' in self.file_type:
                    ftypes = [("Shapefiles", "*.shp"), ('Raster files', ('*.dep', '*.tif',
                                                '*.tiff', '*.flt',
                                                '*.sdat', '*.rdc',
                                                '*.asc'))]
            elif 'Raster' in self.file_type:
                ftypes = [('Raster files', ('*.dep', '*.tif',
                                            '*.tiff', '*.flt',
                                            '*.sdat', '*.rdc',
                                            '*.asc'))]
            elif 'Lidar' in self.file_type:
                ftypes = [("LiDAR files", ('*.las', '*.zlidar', '*.zip'))]
            elif 'Vector' in self.file_type:
                ftypes = [("Shapefiles", "*.shp")]
            elif 'Text' in self.file_type:
                ftypes = [("Text files", "*.txt"), ("all files", "*.*")]
            elif 'Csv' in self.file_type:
                ftypes = [("CSC files", "*.csv"), ("all files", "*.*")]
            elif 'Html' in self.file_type:
                ftypes = [("HTML files", "*.html")]

            result = filedialog.askopenfilename(
                initialdir=self.runner.working_dir, title="Select file", filetypes=ftypes)

            self.value.set(result)
            # update the working directory
            self.runner.working_dir = os.path.dirname(result)

        except:
            t = "file"
            if self.parameter_type == "Directory":
                t = "directory"
            messagebox.showinfo("Warning", "Could not find {}".format(t))

    def RepresentsFloat(self, s):
        try:
            float(s)
            return True
        except ValueError:
            return False

    def get_value(self):
        if self.value.get():
            v = self.value.get()
            # Do some quality assurance here.
            # Is there a directory included?
            if not path.dirname(v):
                v = path.join(self.runner.working_dir, v)

            # What about a file extension?
            ext = os.path.splitext(v)[-1].lower()
            if not ext:
                ext = ""
                if 'RasterAndVector' in self.file_type:
                    ext = '.tif'
                elif 'Raster' in self.file_type:
                    ext = '.tif'
                elif 'Lidar' in self.file_type:
                    ext = '.las'
                elif 'Vector' in self.file_type:
                    ext = '.shp'
                elif 'Text' in self.file_type:
                    ext = '.txt'
                elif 'Csv' in self.file_type:
                    ext = '.csv'
                elif 'Html' in self.file_type:
                    ext = '.html'

                v = v + ext

            v = path.normpath(v)

            return "{}='{}'".format(self.flag, v)
        elif self.value2.get():
            v = self.value2.get()
            if self.RepresentsFloat(v):
                return "{}={}".format(self.flag, v)
            else:
                messagebox.showinfo(
                    "Error", "Error converting parameter {} to type Float.".format(self.flag))
        else:
            if not self.optional:
                messagebox.showinfo(
                    "Error", "Unspecified file/numeric parameter {}.".format(self.flag))

        return None

    def select_all(self, event):
        self.entry.select_range(0, tk.END)
        return 'break'


class MultifileSelector(tk.Frame):
    def __init__(self, json_str, runner, master=None):
        # first make sure that the json data has the correct fields
        j = json.loads(json_str)
        self.name = j['name']
        self.description = j['description']
        self.flag = j['flags'][len(j['flags']) - 1]
        self.parameter_type = j['parameter_type']
        self.file_type = ""
        self.file_type = j['parameter_type']['FileList']
        self.optional = j['optional']
        default_value = j['default_value']

        self.runner = runner

        ttk.Frame.__init__(self, master)
        self.grid()
        self['padding'] = '0.05i'

        self.label = ttk.Label(self, text=self.name, justify=tk.LEFT)
        self.label.grid(row=0, column=0, sticky=tk.W)
        self.label.columnconfigure(0, weight=1)

        if not self.optional:
            self.label['text'] = self.label['text'] + "*"

        fs_frame = ttk.Frame(self, padding='0.0i')
        # , variable=self.value)
        self.opt = tk.Listbox(fs_frame, width=44, height=4)
        self.opt.grid(row=0, column=0, sticky=tk.NSEW)
        s = ttk.Scrollbar(fs_frame, orient=tk.VERTICAL, command=self.opt.yview)
        s.grid(row=0, column=1, sticky=(tk.N, tk.S))
        self.opt['yscrollcommand'] = s.set

        btn_frame = ttk.Frame(fs_frame, padding='0.0i')
        self.open_button = ttk.Button(
            btn_frame, width=4, text="...", command=self.select_file)
        self.open_button.grid(row=0, column=0, sticky=tk.NE)
        self.open_button.columnconfigure(0, weight=1)
        self.open_button.rowconfigure(0, weight=1)

        self.delete_button = ttk.Button(
            btn_frame, width=4, text="del", command=self.delete_entry)
        self.delete_button.grid(row=1, column=0, sticky=tk.NE)
        self.delete_button.columnconfigure(0, weight=1)
        self.delete_button.rowconfigure(1, weight=1)

        btn_frame.grid(row=0, column=2, sticky=tk.NE)

        fs_frame.grid(row=1, column=0, sticky=tk.NSEW)
        fs_frame.columnconfigure(0, weight=10)
        fs_frame.columnconfigure(1, weight=1)
        fs_frame.columnconfigure(2, weight=1)
        # self.pack(fill=tk.BOTH, expand=1)
        self.columnconfigure(0, weight=1)
        self.rowconfigure(0, weight=1)
        self.rowconfigure(1, weight=1)

    def select_file(self):
        try:
            #result = self.value.get()
            init_dir = self.runner.working_dir
            ftypes = [('All files', '*.*')]
            if 'RasterAndVector' in self.file_type:
                    ftypes = [("Shapefiles", "*.shp"), ('Raster files', ('*.dep', '*.tif',
                                                '*.tiff', '*.flt',
                                                '*.sdat', '*.rdc',
                                                '*.asc'))]
            elif 'Raster' in self.file_type:
                ftypes = [('Raster files', ('*.dep', '*.tif',
                                            '*.tiff', '*.flt',
                                            '*.sdat', '*.rdc',
                                            '*.asc'))]
            elif 'Lidar' in self.file_type:
                ftypes = [("LiDAR files", ('*.las', '*.zlidar', '*.zip'))]
            elif 'Vector' in self.file_type:
                ftypes = [("Shapefiles", "*.shp")]
            elif 'Text' in self.file_type:
                ftypes = [("Text files", "*.txt"), ("all files", "*.*")]
            elif 'Csv' in self.file_type:
                ftypes = [("CSC files", "*.csv"), ("all files", "*.*")]
            elif 'Html' in self.file_type:
                ftypes = [("HTML files", "*.html")]

            result = filedialog.askopenfilenames(
                initialdir=init_dir, title="Select files", filetypes=ftypes)
            if result:
                for v in result:
                    self.opt.insert(tk.END, v)

                # update the working directory
                self.runner.working_dir = os.path.dirname(result[0])

        except:
            messagebox.showinfo("Warning", "Could not find file")

    def delete_entry(self):
        self.opt.delete(tk.ANCHOR)

    def get_value(self):
        try:
            l = self.opt.get(0, tk.END)
            if l:
                s = ""
                for i in range(0, len(l)):
                    v = l[i]
                    if not path.dirname(v):
                        v = path.join(self.runner.working_dir, v)
                    v = path.normpath(v)
                    if i < len(l) - 1:
                        s += "{};".format(v)
                    else:
                        s += "{}".format(v)

                return "{}='{}'".format(self.flag, s)
            else:
                if not self.optional:
                    messagebox.showinfo(
                        "Error", "Unspecified non-optional parameter {}.".format(self.flag))

        except:
            messagebox.showinfo(
                "Error", "Error formating files for parameter {}".format(self.flag))

        return None


class BooleanInput(tk.Frame):
    def __init__(self, json_str, master=None):
        # first make sure that the json data has the correct fields
        j = json.loads(json_str)
        self.name = j['name']
        self.description = j['description']
        self.flag = j['flags'][len(j['flags']) - 1]
        self.parameter_type = j['parameter_type']
        # just for quality control. BooleanInputs are always optional.
        self.optional = True
        default_value = j['default_value']

        ttk.Frame.__init__(self, master)
        self.grid()
        self['padding'] = '0.05i'

        frame = ttk.Frame(self, padding='0.0i')

        self.value = tk.IntVar()
        c = ttk.Checkbutton(frame, text=self.name,
                            width=55, variable=self.value)
        c.grid(row=0, column=0, sticky=tk.W)

        # set the default value
        if j['default_value'] != None and j['default_value'] != 'false':
            self.value.set(1)
        else:
            self.value.set(0)

        frame.grid(row=1, column=0, sticky=tk.W)
        frame.columnconfigure(0, weight=1)

        # self.pack(fill=tk.BOTH, expand=1)
        self.columnconfigure(0, weight=1)
        self.rowconfigure(0, weight=1)

    def get_value(self):
        if self.value.get() == 1:
            return self.flag
        else:
            return None


class OptionsInput(tk.Frame):
    def __init__(self, json_str, master=None):
        # first make sure that the json data has the correct fields
        j = json.loads(json_str)
        self.name = j['name']
        self.description = j['description']
        self.flag = j['flags'][len(j['flags']) - 1]
        self.parameter_type = j['parameter_type']
        self.optional = j['optional']
        default_value = j['default_value']

        ttk.Frame.__init__(self, master)
        self.grid()
        self['padding'] = '0.02i'

        frame = ttk.Frame(self, padding='0.0i')

        self.label = ttk.Label(self, text=self.name, justify=tk.LEFT)
        self.label.grid(row=0, column=0, sticky=tk.W)
        self.label.columnconfigure(0, weight=1)

        frame2 = ttk.Frame(frame, padding='0.0i')
        opt = ttk.Combobox(frame2, width=40)
        opt.grid(row=0, column=0, sticky=tk.NSEW)

        self.value = None  # initialize in event of no default and no selection
        i = 1
        default_index = -1
        list = j['parameter_type']['OptionList']
        values = ()
        for v in list:
            values += (v,)
            # opt.insert(tk.END, v)
            if v == default_value:
                default_index = i - 1
            i = i + 1

        opt['values'] = values

        # opt.bind("<<ComboboxSelect>>", self.select)
        opt.bind("<<ComboboxSelected>>", self.select)
        if default_index >= 0:
            opt.current(default_index)
            opt.event_generate("<<ComboboxSelected>>")
            # opt.see(default_index)

        frame2.grid(row=0, column=0, sticky=tk.W)
        frame.grid(row=1, column=0, sticky=tk.W)
        frame.columnconfigure(0, weight=1)

        # self.pack(fill=tk.BOTH, expand=1)
        self.columnconfigure(0, weight=1)
        self.rowconfigure(0, weight=1)

        # # first make sure that the json data has the correct fields
        # j = json.loads(json_str)
        # self.name = j['name']
        # self.description = j['description']
        # self.flag = j['flags'][len(j['flags']) - 1]
        # self.parameter_type = j['parameter_type']
        # self.optional = j['optional']
        # default_value = j['default_value']

        # ttk.Frame.__init__(self, master)
        # self.grid()
        # self['padding'] = '0.1i'

        # frame = ttk.Frame(self, padding='0.0i')

        # self.label = ttk.Label(self, text=self.name, justify=tk.LEFT)
        # self.label.grid(row=0, column=0, sticky=tk.W)
        # self.label.columnconfigure(0, weight=1)

        # frame2 = ttk.Frame(frame, padding='0.0i')
        # opt = tk.Listbox(frame2, width=40)  # , variable=self.value)
        # opt.grid(row=0, column=0, sticky=tk.NSEW)
        # s = ttk.Scrollbar(frame2, orient=tk.VERTICAL, command=opt.yview)
        # s.grid(row=0, column=1, sticky=(tk.N, tk.S))
        # opt['yscrollcommand'] = s.set

        # self.value = None  # initialize in event of no default and no selection
        # i = 1
        # default_index = -1
        # list = j['parameter_type']['OptionList']
        # for v in list:
        #     #opt.insert(i, v)
        #     opt.insert(tk.END, v)
        #     if v == default_value:
        #         default_index = i - 1
        #     i = i + 1

        # if i - 1 < 4:
        #     opt['height'] = i - 1
        # else:
        #     opt['height'] = 3

        # opt.bind("<<ListboxSelect>>", self.select)
        # if default_index >= 0:
        #     opt.select_set(default_index)
        #     opt.event_generate("<<ListboxSelect>>")
        #     opt.see(default_index)

        # frame2.grid(row=0, column=0, sticky=tk.W)
        # frame.grid(row=1, column=0, sticky=tk.W)
        # frame.columnconfigure(0, weight=1)

        # # self.pack(fill=tk.BOTH, expand=1)
        # self.columnconfigure(0, weight=1)
        # self.rowconfigure(0, weight=1)

    def get_value(self):
        if self.value:
            return "{}='{}'".format(self.flag, self.value)
        else:
            if not self.optional:
                messagebox.showinfo(
                    "Error", "Unspecified non-optional parameter {}.".format(self.flag))

        return None

    def select(self, event):
        widget = event.widget
        # selection = widget.curselection()
        self.value = widget.get()  # selection[0])


class DataInput(tk.Frame):
    def __init__(self, json_str, master=None):
        # first make sure that the json data has the correct fields
        j = json.loads(json_str)
        self.name = j['name']
        self.description = j['description']
        self.flag = j['flags'][len(j['flags']) - 1]
        self.parameter_type = j['parameter_type']
        self.optional = j['optional']
        default_value = j['default_value']

        ttk.Frame.__init__(self, master)
        self.grid()
        self['padding'] = '0.1i'

        self.label = ttk.Label(self, text=self.name, justify=tk.LEFT)
        self.label.grid(row=0, column=0, sticky=tk.W)
        self.label.columnconfigure(0, weight=1)

        self.value = tk.StringVar()
        if default_value:
            self.value.set(default_value)
        else:
            self.value.set("")

        self.entry = ttk.Entry(self, justify=tk.LEFT, textvariable=self.value)
        self.entry.grid(row=0, column=1, sticky=tk.NSEW)
        self.entry.columnconfigure(1, weight=10)

        if not self.optional:
            self.label['text'] = self.label['text'] + "*"

        if ("Integer" in self.parameter_type or
            "Float" in self.parameter_type or
                "Double" in self.parameter_type):
            self.entry['justify'] = 'right'

        # Add the bindings
        if _platform == "darwin":
            self.entry.bind("<Command-Key-a>", self.select_all)
        else:
            self.entry.bind("<Control-Key-a>", self.select_all)

        # self.pack(fill=tk.BOTH, expand=1)
        self.columnconfigure(0, weight=1)
        self.columnconfigure(1, weight=10)
        self.rowconfigure(0, weight=1)

    def RepresentsInt(self, s):
        try:
            int(s)
            return True
        except ValueError:
            return False

    def RepresentsFloat(self, s):
        try:
            float(s)
            return True
        except ValueError:
            return False

    def get_value(self):
        v = self.value.get()
        if v:
            if "Integer" in self.parameter_type:
                if self.RepresentsInt(self.value.get()):
                    return "{}={}".format(self.flag, self.value.get())
                else:
                    messagebox.showinfo(
                        "Error", "Error converting parameter {} to type Integer.".format(self.flag))
            elif "Float" in self.parameter_type:
                if self.RepresentsFloat(self.value.get()):
                    return "{}={}".format(self.flag, self.value.get())
                else:
                    messagebox.showinfo(
                        "Error", "Error converting parameter {} to type Float.".format(self.flag))
            elif "Double" in self.parameter_type:
                if self.RepresentsFloat(self.value.get()):
                    return "{}={}".format(self.flag, self.value.get())
                else:
                    messagebox.showinfo(
                        "Error", "Error converting parameter {} to type Double.".format(self.flag))
            else:  # String or StringOrNumber types
                return "{}='{}'".format(self.flag, self.value.get())
        else:
            if not self.optional:
                messagebox.showinfo(
                    "Error", "Unspecified non-optional parameter {}.".format(self.flag))

        return None

    def select_all(self, event):
        self.entry.select_range(0, tk.END)
        return 'break'


class WbRunner(tk.Frame):
    def __init__(self, tool_name=None, master=None):
        if platform.system() == 'Windows':
            self.ext = '.exe'
        else:
            self.ext = ''

        exe_name = "whitebox_tools{}".format(self.ext)

        self.exe_path = path.dirname(path.abspath(__file__))
        os.chdir(self.exe_path)
        for filename in glob.iglob('**/*', recursive=True):
            if filename.endswith(exe_name):
                self.exe_path = path.dirname(path.abspath(filename))
                break

        wbt.set_whitebox_dir(self.exe_path)

        ttk.Frame.__init__(self, master)
        self.script_dir = os.path.dirname(os.path.realpath(__file__))
        self.grid()
        self.tool_name = tool_name
        self.master.title("WhiteboxTools Runner")
        if _platform == "darwin":
            os.system(
                '''/usr/bin/osascript -e 'tell app "Finder" to set frontmost of process "Python" to true' ''')
        self.create_widgets()
        self.working_dir = str(Path.home())

    def create_widgets(self):

        #########################################################
        #              Overall/Top level Frame                  #
        #########################################################     
        #define left-side frame (toplevel_frame) and right-side frame (overall_frame)
        toplevel_frame = ttk.Frame(self, padding='0.1i')
        overall_frame = ttk.Frame(self, padding='0.1i')
        #set-up layout
        overall_frame.grid(row=0, column=1, sticky=tk.NSEW)
        toplevel_frame.grid(row=0, column=0, sticky=tk.NSEW)   
        #########################################################
        #                  Calling basics                       #
        #########################################################
        #Create all needed lists of tools and toolboxes
        self.toolbox_list = self.get_toolboxes()
        self.sort_toolboxes()
        self.tools_and_toolboxes = wbt.toolbox('')
        self.sort_tools_by_toolbox()        
        self.get_tools_list()  
        #Icons to be used in tool treeview
        self.tool_icon = tk.PhotoImage(file = self.script_dir + '//img//tool.gif')  
        self.open_toolbox_icon = tk.PhotoImage(file =  self.script_dir + '//img//open.gif')
        self.closed_toolbox_icon = tk.PhotoImage(file =  self.script_dir + '//img//closed.gif')
        #########################################################
        #                  Toolboxes Frame                      # FIXME: change width or make horizontally scrollable
        #########################################################
        #define tools_frame and tool_tree
        self.tools_frame = ttk.LabelFrame(toplevel_frame, text="{} Available Tools".format(len(self.tools_list)), padding='0.1i')   
        self.tool_tree = ttk.Treeview(self.tools_frame, height = 21)    
        #Set up layout
        self.tool_tree.grid(row=0, column=0, sticky=tk.NSEW)
        self.tool_tree.column("#0", width = 280)    #Set width so all tools are readable within the frame
        self.tools_frame.grid(row=0, column=0, sticky=tk.NSEW)
        self.tools_frame.columnconfigure(0, weight=10)
        self.tools_frame.columnconfigure(1, weight=1)
        self.tools_frame.rowconfigure(0, weight=10)
        self.tools_frame.rowconfigure(1, weight=1)
        #Add toolboxes and tools to treeview
        index = 0
        for toolbox in self.lower_toolboxes:
            if toolbox.find('/') != (-1):    #toolboxes 
                self.tool_tree.insert(toolbox[:toolbox.find('/')], 0, text = "  " + toolbox[toolbox.find('/') + 1:], iid = toolbox[toolbox.find('/') + 1:], tags = 'toolbox', image = self.closed_toolbox_icon)
                for tool in self.sorted_tools[index]:    #add tools within toolbox
                    self.tool_tree.insert(toolbox[toolbox.find('/') + 1:], 'end', text = "  " + tool, tags = 'tool', iid = tool, image = self.tool_icon)       
            else:    #subtoolboxes
                self.tool_tree.insert('', 'end', text = "  " + toolbox, iid = toolbox, tags = 'toolbox', image = self.closed_toolbox_icon)                         
                for tool in self.sorted_tools[index]:  #add tools within subtoolbox
                    self.tool_tree.insert(toolbox, 'end', text = "  " + tool, iid = tool, tags = 'tool', image = self.tool_icon) 
            index = index + 1 
        #bind tools in treeview to self.tree_update_tool_help function and toolboxes to self.update_toolbox_icon function
        self.tool_tree.tag_bind('tool', "<<TreeviewSelect>>", self.tree_update_tool_help)   
        self.tool_tree.tag_bind('toolbox', "<<TreeviewSelect>>", self.update_toolbox_icon)  
        #Add vertical scrollbar to treeview frame
        s = ttk.Scrollbar(self.tools_frame, orient=tk.VERTICAL,command=self.tool_tree.yview)    
        s.grid(row=0, column=1, sticky=(tk.N, tk.S))
        self.tool_tree['yscrollcommand'] = s.set
        #########################################################
        #                     Search Bar                        #
        #########################################################
        #create variables for search results and search input
        self.search_list = []    
        self.search_text = tk.StringVar()
        #Create the elements of the search frame
        self.search_frame = ttk.LabelFrame(toplevel_frame, padding='0.1i', text="{} Tools Found".format(len(self.search_list)))         
        self.search_label = ttk.Label(self.search_frame, text = "Search: ") 
        self.search_bar = ttk.Entry(self.search_frame, width = 30, textvariable = self.search_text)
        self.search_results_listbox = tk.Listbox(self.search_frame, height=11) 
        self.search_scroll = ttk.Scrollbar(self.search_frame, orient=tk.VERTICAL, command=self.search_results_listbox.yview)
        self.search_results_listbox['yscrollcommand'] = self.search_scroll.set
        #Add bindings
        self.search_results_listbox.bind("<<ListboxSelect>>", self.search_update_tool_help)
        self.search_bar.bind('<Return>', self.update_search)
        #Define layout of the frame
        self.search_frame.grid(row = 1, column = 0, sticky=tk.NSEW)
        self.search_label.grid(row = 0, column = 0, sticky=tk.NW)
        self.search_bar.grid(row = 0, column = 1, sticky=tk.NE) 
        self.search_results_listbox.grid(row = 1, column = 0, columnspan = 2, sticky=tk.NSEW, pady = 5)
        self.search_scroll.grid(row=1, column=2, sticky=(tk.N, tk.S))
        #Configure rows and columns of the frame
        self.search_frame.columnconfigure(0, weight=1)
        self.search_frame.columnconfigure(1, weight=10)
        self.search_frame.columnconfigure(1, weight=1)
        self.search_frame.rowconfigure(0, weight=1)
        self.search_frame.rowconfigure(1, weight = 10)
        #########################################################
        #                 Current Tool Frame                    #
        #########################################################
        #Create the elements of the current tool frame
        self.current_tool_frame = ttk.Frame(overall_frame, padding='0.01i')
        self.current_tool_lbl = ttk.Label(self.current_tool_frame, text="Current Tool: {}".format(self.tool_name), justify=tk.LEFT)  # , font=("Helvetica", 12, "bold") 
        self.view_code_button = ttk.Button(self.current_tool_frame, text="View Code", width=12, command=self.view_code)
        #Define layout of the frame
        self.view_code_button.grid(row=0, column=1, sticky=tk.E)
        self.current_tool_lbl.grid(row=0, column=0, sticky=tk.W)
        self.current_tool_frame.grid(row=0, column=0, columnspan = 2, sticky=tk.NSEW)
        #Configure rows and columns of the frame
        self.current_tool_frame.columnconfigure(0, weight=1)
        self.current_tool_frame.columnconfigure(1, weight=1)
        #########################################################
        #                      Args Frame                       #
        #########################################################
        # #Create the elements of the tool arguments frame
        # self.arg_scroll = ttk.Scrollbar(overall_frame, orient='vertical')
        # self.arg_canvas = tk.Canvas(overall_frame, bd=0, highlightthickness=0, yscrollcommand=self.arg_scroll.set)
        # self.arg_scroll.config(command=self.arg_canvas.yview)    #self.arg_scroll scrolls over self.arg_canvas
        # self.arg_scroll_frame = ttk.Frame(self.arg_canvas)    # create a frame inside the self.arg_canvas which will be scrolled with it
        # self.arg_scroll_frame_id = self.arg_canvas.create_window(0, 0, window=self.arg_scroll_frame, anchor="nw")
        # #Define layout of the frame
        # self.arg_scroll.grid(row = 1, column = 1, sticky = (tk.NS, tk.E))
        # self.arg_canvas.grid(row = 1, column = 0, sticky = tk.NSEW)
        # # reset the view
        # self.arg_canvas.xview_moveto(0)
        # self.arg_canvas.yview_moveto(0)
        # #Add bindings
        # self.arg_scroll_frame.bind('<Configure>', self.configure_arg_scroll_frame)
        # self.arg_canvas.bind('<Configure>', self.configure_arg_canvas)
        self.arg_scroll_frame = ttk.Frame(overall_frame, padding='0.0i')
        self.arg_scroll_frame.grid(row=1, column=0, sticky=tk.NSEW)
        self.arg_scroll_frame.columnconfigure(0, weight=1)
        #########################################################
        #                   Buttons Frame                       #
        #########################################################
        #Create the elements of the buttons frame
        buttons_frame = ttk.Frame(overall_frame, padding='0.1i')
        self.run_button = ttk.Button(buttons_frame, text="Run", width=8, command=self.run_tool)
        self.quit_button = ttk.Button(buttons_frame, text="Cancel", width=8, command=self.cancel_operation)
        self.help_button = ttk.Button(buttons_frame, text="Help", width=8, command=self.tool_help_button)
        #Define layout of the frame
        self.run_button.grid(row=0, column=0)
        self.quit_button.grid(row=0, column=1)
        self.help_button.grid(row = 0, column = 2)
        buttons_frame.grid(row=2, column=0, columnspan = 2, sticky=tk.E)
        #########################################################
        #                  Output Frame                      #
        #########################################################                
        #Create the elements of the output frame
        output_frame = ttk.Frame(overall_frame)
        outlabel = ttk.Label(output_frame, text="Output:", justify=tk.LEFT)
        self.out_text = ScrolledText(output_frame, width=63, height=15, wrap=tk.NONE, padx=7, pady=7, exportselection = 0)
        output_scrollbar = ttk.Scrollbar(output_frame, orient=tk.HORIZONTAL, command = self.out_text.xview)
        self.out_text['xscrollcommand'] = output_scrollbar.set
        #Retreive and insert the text for the current tool
        k = wbt.tool_help(self.tool_name)   
        self.out_text.insert(tk.END, k)
        #Define layout of the frame
        outlabel.grid(row=0, column=0, sticky=tk.NW)
        self.out_text.grid(row=1, column=0, sticky=tk.NSEW)
        output_frame.grid(row=3, column=0, columnspan = 2, sticky=(tk.NS, tk.E))
        output_scrollbar.grid(row=2, column=0, sticky=(tk.W, tk.E))
        #Configure rows and columns of the frame
        self.out_text.columnconfigure(0, weight=1)
        output_frame.columnconfigure(0, weight=1)
        # Add the binding
        if _platform == "darwin":
            self.out_text.bind("<Command-Key-a>", self.select_all)
        else:
            self.out_text.bind("<Control-Key-a>", self.select_all)
        #########################################################
        #                  Progress Frame                       #
        #########################################################        
        #Create the elements of the progress frame
        progress_frame = ttk.Frame(overall_frame, padding='0.1i')
        self.progress_label = ttk.Label(progress_frame, text="Progress:", justify=tk.LEFT)
        self.progress_var = tk.DoubleVar()
        self.progress = ttk.Progressbar(progress_frame, orient="horizontal", variable=self.progress_var, length=200, maximum=100)
        #Define layout of the frame
        self.progress_label.grid(row=0, column=0, sticky=tk.E, padx=5)
        self.progress.grid(row=0, column=1, sticky=tk.E)
        progress_frame.grid(row=4, column=0, columnspan = 2, sticky=tk.SE)
        #########################################################
        #                  Tool Selection                       #
        #########################################################        
        # Select the appropriate tool, if specified, otherwise the first tool
        self.tool_tree.focus(self.tool_name)
        self.tool_tree.selection_set(self.tool_name)
        self.tool_tree.event_generate("<<TreeviewSelect>>")
        #########################################################
        #                       Menus                           #
        #########################################################        
        menubar = tk.Menu(self)

        self.filemenu = tk.Menu(menubar, tearoff=0)
        self.filemenu.add_command(label="Set Working Directory", command=self.set_directory)
        self.filemenu.add_command(label="Locate WhiteboxTools exe", command=self.select_exe)
        self.filemenu.add_command(label="Refresh Tools", command=self.refresh_tools)
        wbt.set_compress_rasters(True)
        self.filemenu.add_command(label="Do Not Compress Output TIFFs", command=self.update_compress)
        self.filemenu.add_separator()
        self.filemenu.add_command(label="Exit", command=self.quit)
        menubar.add_cascade(label="File", menu=self.filemenu)

        editmenu = tk.Menu(menubar, tearoff=0)
        editmenu.add_command(label="Cut", command=lambda: self.focus_get().event_generate("<<Cut>>"))
        editmenu.add_command(label="Copy", command=lambda: self.focus_get().event_generate("<<Copy>>"))
        editmenu.add_command(label="Paste", command=lambda: self.focus_get().event_generate("<<Paste>>"))
        menubar.add_cascade(label="Edit ", menu=editmenu)

        helpmenu = tk.Menu(menubar, tearoff=0)
        helpmenu.add_command(label="About", command=self.help)
        helpmenu.add_command(label="License", command=self.license)
        menubar.add_cascade(label="Help ", menu=helpmenu)

        self.master.config(menu=menubar)     

    def update_compress(self):
        if wbt.get_compress_rasters():
            wbt.set_compress_rasters(False)
            self.filemenu.entryconfig(3, label = "Compress Output TIFFs")
        else:
            wbt.set_compress_rasters(True)
            self.filemenu.entryconfig(3, label = "Do Not Compress Output TIFFs")

    def get_toolboxes(self):
        toolboxes = set()
        for item in wbt.toolbox().splitlines():  # run wbt.toolbox with no tool specified--returns all
            if item:
                tb = item.split(":")[1].strip()
                toolboxes.add(tb)
        return sorted(toolboxes)

    def sort_toolboxes(self):
        self.upper_toolboxes = []
        self.lower_toolboxes = []
        for toolbox in self.toolbox_list:
            if toolbox.find('/') == (-1):    #Does not contain a subtoolbox, i.e. does not contain '/'
                self.upper_toolboxes.append(toolbox)    #add to both upper toolbox list and lower toolbox list
                self.lower_toolboxes.append(toolbox)
            else:    #Contains a subtoolbox
                self.lower_toolboxes.append(toolbox)    #add to only the lower toolbox list  
        self.upper_toolboxes = sorted(self.upper_toolboxes)    #sort both lists alphabetically
        self.lower_toolboxes = sorted(self.lower_toolboxes)

    def sort_tools_by_toolbox(self): 
        self.sorted_tools = [[] for i in range(len(self.lower_toolboxes))]    #One list for each lower toolbox
        count = 1
        for toolAndToolbox in self.tools_and_toolboxes.split('\n'):
            if toolAndToolbox.strip():
                tool = toolAndToolbox.strip().split(':')[0].strip().replace("TIN", "Tin").replace("KS", "Ks").replace("FD", "Fd")    #current tool
                itemToolbox = toolAndToolbox.strip().split(':')[1].strip()    #current toolbox
                index = 0
                for toolbox in self.lower_toolboxes:    #find which toolbox the current tool belongs to
                    if toolbox == itemToolbox:
                        self.sorted_tools[index].append(tool)    #add current tool to list at appropriate index
                        break
                    index = index + 1
                count = count + 1

    def get_tools_list(self):
        self.tools_list = []
        selected_item = -1
        for item in wbt.list_tools().keys():
            if item:
                value = to_camelcase(item).replace("TIN", "Tin").replace("KS", "Ks").replace("FD", "Fd")    #format tool name
                self.tools_list.append(value)    #add tool to list
                if item == self.tool_name:    #update selected_item it tool found
                    selected_item = len(self.tools_list) - 1
        if selected_item == -1:    #set self.tool_name as default tool
            selected_item = 0
            self.tool_name = self.tools_list[0]

    def tree_update_tool_help(self, event):    # read selection when tool selected from treeview then call self.update_tool_help
        curItem = self.tool_tree.focus()
        self.tool_name = self.tool_tree.item(curItem).get('text').replace("  ", "")
        self.update_tool_help()

    def search_update_tool_help(self, event):    # read selection when tool selected from search results then call self.update_tool_help
        selection = self.search_results_listbox.curselection()
        self.tool_name = self.search_results_listbox.get(selection[0])
        self.update_tool_help()
  
    def update_tool_help(self):
        self.out_text.delete('1.0', tk.END)
        for widget in self.arg_scroll_frame.winfo_children():
            widget.destroy()

        k = wbt.tool_help(self.tool_name)
        self.print_to_output(k)

        j = json.loads(wbt.tool_parameters(self.tool_name))
        param_num = 0
        for p in j['parameters']:
            json_str = json.dumps(
                p, sort_keys=True, indent=2, separators=(',', ': '))
            pt = p['parameter_type']
            if 'ExistingFileOrFloat' in pt:
                ff = FileOrFloat(json_str, self, self.arg_scroll_frame)
                ff.grid(row=param_num, column=0, sticky=tk.NSEW)
                param_num = param_num + 1
            elif ('ExistingFile' in pt or 'NewFile' in pt or 'Directory' in pt):
                fs = FileSelector(json_str, self, self.arg_scroll_frame)
                fs.grid(row=param_num, column=0, sticky=tk.NSEW)
                param_num = param_num + 1
            elif 'FileList' in pt:
                b = MultifileSelector(json_str, self, self.arg_scroll_frame)
                b.grid(row=param_num, column=0, sticky=tk.W)
                param_num = param_num + 1
            elif 'Boolean' in pt:
                b = BooleanInput(json_str, self.arg_scroll_frame)
                b.grid(row=param_num, column=0, sticky=tk.W)
                param_num = param_num + 1
            elif 'OptionList' in pt:
                b = OptionsInput(json_str, self.arg_scroll_frame)
                b.grid(row=param_num, column=0, sticky=tk.W)
                param_num = param_num + 1
            elif ('Float' in pt or 'Integer' in pt or
                  'String' in pt or 'StringOrNumber' in pt or
                  'StringList' in pt or 'VectorAttributeField' in pt):
                b = DataInput(json_str, self.arg_scroll_frame)
                b.grid(row=param_num, column=0, sticky=tk.NSEW)
                param_num = param_num + 1
            else:
                messagebox.showinfo(
                    "Error", "Unsupported parameter type: {}.".format(pt))
        self.update_args_box()
        self.out_text.see("%d.%d" % (1, 0))

    def update_toolbox_icon(self, event):
        curItem = self.tool_tree.focus()
        dict = self.tool_tree.item(curItem)    #retreive the toolbox name
        self.toolbox_name = dict.get('text'). replace("  ", "")    #delete the space between the icon and text
        self.toolbox_open = dict.get('open')    #retreive whether the toolbox is open or not
        if self.toolbox_open == True:    #set image accordingly
            self.tool_tree.item(self.toolbox_name, image = self.open_toolbox_icon)
        else:
            self.tool_tree.item(self.toolbox_name, image = self.closed_toolbox_icon)
    
    def update_search(self, event):
        self.search_list = [] 
        self.search_string = self.search_text.get().lower()
        self.search_results_listbox.delete(0, 'end') #empty the search results
        num_results = 0
        for tool in self.tools_list:  #search tool names
            toolLower = tool.lower()
            if toolLower.find(self.search_string) != (-1): #search string found within tool name
                num_results = num_results + 1
                self.search_results_listbox.insert(num_results, tool) #tool added to listbox and to search results string
                self.search_list.append(tool)
        index = 0
        self.get_descriptions()
        for description in self.descriptionList: #search tool descriptions
            descriptionLower = description.lower()
            if descriptionLower.find(self.search_string) != (-1): #search string found within tool description
                found = 0
                for item in self.search_list: # check if this tool is already in the listbox
                    if self.tools_list[index] == item:
                        found = 1
                if found == 0:  # add to listbox
                    num_results = num_results + 1
                    self.search_results_listbox.insert(num_results, self.tools_list[index])    #tool added to listbox and to search results string
            index = index + 1
        self.search_frame['text'] = "{} Tools Found".format(num_results) #update search label

    def get_descriptions(self):
        self.descriptionList = []
        tools = wbt.list_tools()
        toolsItems = tools.items()
        for t in toolsItems:
            self.descriptionList.append(t[1])    #second entry in tool dictionary is the description
     
    # def configure_arg_scroll_frame(self, event):
    #     # update the scrollbars to match the size of the inner frame
    #     size = (self.arg_scroll_frame.winfo_reqwidth(), self.arg_scroll_frame.winfo_reqheight())
    #     self.arg_canvas.config(scrollregion="0 0 %s %s" % size)
    #     if self.arg_scroll_frame.winfo_reqwidth() != self.arg_canvas.winfo_width():
    #         # update the canvas's width to fit the inner frame
    #         self.arg_canvas.config(width=self.arg_scroll_frame.winfo_reqwidth())

    # def configure_arg_canvas(self, event):
    #     if self.arg_scroll_frame.winfo_reqwidth() != self.arg_canvas.winfo_width():
    #         # update the inner frame's width to fill the canvas
    #         self.arg_canvas.itemconfigure(self.arg_scroll_frame_id, width=self.arg_canvas.winfo_width())
    
    def tool_help_button(self):
        index = 0
        found = False
        #find toolbox corresponding to the current tool
        for toolbox in self.lower_toolboxes:
            for tool in self.sorted_tools[index]:
                if tool == self.tool_name:
                    self.toolbox_name = toolbox
                    found = True
                    break
            if found:
                break
            index = index + 1
        #change LiDAR to Lidar
        if index == 10:
            self.toolbox_name = to_camelcase(self.toolbox_name)
        #format subtoolboxes as for URLs
        self.toolbox_name = self.camel_to_snake(self.toolbox_name).replace('/', '').replace(' ', '') 
        #open the user manual section for the current tool
        webbrowser.open_new_tab("https://jblindsay.github.io/wbt_book/available_tools/" + self.toolbox_name + ".html#" + self.tool_name)    
    
    def camel_to_snake(self, s):    # taken from tools_info.py
        _underscorer1 = re.compile(r'(.)([A-Z][a-z]+)')
        _underscorer2 = re.compile('([a-z0-9])([A-Z])')
        subbed = _underscorer1.sub(r'\1_\2', s)
        return _underscorer2.sub(r'\1_\2', subbed).lower()

    def refresh_tools(self):
        #refresh lists
        self.tools_and_toolboxes = wbt.toolbox('')
        self.sort_tools_by_toolbox()
        self.get_tools_list()
        #clear self.tool_tree
        self.tool_tree.delete(*self.tool_tree.get_children())
        #Add toolboxes and tools to treeview
        index = 0
        for toolbox in self.lower_toolboxes:
            if toolbox.find('/') != (-1):    #toolboxes 
                self.tool_tree.insert(toolbox[:toolbox.find('/')], 0, text = "  " + toolbox[toolbox.find('/') + 1:], iid = toolbox[toolbox.find('/') + 1:], tags = 'toolbox', image = self.closed_toolbox_icon)
                for tool in self.sorted_tools[index]:    #add tools within toolbox
                    self.tool_tree.insert(toolbox[toolbox.find('/') + 1:], 'end', text = "  " + tool, tags = 'tool', iid = tool, image = self.tool_icon)       
            else:    #subtoolboxes
                self.tool_tree.insert('', 'end', text = "  " + toolbox, iid = toolbox, tags = 'toolbox', image = self.closed_toolbox_icon)                         
                for tool in self.sorted_tools[index]:  #add tools within subtoolbox
                    self.tool_tree.insert(toolbox, 'end', text = "  " + tool, iid = tool, tags = 'tool', image = self.tool_icon) 
            index = index + 1 
        #Update label
        self.tools_frame["text"] = "{} Available Tools".format(len(self.tools_list))

    #########################################################
    #               Functions (original)                    #
    #########################################################
    def help(self):
        self.print_to_output(wbt.version())

    def license(self):
        self.print_to_output(wbt.license())

    def set_directory(self):
        try:
            self.working_dir =filedialog.askdirectory(initialdir=self.working_dir)
            wbt.set_working_dir(self.working_dir)
        except:
            messagebox.showinfo(
                "Warning", "Could not set the working directory.")

    def select_exe(self):
        try:
            filename = filedialog.askopenfilename(initialdir=self.exe_path)
            self.exe_path = path.dirname(path.abspath(filename))
            wbt.set_whitebox_dir(self.exe_path)
            self.refresh_tools()
        except:
            messagebox.showinfo(
                "Warning", "Could not find WhiteboxTools executable file.")

    def run_tool(self):
        # wd_str = self.wd.get_value()
        wbt.set_working_dir(self.working_dir)
        # args = shlex.split(self.args_value.get())

        args = []
        for widget in self.arg_scroll_frame.winfo_children():
            v = widget.get_value()
            if v:
                args.append(v)
            elif not widget.optional:
                messagebox.showinfo(
                    "Error", "Non-optional tool parameter not specified.")
                return

        self.print_line_to_output("")
        # self.print_line_to_output("Tool arguments:{}".format(args))
        # self.print_line_to_output("")
        # Run the tool and check the return value for an error
        if wbt.run_tool(self.tool_name, args, self.custom_callback) == 1:
            print("Error running {}".format(self.tool_name))

        else:
            self.run_button["text"] = "Run"
            self.progress_var.set(0)
            self.progress_label['text'] = "Progress:"
            self.progress.update_idletasks()

    def print_to_output(self, value):
        self.out_text.insert(tk.END, value)
        self.out_text.see(tk.END)

    def print_line_to_output(self, value):
        self.out_text.insert(tk.END, value + "\n")
        self.out_text.see(tk.END)

    def cancel_operation(self):
        wbt.cancel_op = True
        self.print_line_to_output("Cancelling operation...")
        self.progress.update_idletasks()

    def view_code(self):
        webbrowser.open_new_tab(wbt.view_code(self.tool_name).strip())
    
    def update_args_box(self):
        s = ""
        self.current_tool_lbl['text'] = "Current Tool: {}".format(
            self.tool_name)
        # self.spacer['width'] = width=(35-len(self.tool_name))
        for item in wbt.tool_help(self.tool_name).splitlines():
            if item.startswith("-"):
                k = item.split(" ")
                if "--" in k[1]:
                    value = k[1].replace(",", "")
                else:
                    value = k[0].replace(",", "")

                if "flag" in item.lower():
                    s = s + value + " "
                else:
                    if "file" in item.lower():
                        s = s + value + "='{}' "
                    else:
                        s = s + value + "={} "

        # self.args_value.set(s.strip())

    def custom_callback(self, value):
        ''' A custom callback for dealing with tool output.
        '''
        if "%" in value:
            try:
                str_array = value.split(" ")
                label = value.replace(
                    str_array[len(str_array) - 1], "").strip()
                progress = float(
                    str_array[len(str_array) - 1].replace("%", "").strip())
                self.progress_var.set(int(progress))
                self.progress_label['text'] = label
            except ValueError as e:
                print("Problem converting parsed data into number: ", e)
            except Exception as e:
                print(e)
        else:
            self.print_line_to_output(value)

        self.update()  # this is needed for cancelling and updating the progress bar

    def select_all(self, event):
        self.out_text.tag_add(tk.SEL, "1.0", tk.END)
        self.out_text.mark_set(tk.INSERT, "1.0")
        self.out_text.see(tk.INSERT)
        return 'break'

class JsonPayload(object):
    def __init__(self, j):
        self.__dict__ = json.loads(j)


def main():
    tool_name = None
    if len(sys.argv) > 1:
        tool_name = str(sys.argv[1])
    wbr = WbRunner(tool_name)
    wbr.mainloop()


if __name__ == '__main__':
    main()