# -*- Mode: python; coding: utf-8; tab-width: 4; indent-tabs-mode: nil; -*-
#
# GNOME Activity Journal
#
# Copyright © 2010 Randal Barlow <email.tehk@gmail.com>
# Copyright © 2010 Siegfried Gevatter <siegfried@gevatter.com>
# Copyright © 2010 Markus Korn <thekorn@gmx.de>
# Copyright © 2020 The GNOME Activity Journal developers
#
# 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 3 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 General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program.  If not, see <http://www.gnu.org/licenses/>.

"""
Common Functions and classes which are used to create the alternative views,
and handle colors, pixbufs, and text
"""


import collections
import gettext
import os
import time
import math
import operator
import subprocess
import tempfile
import urllib.request, urllib.parse, urllib.error
import zipfile
import gi
import cairo
import sys

gi.require_version('PangoCairo', '1.0')

try:
    gi.require_version('GnomeDesktop', '3.0')
except ValueError as e:
    print("ERROR: %s." % e)
    print()
    print("Please install 'gnomedesktop-3.0' gir package.")
    print()
    print("See the README.md file for more information.")
    sys.exit(1)

from gi.repository import GObject, Gtk, GLib, Gdk, Gio, GnomeDesktop
from gi.repository import Pango, PangoCairo, GdkPixbuf

try:
    import pygments
except ImportError:
    pygments = None
else:
    from pygments.lexers import get_lexer_for_filename, get_lexer_for_mimetype
    from pygments import highlight
    from pygments.formatters import ImageFormatter

try:
    import chardet
except ImportError:
    chardet = None

from . import config
from .config import get_data_path, get_icon_path
from .sources import INTERPRETATION_UNKNOWN, MANIFESTATION_UNKNOWN

from zeitgeist.datamodel import Interpretation, Event, Subject


THUMBS = collections.defaultdict(dict)
ICONS = collections.defaultdict(dict)
ICON_SIZES = SIZE_NORMAL, SIZE_LARGE = ((128, 128), (256, 256))

# Thumbview and Timelineview sizes
SIZE_THUMBVIEW = [(48+32, 36+32), (96+32, 72+32), (150+32, 108+32), (256+32, 192+32)]
SIZE_TIMELINEVIEW = [(16, 12), (32, 24), (64, 48), (96, 72)]
SIZE_TEXT_TIMELINEVIEW = ["small", "medium", "large", "large"]
SIZE_TEXT_THUMBVIEW = ["x-small", "small", "medium", "large"]

APP_ICON_NORMAL = "hicolor/scalable/apps/org.gnome.ActivityJournal.svg"
APP_ICON_PAUSED = "hicolor/scalable/apps/org.gnome.ActivityJournal-paused.svg"

THUMBNAIL_FACTORIES = {
    SIZE_NORMAL: GnomeDesktop.DesktopThumbnailFactory.new(GnomeDesktop.DesktopThumbnailSize.NORMAL),
    SIZE_LARGE: GnomeDesktop.DesktopThumbnailFactory.new(GnomeDesktop.DesktopThumbnailSize.LARGE)
}
ICON_THEME = Gtk.IconTheme.get_default()

# Caches desktop files
DESKTOP_FILES = {}
DESKTOP_FILE_PATHS = []
try:
    desktop_file_paths = os.environ["XDG_DATA_DIRS"].split(":")
    for path in desktop_file_paths:
        if path.endswith("/"):
            DESKTOP_FILE_PATHS.append(path + "applications/")
        else:
            DESKTOP_FILE_PATHS.append(path + "/applications/")
except KeyError:pass

# Placeholder pixbufs for common sizes
PLACEHOLDER_PIXBUFFS = {
    24 : GdkPixbuf.Pixbuf.new_from_file_at_size(get_icon_path(APP_ICON_NORMAL), 24, 24),
    16 : GdkPixbuf.Pixbuf.new_from_file_at_size(get_icon_path(APP_ICON_NORMAL), 16, 16)
    }

# Color magic
TANGOCOLORS = [
    (252/255.0, 234/255.0,  79/255.0),#0
    (237/255.0, 212/255.0,   0/255.0),
    (196/255.0, 160/255.0,   0/255.0),

    (252/255.0, 175/255.0,  62/255.0),#3
    (245/255.0, 121/255.0,   0/255.0),
    (206/255.0,  92/255.0,   0/255.0),

    (233/255.0, 185/255.0, 110/255.0),#6
    (193/255.0, 125/255.0,  17/255.0),
    (143/255.0,  89/255.0,   2/255.0),

    (138/255.0, 226/255.0,  52/255.0),#9
    (115/255.0, 210/255.0,  22/255.0),
    ( 78/255.0, 154/255.0,   6/255.0),

    (114/255.0, 159/255.0, 207/255.0),#12
    ( 52/255.0, 101/255.0, 164/255.0),
    ( 32/255.0,  74/255.0, 135/255.0),

    (173/255.0, 127/255.0, 168/255.0),#15
    (117/255.0,  80/255.0, 123/255.0),
    ( 92/255.0,  53/255.0, 102/255.0),

    (239/255.0,  41/255.0,  41/255.0),#18
    (204/255.0,   0/255.0,   0/255.0),
    (164/255.0,   0/255.0,   0/255.0),

    (136/255.0, 138/255.0, 133/255.0),#21
    ( 85/255.0,  87/255.0,  83/255.0),
    ( 46/255.0,  52/255.0,  54/255.0),
]

FILETYPES = {
    Interpretation.VIDEO.uri : 0,
    Interpretation.AUDIO.uri : 3,
    Interpretation.DOCUMENT.uri : 12,
    Interpretation.IMAGE.uri : 15,
    Interpretation.SOURCE_CODE.uri : 12,
    INTERPRETATION_UNKNOWN : 21,
    Interpretation.IMMESSAGE.uri : 21,
    Interpretation.EMAIL.uri : 21
}

FILETYPESNAMES = {
    Interpretation.VIDEO.uri : _("Video"),
    Interpretation.AUDIO.uri : _("Music"),
    Interpretation.DOCUMENT.uri : _("Document"),
    Interpretation.IMAGE.uri : _("Image"),
    Interpretation.SOURCE_CODE.uri : _("Source Code"),
    INTERPRETATION_UNKNOWN : _("Unknown"),
    Interpretation.IMMESSAGE.uri : _("IM Message"),
    Interpretation.EMAIL.uri :_("Email"),

}

INTERPRETATION_PARENTS = {
    Interpretation.DOCUMENT.uri : Interpretation.DOCUMENT.uri,
    Interpretation.TEXT_DOCUMENT.uri : Interpretation.DOCUMENT.uri,
    Interpretation.PAGINATED_TEXT_DOCUMENT.uri : Interpretation.DOCUMENT.uri,
    Interpretation.RASTER_IMAGE.uri : Interpretation.IMAGE.uri,
    Interpretation.VECTOR_IMAGE.uri : Interpretation.IMAGE.uri,
    Interpretation.IMAGE.uri : Interpretation.IMAGE.uri,
    Interpretation.AUDIO.uri : Interpretation.AUDIO.uri,
    Interpretation.MUSIC_PIECE.uri : Interpretation.MUSIC_PIECE.uri,
}

MEDIAINTERPRETATIONS = [
    Interpretation.VIDEO.uri,
    Interpretation.IMAGE.uri,
]

def get_hex_color(color):
    return ("#%x%x%x" % (int(color.red * 255), int(color.green * 255), int(color.blue * 255)))

def get_file_color(ftype, fmime):
    """Uses hashing to choose a shade from a hue in the color tuple above

    :param ftype: a :class:`Event <zeitgeist.datamodel.Interpretation>`
    :param fmime: a mime type string
    """
    if ftype in list(FILETYPES.keys()):
        i = FILETYPES[ftype]
        l = int(math.fabs(hash(fmime))) % 3
        return TANGOCOLORS[min(i+l, len(TANGOCOLORS)-1)]
    return (136/255.0, 138/255.0, 133/255.0)

def get_text_or_uri(obj, ellipsize=True, molteplicity=False):
    if molteplicity and obj.molteplicity:
        text = obj.molteplicity_text
    else:
        text = str(obj.text.replace("&", "&amp;"))
    if text.strip() == "":
        text = str(obj.uri.replace("&", "&amp;"))
        text = urllib.parse.unquote(text)
    if len(text) > 50 and ellipsize: text = text[:50] + "..." #ellipsize--it's ugly!
    return text

##
## Zeitgeist event helper functions

def get_event_typename(event):
    """
    :param event: a :class:`Event <zeitgeist.datamodel.Event>`

    :returns: a plain text version of a interpretation
    """
    try:
        return Interpretation[event.subjects[0].interpretation].display_name
    except KeyError:
        pass
    return FILETYPESNAMES[event.subjects[0].interpretation]

##
# Cairo drawing functions

def draw_frame(context, x, y, w, h):
    """
    Draws a 2 pixel frame around a area defined by x, y, w, h using a cairo context

    :param context: a cairo context
    :param x: x position of the frame
    :param y: y position of the frame
    :param w: width of the frame
    :param h: height of the frame
    """
    x, y = int(x)+0.5, int(y)+0.5
    w, h = int(w), int(h)
    context.set_line_width(1)
    context.rectangle(x-1, y-1, w+2, h+2)
    context.set_source_rgba(0.5, 0.5, 0.5)#0.3, 0.3, 0.3)
    context.stroke()

    return

    # FixMe: The above frame looks good enough, and we don't the
    # extras below. So, we're returning there for now. Revisit this, if
    # necessary.
    context.set_source_rgba(0.7, 0.7, 0.7)
    context.rectangle(x, y, w, h)
    context.stroke()
    context.set_source_rgba(0.4, 0.4, 0.4)
    context.rectangle(x+1, y+1, w-2, h-2)
    context.stroke()

def draw_rounded_rectangle(context, x, y, w, h, r=5):
    """Draws a rounded rectangle

    :param context: a cairo context
    :param x: x position of the rectangle
    :param y: y position of the rectangle
    :param w: width of the rectangle
    :param h: height of the rectangle
    :param r: radius of the rectangle
    """
    context.new_sub_path()
    context.arc(r+x, r+y, r, math.pi, 3 * math.pi /2)
    context.arc(w-r+x, r+y, r, 3 * math.pi / 2, 0)
    context.arc(w-r+x, h-r+y, r, 0, math.pi/2)
    context.arc(r+x, h-r+y, r, math.pi/2, math.pi)
    context.close_path()
    return context

def draw_speech_bubble(context, layout, x, y, w, h):
    """
    Draw a speech bubble at a position

    Arguments:
    :param context: a cairo context
    :param layout: a pango layout
    :param x: x position of the bubble
    :param y: y position of the bubble
    :param w: width of the bubble
    :param h: height of the bubble
    """
    layout.set_width((w-10)*1024)
    layout.set_ellipsize(Pango.EllipsizeMode.MIDDLE)
    textw, texth = layout.get_pixel_size()
    context.new_path()
    context.move_to(x + 0.45*w, y+h*0.1 + 2)
    context.line_to(x + 0.5*w, y)
    context.line_to(x + 0.55*w, y+h*0.1 + 2)
    h = max(texth + 5, h)
    draw_rounded_rectangle(context, x, y+h*0.1, w, h, r = 5)
    context.close_path()
    context.set_line_width(2)
    context.set_source_rgb(168/255.0, 165/255.0, 134/255.0)
    context.stroke_preserve()
    context.set_source_rgb(253/255.0, 248/255.0, 202/255.0)
    context.fill()
    context.set_source_rgb(0, 0, 0)
    context.move_to(x+5, y+5)
    PangoCairo.update_layout(context, layout)
    PangoCairo.show_layout(context, layout)

def draw_text(context, layout, markup, x, y, maxw = 0, color = (0.3, 0.3, 0.3)):
    """
    Draw text using a cairo context and a pango layout

    Arguments:
    :param context: a cairo context
    :param layout: a pango layout
    :param x: x position of the bubble
    :param y: y position of the bubble
    :param maxw: the max text width in pixels
    :param color: a rgb tuple
    """
    layout.set_markup(markup)
    layout.set_ellipsize(Pango.EllipsizeMode.END)
    context.set_source_rgba(*color)
    if maxw:
        layout.set_width(maxw*1024)
    context.move_to(x, y)
    PangoCairo.update_layout(context, layout)
    PangoCairo.show_layout(context, layout)
    context.stroke()

def render_pixbuf(context, x, y, pixbuf, w, h, drawframe = True):
    """
    Renders a pixbuf to be displayed on the cell

    Arguments:
    :param context: a cairo context
    :param x: x position
    :param y: y position
    :param drawframe: if true we draw a frame around the pixbuf
    """
    context.save()
    context.rectangle(x, y, w, h)
    if drawframe:
        context.set_source_rgb(1, 1, 1)
        context.fill_preserve()
    Gdk.cairo_set_source_pixbuf (context, pixbuf, x, y)
    context.clip()
    context.paint()
    context.restore();
    if drawframe: # Draw a pretty frame
        draw_frame(context, x, y, w, h)

def render_emblems(context, x, y, w, h, emblems, emblem_size):
    """
    Renders emblems on the four corners of the rectangle

    Arguments:
    :param context: a cairo context
    :param x: x position
    :param y: y position
    :param w: the width of the rectangle
    :param y: the height of the rectangle
    :param emblems: a list of pixbufs
    """
    # w = max(self.width, w)
    corners = [[x, y],
               [x+w, y],
               [x, y+h],
               [x+w, y+h]]
    for i in range(len(emblems)):
        i = i % len(emblems)
        pixbuf = emblems[i]

        if pixbuf:
            # FixMe: Optimize
            if pixbuf.get_width() != emblem_size:
                pixbuf = pixbuf.scale_simple(emblem_size, emblem_size, GdkPixbuf.InterpType.BILINEAR)

            pbw, pbh = pixbuf.get_width(), pixbuf.get_height()
            pixbuf_x = corners[i][0] - pbw/2
            pixbuf_y = corners[i][1] - pbh/2

            Gdk.cairo_set_source_pixbuf(context, pixbuf, pixbuf_x, pixbuf_y)
            context.rectangle(pixbuf_x, pixbuf_y, pbw, pbh)
            context.fill()

def render_molteplicity(context, x, y, w, h, molteplicity, style, radius):
    """
    Renders the molteplicity number on the top-right corner of the rectangle

    Arguments:
    :param context: a cairo context
    :param x: x position
    :param y: y position
    :param w: the width of the rectangle
    :param y: the height of the rectangle
    :param emblems: the molteplicity number
    """
    center = [x + w, y ]

    color = get_gtk_rgba(style, "bg", 2)
    text_color = Gdk.RGBA(1, 1, 1, 1)

    # for border
    radius -= 1

    context.set_source_rgba(*color)
    context.arc(center[0], center[1], radius , 0, 2 * math.pi)
    context.fill_preserve()
    context.set_source_rgba(*color)
    context.stroke()

    context.set_source_rgba(*text_color)
    context.select_font_face("Cantarell", cairo.FontSlant.NORMAL,
            cairo.FontWeight.BOLD)

    if molteplicity < 10:
        font_size = radius
    elif molteplicity < 100:
        font_size = radius - 2
    else:
        font_size = radius - 3

    context.set_font_size(font_size)

    if molteplicity >= 100:
        text = "99+"
    else:
        text = str(molteplicity)

    x_bearing, y_bearing, width, height = context.text_extents(text)[:4]
    context.move_to(center[0] - width / 2 - x_bearing, center[1] - height / 2 - y_bearing)
    context.show_text(text)


##
## Color functions

def get_gdk_rgba_from_string(color_string):
    rgba = Gdk.RGBA()
    Gdk.RGBA.parse(rgba, color_string)

    return rgba

def shade_gdk_color(color, shade):
    """
    Shades a color by a fraction

    Arguments:
    :param color: a gdk color
    :param shade: fraction by which to shade the color

    :returns: a :class:`Color <Gdk.Color>`
    """
    f = lambda num: min((num * shade, 1.0))

    color.red = f(color.red)
    color.green = f(color.green)
    color.blue = f(color.blue)

    return color

def combine_gdk_color(color, fcolor):
    """
    Combines a color with another color

    Arguments:
    :param color: a gdk color
    :param fcolor: a gdk color to combine with color

    :returns: a :class:`Color <Gdk.Color>`
    """
    color.red = (2*color.red + fcolor.red)/3
    color.green = (2*color.green + fcolor.green)/3
    color.blue = (2*color.blue + fcolor.blue)/3

    return color

def get_histogram_default_search_color(style):
    pal = get_gtk_rgba(style, "bg", 3, 1.2)
    return Gdk.RGBA(pal.blue, pal.green, pal.red, 1)

def get_histogram_search_color(style):
    if config.SEARCH_RESULTS_USE_DEFAULT_COLOR:
        color = get_histogram_default_search_color(style)
    else:
        color = get_gdk_rgba_from_string(config.SEARCH_RESULTS_HISTOGRAM_COLOR)

    return color

def get_color_by_name(style, name):
    (found, bg) = style.lookup_color(name)

    if found:
        return bg

    raise Exception ("Color '%s' not found" % name)

def get_gtk_rgba(style, palette, i, shade = 1, alpha = 1):
    """Takes a gtk style and returns a RGB tuple

    Arguments:
    :param style: a gtk_style object
    :param palette: a string representing the palette you want to pull a color from
        Example: "bg", "fg"
    :param shade: how much you want to shade the color

    :returns: a rgba tuple
    """
    f = lambda num: (num) * shade

    color = None

    # FixMe: move to external css file
    if palette == "bg":
        if i == 0: # normal
            color = get_color_by_name(style, "theme_bg_color")
        elif i == 1: # prelight
            color = get_color_by_name(style, "theme_bg_color")
        elif i == 2: # selected
            color = get_color_by_name(style, "theme_selected_bg_color")
        elif i == 3: # insensitive
            color = get_color_by_name(style, "theme_selected_bg_color")
    elif palette == "fg":
        if i == 0:
            color = get_color_by_name(style, "theme_fg_color")
    elif palette == "text":
        if i == 0:
            color = get_color_by_name(style, "theme_text_color")
        elif i == 2: # selected
            color = get_color_by_name(style, "theme_text_color")
        elif i == 3: # insensitive
            color = get_color_by_name(style, "insensitive_base_color")
        elif i == 4: # active
            color = get_color_by_name(style, "theme_bg_color")
    elif palette == "base":
        if i == 0:
            color = get_color_by_name(style, "theme_base_color")
        elif i == 2: # selected
            color = get_color_by_name(style, "theme_selected_bg_color")
    else:
        raise Exception("Unexpected pallete value: %s" % (palette))

    if not color:
        raise Exception("Missing color for specified palette and index")

    if isinstance(color, Gdk.RGBA):
        red = f(color.red)
        green = f(color.green)
        blue = f(color.blue)

        return Gdk.RGBA(red=min(red, 1), green=min(green, 1), blue=min(blue, 1), alpha=alpha)
    else:
        raise TypeError("Not a valid Gdk.RGBA color")


##
## Pixbuff work
##

def make_icon_frame(pixbuf, blend=False, border=1, color=0x000000ff):
    """creates a new Pixmap which contains 'pixbuf' with a border at given
    size around it."""
    result = GdkPixbuf.Pixbuf.new(pixbuf.get_colorspace(),
                            True,
                            pixbuf.get_bits_per_sample(),
                            pixbuf.get_width(),
                            pixbuf.get_height())
    result.fill(color)
    if blend:
        pixbuf.composite(result, 0.5, 0.5,
                        pixbuf.get_width(), pixbuf.get_height(),
                        0.5, 0.5,
                        0.5, 0.5,
                        GdkPixbuf.InterpType.NEAREST,
                        256)
    pixbuf.copy_area(border, border,
                    pixbuf.get_width() - (border * 2), pixbuf.get_height() - (border * 2),
                    result,
                    border, border)
    return result

def add_background(pixbuf, color=0xffffffff):
    """ adds a background with given color to the pixbuf and returns the result
    as new Pixbuf"""
    result = GdkPixbuf.Pixbuf.new(pixbuf.get_colorspace(),
                            True,
                            pixbuf.get_bits_per_sample(),
                            pixbuf.get_width(),
                            pixbuf.get_height())
    result.fill(color)
    pixbuf.composite(result, 0, 0,
                        pixbuf.get_width(), pixbuf.get_height(),
                        0, 0,
                        1, 1,
                        GdkPixbuf.InterpType.NEAREST,
                        255)
    return result

def create_opendocument_thumb(path):
    """ extracts the thumbnail of an Opendocument document as pixbuf """
    thumb = tempfile.NamedTemporaryFile()
    try:
        f = zipfile.ZipFile(path, mode="r")
        try:
            thumb.write(f.read("Thumbnails/thumbnail.png"))
        except KeyError:
            thumb.close()
            return None
        finally:
            f.close()
    except IOError:
        thumb.close()
        return None
    thumb.flush()
    thumb.seek(0)
    try:
        pixbuf = GdkPixbuf.Pixbuf.new_from_file(thumb.name)
    except GLib.GError:
        return None # (LP: #650917)
    thumb.close()
    return add_background(pixbuf)

def __crop_pixbuf(pixbuf, x, y, size=SIZE_LARGE):
    """ returns a part of the given pixbuf as new one """
    result = GdkPixbuf.Pixbuf.new(pixbuf.get_colorspace(),
                            True,
                            pixbuf.get_bits_per_sample(),
                            size[0], size[1])
    pixbuf.copy_area(x, y, x+size[0], y+size[1], result, 0, 0)
    return result

def create_text_thumb(gio_file, size=None, threshold=2):
    """ tries to use pygments to get a thumbnail of a text file """
    if pygments is None:
        return None
    try:
        lexer = get_lexer_for_mimetype(gio_file.mime_type)
    except pygments.util.ClassNotFound:
        lexer = get_lexer_for_mimetype("text/plain")
    if chardet:
        lexer.encoding = "chardet"
    thumb = tempfile.NamedTemporaryFile()
    formatter = ImageFormatter(font_name="DejaVu Sans Mono", line_numbers=False, font_size=10)
    # to speed things up only highlight the first 20 lines
    content = "\n".join(gio_file.get_content().split("\n")[:20])
    try:
        content = highlight(content, lexer, formatter)
    except (UnicodeDecodeError, TypeError):
        # we can't create the pixbuf
        return None
    thumb.write(content)
    thumb.flush()
    thumb.seek(0)
    try:
        pixbuf = GdkPixbuf.Pixbuf.new_from_file(thumb.name)
    except GLib.GError:
        return None # (LP: #743125)
    thumb.close()
    if size is not None:
        new_height = None
        new_width = None
        height = pixbuf.get_height()
        width = pixbuf.get_width()
        if width > threshold*size[0]:
            new_width = threshold*size[0]
        if height > threshold*size[1]:
            new_height = threshold*size[1]
        if new_height is not None or new_width is not None:
            pixbuf = __crop_pixbuf(pixbuf, 0, 0, (new_width or width, new_height or height))
    return pixbuf


def new_grayscale_pixbuf(pixbuf):
    """
    Makes a pixbuf grayscale

    :param pixbuf: a :class:`Pixbuf <GdkPixbuf>`

    :returns: a :class:`Pixbuf <GdkPixbuf>`

    """
    pixbuf2 = pixbuf.copy()
    pixbuf.saturate_and_pixelate(pixbuf2, 0.0, False)
    return pixbuf2

def crop_pixbuf(pixbuf, x, y, width, height):
    """
    Crop a pixbuf

    Arguments:
    :param pixbuf: a :class:`Pixbuf <GdkPixbuf>`
    :param x: the x position to crop from in the source
    :param y: the y position to crop from in the source
    :param width: crop width
    :param height: crop height

    :returns: a :class:`Pixbuf <GdkPixbuf>`
    """
    dest_pixbuf = GdkPixbuf.Pixbuf.new(GdkPixbuf.Colorspace.RGB, True, 8, width, height)
    pixbuf.copy_area(x, y, width, height, dest_pixbuf, 0, 0)
    return dest_pixbuf

def scale_to_fill(pixbuf, neww, newh):
    """
    Scales/crops a new pixbuf to a width and height at best fit and returns it

    Arguments:
    :param pixbuf: a :class:`Pixbuf <GdkPixbuf>`
    :param neww: new width of the new pixbuf
    :param newh: a new height of the new pixbuf

    :returns: a :class:`Pixbuf <GdkPixbuf>`
    """
    imagew, imageh = pixbuf.get_width(), pixbuf.get_height()
    if (imagew, imageh) != (neww, newh):
        imageratio = float(imagew) / float(imageh)
        newratio = float(neww) / float(newh)
        if imageratio > newratio:
            transformw = int(round(newh * imageratio))
            pixbuf = pixbuf.scale_simple(transformw, newh, GdkPixbuf.InterpType.BILINEAR)
            pixbuf = crop_pixbuf(pixbuf, 0, 0, neww, newh)
        elif imageratio < newratio:
            transformh = int(round(neww / imageratio))
            pixbuf = pixbuf.scale_simple(neww, transformh, GdkPixbuf.InterpType.BILINEAR)
            pixbuf = crop_pixbuf(pixbuf, 0, 0, neww, newh)
        else:
            pixbuf = pixbuf.scale_simple(neww, newh, GdkPixbuf.InterpType.BILINEAR)
    return pixbuf

def scale_to_center(pixbuf, neww, newh):
    """
    Scales and centers a new pixbuf to a width and height at best fit and returns it

    Arguments:
    :param pixbuf: a :class:`Pixbuf <GdkPixbuf>`
    :param neww: new width of the new pixbuf
    :param newh: new height of the new pixbuf

    :returns: a :class:`Pixbuf <GdkPixbuf>`
    """

    imagew, imageh = pixbuf.get_width(), pixbuf.get_height()
    if (imagew, imageh) == (neww, newh):
        return pixbuf

    dest_pixbuf = GdkPixbuf.Pixbuf.new(GdkPixbuf.Colorspace.RGB, True, 8, neww, newh)
    dest_pixbuf.fill(0xFFFFFFFF)

    # scale down pixbuf if required, preserving aspect ratio
    if imagew > neww or imageh > newh:
        if (imageh * neww) > (imagew * newh):
            imagew = int(0.5 + imagew * (newh / imageh))
            imageh = newh
        else:
            imageh = int(0.5 + imageh * (neww / imagew))
            imagew = neww

        pixbuf = pixbuf.scale_simple(imagew, imageh, GdkPixbuf.InterpType.BILINEAR)

    # center pixbuf in destination pixbuf
    imagew, imageh = pixbuf.get_width(), pixbuf.get_height()
    dest_x = int((neww - imagew)/2)
    dest_y = int((newh - imageh)/2)
    pixbuf.copy_area(0, 0, imagew, imageh, dest_pixbuf, dest_x ,dest_y)

    return dest_pixbuf

class PixbufCache(dict):
    """
    A pixbuf cache dict which stores, loads, and saves pixbufs to a cache and to
    the users filesystem. The naming scheme for thumb files is based on hash()

    There are huge flaws with this object. It does not have a ceiling, and it
    does not remove thumbnails from the file system. Essentially meaning the
    cache directory can grow forever.
    """
    def __init__(self, *args, **kwargs):
        super(PixbufCache, self).__init__()

    def check_cache(self, uri):
        return self[uri]

    def get_buff(self, key):
        thumbpath = os.path.expanduser("~/.cache/gnome-activity-journal/1_" + str(hash(key)))
        if os.path.exists(thumbpath):
            self[key] = (GdkPixbuf.Pixbuf.new_from_file(thumbpath), True)
            return self[key]
        return None

    def __getitem__(self, key):
        if key in self: # FixMe: revisit this
            return super(PixbufCache, self).__getitem__(key)
        return self.get_buff(key)

    def __setitem__(self, key, value):
        (pb, isthumb) = value
        dir_ = os.path.expanduser("~/.cache/gnome-activity-journal/")
        if not os.path.exists(os.path.expanduser("~/.cache/gnome-activity-journal/")):
            os.makedirs(dir_)
        path = dir_ + str(hash(isthumb)) + "_" + str(hash(key))
        if not os.path.exists(path):
            open(path, 'w').close()
            pb.savev(path, "png", [], [])
        return super(PixbufCache, self).__setitem__(key, (pb, isthumb))

    def get_pixbuf_from_uri(self, uri, size=SIZE_LARGE, iconscale=1):
        """
        Returns a pixbuf and True if a thumbnail was found, else False. Uses the
        Pixbuf Cache for thumbnail compatible files. If the pixbuf is a thumb
        it is cached.

        Arguments:
        :param uri: a uri on the disk
        :param size: a size tuple from thumbfactory
        :param iconscale: a factor to reduce icons by (not thumbs)
        :param w: resulting width
        :param h: resulting height

        Warning! This function is in need of a serious clean up.

        :returns: a tuple containing a :class:`Pixbuf <GdkPixbuf>` and bool
        which is True if a thumbnail was found
        """
        try:
            cached = self.check_cache(uri)
        except GLib.GError:
            cached = None
        if cached:
            return cached
        gfile = GioFile.create(uri)
        thumb = True

        if gfile:
            if gfile.has_preview():
                pb = gfile.get_thumbnail(size=size)
                if pb is None and "audio-x-generic" in gfile.icon_names:
                    pb = gfile.get_icon(size=SIZE_NORMAL[0])
                    thumb = False
            else:
                pb = gfile.get_icon(size=SIZE_NORMAL[0])
                thumb = False
        else: pb = None
        if not pb:
            pb = ICON_THEME.lookup_icon(Gtk.STOCK_MISSING_IMAGE, SIZE_NORMAL[0], Gtk.IconLookupFlags.FORCE_SVG).load_icon()
            thumb = False
        if thumb:
            if "audio-x-generic" in gfile.icon_names: thumb = False
            self[uri] = (pb, thumb)
        return pb, thumb

PIXBUFCACHE = PixbufCache()


def get_icon_for_name(name, size):
    """
    return a icon for a name
    """
    size = int(size)
    ICONS[(size, size)]
    if name in ICONS[(size, size)]:
        return ICONS[(size, size)][name]
    info = ICON_THEME.lookup_icon(name, size, Gtk.IconLookupFlags.USE_BUILTIN)
    if not info:
        return None
    location = info.get_filename()
    return get_icon_for_uri(location, size)

def get_icon_for_uri(uri, size):
    if uri in ICONS[(size, size)]:
        return ICONS[(size, size)][uri]
    pixbuf = GdkPixbuf.Pixbuf.new_from_file_at_size(uri, size, size)
    ICONS[(size, size)][uri] = pixbuf
    return pixbuf

def get_icon_from_object_at_uri(uri, size):
    """
    Returns a icon from a event at size

    :param uri: a uri string
    :param size: a int representing the size in pixels of the icon

    :returns: a :class:`Pixbuf <GdkPixbuf>`
    """
    gfile = GioFile.create(uri)
    if gfile:
        pb = gfile.get_icon(size=size)
        if pb:
            return pb
    return False


##
## Other useful methods
##

def get_menu_item_with_stock_id_and_text(stock_id, text):
    item = Gtk.MenuItem.new_with_mnemonic (text)
    return item


def is_command_available(command):
    """
    Checks whether the given command is available, by looking for it in
    the PATH.

    This is useful for ensuring that optional dependencies on external
    applications are fulfilled.
    """
    assert len(" a".split()) == 1, "No arguments are accepted in command"
    for directory in os.environ["PATH"].split(os.pathsep):
        if os.path.exists(os.path.join(directory, command)):
            return True
    return False

def launch_command(command, arguments=None):
    """
    Launches a program as an independent process.
    """
    if not arguments:
        arguments = []
    null = os.open(os.devnull, os.O_RDWR)
    subprocess.Popen([command] + arguments, stdout=null, stderr=null,
                     close_fds=True)

def launch_string_command(command):
    """
    Launches a program as an independent from a string
    """
    command = command.split(" ")
    null = os.open(os.devnull, os.O_RDWR)
    subprocess.Popen(command, stdout=null, stderr=null,
                     close_fds=True)

##
## GioFile
##

GIO_FILES = {}

class GioFile(object):

    def __new__(classtype, *args, **kwargs):
        # Check to see if a __single exists already for this class
        # Compare class types instead of just looking for None so
        # that subclasses will create their own __single objects
        subj = args[0][1][0][0]
        if subj not in GIO_FILES:
            GIO_FILES[subj] = object.__new__(classtype)
        return GIO_FILES[subj]

    @classmethod
    def create(cls, path):
        """ save method to create a GioFile object, if a file does not exist
        None is returned"""
        try:
            if path not in GIO_FILES:
                GIO_FILES[path] = cls(path)
            return GIO_FILES[path]
        except IOError as e:
            return None

    def __init__(self, path):
        self._file_object = Gio.File.new_for_uri(path) #fixme: naming
        self._file_info = self._file_object.query_info(
            "standard::content-type,standard::icon,time::modified",
            Gio.FileQueryInfoFlags.NONE, None)
        self._file_annotation = self._file_object.query_info(
            "metadata::annotation",
            Gio.FileQueryInfoFlags.NONE, None)

    @property
    def mime_type(self):
        return self._file_info.get_attribute_string("standard::content-type")

    @property
    def mtime(self):
        return self._file_info.get_attribute_uint64("time::modified")

    @property
    def basename(self):
        return self._file_object.get_basename()

    @property
    def uri(self):
        return self._file_object.get_uri()

    def get_annotation(self):
        self.refresh_annotation()
        return self._file_annotation.get_attribute_as_string("metadata::annotation")

    def set_annotation(self, annotation):
        self._file_annotation.set_attribute_string("metadata::annotation", annotation)
        self._file_object.set_attributes_from_info(self._file_annotation,
                                                   Gio.FileQueryInfoFlags.NONE,
                                                   None)

    annotation = property(get_annotation, set_annotation)

    def get_content(self):
        f = open(self._file_object.get_path())
        try:
            content = f.read()
        finally:
            f.close()
        return content

    @property
    def icon_names(self):
        try:
            return self._file_info.get_attribute_object("standard::icon").get_names()
        except AttributeError:
            return list()

    def get_thumbnail(self, size=SIZE_NORMAL, border=0):
        assert size in ICON_SIZES
        try:
            thumb = THUMBS[size][self.uri]
        except KeyError:
            factory = THUMBNAIL_FACTORIES[size]
            location = factory.lookup(self.uri, self.mtime)
            if location:
                thumb, mtime = THUMBS[size][self.uri] = \
                    (GdkPixbuf.Pixbuf.new_from_file(location), self.mtime)
            else:
                if factory.has_valid_failed_thumbnail(self.uri, self.mtime):
                    thumb = THUMBS[size][self.uri] = None
                else:
                    thumb = factory.generate_thumbnail(self.uri, self.mime_type)
                    if thumb is None:
                        # maybe we are able to use a custom thumbnailer here
                        if [name for name in self.icon_names if "application-vnd.oasis.opendocument" in name]:
                            thumb = create_opendocument_thumb(self._file_object.get_path())
                        elif "text-x-generic" in self.icon_names or "text-x-script" in self.icon_names:
                            try:
                                thumb = create_text_thumb(self, size, 1)
                            except Exception:
                                thumb = None
                        elif "audio-x-generic" in self.icon_names:
                            pass
                            #FIXME
                            #thumb = self.get_audio_cover(size)
                    if thumb is None:
                        factory.create_failed_thumbnail(self.uri, self.mtime)
                    else:
                        width, height = thumb.get_width(), thumb.get_height()
                        if width > size[0] or height > size[1]:
                            scale = min(float(size[0])/width, float(size[1])/height)
                            thumb = thumb.scale_simple(int(scale*width), int(scale*height), GdkPixbuf.InterpType.BILINEAR)
                        factory.save_thumbnail(thumb, self.uri, self.mtime)
                        THUMBS[size][self.uri] = (thumb, self.mtime)
        else:
            if thumb is not None:
                if thumb[1] != self.mtime:
                    del THUMBS[size][self.uri]
                    return self.get_thumbnail(size, border)
                thumb = thumb[0]
        if thumb is not None and border:
            thumb = make_icon_frame(thumb, border=border, color=0x00000080)
        return thumb

    def get_audio_cover(self, size):
        """
        Try to get a cover art in the folder of the song.
        It's s simple hack, but i think it's a good and quick compromise.
        """
        pix = None
        dirname = os.path.dirname(self.event.subjects[0].uri)
        dirname = urllib.parse.unquote(dirname)[7:]
        if not os.path.exists(dirname): return pix
        for f in os.listdir(dirname):
            if f.endswith(".jpg") or f.endswith(".jpeg"):
                path = dirname + os.sep + f
                pix = GdkPixbuf.Pixbuf.new_from_file(path)
                return pix

        return pix

    @property
    def thumbnail(self):
        return self.get_thumbnail()

    def get_monitor(self):
        return self._file_object.monitor_file()

    def refresh(self):
        self._file_info = self._file_object.query_info(
            "standard::content-type,standard::icon,time::modified",
            Gio.FileQueryInfoFlags.NONE, None)

    def refresh_annotation(self):
        self._file_annotation = self._file_object.query_info(
            "metadata::annotation",
            Gio.FileQueryInfoFlags.NONE, None)

    def get_icon(self, size=24, can_thumb=False, border=0):
        icon = None
        if can_thumb:
            # let's try to find a thumbnail
            # we only allow thumbnails as icons for a few content types
            if self.thumb_icon_allowed():
                if (size,) < SIZE_NORMAL:
                    thumb_size = SIZE_NORMAL
                else:
                    thumb_size = SIZE_LARGE
                thumb = self.get_thumbnail(size=thumb_size)
                if thumb:
                    s = float(size)
                    width = thumb.get_width()
                    height = thumb.get_height()
                    scale = min(s/width, s/height)
                    icon = thumb.scale_simple(int(width*scale), int(height*scale), GdkPixbuf.InterpType.NEAREST)
        if icon is None:
            try:
                return ICONS[size][self.uri]
            except KeyError:
                for name in self.icon_names:
                    info = ICON_THEME.lookup_icon(name, size, Gtk.IconLookupFlags.USE_BUILTIN)
                    if info is None:
                        continue
                    location = info.get_filename()
                    if not location:
                        continue # (LP: #722227)
                    icon = GdkPixbuf.Pixbuf.new_from_file_at_size(location, size, size)
                    if icon:
                        break
        ICONS[size][self.uri] = icon
        if icon is not None and border:
            icon = make_icon_frame(icon, border=border, color=0x00000080)
        return icon

    @property
    def icon(self):
        return self.get_icon()

    def launch(self):
        appinfo = Gio.AppInfo.get_default_for_type(self.mime_type, False)
        appinfo.launch([self._file_object,], None)

    def has_preview(self):
        icon_names = self.icon_names
        is_opendocument = [name for name in icon_names if "application-vnd.oasis.opendocument" in name]
        return "video-x-generic" in icon_names \
            or "image-x-generic" in icon_names \
            or "audio-x-generic" in icon_names \
            or "application-pdf" in icon_names \
            or (("text-x-generic" in icon_names or "text-x-script" in icon_names) and pygments is not None) \
            or is_opendocument

    def thumb_icon_allowed(self):
        icon_names = self.icon_names
        is_opendocument = [name for name in icon_names if "application-vnd.oasis.opendocument" in name]
        return "video-x-generic" in icon_names \
            or "image-x-generic" in icon_names \
            or "application-pdf" in icon_names \
            or is_opendocument

    def __eq__(self, other):
        if not isinstance(other, GioFile):
            return False
        return self.uri == other.uri

class DayParts:

    TIMELABELS = [_("Early Morning"), _("Morning"), _("Afternoon"), _("Evening"), _("Night")]
    CHANGEHOURS = config.CHANGEHOURS

    @classmethod
    def _local_minimum(cls, hour):
        for i in range(1, len(cls.CHANGEHOURS)):
            if hour < cls.CHANGEHOURS[i]:
                return i - 1
        return len(cls.CHANGEHOURS) - 1

    @classmethod
    def get_day_parts(cls):
        return cls.TIMELABELS

    @classmethod
    def get_day_part_for_item(cls, item):
        t = time.localtime(int(item.event.timestamp) / 1000)
        return cls._local_minimum(t.tm_hour)

    @classmethod
    def get_day_part_range_for_item(cls, item):
        start = list(time.localtime(int(item.event.timestamp) / 1000))
        i = cls._local_minimum(start[3])
        start[3] = cls.CHANGEHOURS[i] # Reset hour
        start[4] = start[5] = 0 # Reset minutes and seconds
        end = list(start)
        end[3] = cls.CHANGEHOURS[i+1] if len(cls.CHANGEHOURS) > (i + 1) else 23
        end[4] = end[5] = 59
        return time.mktime(tuple(start)) * 1000, time.mktime(tuple(end)) * 1000

def ignore_exceptions(return_value_on_failure=None):
    def wrap(f):
        def safe_method(*args, **kwargs):
            try:
                return f(*args, **kwargs)
            except Exception as e:
                from traceback import print_exc
                print('\n --- Error running %s ---' % f.__name__)
                print_exc()
                print()
                return return_value_on_failure
        return safe_method
    return wrap

def update_cursor(widget, *args):
    gdk_display = Gdk.Display.get_default()

    if config.IN_ERASE_MODE:
        cursor = Gdk.Cursor.new(Gdk.CursorType.PIRATE)
        if not cursor:
            cursor = Gdk.Cursor.new_from_name(gdk_display, "cell")
    else:
        cursor = Gdk.Cursor.new(Gdk.CursorType.ARROW)
        if not cursor:
            cursor = Gdk.Cursor.new_from_name(gdk_display, "default")

    widget.props.window.set_cursor(cursor)

def get_readable_uri(uri):
    uri = Gio.File.new_for_uri(uri).get_uri()

    if uri.startswith("file:///"):
        uri = uri[7:]

    uri = GLib.uri_unescape_string(uri)

    return uri

def confirm_object_deletion(toplevel, uri, delete_all=False, count=0):
    uri = get_readable_uri(uri)
    uri = uri.replace("&", "&amp;")
    primary = _("Confirm event deletion ?")

    if delete_all:
        secondary = _("Delete all <b>%d</b> events for <b>%s</b> from activity journal") % (count, uri)
    else:
        secondary = _("Delete this event for <b>%s</b> from activity journal") % (uri)

    dialog = Gtk.MessageDialog(toplevel,
                               Gtk.DialogFlags.MODAL,
                               Gtk.MessageType.WARNING,
                               Gtk.ButtonsType.OK_CANCEL,
                               primary)

    if delete_all:
        ok_button = dialog.get_widget_for_response(Gtk.ResponseType.OK)
        sc = ok_button.get_style_context()
        sc.add_class("destructive-action")

    dialog.format_secondary_markup(secondary)
    response = dialog.run()
    dialog.destroy()

    return response == Gtk.ResponseType.OK

def update_default_search_colors():
    label = Gtk.Label()
    style = label.get_style_context()

    # save default text color to gsettings if it's
    # not customized
    if not config.SEARCH_RESULTS_TEXT_COLOR_UPDATED:
        text_rgba = get_gtk_rgba(style, "text", 0)
        color = Gdk.RGBA.to_string(text_rgba)
        config.preference_settings["search-results-text-color"] = color
        config.SEARCH_RESULTS_TEXT_COLOR = color

    if not config.SEARCH_RESULTS_HISTOGRAM_COLOR_UPDATED:
        # save default histogram color to gsettings if
        # it's not customized
        histogram_rgba = get_histogram_default_search_color(style)
        color = Gdk.RGBA.to_string(histogram_rgba)
        config.preference_settings["search-results-histogram-color"] = color
        config.SEARCH_RESULTS_HISTOGRAM_COLOR = color

def style_search_result_text(text):
    if config.SEARCH_RESULTS_USE_BOLD_STYLE:
        text = "<b>" + text + "</b>"
    if config.SEARCH_RESULTS_USE_LARGE_STYLE:
        text = "<span size='large'>" + text + "</span>"
    return text

# simplistic for now
def is_light_theme():
    label = Gtk.Label()
    style = label.get_style_context()
    theme_bg_color = c = get_gtk_rgba(style, "bg", 0)

    f = lambda num: (num * 255 > 200)
    return f(c.red) and f(c.green) and f(c.blue)

