class textEditor():
    alltabs = None

    def __init__(self,
                 window,
                 labelFrame,
                 tabs,
                 vocab,
                 startWithSameLetter,
                 tabsOpen,
                 file_path=""):
        self.window = window
        # record the directory path of the file
        self.file_path = file_path
        if file_path:
            self.file_name = self.file_path
        else:
            # if the file path doesn't exist, name it accordingly
            self.file_name = 'Untitled'
        # record the necessary passed-in parameters
        self.labelFrame = labelFrame
        self.tabsOpen = tabsOpen
        self.tabs = tabs
        self.vocab = vocab
        self.startWithSameLetter = startWithSameLetter
        # create the main gui elements in the respective tab frame
        self.notepad = ScrolledText(self.labelFrame, font=("Calibri", 15))
        editorbox = self.notepad
        self.var = tk.IntVar()
        self.autoCorrectOption = tk.Checkbutton(self.labelFrame, \
                                                text="Enable Auto-Correct", variable=self.var, command=self.switchSpellChecker)
        self.autoComplete_suggestions = tk.Listbox(self.labelFrame)
        self.autoCorrect_suggestions = tk.Listbox(self.labelFrame)
        myFont = Font(family="Calibri", size=15)
        self.autoComplete_suggestions.configure(font=myFont)
        self.autoCorrect_suggestions.configure(font=myFont)

        # create funtionality bars inside tab frame
        self.createMenuBar()
        self.createToolBar(self.labelFrame)

        self.autoCorrectOption.grid(row=1, column=9)
        self.notepad.config(undo=True)
        self.notepad.config(height=900)
        self.notepad.grid(row=2, column=0, columnspan=11, sticky="WE")
        self.window.protocol("WM_DELETE_WINDOW",
                             lambda: newFileTab.closeCheck(self.tabsOpen))
        # add pre-set markup on the entire text widget in the tab frame
        self.notepad.tag_configure("misspelling",
                                   foreground="red",
                                   underline=True)

        # bind all navigation to checking the spelling of the word
        self.nav_click = self.notepad.bind("<ButtonRelease-1>",
                                           self.spellChecker)
        self.nav_up = self.notepad.bind("<Up>", self.spellChecker)
        self.nav_down = self.notepad.bind("<Down>", self.spellChecker)
        self.nav_left = self.notepad.bind("<Left>", self.spellChecker)
        self.nav_right = self.notepad.bind("<Right>", self.spellChecker)

        # check each word's spelling after typed and mark it up
        self.notepad.bind("<space>", self.markUp)
        self.notepad.bind(".", self.markUp)

        # keep calling autocomplete while user is writing
        for letter in "qwertyuiopasdfghjklzxcvbnmQWERTYUIOPASDFGHJKLZXCVBNM":
            self.notepad.bind("<KeyRelease-" + letter + ">", self.autoComplete)
        self.notepad.bind("<KeyRelease-BackSpace>", self.autoComplete)

        # bind file shortcuts
        self.notepad.bind('<Control-s>', newFileTab.saveToFile)
        self.notepad.bind('<Control-o>', newFileTab.openFile)
        self.notepad.bind('<Control-n>', newFileTab.createFile)
        self.notepad.bind('<Control-c>', newFileTab.copySelected)
        self.notepad.bind('<Control-x>', newFileTab.cutSelected)
        self.notepad.bind('<Control-v>', newFileTab.pasteClipboard)

    # this function creates the top menu bar including all functionalities
    def createMenuBar(self):
        menuBar = Menu(self.window)
        # create drop-down options for the file menu
        fileMenu = tk.Menu(menuBar, tearoff=0)
        fileMenu.add_command(
            label="New Document",
            command=lambda: newFileTab.createFile(self.tabsOpen))
        fileMenu.add_command(
            label="Open Local File",
            command=lambda: newFileTab.openFile(self.tabsOpen))
        fileMenu.add_command(
            label="Save file",
            command=lambda: newFileTab.saveToFile(self.tabsOpen))
        fileMenu.add_separator()
        fileMenu.add_command(
            label="Close File",
            command=lambda: newFileTab.closeFile(self.tabsOpen))
        fileMenu.add_command(label="Exit",
                             command=lambda: newFileTab.quit(self.tabsOpen))
        menuBar.add_cascade(label="File", menu=fileMenu)

        # create drop-down options for the edit menu
        editMenu = tk.Menu(menuBar, tearoff=0)
        editMenu.add_command(
            label="Undo", command=lambda: newFileTab.undoEdit(self.tabsOpen))
        editMenu.add_command(
            label="Redo", command=lambda: newFileTab.redoEdit(self.tabsOpen))
        editMenu.add_command(
            label="Copy",
            command=lambda: newFileTab.copySelected(self.tabsOpen))
        editMenu.add_command(
            label="Cut", command=lambda: newFileTab.cutSelected(self.tabsOpen))
        editMenu.add_command(
            label="Paste",
            command=lambda: newFileTab.pasteClipboard(self.tabsOpen))
        menuBar.add_cascade(label="Edit", menu=editMenu)

        self.window.config(menu=menuBar)

    '''icon pics retrieved from:
    https://icons-for-free.com/folder+open+icon-1320161390409087972/'''

    # this function creates the tool bar with clickable icon shortcuts for the functionalities
    def createToolBar(self, labelFrame):
        # add icon for handling creating new files
        new_img = tk.PhotoImage(file="newicon.png")
        new_img = new_img.zoom(1)
        new_img = new_img.subsample(15)

        # add icon for handling opening local files
        open_img = tk.PhotoImage(file="openicon.png")
        open_img = open_img.zoom(1)
        open_img = open_img.subsample(15)

        # add icon for handling saving files
        save_img = tk.PhotoImage(file="saveicon.png")
        save_img = save_img.zoom(1)
        save_img = save_img.subsample(4)

        # add icon for handling copying from files
        copy_img = tk.PhotoImage(file="copyicon.png")
        copy_img = copy_img.zoom(1)
        copy_img = copy_img.subsample(4)

        # add icon for handling cutting from files
        cut_img = tk.PhotoImage(file="cuticon.png")
        cut_img = cut_img.zoom(1)
        cut_img = cut_img.subsample(4)

        # add icon for handling cutting from clipboard
        paste_img = tk.PhotoImage(file="pasteicon.png")
        paste_img = paste_img.zoom(1)
        paste_img = paste_img.subsample(4)

        # add icon for handling undo edits
        undo_img = tk.PhotoImage(file="undoicon.png")
        undo_img = undo_img.zoom(1)
        undo_img = undo_img.subsample(4)

        # add icon for handling redo edits
        redo_img = tk.PhotoImage(file="redoicon.png")
        redo_img = redo_img.zoom(1)
        redo_img = redo_img.subsample(4)

        # add icon for handling closing current file tab
        close_img = tk.PhotoImage(file="closeicon.png")
        close_img = close_img.zoom(1)
        close_img = close_img.subsample(4)

        # create all respective buttons and configure them to their appropriate icons and function calls
        new_button = tk.Button(
            labelFrame,
            image=new_img,
            command=lambda: newFileTab.createFile(self.tabsOpen))
        open_button = tk.Button(
            labelFrame,
            image=open_img,
            command=lambda: newFileTab.openFile(self.tabsOpen))
        save_button = tk.Button(
            labelFrame,
            image=save_img,
            command=lambda: newFileTab.saveToFile(self.tabsOpen))
        copy_button = tk.Button(
            labelFrame,
            image=copy_img,
            command=lambda: newFileTab.copySelected(self.tabsOpen))
        cut_button = tk.Button(
            labelFrame,
            image=cut_img,
            command=lambda: newFileTab.cutSelected(self.tabsOpen))
        paste_button = tk.Button(
            labelFrame,
            image=paste_img,
            command=lambda: newFileTab.pasteClipboard(self.tabsOpen))
        undo_button = tk.Button(
            labelFrame,
            image=undo_img,
            command=lambda: newFileTab.undoEdit(self.tabsOpen))
        redo_button = tk.Button(
            labelFrame,
            image=redo_img,
            command=lambda: newFileTab.redoEdit(self.tabsOpen))
        close_button = tk.Button(
            labelFrame,
            image=close_img,
            command=lambda: newFileTab.closeFile(self.tabsOpen))

        new_button.image = new_img
        open_button.image = open_img
        save_button.image = save_img
        copy_button.image = copy_img
        cut_button.image = cut_img
        paste_button.image = paste_img
        undo_button.image = undo_img
        redo_button.image = redo_img
        close_button.image = close_img

        # grid the buttons appropriately onto the tab frame
        new_button.grid(row=1, column=1)
        open_button.grid(row=1, column=2)
        save_button.grid(row=1, column=3)
        copy_button.grid(row=1, column=4)
        cut_button.grid(row=1, column=5)
        paste_button.grid(row=1, column=6)
        undo_button.grid(row=1, column=7)
        redo_button.grid(row=1, column=8)
        close_button.grid(row=1, column=10)

    # this function takes automatically the first choice from the suggestion
    # box and replaces it with the word that is underlined as misspelt
    def autoCorrect(self, event):
        lastWord = self.getLastWord()
        if self.spellCheckerList(lastWord):
            # get first suggestiosn from listbox contents
            bestSuggestion = self.spellCheckerList(lastWord)[0]
            # configure and find first and last index of word to be replaced
            start = self.notepad.get('1.0', tk.END).index(lastWord)
            end = start + len(lastWord)
            line_num = int(float(self.notepad.index(tk.CURRENT)))
            start_i = str(line_num) + '.' + str(start)
            end_i = str(line_num) + '.' + str(end)
            # delete the misspelled word by the best suggestion in text widget
            self.notepad.delete(start_i, end_i)
            self.notepad.insert(start_i, bestSuggestion)

    # this function unbinds the arrows with the list from the dictionary
    # so that the list dosen't appear when pressing the auto-correct option
    def switchSpellChecker(self):
        self.notepad.unbind('<ButtonRelease-1>')
        self.notepad.unbind('<Up>')
        self.notepad.unbind('<Down>')
        self.notepad.unbind('<Left>')
        self.notepad.unbind('<Right>')
        self.notepad.unbind("<space>")
        # if the autocorrect option is pressed
        if self.var.get():
            # replace the spellchecker bindings to autocorrect
            self.notepad.bind("<space>", self.autoCorrect)
            self.notepad.bind(".", self.autoCorrect)
        # if it is not pressed
        else:
            # rebind the orginal keys for the spellchecker listbox functionality
            self.notepad.bind("<ButtonRelease-1>", self.spellChecker)
            self.notepad.bind("<Up>", self.spellChecker)
            self.notepad.bind("<Down>", self.spellChecker)
            self.notepad.bind("<Left>", self.spellChecker)
            self.notepad.bind("<Right>", self.spellChecker)

            # check each word's spelling after typed and mark it up
            self.notepad.bind("<space>", self.isSpeltCorrect)
            self.notepad.bind(".", self.isSpeltCorrect)

    # this function gets the last word that was typed by the user
    def getLastWord(self):
        # split all input text at all white space characters (e.g. space, tab, enter)
        wordsList = re.split("\s+", self.notepad.get("1.0", tk.END))
        # remove last empty string
        wordsList.remove('')
        # last word is at the last index of the words list
        lastWord = wordsList[len(wordsList) - 1]
        # remove unnecessary punctuations next to the last word
        lastWord_stripped = lastWord.translate(
            str.maketrans('', '', string.punctuation))
        return lastWord_stripped.lower()

    # here we edit the words that are misspelt after
    # getting the words from the screen and correct their form
    def spellCheckerList(self, word_to_check):
        edits = {}
        # if there's no word selected or a space is selected
        if word_to_check == "" or word_to_check == " ":
            return
        # if the word is misspelt, record its respective edit distances and frequencies
        elif not self.isSpeltCorrect(word_to_check):
            # compute for min edit distance from each word in dictionary
            for word in self.vocab:
                edits_num = self.minEditDistance(word_to_check, word)
                # record all words corresponding to edits numbers 1 and 2 in a dictionary
                if edits_num <= 2:
                    # if there is a key in the dictionary corresponding to the same edit distance
                    if edits_num in edits:
                        # add it to its list of values
                        edits[edits_num].append(word)
                    else:
                        # if not, create a new key for the number of edits and add it
                        edits[edits_num] = [word]

        # record and sort frequencies of words corresponding for 1 edit and 2 edits
        freqs1 = []
        freqs2 = []
        # sorting words with edit distance 1 one based on frequency
        if 1 in edits:
            for similar_word in edits.get(1):
                # record frequency of each word with the same edit distance
                freq = self.vocab.get(similar_word)
                freqs1.append(freq)
        # sorting words with edit distance 1 one based on frequency
        if 2 in edits:
            for similar_word in edits.get(2):
                # record frequency of each word with the same edit distance
                freq = self.vocab.get(similar_word)
                freqs2.append(freq)
        # rearrange frequencies individually
        freqs1.sort()
        freqs2.sort()
        # combine the two frequency lists in order of 1 then 2 to get appropriate suggestions list
        # the smallest edit distance if the first priority, then its frequency
        freqs = freqs1 + freqs2
        suggestions = []
        for f in freqs:
            for word in self.vocab:
                # get words based on their corresponding frequencies in order
                if self.vocab.get(word) == f:
                    # add each corresponding word to the suggestions list
                    suggestions.append(word)
        return suggestions

    '''STILL TO DO, CONSIDER CAPITALIZATION IN SPELLCHECKER and replacement word/pop-up lists'''

    # this function checks if the word is spelt correctly or not
    # it returns a boolean value based on this condition
    def isSpeltCorrect(self, word):
        if word in self.vocab:
            return True
        return False

    # this functions recognizes the word and finds similar words
    # depending on their frequency, this will be then added to the
    # suggestion list. this function updates after typing each charachter
    def autoCompleteList(self, e):
        typed_word = self.getCurrWord(e).lower()
        if typed_word == "":
            return
        freqs = []
        suggestions = []
        inp_length = len(typed_word)
        for word in self.vocab:
            # check for english words that start with the same characters
            if word[:inp_length].lower() == typed_word:
                print('hi')
                # record the frequency ranks of such words
                freq = self.vocab.get(word)
                freqs.append(freq)
        # order frequencies
        freqs.sort()
        for f in freqs:
            for word in self.vocab:
                # get words based on their corresponding frequencies in order
                if self.vocab.get(word) == f:
                    suggestions.append(word)
        return suggestions

    # this function takes the list of words suggested if any and
    # inserts them on the screen in the autocomplete suggestions listbox
    def autoComplete(self, event):
        self.autoComplete_suggestions.destroy()
        self.autoComplete_suggestions = tk.Listbox(window)
        myFont = Font(family="Calibri", size=15)
        self.autoComplete_suggestions.configure(font=myFont)
        word = self.getCurrWord(event).lower()
        # ignore autocomplete call if the word is empty
        if not word:
            return
        # if there is one character typed
        if len(word) == 1:
            # use pre-loaded dictionary to get suggestiosn into listbox
            suggestions = self.startWithSameLetter.get(word)
            i = 0
            # add the first 10 word suggestions, as long as they exist
            while i < 11 and i < len(suggestions):
                for l in suggestions:
                    # add them to the suggestions listbox in order
                    self.autoComplete_suggestions.insert(i, l + " ")
                    i += 1
        else:
            # if typed portion is a part of a valid word
            if self.autoCompleteList(event):
                # get autocomplete list and append its first 10 values into the listbox
                suggestions = self.autoCompleteList(event)[:10]
                for i in range(len(suggestions)):
                    self.autoComplete_suggestions.insert(
                        i, suggestions[i] + " ")
            # if not, indicate lack of matches on listbox
            else:
                self.autoComplete_suggestions.insert(0, "No matches found.")

        # remove duplicate words in the suggestions listbox and the typed word
        if word in self.autoComplete_suggestions.get(0, tk.END):
            index = self.autoComplete_suggestions.get(0, tk.END).index(word)
            # delete duplicate word from suggestions listbox
            self.autoComplete_suggestions.delete(index)
            # if there are more suggestions available after 10, add the next one
            if len(self.autoCompleteList(event)) >= 11:
                self.autoComplete_suggestions.insert(
                    10,
                    self.autoCompleteList(event)[10] + " ")

        # place the listbox where the typing cursor is
        (x, y, w, h) = self.notepad.bbox('insert')
        self.autoComplete_suggestions.place(x=x + 140,
                                            y=y + 200,
                                            anchor="center")
        self.autoComplete_suggestions.bind('<<ListboxSelect>>',
                                           self.autoCompleteClickSelect)

    # this function also draws a list box with all the suggested words that
    # could replace the misspelt word.
    def spellChecker(self, event):
        self.autoComplete_suggestions.destroy()
        self.autoCorrect_suggestions.destroy()
        self.autoCorrect_suggestions = tk.Listbox(self.labelFrame)
        myFont = Font(family="Calibri", size=15)
        self.autoCorrect_suggestions.configure(font=myFont)
        # if the selected word is the one being currently typed
        # autocomplete it and don't spellcheck it (word not fully typed yet)
        '''if self.getCurrWord(event) and self.getNavigWord(event):
            self.autoComplete(event)
            return'''
        word = self.getNavigWord(event)
        # if the suggestions listbox is not empty, clear it
        if len(self.autoCorrect_suggestions.get(0, tk.END)) != 0:
            self.autoCorrect_suggestions.delete(0, tk.END)
        # exit spell checker if the word is spelt correctly
        if self.isSpeltCorrect(word):
            return
        # if current word is not empty and is spelled incorrectly
        elif len(self.notepad.get('1.0', 'end-1c')) != 0:
            if self.spellCheckerList(word):
                # append first 10 suggestions into listbox
                suggestions = self.spellCheckerList(word)[:10]
                for i in range(len(suggestions)):
                    self.autoCorrect_suggestions.insert(i, suggestions[i])
            else:
                # if not close matches from min edit function, display appropriate message
                self.autoCorrect_suggestions.insert(0, "No matches found.")
                self.autoCorrect_suggestions.insert(1,
                                                    "Add word to dictionary")

        if len(word) != 1:
            # place the listbox where the cursor is
            (x, y, w, h) = self.notepad.bbox('insert')
            self.autoCorrect_suggestions.place(x=x + 115,
                                               y=y + 160,
                                               anchor="center")

        self.autoComplete_suggestions = tk.Listbox(self.labelFrame)
        myFont = Font(family="Calibri", size=15)
        self.autoComplete_suggestions.configure(font=myFont)
        self.autoCorrect_suggestions.bind('<<ListboxSelect>>',
                                          self.autoCorrectClickSelect)

    # this function takes the selection that the user made from the suggestion box
    # and overwrites the word in he screen
    def autoCorrectClickSelect(self, event):
        selected_word = self.autoCorrect_suggestions.get(
            self.autoCorrect_suggestions.curselection())
        # get the entire word the cursor is on
        navigWord = self.getNavigWord(event)
        if selected_word == "No matches found.":
            self.autoCorrect_suggestions.destroy()
            return
        elif selected_word == "Add word to dictionary":
            self.vocab[navigWord] = len(self.vocab) + 1
        else:
            start = self.notepad.get('1.0', tk.END).index(navigWord)
            end = start + len(navigWord)
            line_num = int(float(self.notepad.index(tk.CURRENT)))
            # configure start and end indices of the word to be corrected syntax correctly
            start_i = str(line_num) + '.' + str(start)
            end_i = str(line_num) + '.' + str(end)
            # delete the misspelled word and replace it by the correct one selected from the listbox
            self.notepad.delete(start_i, end_i)
            self.notepad.insert(start_i, selected_word)

        if self.autoCorrect_suggestions.winfo_exists:
            self.autoCorrect_suggestions.destroy()

    # this function takes the selection that the user made from the suggestion box
    # and overwrites the word in the screen for the autocomplete option
    def autoCompleteClickSelect(self, event):
        if self.autoComplete_suggestions.curselection():
            selected_word = self.autoComplete_suggestions.get(
                self.autoComplete_suggestions.curselection())

            if selected_word == "No matches found.":
                self.autoComplete_suggestions.destroy()
                return
            # get the partial word currently being typed
            currWord = self.getCurrWord(event).lower()
            # configure start and end indices of the word to be corrected syntax correctly
            start = self.notepad.get('1.0', tk.END).index(currWord)
            end = start + len(currWord)
            line_num = int(float(self.notepad.index(tk.CURRENT)))
            start_i = str(line_num) + '.' + str(start)
            end_i = str(line_num) + '.' + str(end)
            # delete the misspelled word and replace it by the correct one selected from the listbox
            self.notepad.delete(start_i, end_i)
            self.notepad.insert(start_i, selected_word)
            self.autoComplete_suggestions.destroy()

    # this function underlines the word that is misspelt and
    # colors it with red
    def markUp(self, misspelt_word):
        lastWord = self.getLastWord()
        # if word contains numbers of special characters, don't mark it up
        if not lastWord.isalpha():
            return
        self.autoComplete_suggestions.destroy()
        # search for starting index of the misspelt word
        index = self.notepad.search(r'\s',
                                    "insert",
                                    backwards=True,
                                    regexp=True)
        if index == "":
            index = "1.0"
        else:
            index = self.notepad.index("%s+1c" % index)
        word = self.notepad.get(index, "insert").translate(
            str.maketrans('', '', string.punctuation))
        # if word spelled correctly, remove pre-set misspelling tag
        if word.lower() in self.vocab:
            self.notepad.tag_remove("misspelling", index,
                                    "%s+%dc" % (index, len(word)))
        else:
            self.notepad.tag_add("misspelling", index,
                                 "%s+%dc" % (index, len(word)))

    '''modfiied code from: 
    https://stackoverflow.com/questions/3732605/add-advanced-features-to-a-tkinter-text-widget'''

    # This function finds the minimum edit distance using a modified version of the Levistein algorithm
    # This is my own implementation of the algorithm
    def minEditDistance(self, misspelt_word, vocab_word):
        rows = len(misspelt_word) + 1
        columns = len(vocab_word) + 1
        matrix = []
        # split list of lists based on rows
        # initialize values for column contents for each row
        for i in range(rows):
            matrix.append([])
            for j in range(columns):
                matrix[i].append(-1)
        # empty string row
        first_row = []
        for n in range(columns):
            first_row.append(n)
        matrix = [first_row] + matrix[1:]
        # add first column values in matrix
        n = 0
        for i in range(rows):
            matrix[i][0] = n
            n += 1
        # for each letter of the misspelt word
        for r in range(rows - 1):
            # go through each letter in the vocab word
            for c in range(columns - 1):
                # if the letters are the same
                if vocab_word[c] == misspelt_word[r]:
                    # copy down the value at the relative left diagonal position in the matrix
                    # into the corresponding matrix position of the current string comparison
                    matrix[r + 1][c + 1] = matrix[r][c]
                # if letters are different
                else:
                    # take the minimum value of the three upper left diagonals to the current position
                    adj_min = min(matrix[r][c], matrix[r][c + 1],
                                  matrix[r + 1][c])
                    # add 1 to get the minimum additional edit to transform the two strings parts so far
                    # add resulting value into corresponding matrix position
                    matrix[r + 1][c + 1] = adj_min + 1
        # minimum number of edits is the last computed value of the matrix
        minEdits = matrix[rows - 1][columns - 1]
        return minEdits

    # this function gets the word that the cursor is hovering over in the text widget
    # and returns it.
    def getNavigWord(self, event):
        start = self.notepad.index("insert wordstart")
        end = self.notepad.index("insert wordend")
        nav_word = self.notepad.get(start, end)
        # remove unnecessary punctuations next to the typed word
        nav_word_stripped = nav_word.translate(
            str.maketrans('', '', string.punctuation))
        return nav_word_stripped.lower()

    # this function gets the word that is being modified currently from the user
    # and returns it.
    def getCurrWord(self, event):
        all_typed = self.notepad.get("1.0", "end")
        i = all_typed.rfind(" ")
        curr_word = all_typed[i + 1:].strip()
        # remove unnecessary punctuations next to the typed word
        curr_word_stripped = curr_word.translate(
            str.maketrans('', '', string.punctuation))
        return curr_word_stripped.lower()