#!/usr/local/bin/python3.11
# -*- coding: utf-8 -*-
#
#   Copyright (C) 2003-2010 Daniel Naber, Lutz Haseloff
#   Copyright (C) 2013-2021 Mechtilde Stehmann
#
#   Copyright (C) 2013-2016 for the gettext integration: Dr. Michael Stehmann
#
#   This program is free software; you can redistribute it and/or
#   modify it under the terms of the GNU General Public License
#   as published by the Free Software Foundation; either version 2
#   of the License, or any later version.
#
#   This program is distributed in the hope that it will be useful,
#   but WITHOUT ANY WARRANTY; without even the implied warranty of
#   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
#   GNU General Public License for more details.
#
#   You should have received a copy of the GNU General Public License
#   along with this program; if not, write to the Free Software
#   Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
#   MA 02110-1301, USA.

import configparser
import codecs
import os
import re
import string
import time
import zipfile
import locale
import gettext
import sys

from tkinter import *
import tkinter.filedialog
import tkinter.messagebox

class Application(object):

    CONFIGFILE = ".loook.cfg"

    def __init__(self, master=None):
        """Load configuration or use sensible default values."""
        self.master = master
        self.stopped = 0
        self.ooo_path_str = None
        self.search_path_str = None
        config_path = None
        if os.getenv('USERPROFILE'):
            config_path = os.getenv('USERPROFILE')
        elif os.getenv('HOME'):
            config_path = os.getenv('HOME')
        if config_path:
            self.configfile = os.path.join(config_path, self.CONFIGFILE)
            self.config =  configparser.ConfigParser()
            self.config.read(self.configfile)
            try:
                self.ooo_path_str = self.config.get("General", "ooo_path")
                self.search_path_str = self.config.get("General", "search_path")
            except configparser.NoSectionError:
                pass
        else:
            print >> sys.stderr, _("Cannot find home directory, settings will not be saved.")
            self.configfile = None
        self.createWidgets()
        return

    def createWidgets(self):
        """Build and show the GUI elements."""
        Label(self.master, text=_("Viewer:")).grid(row=0, sticky=E)
        Label(self.master, text=_("Search path:")).grid(row=1, sticky=E)
        Label(self.master, text=_("Search terms:")).grid(row=2, sticky=E)
        Label(self.master, text=_("Mode:")).grid(row=3, sticky=E)
        Label(self.master, text=_("Matches:")).grid(row=4, sticky=N+E)

        self.ooo_path = Entry(self.master)
        if self.ooo_path_str:
            self.ooo_path.insert(END, self.ooo_path_str)
        else:
            self.ooo_path.insert(END, "soffice")
        self.ooo_path_b = Button(self.master)
        self.ooo_path_b.bind("<Button-1>", self.selectOOoPath)
        self.ooo_path_b["text"] = ">"

        self.search_path = Entry(self.master)
        if len(sys.argv) >= 2:
            self.search_path.insert(END, sys.argv[1])
        elif self.search_path_str:
            self.search_path.insert(END, self.search_path_str)
        else:
            self.search_path.insert(END, os.getcwd())
        self.search_path_b = Button(self.master)
        self.search_path_b.bind("<Button-1>", self.selectSearchPath)
        self.search_path_b["text"] = ">"

        self.search_query = Entry(self.master)
        self.search_query.bind('<Return>', self.startSearch)
        if len(sys.argv) >= 2:
            lang, enc = locale.getdefaultlocale()
            self.search_query.insert(END, ' '.join(sys.argv[2:]))
        self.search_query.focus()

        self.mode_button = Button(self.master)
        self.mode_button.bind("<Button-1>", self.popupMode)
        self.mode_button["text"] = _("AND")
        self.mode_menu = Menu(self.master, tearoff=0)
        self.mode_menu.add_command(label=_("AND"), command=self.setModeAND)
        self.mode_menu.add_command(label=_("OR"), command=self.setModeOR)
        self.mode_menu.add_command(label=_("Phrase"), command=self.setModePhrase)

        pad = 1
        self.ooo_path.grid(columnspan=2, row=0, column=1, sticky=E+W, pady=pad, padx=pad)
        self.ooo_path_b.grid(row=0, column=3, sticky=E+W, pady=pad, padx=pad)

        self.search_path.grid(columnspan=2, row=1, column=1, sticky=E+W, pady=pad, padx=pad)
        self.search_path_b.grid(row=1, column=3, sticky=E+W, pady=pad, padx=pad)
        self.search_query.grid(columnspan=3, row=2, column=1, sticky=E+W, pady=pad, padx=pad)
        self.mode_button.grid(columnspan=3, row=3, column=1, sticky=W, pady=pad, padx=pad)

        self.scrollbar = Scrollbar(self.master)
        self.scrollbar.grid(row=4, column=3, sticky=N+S, pady=pad, padx=pad)
        self.listbox = Listbox(self.master, yscrollcommand=self.scrollbar.set)
        self.listbox.bind('<Double-Button-1>', self.showDoc)
        self.listbox.grid(columnspan=2, row=4, column=1, sticky=E+W+S+N, pady=pad, padx=pad)
        self.scrollbar.config(command=self.listbox.yview)

        f = Frame(self.master)
        self.search = Button(f)
        self.search["text"] = _("Search")
        self.search["command"] = self.startSearch
        self.search.pack(side=LEFT)
        self.quit_button = Button(f)
        self.quit_button["text"] = _("Quit")
        self.quit_button["command"] = self.doQuit
        self.quit_button.pack(side=RIGHT)
        self.stop_button = Button(f)
        self.stop_button["text"] = _("Stop")
        self.stop_button["command"] = self.stop
        self.stop_button["state"] = DISABLED
        self.stop_button.pack(side=RIGHT)
        self.save_button = Button(f)
        self.save_button["text"] = _("Save")
        self.save_button["command"] = self.doSave
        self.save_button["state"] = DISABLED
        self.save_button.pack(side=LEFT)

        f.grid(row=5, column=2, sticky=E, pady=pad, padx=pad)
        self.status = Label(self.master, text="", bd=1, relief=SUNKEN, anchor=W)
        self.status.config(text=_("Ready."))
        self.status.grid(row=6, columnspan=4, column=0, sticky=E+W, pady=pad, padx=pad)
        return

    def doQuit(self):
        """Save configuration, the quit."""
        self.saveConfig()
        self.master.quit()
        return

    def saveConfig(self):
        """Save path settings in configuration file in the user's HOME."""
        if self.configfile:
            file = codecs.open(self.configfile, "w", "utf-8")
            if not self.config.has_section("General"):
                self.config.add_section("General")
            file.write("[General]\n")
            file.write("ooo_path=%s\n" % self.ooo_path.get())
            file.write("search_path=%s\n" % self.search_path.get())
            file.close()
        return

    def selectOOoPath(self, event):
        d = tkinter.filedialog.askopenfilename()
        self.ooo_path.delete(0, END)
        self.ooo_path.insert(END, d)
        return

    def selectSearchPath(self, event):
        d = tkinter.filedialog.askdirectory()
        self.search_path.delete(0, END)
        self.search_path.insert(END, d)
        return

    def setMode(self, mode):
        self.mode = mode
        self.mode_button["text"] = mode
        return

    def setModeAND(self):
        self.setMode(_("AND"))
        return

    def setModeOR(self):
        self.setMode(_("OR"))
        return

    def setModePhrase(self):
        self.setMode(_("Phrase"))
        return

    def popupMode(self, event):
        try:
            self.mode_menu.tk_popup(event.x_root, event.y_root, 0)
        finally:
            # make sure to release the grab (Tk 8.0a1 only)
            self.mode_menu.grab_release()
        return

    def stop(self):
        self.stopped = 1
        return

    def showDoc(self, event):
        """Start OOo to view the file. This method lacks
        error handling (TODO)."""
        items = event.widget.curselection()
        filename = "%s%s" % (self.search_path.get(), event.widget.get(items[0]))
        filename = os.path.normpath(filename)
        prg = self.ooo_path.get()
        if not prg:
            tkinter.messagebox.showwarning(_('Error'), _('Set viewer first.'))
        else:
            filename = filename.replace('"', '\\" ')
            cmd = "\"%s\" \"%s\" &" % (prg, filename)
            self.status.config(text=_("Starting viewer..."))
            print(cmd)
            res = os.system(cmd)
            if res != 0:
                # don't show a dialog, this check might not be system-indepenent:
                print(_("Warning: Command returned code != 0: ") + cmd)
        return

    def removeXMLMarkup(self, s, replace_with_space):
        s = re.compile("<!--.*?-->", re.DOTALL).sub('', s)
        repl = ''
        if replace_with_space:
            repl = ' '
        s = re.compile("<[^>]*>", re.DOTALL).sub(repl, s)
        return s

    def match(self, query, docstring):
        mode = self.mode_button["text"]
        if mode == _("Phrase"):
            # match only documents that contain the phrase:
            regex = re.compile(re.escape(query.lower()), re.DOTALL)
            if regex.search(docstring):
                return 1
        else:
            parts = re.split("\s+", query.strip())
            if mode == _("AND"):
                # match only documents that contain all words:
                match = 1
                for part in parts:
                    regex = re.compile(re.escape(part.lower()), re.DOTALL)
                    if not regex.search(docstring):
                        match = 0
                        break
                return match
            elif mode == _("OR"):
                # match documents that contain at leats one word:
                match = 0
                for part in parts:
                    regex = re.compile(re.escape(part.lower()), re.DOTALL)
                    if regex.search(docstring):
                        match = 1
                        break
                return match
            else:
                print("Error: unknown search mode '%s'" % mode)
        return 0

    def processFile(self, filename, query):
        suffix = self.getSuffix(filename)
        # needed for error messages
        fsfilename =filename
        try:
            # Handle OpenOffice.org files:
            if suffix in ('sxw', 'stw',     # OOo   1.x swriter
                    'sxc', 'stc',       # OOo   1.x scalc
                    'sxi', 'sti',       # OOo   1.x simpress
                    'sxg',              # OOo   1.x master document
                    'sxm',              # OOo   1.x formula
                    'sxd', 'std',       # OOo   1.x sdraw
                    'odt', 'ott',       # OOo > 2.x swriter
                    'odp', 'otp',       # OOo > 2.x simpress
                    'odf',              # OOo > 2.x formula
                    'odg', 'otg',       # OOo > 2.x sdraw
                    'ods', 'ots',       # OOo > 2.x scalc
                    ):
                zip = zipfile.ZipFile(filename, "r")
                content = ""
                docinfo = ""
                try:
                    # search embedded files:
                    filelist = zip.namelist()
                    for filename in filelist:
                        if filename.endswith("content.xml"):
                            content += str(zip.read(filename), 'utf-8')

                        if filename.endswith("document.xml"):
                            content += str(zip.read(filename), 'utf-8')

                    content = self.removeXMLMarkup(content, replace_with_space=0)
                    docinfo = str(zip.read("meta.xml"), 'utf-8')
                    docinfo = self.removeXMLMarkup(docinfo, replace_with_space=0)
                    self.ooo_count = self.ooo_count + 1
                except KeyError as err:
                    print("Warning: %s not found in '%s'" % (err, filename))
                    return None
                # Patch for encrypted files
                except UnicodeDecodeError:
                    print("Warning: cannot open '%s'" % (fsfilename))
                    return None
                title = ""
                title_match = re.compile("<dc:title>(.*?)</dc:title>", re.DOTALL|re.IGNORECASE).search(docinfo)
                if title_match:
                    title = title_match.group(1)
                if self.match(query, "%s %s" % (content.lower(), docinfo.lower())):
                    return (filename, title)

            # Handle MS-Office (>= 2007) files:
            if suffix in ('docx', 'dotx',       # MS-Word Documents >= 2007
                'xlsx', 'xltx',     # MS-Excel-Documents >= 2007
                ):
                zip = zipfile.ZipFile(filename, "r")
                content = ""
                docinfo = ""
                try:
                    # search embedded files:
                    filelist = zip.namelist()
                    for filename in filelist:

                        if filename.endswith("document.xml"):
                            content += str(zip.read(filename), 'utf-8')

                        if filename.endswith("sharedStrings.xml"):
                            content += str(zip.read(filename), 'utf-8')

                    content = self.removeXMLMarkup(content, replace_with_space=0)
                    docinfo = str(zip.read("docProps/core.xml"), 'utf-8')
                    docinfo = self.removeXMLMarkup(docinfo, replace_with_space=0)
                    self.ooo_count = self.ooo_count + 1
                except KeyError as err:
                    print("Warning: %s not found in '%s'" % (err, filename))
                    return None
                # Patch for encrypted files
                except UnicodeDecodeError:
                    print("Warning: cannot open '%s'" % (fsfilename))
                    return None
                title = ""
                title_match = re.compile("<dc:title>(.*?)</dc:title>", re.DOTALL|re.IGNORECASE).search(docinfo)
                if title_match:
                    title = title_match.group(1)
                if self.match(query, "%s %s" % (content.lower(), docinfo.lower())):
                    return (filename, title)
            # Handle MS-Office (>= 2007) MS-PowerPoint files:
            if suffix in ('pptx',       # MS-PowerPoint-Documents >= 2007
                ):
                zip = zipfile.ZipFile(filename, "r")
                content = ""
                docinfo = ""
                try:
                    # search embedded files:
                    filelist = zip.namelist()
                    slideslist = []
                    for element in filelist:
                        if element[4:12]  == "slides/s":
                            slideslist.append(element)
                    content =""
                    for element in slideslist:
                        content += str(zip.read(element), 'utf-8')

                    content = self.removeXMLMarkup(content, replace_with_space=0)
                    docinfo = str(zip.read("docProps/core.xml"), 'utf-8')
                    docinfo = self.removeXMLMarkup(docinfo, replace_with_space=0)
                except KeyError as err:
                    print("Warning: %s not found in '%s'" % (err, filename))
                    return None
                # Patch for encrypted files
                except UnicodeDecodeError:
                    print("Warning: cannot open '%s'" % (fsfilename))
                    return None
                title = ""
                title_match = re.compile("<dc:title>(.*?)</dc:title>", re.DOTALL|re.IGNORECASE).search(docinfo)
                if title_match:
                    title = title_match.group(1)
                if self.match(query, "%s %s" % (content.lower(), docinfo.lower())):
                    return (filename, title)
                self.ooo_count = self.ooo_count + 1

        except zipfile.BadZipfile as err:
            print(_("Warning: Supposed ZIP file ") + filename + _("could not be opened: ") + str(err))
        except IOError as err:
            print(_("Warning: File ") + filename + _("could not be opened: ") + str(err))
        return None

    def startSearch(self, event=None):
        self.stopped = 0
        self.last_update = 0
        self.match_count = 0
        self.ooo_count = 0
        self.savestring = ""
        self.listbox.delete(0, END)
        self.stop_button["state"] = NORMAL
        self.quit_button["state"] = DISABLED
        if not os.path.exists(self.search_path.get()):
            tkinter.messagebox.showwarning(_("Error"), _("Path ") + self.search_path.get() +_("doesn\'t exist."))
        else:
            #start_time = time.time()
            self.recursiveSearch(self.search_path.get())
            #duration = time.time() - start_time
            #print("time=%.2f" % duration)
            count_str = "%s %d %s" % (_("in"), self.ooo_count, _("files"))
            if self.stopped:
                self.status.config(text="%d %s %s %s" % (self.match_count, _("matches so far"), count_str, _("(search stopped)")))
                self.stopped = 0
            else:
                self.status.config(text="%d %s %s" % (self.match_count, _("matches"), count_str))
        self.stop_button["state"] = DISABLED
        self.quit_button["state"] = NORMAL
        return

    def getSuffix(self, filename):
        suffix_match = re.compile(".*\.(.*)").match(filename)
        if suffix_match:
            suffix = suffix_match.group(1).lower()
        else:
            suffix = None
        return suffix

    def recursiveSearch(self, directory):
        len_limit = 15      # avoid resizing window
        dir_part = os.path.split(directory)[1]
        if len(dir_part) > len_limit:
            dir_part = "%s..." % dir_part[0:len_limit]
        #print("'%s'" % dir_part)
        self.status.config(text=_("Searching in ") + "%s" % dir_part)
        try:
            dir_content = os.listdir(directory)
            dir_content.sort(key=str.lower)
        except OSError as err:
            print(_("Warning: ") + directory + (": ") + str(err))
            return
        except UnicodeError as err:
            print(_("Warning: Unicode problem with directory name..."))
            return
        enable_save = False
        for filename in dir_content:
            if self.stopped != 0:
                return
            filename = os.path.join(directory, filename)
            if os.path.isfile(filename):
                match = self.processFile(filename, self.search_query.get())
                update_interval = 0.05
                time_diff = time.time() - self.last_update
                if time_diff > update_interval:
                    self.master.update()
                    self.last_update = time.time()
                if match:
                    title = match[1]
                    if not title:
                        title = "Untitled"
                    display_filename = filename.replace(self.search_path.get(), '')
                    self.listbox.insert('end', "%s" % display_filename)
                    self.savestring = self.savestring + filename + "\n"
                    if enable_save == False:
                        self.save_button["state"] = NORMAL
                        enable_save = True
                    self.match_count = self.match_count + 1
            elif os.path.isdir(filename) and not os.path.islink(filename):
                self.recursiveSearch(filename)
        return

    def doSave(self):
        savestring = _("Search path:") + " " + self.search_path.get() + "\n"
        savestring = savestring + _("Search terms:") + " " + self.search_query.get() + "\n"
        savestring = savestring + _("Mode:") + " " + self.mode_button["text"] + "\n\n"
        savestring = savestring + _("Matches:") + "\n" + self.savestring
        fo = tkinter.filedialog.asksaveasfile()
        fo.write(savestring)
        self.stopped = 1
        self.save_button["state"] = DISABLED
        return

# Begin gettext integration
class TranslationIntegration(object):
    """GNU-gettext for GNU/Linux"""

    def __init__(self, pname):

        self.moname = pname + ".mo"
        self.syslanguage()
        # check, whether it's a unix system installation
        _localepath = "/usr/local/share/locale/"

        _result = self.look4mo(_localepath)

        if _result == "xxx":
            self.syslanguage()
            _localepath = "locale/"
            _result = self.look4mo(_localepath)

        if not _result == "xxx":
            # chooses the right .mo file and installs translation
            trans = gettext.translation(pname, _localepath, [self.language])
            trans.install()
        else:
            # Translating the following strings is absurd!
            serror = "Bad package! No "+self.moname+" file found!"
            from tkinter import messagebox
            messagebox.showerror('Error!', serror)
            import sys; sys.exit(1)

    def syslanguage(self):
        """which is the language of the system (GNU/Linux)?"""
        _loclang = locale.getdefaultlocale()
        self.language = _loclang[0]
        if self.language == None:
            self.language = "en"
        return self.language

    def look4mo(self, localepath):
        """choose only a language which a .mo file exists for,
        else use english or return 'xxx' """
        _lpath = localepath + self.language + "/LC_MESSAGES/" + self.moname

        if not os.path.exists(_lpath):

            _newlang = self.dosplit(self.language, "_")
            _lpath = localepath + _newlang + "/LC_MESSAGES/"+self.moname

            if os.path.exists(_lpath):
                self.language = _newlang
            else:
                _newlang = self.dosplit(self.language, "@")
                _lpath = localepath + _newlang + "/LC_MESSAGES/" + self.moname

                if os.path.exists(_lpath):
                    self.language = _newlang
                else:
                    _lpath = localepath + "en/LC_MESSAGES/" + self.moname

                    if os.path.exists(_lpath):
                        self.language = "en"
                    else:
                        self.language = "xxx"

        return self.language
    def dosplit(self, language, splitter):

        """
            splits language descriptor

            >>> gnugettext.dosplit(gnugettext, "de_DE", "_")
            'de'

            >>> gnugettext.dosplit(gnugettext, "be@latin", "@")
            'be'
        """

        _newlang = language.split(splitter)
        _newlang = _newlang[0]

        return _newlang

# End gettext integration

if __name__ == "__main__":

    version = "0.9.0"
    loookversion = "loook"

    g = TranslationIntegration(loookversion)

    if len(sys.argv) >= 2 and (sys.argv[1] == '--help' or sys.argv[1] == '-h'):
        print(_("Usage:") +" loook [-h|--help] " + _("[search path] [search term]..."))
        sys.exit(1)
    root = Tk()
    root.minsize(380, 200)
    root.title("Loook " + version + " - " + _("OpenOffice.org File Finder"))
    root.columnconfigure(1, weight=1)
    root.rowconfigure(4, weight=1)
    root.columnconfigure(1, weight=1)
    root.rowconfigure(5, weight=0)

    app = Application(root)
    root.protocol("WM_DELETE_WINDOW", app.doQuit)
    root.mainloop()
