# -*- coding: utf-8; -*-
"""
Copyright (C) 2018 Mario Aichinger <aichingm@gmail.com>
Copyright (C) 2007-2012 Lincoln de Sousa <lincoln@minaslivre.org>
Copyright (C) 2007 Gabriel Falcão <gabrielteratos@gmail.com>

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
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 enum
import logging
import os
import re
import subprocess
import time
import yaml

import cairo

import gi

gi.require_version("Gtk", "3.0")
gi.require_version("Gdk", "3.0")

from gi.repository import Gdk
from gi.repository import Gtk
from guake.globals import ALIGN_BOTTOM
from guake.globals import ALIGN_CENTER
from guake.globals import ALIGN_LEFT
from guake.globals import ALIGN_RIGHT
from guake.globals import ALIGN_TOP

try:
    from gi.repository import GdkX11
except ImportError:
    GdkX11 = False

log = logging.getLogger(__name__)


def gdk_is_x11_display(instance):
    if GdkX11:
        return isinstance(instance, GdkX11.X11Display)
    return False


def get_server_time(widget):
    try:
        return GdkX11.x11_get_server_time(widget.get_window())
    except (TypeError, AttributeError):
        # Issue: https://github.com/Guake/guake/issues/1071
        # Wayland does not seem to like `x11_get_server_time`.
        # Use local timestamp instead
        ts = time.time()
        return ts


# Decorator for save-tabs-when-changed
def save_tabs_when_changed(func):
    """Decorator for save-tabs-when-changed"""

    def wrapper(*args, **kwargs):
        # Find me the Guake!
        clsname = args[0].__class__.__name__
        g = None
        if clsname == "Guake":
            g = args[0]
        elif getattr(args[0], "get_guake", None):
            g = args[0].get_guake()
        elif getattr(args[0], "get_notebook", None):
            g = args[0].get_notebook().guake
        elif getattr(args[0], "guake", None):
            g = args[0].guake
        elif getattr(args[0], "notebook", None):
            g = args[0].notebook.guake

        func(*args, **kwargs)
        log.debug("mom, I've been called: %s %s", func.__name__, func)

        # Tada!
        if g and g.settings.general.get_boolean("save-tabs-when-changed"):
            g.save_tabs()

    return wrapper


def save_preferences(filename):
    # XXX: Hardcode?
    prefs = subprocess.check_output(["dconf", "dump", "/org/guake/"])
    with open(filename, "wb") as f:
        f.write(prefs)


def restore_preferences(filename):
    # XXX: Hardcode?
    with open(filename, "rb") as f:
        prefs = f.read()
    with subprocess.Popen(["dconf", "load", "/org/guake/"], stdin=subprocess.PIPE) as p:
        p.communicate(input=prefs)


class FileManager:
    def __init__(self, delta=1.0):
        self._cache = {}
        self._delta = max(0.0, delta)

    def clear(self):
        self._cache.clear()

    def read_yaml(self, filename: str):

        content = None

        try:
            content = self.read(filename)
        except PermissionError:
            log.debug("PermissionError while reading %s.", filename)
        except FileNotFoundError:
            log.debug("File %s does not exists.", filename)
        except UnicodeDecodeError:
            log.debug("Encoding error %s (we assume is utf-8).", filename)

        if content is not None:
            try:
                content = yaml.safe_load(content)
            except yaml.YAMLError:
                log.debug("YAMLError reading %s.", filename)
                content = None
        return content

    def read(self, filename: str) -> str:
        # Return the content of a file from the fs or from cache.
        if (
            filename not in self._cache
            or self._cache[filename]["time"] + self._delta < time.monotonic()
        ):
            with open(filename, mode="r", encoding="utf-8") as fd:
                self._cache[filename] = {"time": time.monotonic(), "content": fd.read()}
        return self._cache[filename]["content"]


class TabNameUtils:
    @classmethod
    def shorten(cls, text, settings):
        use_vte_titles = settings.general.get_boolean("use-vte-titles")
        if not use_vte_titles:
            return text
        max_name_length = settings.general.get_int("max-tab-name-length")
        if max_name_length != 0 and len(text) > max_name_length:
            text = "..." + text[-max_name_length:]
        return text


class HidePrevention:
    def __init__(self, window):
        """Create a new HidePrevention object like `HidePrevention(window)`"""
        if not isinstance(window, Gtk.Window):
            raise ValueError(f"window must be of type Gtk.Window, not of type {type(window)}")
        self.window = window

    def may_hide(self):
        """returns True if the window is allowed to hide and
        False if `prevent()` is called from some where
        """
        return getattr(self.window, "can_hide", True)

    def prevent(self):
        """sets a flag on the window object which indicates to
        may_hide that the window is NOT allowed to be hidden.
        """
        setattr(self.window, "can_hide", False)

    def allow(self):
        """sets the flag so that it indicates to may_hide that the window is allowed to be hidden"""
        setattr(self.window, "can_hide", True)


class FullscreenManager:
    FULLSCREEN_ATTR = "is_fullscreen"

    def __init__(self, settings, window, guake=None):
        self.settings = settings
        self.window = window
        self.guake = guake
        self.window_state = None

    def is_fullscreen(self):
        return getattr(self.window, self.FULLSCREEN_ATTR, False)

    def set_window_state(self, window_state):
        self.window_state = window_state
        setattr(
            self.window,
            self.FULLSCREEN_ATTR,
            bool(window_state & Gdk.WindowState.FULLSCREEN),
        )

        if not window_state & Gdk.WindowState.WITHDRAWN:
            if self.is_fullscreen():
                self.fullscreen()
            elif window_state & Gdk.WindowState.FOCUSED and self.guake.hidden:
                self.unfullscreen()

    def fullscreen(self):
        self.window.fullscreen()
        setattr(self.window, self.FULLSCREEN_ATTR, True)
        self.toggle_fullscreen_hide_tabbar()

    def unfullscreen(self):
        self.window.unfullscreen()
        setattr(self.window, self.FULLSCREEN_ATTR, False)
        self.toggle_fullscreen_hide_tabbar()

        # FIX to unfullscreen after show, fullscreen, hide, unfullscreen
        # (unfullscreen breaks/does not shrink window size)
        RectCalculator.set_final_window_rect(self.settings, self.window)

    def toggle(self):
        if self.is_fullscreen():
            self.unfullscreen()
        else:
            self.fullscreen()

    def toggle_fullscreen_hide_tabbar(self):
        if self.is_fullscreen():
            if (
                self.settings.general.get_boolean("fullscreen-hide-tabbar")
                and self.guake
                and self.guake.notebook_manager
            ):
                self.guake.notebook_manager.set_notebooks_tabbar_visible(False)
        else:
            if self.guake and self.guake.notebook_manager:
                v = self.settings.general.get_boolean("window-tabbar")
                self.guake.notebook_manager.set_notebooks_tabbar_visible(v)


class RectCalculator:
    @classmethod
    def set_final_window_rect(cls, settings, window):
        """Sets the final size and location of the main window of guake. The height
        is the window_height property, width is window_width and the
        horizontal alignment is given by window_alignment.
        """
        # fetch settings
        height_percents = settings.general.get_int("window-height")
        width_percents = settings.general.get_int("window-width")
        halignment = settings.general.get_int("window-halignment")
        valignment = settings.general.get_int("window-valignment")
        vdisplacement = settings.general.get_int("window-vertical-displacement")
        hdisplacement = settings.general.get_int("window-horizontal-displacement")

        log.debug("set_final_window_rect")
        log.debug("  height_percents = %s", height_percents)
        log.debug("  width_percents = %s", width_percents)
        log.debug("  halignment = %s", halignment)
        log.debug("  valignment = %s", valignment)
        log.debug("  hdisplacement = %s", hdisplacement)
        log.debug("  vdisplacement = %s", vdisplacement)

        # get the rectangle just from the destination monitor
        monitor = cls.get_final_window_monitor(settings, window)
        window_rect = monitor.get_workarea()
        log.debug("Current monitor geometry")
        log.debug("  window_rect.x: %s", window_rect.x)
        log.debug("  window_rect.y: %s", window_rect.y)
        log.debug("  window_rect.height: %s", window_rect.height)
        log.debug("  window_rect.width: %s", window_rect.width)

        total_height = window_rect.height
        total_width = window_rect.width

        if halignment == ALIGN_CENTER:
            log.debug("aligning to center!")
            window_rect.width = int(float(total_width) * float(width_percents) / 100.0)
            window_rect.x += (total_width - window_rect.width) / 2
        elif halignment == ALIGN_LEFT:
            log.debug("aligning to left!")
            window_rect.width = int(
                float(total_width - hdisplacement) * float(width_percents) / 100.0
            )
            window_rect.x += hdisplacement
        elif halignment == ALIGN_RIGHT:
            log.debug("aligning to right!")
            window_rect.width = int(
                float(total_width - hdisplacement) * float(width_percents) / 100.0
            )
            window_rect.x += total_width - window_rect.width - hdisplacement

        window_rect.height = int(float(total_height) * float(height_percents) / 100.0)
        if valignment == ALIGN_TOP:
            window_rect.y += vdisplacement
        elif valignment == ALIGN_BOTTOM:
            window_rect.y += total_height - window_rect.height - vdisplacement

        if width_percents == 100 and height_percents == 100:
            log.debug("MAXIMIZING MAIN WINDOW")
            window.move(window_rect.x, window_rect.y)
            window.maximize()
        elif not FullscreenManager(settings, window).is_fullscreen():
            log.debug("RESIZING MAIN WINDOW TO THE FOLLOWING VALUES:")
            window.unmaximize()
            log.debug("  window_rect.x: %s", window_rect.x)
            log.debug("  window_rect.y: %s", window_rect.y)
            log.debug("  window_rect.height: %s", window_rect.height)
            log.debug("  window_rect.width: %s", window_rect.width)
            # Note: move_resize is only on GTK3
            window.resize(window_rect.width, window_rect.height)
            window.move(window_rect.x, window_rect.y)
            log.debug("Updated window position: %r", window.get_position())

        return window_rect

    @classmethod
    def get_final_window_monitor(cls, settings, window):
        """Gets the final monitor for the main window of guake."""

        display = window.get_display()

        # fetch settings
        use_mouse = settings.general.get_boolean("mouse-display")
        num_monitor = settings.general.get_int("display-n")

        if use_mouse:
            pointer = display.get_default_seat().get_pointer()
            if pointer is None:
                monitor = display.get_primary_monitor()
            else:
                _, x, y = pointer.get_position()
                monitor = display.get_monitor_at_point(x, y)
        else:
            monitor = display.get_monitor(num_monitor)
            if monitor is None:
                # monitor not found or num_monitor is wrong
                # by default we use the primary monitor
                monitor = display.get_primary_monitor()

        return monitor


class ImageLayoutMode(enum.IntEnum):
    SCALE = 0
    TILE = 1
    CENTER = 2
    STRETCH = 3


class BackgroundImageManager:
    def __init__(self, window, filename=None, layout_mode=ImageLayoutMode.SCALE):
        self.window = window
        self.filename = ""
        self.bg_surface = self.load_from_file(filename) if filename else None
        self.target_surface = None
        self.target_info = (-1, -1, -1)  # (width, height, model)
        self._layout_mode = layout_mode

    @property
    def layout_mode(self):
        return self._layout_mode

    @layout_mode.setter
    def layout_mode(self, mode):
        mode = ImageLayoutMode(mode)
        if mode not in ImageLayoutMode:
            raise ValueError("Unknown layout mode")
        self._layout_mode = mode
        self.window.queue_draw()

    def load_from_file(self, filename):
        if not filename:
            # Clear the background image
            self.bg_surface = None
            self.window.queue_draw()
            return

        if not os.path.exists(filename):
            raise FileNotFoundError(f"Background file not found: {filename}")

        if self.filename:
            # Cached rendered surface
            if os.path.samefile(self.filename, filename):
                return self.bg_surface

        self.filename = filename
        img = Gtk.Image.new_from_file(filename)
        pixbuf = img.get_pixbuf()
        surface = cairo.ImageSurface(cairo.FORMAT_ARGB32, pixbuf.get_width(), pixbuf.get_height())
        cr = cairo.Context(surface)
        Gdk.cairo_set_source_pixbuf(cr, pixbuf, 0, 0)
        cr.set_operator(cairo.OPERATOR_SOURCE)
        cr.paint()

        self.bg_surface = surface
        self.target_info = (-1, -1, -1)
        self.window.queue_draw()
        return surface

    def render_target(self, width, height, mode, scale_mode=cairo.FILTER_BILINEAR):
        """Paint bacground image to the specific size target surface with different layout mode"""
        if not self.bg_surface:
            return None

        # Check if target surface has been rendered
        if self.target_info == (width, height, mode):
            return self.target_surface

        # Render new target
        self.target_info = (width, height, mode)
        surface = cairo.ImageSurface(cairo.FORMAT_ARGB32, width, height)
        cr = cairo.Context(surface)
        cr.set_operator(cairo.OPERATOR_SOURCE)

        xscale = width / self.bg_surface.get_width()
        yscale = height / self.bg_surface.get_height()
        if mode == ImageLayoutMode.SCALE:
            ratio = max(xscale, yscale)
            xoffset = (width - (self.bg_surface.get_width() * ratio)) / 2.0
            yoffset = (height - (self.bg_surface.get_height() * ratio)) / 2.0
            cr.translate(xoffset, yoffset)
            cr.scale(ratio, ratio)
            cr.set_source_surface(self.bg_surface, 0, 0)
            cr.get_source().set_filter(scale_mode)
        elif mode == ImageLayoutMode.TILE:
            cr.set_source_surface(self.bg_surface, 0, 0)
            cr.get_source().set_extend(cairo.EXTEND_REPEAT)
        elif mode == ImageLayoutMode.CENTER:
            x = (width - self.bg_surface.get_width()) / 2.0
            y = (height - self.bg_surface.get_height()) / 2.0
            cr.translate(x, y)
            cr.set_source_surface(self.bg_surface, 0, 0)
        elif mode == ImageLayoutMode.STRETCH:
            cr.scale(xscale, yscale)
            cr.set_source_surface(self.bg_surface, 0, 0)
            cr.get_source().set_filter(scale_mode)
        cr.paint()

        self.target_surface = surface
        return surface

    def draw(self, widget, cr):
        if not self.bg_surface:
            return

        # Step 1. Get target surface
        #         (paint background image into widget size surface by layout mode)
        surface = self.render_target(
            widget.get_allocated_width(),
            widget.get_allocated_height(),
            self.layout_mode,
        )

        cr.save()
        # Step 2. Paint target surface to context (in our case, the RootTerminalBox)
        #
        #         At this step, RootTerminalBox will be painted to our background image,
        #         and all the draw done by VTE has been overlapped.
        #
        cr.set_source_surface(surface, 0, 0)
        cr.paint()

        # Step 3. Re-paint child draw result to a new surface
        #
        #         We need to re-paint what we overlapped in previous step,
        #         so we paint our child (again, in lifecycle view) into a similar surface.
        #
        child = widget.get_child()
        child_surface = cr.get_target().create_similar(
            cairo.CONTENT_COLOR_ALPHA,
            child.get_allocated_width(),
            child.get_allocated_height(),
        )
        child_cr = cairo.Context(child_surface)

        # Re-paint child draw into child context (which using child_surface as target)
        widget.propagate_draw(child, child_cr)

        # Step 3.1 Re-paint search revealer
        child_sr_surface = None
        if getattr(widget, "search_revealer", None):
            child_sr = widget.search_revealer
            child_sr_surface = cr.get_target().create_similar(
                cairo.CONTENT_COLOR_ALPHA,
                child_sr.get_allocated_width(),
                child_sr.get_allocated_height(),
            )
            child_sr_cr = cairo.Context(child_surface)

            # Re-paint child draw into child context (which using child_surface as target)
            widget.propagate_draw(child_sr, child_sr_cr)

        # Step 4. Paint child surface into our context (RootTerminalBox)
        #
        #         Before this step, we have two important context/surface
        #             1. cr       / RootTerminalBox
        #             2. child_cr / child (RootTerminalBox's child)
        #         And current context have these draw:
        #             1. cr       - background image
        #             2. child_cr - child re-paint (split terminal, VTE draw ...etc)
        #         In this step, we are going to paint child_cr result back to cr,
        #         so in the end, we will get background image + other child stuff
        #
        # DEBUG: If you don't believe, use cairo.Surface.wrte_to_png(filename) to check what is
        #        inside the surface.
        #
        cr.set_source_surface(child_surface, 0, 0)
        cr.set_operator(cairo.OPERATOR_OVER)
        cr.paint()

        # Paint search revealer
        if child_sr_surface:
            cr.set_source_surface(child_sr_surface, 0, 0)
            cr.set_operator(cairo.OPERATOR_OVER)
            cr.paint()

        cr.restore()


def get_process_name(pid):
    stat_file = f"/proc/{pid}/stat"
    try:
        with open(stat_file, "r", encoding="utf-8") as fp:
            status = fp.read()
    except IOError as ex:
        log.debug("Unable to read %s: %s", stat_file, ex)
        status = ""

    match = re.match(r"\d+ \(([^)]+)\)", status)
    return match.group(1) if match else None
