# -*- coding: UTF-8 -*-

__revision__ = '$Id$'

# Copyright (c) 2005-2009 Vasco Nunes, Piotr Ożarowski
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 2 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU Library 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
# 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA

# You may use and distribute this software under the terms of the
# GNU General Public License, version 2 or later

import gzip
import html.entities
import logging
from logging.handlers import RotatingFileHandler
import os
import re
import sys
import webbrowser
from io import StringIO
from gettext import gettext as _
from pathlib import Path
from logging import getLogger

import gi

gi.require_version('Gdk', '3.0')
from gi.repository import Gdk
from gi.repository import GdkPixbuf

# from griffith import log
log = getLogger('Griffith')


try:
    gi.require_version('Gtk', '3.0')
    from gi.repository import Gtk
    import db
except Exception as _ex:
    Gtk = None
    pass

mac = False

if os.name in 'mac' or \
        (hasattr(os, 'uname') and os.uname()[0] == 'Darwin'):
    from . import macutils

    mac = True

log = logging.getLogger("Griffith")

ENTITY = re.compile(r'\&.\w*?\;')


def remove_accents(txt, _encoding='iso-8859-1'):
    d = {192: 'A', 193: 'A', 194: 'A', 195: 'A', 196: 'A', 197: 'A',
         199: 'C', 200: 'E', 201: 'E', 202: 'E', 203: 'E', 204: 'I',
         205: 'I', 206: 'I', 207: 'I', 209: 'N', 210: 'O', 211: 'O',
         212: 'O', 213: 'O', 214: 'O', 216: 'O', 217: 'U', 218: 'U',
         219: 'U', 220: 'U', 221: 'Y', 224: 'a', 225: 'a', 226: 'a',
         227: 'a', 228: 'a', 229: 'a', 231: 'c', 232: 'e', 233: 'e',
         234: 'e', 235: 'e', 236: 'i', 237: 'i', 238: 'i', 239: 'i',
         241: 'n', 242: 'o', 243: 'o', 244: 'o', 245: 'o', 246: 'o',
         248: 'o', 249: 'u', 250: 'u', 251: 'u', 252: 'u', 253: 'y',
         255: 'y'}
    return txt.translate(d)


def is_number(x):
    return isinstance(x, int)


def find_next_available(gsql):
    """
    finds next available movie number.
    This is the first empty position.
    If none is empty then increments the last position.
    """
    first = 0

    movies = gsql.session.query(
        db.Movie.number).order_by(db.Movie.number.asc()).all()
    for movie in movies:
        second = int(movie.number)
        if second is None:
            second = 0
        if second > first + 1:
            break
        first = second

    if first is None:
        return 1
    else:
        number = first + 1
        return number


def trim(text, key1, key2):
    if not text:
        return ''
    p1 = text.find(key1)
    if p1 == -1:
        return ''
    else:
        p1 += len(key1)
    p2 = text[p1:].find(key2)
    if p2 == -1:
        return ""
    else:
        p2 = p1 + p2
    return text[p1:p2]


def rtrim(text, key1, key2):
    p1 = text.rfind(key2)
    if p1 == -1:
        return ''
    p2 = text[:p1].rfind(key1)
    if p2 == -1:
        return ""
    else:
        p2 += len(key1)
    return text[p2:p1]


def regextrim(text, key1, key2):
    if not text:
        return ''
    obj = re.search(key1, text)
    if obj is None:
        return ''
    else:
        p1 = obj.end()
    obj = re.search(key2, text[p1:])
    if obj is None:
        return ''
    else:
        p2 = p1 + obj.start()
    return text[p1:p2]


def after(text, key):
    p1 = text.find(key)
    return text[p1 + len(key):]


def before(text, key):
    p1 = text.find(key)
    return text[:p1]


def gescape(text):
    text = text.replace("'", "''")
    text = text.replace("--", "-")
    return text


def progress(blocks, size_block, size):
    transferred = blocks * size_block
    if 0 < size < transferred:
        transferred = size
    elif size < 0:
        size = "?"
    print(transferred, '/', size, 'bytes')

# functions to handle comboboxentry stuff


def set_model_from_list(cb, items):
    """Set up a ComboBox or ComboBoxEntry based on a list of strings."""
    model = Gtk.ListStore(str)
    for i in items:
        model.append([i])
    cb.set_model(model)
    if type(cb) is Gtk.ComboBoxEntry:
        cb.set_text_column(0)
    elif type(cb) is Gtk.ComboBox:
        cell = Gtk.CellRendererText()
        cb.pack_start(cell, True)
        cb.add_attribute(cell, 'text', 0)


def on_combo_box_entry_changed(widget):
    model = widget.get_model()
    m_iter = widget.get_active_iter()
    if m_iter:
        value = model.get_value(m_iter, 0)
        # if type(value) is str:
        #     value = value.decode('utf-8')
        return value
    else:
        return 0


def on_combo_box_entry_changed_name(widget):
    return widget.get_active_text()


def convert_entities(text):

    def conv(ents):
        entities = html.entities.entitydefs
        ents = ents.group(0)
        ent_code = entities.get(ents[1:-1], None)
        if ent_code:
            ents = ent_code

            # check if it still needs conversion
            if not ENTITY.search(ents):
                return ents

        if ents[1] == '#':
            code = ents[2:-1]
            base = 10
            if code[0] == 'x':
                code = code[1:]
                base = 16
            return chr(int(code, base))
        else:
            return

    if text and isinstance(text, str):
        in_entity = ENTITY.search(text)
    else:
        in_entity = None
    if not in_entity:
        return text
    else:
        ctext = in_entity.re.sub(conv, text)
        return ctext


def strip_tags(text):
    if text is None:
        return ''
    finished = 0
    while not finished:
        finished = 1
        # check if there is an open tag left
        start = text.find('<')
        if start >= 0:
            # if there is, check if the tag gets closed
            stop = text[start:].find(">")
            if stop >= 0:
                # if it does, strip it, and continue loop
                text = text[:start] + text[start + stop + 1:]
                finished = 0
    return text


def clean(text):
    t = strip_tags(text)
    t = t.replace('&nbsp;', ' ')
    t = t.replace('&#34;', '')
    t = t.replace('&#160;', ' ')
    return t.strip()


def gdecode(txt, encode):
    try:
        return txt.decode(encode)
    except Exception:
        return txt

# Messages


def error(msg, parent=None):
    dialog = Gtk.MessageDialog(
        parent,
        Gtk.DialogFlags.MODAL | Gtk.DialogFlags.DESTROY_WITH_PARENT,
        Gtk.MessageType.ERROR, Gtk.ButtonsType.OK, msg)
    dialog.set_skip_taskbar_hint(False)
    dialog.run()
    dialog.destroy()


def urllib_error(msg, parent=None):
    dialog = Gtk.MessageDialog(
        parent,
        Gtk.DialogFlags.MODAL | Gtk.DialogFlags.DESTROY_WITH_PARENT,
        Gtk.MessageType.ERROR, Gtk.ButtonsType.OK, msg)
    dialog.set_skip_taskbar_hint(False)
    dialog.run()
    dialog.destroy()


def warning(msg, parent=None):
    if mac:
        macutils.createAlert(msg)
    else:
        dialog = Gtk.MessageDialog(
            parent,
            Gtk.DialogFlags.MODAL | Gtk.DialogFlags.DESTROY_WITH_PARENT,
            Gtk.MessageType.WARNING, Gtk.ButtonsType.OK, msg)
        dialog.set_skip_taskbar_hint(False)
        dialog.run()
        dialog.destroy()


def info(msg, parent=None):
    if mac:
        macutils.createAlert(msg)
    else:
        dialog = Gtk.MessageDialog(
            parent,
            Gtk.DialogFlags.MODAL | Gtk.DialogFlags.DESTROY_WITH_PARENT,
            Gtk.MessageType.INFO, Gtk.ButtonsType.OK, msg)
        dialog.set_skip_taskbar_hint(False)
        dialog.run()
        dialog.destroy()


def question(msg, window=None):
    if mac:
        response = macutils.question(msg)
        return response
    else:
        dialog = Gtk.MessageDialog(
            window,
            Gtk.DialogFlags.MODAL | Gtk.DialogFlags.DESTROY_WITH_PARENT,
            Gtk.MessageType.QUESTION, Gtk.ButtonsType.NONE, msg)
        dialog.add_buttons(
            Gtk.STOCK_YES, Gtk.ResponseType.YES, Gtk.STOCK_NO,
            Gtk.ResponseType.NO)
        dialog.set_default_response(Gtk.ResponseType.NO)
        dialog.set_skip_taskbar_hint(False)
        response = dialog.run()
        dialog.destroy()
        return response in (Gtk.ResponseType.OK, Gtk.ResponseType.YES)


def popup_message(message):
    """shows popup message while executing decorated function"""
    def wrap(f):
        def wrapped_f(*args, **kwargs):
            if Gtk:
                window = Gtk.Window()
                window.set_title('Griffith info')
                window.set_position(Gtk.WindowPosition.CENTER)
                window.set_keep_above(True)
                window.stick()
                window.set_default_size(200, 50)
                label = Gtk.Label()
                label.set_markup("""<big><b>Griffith:</b>
%s</big>""" % message)
                window.add(label)
                window.set_modal(True)
                window.set_type_hint(Gdk.WindowTypeHint.DIALOG)
                window.show_all()
                while Gtk.events_pending():    # give GTK some time for updates
                    Gtk.main_iteration()
            else:
                print(message, end=' ')
            res = f(*args, **kwargs)
            if Gtk:
                window.destroy()
            else:
                print(' [done]')
            return res
        return wrapped_f
    return wrap


def file_chooser(title, action=None, buttons=None, name='',
                 folder=os.path.expanduser('~'), picture=False, backup=False):
    if mac:
        if "SAVE" in str(action):
            if backup:
                status, filename, path = macutils.saveDialog(['zip'])
            else:
                status, filename, path = macutils.saveDialog()
        else:
            status, filename, path = macutils.openDialog(['zip'])
        if status:
            if filename.lower().endswith('.zip'):
                pass
            else:
                filename += ".zip"
            return filename, path
        else:
            return False
    else:
        dialog = Gtk.FileChooserDialog(title=title, action=action,
                                       buttons=buttons)
        dialog.set_default_response(Gtk.ResponseType.OK)
        if name:
            dialog.set_current_name(name)
        if folder:
            dialog.set_current_folder(folder)
        mfilter = Gtk.FileFilter()
        if picture:
            preview = Gtk.Image()
            dialog.set_preview_widget(preview)
            dialog.connect("update-preview", update_preview_cb, preview)
            mfilter.set_name(_("Images"))
            mfilter.add_mime_type("image/png")
            mfilter.add_mime_type("image/jpeg")
            mfilter.add_mime_type("image/gif")
            mfilter.add_pattern("*.[pP][nN][gG]")
            mfilter.add_pattern("*.[jJ][pP][eE]?[gG]")
            mfilter.add_pattern("*.[gG][iI][fF]")
            mfilter.add_pattern("*.[tT][iI][fF]{1,2}")
            mfilter.add_pattern("*.[xX][pP][mM]")
            dialog.add_filter(mfilter)
        elif backup:
            mfilter.set_name(_('backups'))
            mfilter.add_pattern('*.[zZ][iI][pP]')
            mfilter.add_pattern('*.[gG][rR][iI]')
            mfilter.add_pattern('*.[dD][bB]')
            dialog.add_filter(mfilter)
        mfilter = Gtk.FileFilter()
        mfilter.set_name(_("All files"))
        mfilter.add_pattern("*")
        dialog.add_filter(mfilter)

        response = dialog.run()
        if response == Gtk.ResponseType.OK:
            filename = dialog.get_filename()
        elif response == Gtk.ResponseType.CANCEL:
            filename = None
        else:
            return False
        path = dialog.get_current_folder()
        dialog.destroy()
        return filename, path


def update_preview_cb(file_chooser, preview):
    filename = file_chooser.get_preview_filename()
    try:
        pixbuf = GdkPixbuf.Pixbuf.new_from_file_at_size(filename, 128, 128)
        preview.set_from_pixbuf(pixbuf)
        have_preview = True
    except Exception:
        have_preview = False
    file_chooser.set_preview_widget_active(have_preview)
    return


def run_browser(url):
    webbrowser.register('open', webbrowser.GenericBrowser("open '%s'"))
    # webbrowser._tryorder.append('open')
    webbrowser.open(url)


def read_plugins(prefix, directory):
    """returns available plugins"""

    import glob
    return glob.glob("%s/%s*.py" % (directory, prefix))


def find_key(val, a_dict):
    for key, value in list(a_dict.items()):
        if value == val:
            return key
    return None


def garbage(handler):
    pass


def clean_posters_dir(self):
    posters_dir = self.locations['posters']
    counter = 0

    for dirpath, dirnames, filenames in os.walk(posters_dir):
        for name in filenames:
            filepath = os.path.join(dirpath, name)
            if name.endswith('_m.jpg') or name.endswith('_s.jpg'):
                # it's safe to remove all thumbs, they'll be regenerated later
                os.unlink(filepath)
            else:
                with open(filepath, 'rb') as fp:
                    poster_md5 = md5sum(fp)
                # let's check if this poster is orphan
                used = self.db.session.query(db.Poster).filter(
                    db.Poster.md5sum == poster_md5).count()
                if not used:
                    counter += 1
                    os.unlink(filepath)

    if counter:
        print("%d orphan files cleaned." % counter)
    else:
        print("No orphan files found.")


def decompress(data):
    try:
        compressed_stream = StringIO(data)
        gzipper = gzip.GzipFile(fileobj=compressed_stream)
        data = gzipper.read()
    except Exception as e:
        log.debug("Cannot decompress data: ", e)
        pass
    return data


def get_dependencies():
    depend = []

    # Python version
    if sys.version_info[:2] < (3, 9):
        depend.append({
            'module': 'python',
            "version": f"-{'.'.join(map(str, sys.version_info))}",
            'module_req': '3.9',
            'url': 'https://www.python.org/',
            'debian': 'python',
            'debian_req': '3.9'})
        # TODO: 'fedora', 'suse', etc.
    """
    try:
        from gi.repository import Gtk
        version = '.'.join([str(i) for i in Gtk.pygtk_version])
        if Gtk.pygtk_version <= (2, 6, 0):
            version = '-%s' % version
    except Exception:
        version = False
    depend.append({'module': 'gtk',
        'version': version,
        'module_req': '2.6',
        'url': 'https://www.pyGtk.org/,
        'debian': 'python-gtk2',
        'debian_req': '2.8.6-1'})
        # TODO: 'fedora', 'suse', etc.

    try:
        import Gtk.glade
        # (version == Gtk.pygtk_version)
    except Exception:
        version = False
    depend.append({'module': 'Gtk.glade',
        'version': version,
        'module_req': '2.6',
        'url': 'https://www.pyGtk.org/',
        'debian': 'python-glade2',
        'debian_req': '2.8.6-1'})
    """
    try:
        import sqlalchemy
        if list(map(int, sqlalchemy.__version__[:3].split('.'))) < [0, 5]:
            version = "-%s" % sqlalchemy.__version__
        else:
            version = sqlalchemy.__version__
    except Exception:
        version = False
    depend.append({
        'module': 'sqlalchemy',
        'version': version,
        'module_req': '0.5rc3',
        'url': 'https://www.sqlalchemy.org/',
        'debian': 'python-sqlalchemy',
        'debian_req': '0.5~rc3'})
    try:
        import sqlite3
        # version = sqlite3.version
        sqliteversion = sqlite3.sqlite_version
        version = sqliteversion
    except ImportError:
        version = False
    if not version:
        try:
            import pysqlite3.dbapi2
            version = pysqlite3.dbapi2.version
            sqliteversion = pysqlite3.dbapi2.sqlite_version
        except Exception:
            version = False
        depend.append({
            'module': 'pysqlite2',
            'version': version + ' (sqlite-lib ' + sqliteversion + ')',
            'url': 'https://pypi.org/project/pysqlite/',
            'debian': 'python-pysqlite2',
            'debian_req': '2.3.0-1'})
    else:
        depend.append({
            'module': 'sqlite3',
            'version': version + ' (sqlite-lib ' + sqliteversion + ')',
            'url': 'http:s//www.python.org',
            'debian': 'python',
            'debian_req': '2.5'})
    try:
        import reportlab
        version = reportlab.Version
    except ImportError as ex:
        log.error(f"Python module 'reportlab' not found, {ex}")
        version = False
    depend.append({
        'module': 'reportlab',
        'version': version,
        'url': 'https://www.reportlab.com/',
        'debian': 'python-reportlab',
        'debian_req': '1.20debian-6'})
    try:
        import PIL
        version = True
    except ImportError as ex:
        log.error(f"Python module 'PIL' not found, {ex}")
        version = False
    depend.append({
        'module': 'PIL',
        'version': version,
        'url': 'https://pillow.readthedocs.io/en/stable/',
        'debian': 'python-imaging',
        'debian_req': '1.1.5-6'})

    # extra dependencies:
    optional = []

    try:
        import tmdbsimple
        version = True
    except Exception as _exc:
        version = False
    optional.append({
        'module': 'tmdbsimple',
        'version': version})

    try:
        import psycopg2
        version = psycopg2.__version__
    except Exception:
        version = False
    optional.append({
        'module': 'psycopg2',
        'version': version,
        'url': 'https://www.psycopg.org/',
        'debian': 'python-psycopg2',
        'debian_req': '1.1.21-6'})
    try:
        import pymysql
        pymysql.install_as_MySQLdb()
        import MySQLdb
        version = '.'.join([str(i) for i in MySQLdb.version_info])
    except Exception:
        version = False
    optional.append({
        'module': 'MySQLdb',
        'version': version,
        'url': 'https://pypi.org/project/MySQL-python/',
        'debian': 'python-mysqldb',
        'debian_req': '1.2.1-p2-2'})
    try:
        import chardet
        version = chardet.__version__
    except Exception:
        version = False
    optional.append({
        'module': 'chardet',
        'version': version,
        'url': 'https://chardet.readthedocs.io/en/latest/usage.html',
        'debian': 'python-chardet'})
    try:
        import sqlite
        version = sqlite.version
    except Exception:
        version = False
    optional.append({
        'module': 'sqlite',
        'version': version,
        'url': 'https://docs.python.org/3/library/sqlite3.html',
        'debian': 'python-sqlite'})
    try:
        import lxml
        version = True
    except ImportError:
        version = False
    optional.append({
        'module': 'lxml',
        'version': version,
        'url': 'https://lxml.de/',
        'debian': 'python-lxml'})
    return depend, optional


def html_encode(s):
    if not isinstance(s, str):
        s = str(s)
    s = s.replace('&', '&amp;')
    s = s.replace('<', '&lt;')
    s = s.replace('>', '&gt;')
    s = s.replace('"', '&quot;')
    return s


def digits_only(s, maximum=None):
    if s is None:
        return 0
    match = re.compile(r"\d+")
    a = match.findall(str(s))
    if len(a):
        s = int(a[0])
    else:
        s = 0
    if maximum is None:
        return s
    else:
        if s > maximum:
            return maximum
        else:
            return s


def copytree(src, dst, symlinks=False):
    """Recursively copy a directory tree using copy2().

    This is shutil's copytree modified version
    """
    from shutil import copy2
    names = os.listdir(src)
    if not os.path.isdir(dst):
        os.mkdir(dst)
    errors = []
    for name in names:
        srcname = os.path.join(src, name)
        dstname = os.path.join(dst, name)
        try:
            if symlinks and os.path.islink(srcname):
                linkto = os.readlink(srcname)
                os.symlink(linkto, dstname)
            elif os.path.isdir(srcname):
                copytree(srcname, dstname, symlinks)
            else:
                copy2(srcname, dstname)
        except (IOError, os.error) as why:
            errors.append((srcname, dstname, why))
        # catch the Error from the recursive copytree so that we can
        # continue with other files
        except EnvironmentError as err:
            errors.extend(err.args[0])
    if errors:
        raise EnvironmentError(errors)


def is_windows_system():
    if os.name == 'nt' or os.name.startswith('win'):  # win32, win64
        return True
    return False


def md5sum(fobj):
    """Returns a md5 hash for an object with read() method."""
    import hashlib

    m = hashlib.md5()
    if hasattr(fobj, 'read'):
        while True:
            d = fobj.read(8096)
            if not d:
                break
            m.update(d)
    else:
        m.update(fobj)
    return str(m.hexdigest())


def create_image_cache(md5sum: str, gsql) -> bool:
    poster = gsql.session.query(db.Poster).filter_by(md5sum=md5sum).first()
    if not poster:
        log.warning(f"poster not available: {md5sum}")
        return False
    if not poster.data:
        log.warning(f"poster data not available: {md5sum}")
        return False

    fn_big = os.path.join(gsql.data_dir, 'posters', f"{md5sum}.jpg")
    fn_medium = os.path.join(gsql.data_dir, 'posters', f"{md5sum}_m.jpg")
    fn_small = os.path.join(gsql.data_dir, 'posters', f"{md5sum}_s.jpg")

    if not os.path.isfile(fn_big):
        with open(fn_big, 'wb') as fp:
            fp.write(poster.data)

    image = Gtk.Image()
    image.set_from_file(fn_big)
    if not os.path.isfile(fn_medium):
        pixbuf = image.get_pixbuf()
        # pixbuf = pixbuf.scale_simple(100, 140, GdkPixbuf.InterpType.BILINEAR)
        pixbuf = pixbuf.scale_simple(200, 280, GdkPixbuf.InterpType.BILINEAR)
        pixbuf.savev(fn_medium, 'jpeg', ['quality'], ['70'])
    if not os.path.isfile(fn_small):
        pixbuf = image.get_pixbuf()
        pixbuf = pixbuf.scale_simple(30, 40, GdkPixbuf.InterpType.BILINEAR)
        pixbuf.savev(fn_small, 'jpeg', ['quality'], ['70'])

    return True


def create_imagefile(destdir, md5sum, gsql, destfilename=None):
    poster = gsql.session.query(db.Poster).filter_by(md5sum=md5sum).first()
    if not poster:
        log.warning("poster not available: %s", md5sum)
        return False
    if not poster.data:
        log.warning("poster data not available: %s", md5sum)
        return False

    if destfilename:
        fulldestpath = os.path.join(destdir, destfilename + '.jpg')
    else:
        fulldestpath = os.path.join(destdir, md5sum + '.jpg')

    f = open(fulldestpath, 'wb')
    try:
        f.write(poster.data)
    finally:
        f.close()

    return True


def get_image_fname(md5sum, gsql, size=None):
    """size: s - small; m - medium, b or None - big"""
    if size not in (None, 's', 'm', 'b'):
        raise TypeError(f"wrong size spec provided: '{size}*" % size)
    if not md5sum:
        raise TypeError("md5sum not set")

    if not size or size == 'b':
        size = ''
    else:
        size = f"_{size}"

    file_name = os.path.join(gsql.data_dir, 'posters', f"{md5sum}{size}.jpg")
    if not os.path.isfile(file_name) and not create_image_cache(md5sum, gsql):
        log.warning("Can't create image file %s for md5sum %s" %
                    (file_name, md5sum))
        return ''
    return file_name


def get_defaultimage_fname(self):
    return os.path.join(self.locations['images'], 'default.png')


def get_defaultthumbnail_fname(self):
    return os.path.join(self.locations['images'], 'default_thumbnail.png')


def get_filesystem_pagesize(path):
    pagesize = 1024
    # retrieve filesystem page size for optimizing filesystem based database
    # systems like sqlite
    try:
        if is_windows_system():
            pagesize = 4096  # almost the best for standard Windows systems

            # try to find the perfect value from the filesystem
            import ctypes

            drive = os.path.splitdrive(path)
            sectors_per_cluster = ctypes.c_ulonglong(0)
            bytes_per_sector = ctypes.c_ulonglong(0)
            root_path_name = ctypes.c_wchar_p(str(drive[0]))

            ctypes.windll.kernel32.GetDiskFreeSpaceW(
                root_path_name,
                ctypes.pointer(sectors_per_cluster),
                ctypes.pointer(bytes_per_sector),
                None,
                None,
                )
            pagesize = sectors_per_cluster.value * bytes_per_sector.value
        else:
            if not os.path.isfile(path):
                return pagesize
            # I could not try it out on non-windows platforms
            # if it doesn't work the default page size is returned
            from os import statvfs
            stats = statvfs(path)
            pagesize = stats.f_bsize
    except Exception as ex:
        log.error(f'Exception occurred, {type(ex).__name__}: {ex}')

    # adjust page size
    if pagesize > 32768:
        pagesize = 32768
    if pagesize < 1024:
        pagesize = 1024

    return pagesize


def read_file(file_path, mode='r'):
    with open(file_path, mode) as fp:
        data = fp.read()
    return data


def cmp(a, b):
    return (a > b) - (a < b)


def define_logger_handler(logger_name):
    fmt = ('%(asctime)s: %(levelname).1s: %(name)s(%(module)s:%(lineno)d):'
           ' %(message)s')
    log_format = logging.Formatter(fmt=fmt, datefmt='%Y-%m-%dT%H:%M:%S')
    # logging.basicConfig(format='%(asctime)s: %(levelname).1s: %(name)s(%('
    #                            'module)s:%(lineno)d): %(message)s',
    #                     datefmt='%Y-%m-%dT%H:%M:%S')
    Path(Path.home(), '.griffith').mkdir(parents=True, exist_ok=True)
    log_filepath = Path(Path.home(), '.griffith', 'griffith.log')
    logger = logging.getLogger(logger_name)
    file_handler = RotatingFileHandler(log_filepath, 'a', maxBytes=10_000_000, backupCount=2)
    file_handler.setLevel(logging.DEBUG)
    file_handler.setFormatter(log_format)
    logger.addHandler(file_handler)
    stream_handler = logging.StreamHandler()
    stream_handler.setLevel(logging.INFO)
    stream_handler.setFormatter(log_format)
    logger.addHandler(stream_handler)
    return logger


def check_module_dependencies():
    required, extra = get_dependencies()
    missing = []
    for pkg in required:
        if (pkg['version'] is False
                or (pkg['version'] is not True and pkg['version'][0] == '-')):
            missing.append(pkg)
    if len(missing) > 0:
        log.error('Error: missing modules:')
        for pkg in missing:
            log.error("%(module)s" % pkg)
            if 'module_req' in pkg:
                log.error("\t:: required version: %(module_req)s" % pkg)
                if pkg['version'] is not False and pkg['version'][0] == '-':
                    log.error("\t:: detected: %s" % pkg['version'][1:])
            log.error("\n")
        sys.exit(1)
    del missing
