#    Gedit snippets plugin
#    Copyright (C) 2005-2006  Jesse van den Kieboom <jesse@icecrew.nl>
#
#    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 St, Fifth Floor, Boston, MA  02110-1301  USA

import os
import re

from gi.repository import Gtk, Gdk, Gio, GLib, Gedit, GObject

from .library import Library
from .snippet import Snippet
from .placeholder import PlaceholderEnd
from . import completion
from .signals import Signals
from .shareddata import SharedData
from . import helper

try:
    import gettext
    gettext.bindtextdomain('gedit')
    gettext.textdomain('gedit')
    _ = gettext.gettext
except:
    _ = lambda s: s

class DynamicSnippet(dict):
    def __init__(self, text):
        self['text'] = text
        self.valid = True

class Document(GObject.Object, Gedit.ViewActivatable, Signals):
    TAB_KEY_VAL = (Gdk.KEY_Tab, Gdk.KEY_ISO_Left_Tab)
    SPACE_KEY_VAL = (Gdk.KEY_space,)

    view = GObject.Property(type=Gedit.View)

    def __init__(self):
        GObject.Object.__init__(self)
        Signals.__init__(self)

        self.placeholders = []
        self.active_snippets = []
        self.active_placeholder = None

        self.ordered_placeholders = []
        self.update_placeholders = []
        self.jump_placeholders = []
        self.language_id = 0
        self.timeout_update_id = 0

        self.provider = completion.Provider(_('Snippets'), self.language_id, self.on_proposal_activated)

    def do_activate(self):
        # Always have a reference to the global snippets
        Library().ref(None)

        buf = self.view.get_buffer()

        self.connect_signal(self.view, 'key-press-event', self.on_view_key_press)
        self.connect_signal(buf, 'notify::language', self.on_notify_language)
        self.connect_signal(self.view, 'drag-data-received', self.on_drag_data_received)

        self.connect_signal_after(self.view, 'draw', self.on_draw)

        self.update_language()

        completion = self.view.get_completion()
        completion.add_provider(self.provider)

        SharedData().register_controller(self.view, self)

    def do_deactivate(self):
        if self.timeout_update_id != 0:
            GLib.source_remove(self.timeout_update_id)
            self.timeout_update_id = 0

            del self.update_placeholders[:]
            del self.jump_placeholders[:]

        # Always release the reference to the global snippets
        Library().unref(None)
        self.active_placeholder = None

        self.disconnect_signals(self.view)
        self.disconnect_signals(self.view.get_buffer())

        # Remove all active snippets
        for snippet in list(self.active_snippets):
            self.deactivate_snippet(snippet, True)

        completion = self.view.get_completion()

        if completion:
            completion.remove_provider(self.provider)

        if self.language_id != 0:
            Library().unref(self.language_id)

        SharedData().unregister_controller(self.view, self)

    # Call this whenever the language in the view changes. This makes sure that
    # the correct language is used when finding snippets
    def update_language(self):
        lang = self.view.get_buffer().get_language()

        if lang is None and self.language_id is None:
            return
        elif lang and lang.get_id() == self.language_id:
            return

        langid = self.language_id

        if lang:
            self.language_id = lang.get_id()
        else:
            self.language_id = None

        if langid != 0:
            Library().unref(langid)

        Library().ref(self.language_id)
        self.provider.language_id = self.language_id

        SharedData().update_state(self.view.get_toplevel())

    def accelerator_activate(self, keyval, mod):
        if not self.view or not self.view.get_editable():
            return False

        accelerator = Gtk.accelerator_name(keyval, mod)
        snippets = Library().from_accelerator(accelerator, \
                self.language_id)

        if len(snippets) == 0:
            return False
        elif len(snippets) == 1:
            self.apply_snippet(snippets[0])
        else:
            # Do the fancy completion dialog
            provider = completion.Provider(_('Snippets'), self.language_id, self.on_proposal_activated)
            provider.set_proposals(snippets)

            cm = self.view.get_completion()
            cm.show([provider], cm.create_context(None))

        return True

    def first_snippet_inserted(self):
        buf = self.view.get_buffer()

        self.connect_signal(buf, 'changed', self.on_buffer_changed)
        self.connect_signal(buf, 'tepl-cursor-moved', self.on_buffer_cursor_moved)
        self.connect_signal_after(buf, 'insert-text', self.on_buffer_insert_text)

    def last_snippet_removed(self):
        buf = self.view.get_buffer()
        self.disconnect_signal(buf, 'changed')
        self.disconnect_signal(buf, 'tepl-cursor-moved')
        self.disconnect_signal(buf, 'insert-text')

    def current_placeholder(self):
        buf = self.view.get_buffer()

        piter = buf.get_iter_at_mark(buf.get_insert())
        found = []

        for placeholder in self.placeholders:
            begin = placeholder.begin_iter()
            end = placeholder.end_iter()

            if piter.compare(begin) >= 0 and piter.compare(end) <= 0:
                found.append(placeholder)

        if self.active_placeholder in found:
            return self.active_placeholder
        elif len(found) > 0:
            return found[0]
        else:
            return None

    def advance_placeholder(self, direction):
        # Returns (CurrentPlaceholder, NextPlaceholder), depending on direction
        buf = self.view.get_buffer()

        piter = buf.get_iter_at_mark(buf.get_insert())
        found = current = next = None
        length = len(self.placeholders)

        placeholders = list(self.placeholders)

        if self.active_placeholder:
            begin = self.active_placeholder.begin_iter()
            end = self.active_placeholder.end_iter()

            if piter.compare(begin) >= 0 and piter.compare(end) <= 0:
                current = self.active_placeholder
                currentIndex = placeholders.index(self.active_placeholder)

        if direction == 1:
            # w = piter, x = begin, y = end, z = found
            nearest = lambda w, x, y, z: (w.compare(x) <= 0 and (not z or \
                    x.compare(z.begin_iter()) < 0))
            indexer = lambda x: x < length - 1
        else:
            # w = piter, x = begin, y = end, z = prev
            nearest = lambda w, x, y, z: (w.compare(x) >= 0 and (not z or \
                    x.compare(z.begin_iter()) >= 0))
            indexer = lambda x: x > 0

        for index in range(0, length):
            placeholder = placeholders[index]
            begin = placeholder.begin_iter()
            end = placeholder.end_iter()

            # Find the nearest placeholder
            if nearest(piter, begin, end, found):
                found = placeholder

            # Find the current placeholder
            if piter.compare(begin) >= 0 and \
                    piter.compare(end) <= 0 and \
                    current is None:
                currentIndex = index
                current = placeholder

        if current and current != found and \
           (current.begin_iter().compare(found.begin_iter()) == 0 or \
            current.end_iter().compare(found.begin_iter()) == 0) and \
           self.active_placeholder and \
           current.begin_iter().compare(self.active_placeholder.begin_iter()) == 0:
            # if current and found are at the same place, then
            # resolve the 'hugging' problem
            current = self.active_placeholder
            currentIndex = placeholders.index(current)

        if current:
            if indexer(currentIndex):
                next = placeholders[currentIndex + direction]
        elif found:
            next = found
        elif length > 0:
            next = self.placeholders[0]

        return current, next

    def next_placeholder(self):
        return self.advance_placeholder(1)

    def previous_placeholder(self):
        return self.advance_placeholder(-1)

    def cursor_on_screen(self):
        buf = self.view.get_buffer()
        self.view.scroll_mark_onscreen(buf.get_insert())

    def set_active_placeholder(self, placeholder):
        self.active_placeholder = placeholder

    def goto_placeholder(self, current, next):
        last = None

        if current:
            # Signal this placeholder to end action
            self.view.get_completion().hide()
            current.leave()

            if current.__class__ == PlaceholderEnd:
                last = current

        self.set_active_placeholder(next)

        if next:
            next.enter()

            if next.__class__ == PlaceholderEnd:
                last = next
            elif len(next.defaults) > 1 and next.get_text() == next.default:
                provider = completion.Defaults(self.on_default_activated)
                provider.set_defaults(next.defaults)

                cm = self.view.get_completion()
                cm.show([provider], cm.create_context(None))

        if last:
            # This is the end of the placeholder, remove the snippet etc
            for snippet in list(self.active_snippets):
                if snippet.placeholders[0] == last:
                    self.deactivate_snippet(snippet)
                    break

        self.cursor_on_screen()

        return next != None

    def skip_to_next_placeholder(self):
        (current, next) = self.next_placeholder()
        return self.goto_placeholder(current, next)

    def skip_to_previous_placeholder(self):
        (current, prev) = self.previous_placeholder()
        return self.goto_placeholder(current, prev)

    def string_in_native_doc_encoding(self, buf, s):
        enc = buf.get_file().get_encoding()

        if not enc or enc.get_charset() == 'UTF-8':
            return s

        try:
            cv = GLib.convert(s, -1, enc.get_charset(), 'UTF-8')
            return cv[0]
        except GLib.GError:
            pass

        return s

    def env_get_selected_text(self, buf):
        bounds = buf.get_selection_bounds()

        if bounds:
            u8 = buf.get_text(bounds[0], bounds[1], False)

            return {'utf8': u8, 'noenc': self.string_in_native_doc_encoding(buf, u8)}
        else:
            return ''

    def env_get_current_word(self, buf):
        start, end = helper.buffer_word_boundary(buf)

        u8 = buf.get_text(start, end, False)

        return {'utf8': u8, 'noenc': self.string_in_native_doc_encoding(buf, u8)}

    def env_get_current_line(self, buf):
        start, end = helper.buffer_line_boundary(buf)

        u8 = buf.get_text(start, end, False)

        return {'utf8': u8, 'noenc': self.string_in_native_doc_encoding(buf, u8)}

    def env_get_current_line_number(self, buf):
        start, end = helper.buffer_line_boundary(buf)

        return str(start.get_line() + 1)

    def location_uri_for_env(self, location):
        if not location:
            return {'utf8': '', 'noenc': ''}

        u8 = location.get_parse_name()

        if location.has_uri_scheme('file'):
            u8 = "file://" + u8

        return {'utf8': u8, 'noenc': location.get_uri()}

    def location_name_for_env(self, location):
        if location:
            try:
                info = location.query_info("standard::display-name", 0, None)
                display_name = info.get_display_name()
            except:
                display_name = ''

            return {'utf8': display_name,
                'noenc': location.get_basename()}
        else:
            return ''

    def location_scheme_for_env(self, location):
        if location:
            return location.get_uri_scheme()
        else:
            return ''

    def location_path_for_env(self, location):
        if location and location.has_uri_scheme('file'):
            return {'utf8': location.get_parse_name(),
                'noenc': location.get_path()}
        else:
            return ''

    def location_dir_for_env(self, location):
        if location:
            parent = location.get_parent()

            if parent and parent.has_uri_scheme('file'):
                return {'utf8': parent.get_parse_name(),
                    'noenc': parent.get_path()}

        return ''

    def env_add_for_location(self, environ, location, prefix):
        parts = {'URI': self.location_uri_for_env,
             'NAME': self.location_name_for_env,
             'SCHEME': self.location_scheme_for_env,
             'PATH': self.location_path_for_env,
             'DIR': self.location_dir_for_env}

        for k in parts:
            v = parts[k](location)
            key = prefix + '_' + k

            if isinstance(v, dict):
                environ['utf8'][key] = v['utf8']
                environ['noenc'][key] = v['noenc']
            else:
                environ['utf8'][key] = v
                environ['noenc'][key] = str(v)

        return environ

    def env_get_document_type(self, buf):
        typ = buf.get_mime_type()

        if typ:
            return typ
        else:
            return ''

    def env_get_documents_uri(self, buf):
        toplevel = self.view.get_toplevel()

        documents_uri = {'utf8': [], 'noenc': []}

        if isinstance(toplevel, Gedit.Window):
            for doc in toplevel.get_documents():
                r = self.location_uri_for_env(doc.get_file().get_location())

                if isinstance(r, dict):
                    documents_uri['utf8'].append(r['utf8'])
                    documents_uri['noenc'].append(r['noenc'])
                else:
                    documents_uri['utf8'].append(r)
                    documents_uri['noenc'].append(str(r))

        return {'utf8': ' '.join(documents_uri['utf8']),
            'noenc': ' '.join(documents_uri['noenc'])}

    def env_get_documents_path(self, buf):
        toplevel = self.view.get_toplevel()

        documents_path = {'utf8': [], 'noenc': []}

        if isinstance(toplevel, Gedit.Window):
            for doc in toplevel.get_documents():
                r = self.location_path_for_env(doc.get_file().get_location())

                if isinstance(r, dict):
                    documents_path['utf8'].append(r['utf8'])
                    documents_path['noenc'].append(r['noenc'])
                else:
                    documents_path['utf8'].append(r)
                    documents_path['noenc'].append(str(r))

        return {'utf8': ' '.join(documents_path['utf8']),
            'noenc': ' '.join(documents_path['noenc'])}

    def get_environment(self):
        buf = self.view.get_buffer()
        environ = {'utf8': {}, 'noenc': {}}

        for k in os.environ:
            # Get the original environment, as utf-8
            v = os.environ[k]
            environ['noenc'][k] = v
            environ['utf8'][k] = os.environ[k].encode('utf-8')

        variables = {'GEDIT_SELECTED_TEXT': self.env_get_selected_text,
                 'GEDIT_CURRENT_WORD': self.env_get_current_word,
                 'GEDIT_CURRENT_LINE': self.env_get_current_line,
                 'GEDIT_CURRENT_LINE_NUMBER': self.env_get_current_line_number,
                 'GEDIT_CURRENT_DOCUMENT_TYPE': self.env_get_document_type,
                 'GEDIT_DOCUMENTS_URI': self.env_get_documents_uri,
                 'GEDIT_DOCUMENTS_PATH': self.env_get_documents_path}

        for var in variables:
            v = variables[var](buf)

            if isinstance(v, dict):
                environ['utf8'][var] = v['utf8']
                environ['noenc'][var] = v['noenc']
            else:
                environ['utf8'][var] = v
                environ['noenc'][var] = str(v)

        self.env_add_for_location(environ, buf.get_file().get_location(), 'GEDIT_CURRENT_DOCUMENT')

        return environ

    def uses_current_word(self, snippet):
        matches = re.findall('(\\\\*)\\$GEDIT_CURRENT_WORD', snippet['text'])

        for match in matches:
            if len(match) % 2 == 0:
                return True

        return False

    def uses_current_line(self, snippet):
        matches = re.findall('(\\\\*)\\$GEDIT_CURRENT_LINE', snippet['text'])

        for match in matches:
            if len(match) % 2 == 0:
                return True

        return False

    def apply_snippet(self, snippet, start = None, end = None, environ = {}):
        if not snippet.valid:
            return False

        # Set environmental variables
        env = self.get_environment()

        if environ:
            for k in environ['utf8']:
                env['utf8'][k] = environ['utf8'][k]

            for k in environ['noenc']:
                env['noenc'][k] = environ['noenc'][k]

        buf = self.view.get_buffer()
        s = Snippet(snippet, env)

        if not start:
            start = buf.get_iter_at_mark(buf.get_insert())

        if not end:
            end = buf.get_iter_at_mark(buf.get_selection_bound())

        if start.equal(end) and self.uses_current_word(s):
            # There is no tab trigger and no selection and the snippet uses
            # the current word. Set start and end to the word boundary so that
            # it will be removed
            start, end = helper.buffer_word_boundary(buf)
        elif start.equal(end) and self.uses_current_line(s):
            # There is no tab trigger and no selection and the snippet uses
            # the current line. Set start and end to the line boundary so that
            # it will be removed
            start, end = helper.buffer_line_boundary(buf)

        # You know, we could be in an end placeholder
        (current, next) = self.next_placeholder()
        if current and current.__class__ == PlaceholderEnd:
            self.goto_placeholder(current, None)

        if len(self.active_snippets) > 0:
            self.block_signal(buf, 'tepl-cursor-moved')

        buf.begin_user_action()

        # Remove the tag, selection or current word
        buf.delete(start, end)

        # Insert the snippet
        if len(self.active_snippets) == 0:
            self.first_snippet_inserted()
            self.block_signal(buf, 'tepl-cursor-moved')

        sn = s.insert_into(self, start)
        self.active_snippets.append(sn)

        # Put cursor at first tab placeholder
        keys = [x for x in sn.placeholders.keys() if x > 0]

        if len(keys) == 0:
            if 0 in sn.placeholders:
                self.goto_placeholder(self.active_placeholder, sn.placeholders[0])
            else:
                buf.place_cursor(sn.begin_iter())
        else:
            self.goto_placeholder(self.active_placeholder, sn.placeholders[keys[0]])

        self.unblock_signal(buf, 'tepl-cursor-moved')

        if sn in self.active_snippets:
            # Check if we can get end_iter in view without moving the
            # current cursor position out of view
            cur = buf.get_iter_at_mark(buf.get_insert())
            last = sn.end_iter()

            curloc = self.view.get_iter_location(cur)
            lastloc = self.view.get_iter_location(last)

            if (lastloc.y + lastloc.height) - curloc.y <= \
               self.view.get_visible_rect().height:
                self.view.scroll_mark_onscreen(sn.end_mark)

        buf.end_user_action()
        self.view.grab_focus()

        return True

    def get_tab_tag(self, buf, end = None):
        if not end:
            end = buf.get_iter_at_mark(buf.get_insert())

        start = end.copy()
        word = None
        first = True

        # Move start backward as long as there is a valid character
        while start.backward_char():
            c = start.get_char()

            if not helper.is_tab_trigger_character(c):
                # Check this for a single special char
                if first and helper.is_tab_trigger(c):
                    break

                # Make sure first char is valid
                while not start.equal(end) and \
                      not helper.is_first_tab_trigger_character(start.get_char()):
                    start.forward_char()

                break

            first = False

        if not start.equal(end):
            word = buf.get_text(start, end, False)

            if word and word != '':
                return (word, start, end)

        return (None, None, None)

    def parse_and_run_snippet(self, data, iter):
        if not self.view.get_editable():
            return

        self.apply_snippet(DynamicSnippet(data), iter, iter)

    def run_snippet_trigger(self, trigger, bounds):
        if not self.view:
            return False

        if not self.view.get_editable():
            return False

        buf = self.view.get_buffer()

        if buf.get_has_selection():
            return False

        snippets = Library().from_tag(trigger, self.language_id)

        if snippets:
            if len(snippets) == 1:
                return self.apply_snippet(snippets[0], bounds[0], bounds[1])
            else:
                # Do the fancy completion dialog
                provider = completion.Provider(_('Snippets'), self.language_id, self.on_proposal_activated)
                provider.set_proposals(snippets)

                cm = self.view.get_completion()
                cm.show([provider], cm.create_context(None))

                return True

        return False

    def run_snippet(self):
        if not self.view:
            return False

        if not self.view.get_editable():
            return False

        buf = self.view.get_buffer()

        # get the word preceding the current insertion position
        (word, start, end) = self.get_tab_tag(buf)

        if not word:
            return self.skip_to_next_placeholder()

        if not self.run_snippet_trigger(word, (start, end)):
            return self.skip_to_next_placeholder()
        else:
            return True

    def deactivate_snippet(self, snippet, force = False):
        remove = []
        ordered_remove = []

        for tabstop in snippet.placeholders:
            if tabstop == -1:
                placeholders = snippet.placeholders[-1]
            else:
                placeholders = [snippet.placeholders[tabstop]]

            for placeholder in placeholders:
                if placeholder in self.placeholders:
                    if placeholder in self.update_placeholders:
                        placeholder.update_contents()

                        self.update_placeholders.remove(placeholder)
                    elif placeholder in self.jump_placeholders:
                        placeholder[0].leave()

                    remove.append(placeholder)
                elif placeholder in self.ordered_placeholders:
                    ordered_remove.append(placeholder)

        for placeholder in remove:
            if placeholder == self.active_placeholder:
                self.active_placeholder = None

            self.placeholders.remove(placeholder)
            self.ordered_placeholders.remove(placeholder)

            placeholder.remove(force)

        for placeholder in ordered_remove:
            self.ordered_placeholders.remove(placeholder)
            placeholder.remove(force)

        snippet.deactivate()
        self.active_snippets.remove(snippet)

        if len(self.active_snippets) == 0:
            self.last_snippet_removed()

        self.view.queue_draw()

    def update_snippet_contents(self):
        self.timeout_update_id = 0

        for placeholder in self.update_placeholders:
            placeholder.update_contents()

        for placeholder in self.jump_placeholders:
            self.goto_placeholder(placeholder[0], placeholder[1])

        del self.update_placeholders[:]
        del self.jump_placeholders[:]

        return False

    def on_buffer_cursor_moved(self, buf):
        piter = buf.get_iter_at_mark(buf.get_insert())

        # Check for all snippets if the cursor is outside its scope
        for snippet in list(self.active_snippets):
            if snippet.begin_mark.get_deleted() or snippet.end_mark.get_deleted():
                self.deactivate(snippet)
            else:
                begin = snippet.begin_iter()
                end = snippet.end_iter()

                if piter.compare(begin) < 0 or piter.compare(end) > 0:
                    # Oh no! Remove the snippet this instant!!
                    self.deactivate_snippet(snippet)

        current = self.current_placeholder()

        if current != self.active_placeholder:
            self.jump_placeholders.append((self.active_placeholder, current))

            if self.timeout_update_id == 0:
                self.timeout_update_id = GLib.timeout_add(0,
                        self.update_snippet_contents)

    def on_buffer_changed(self, buf):
        for snippet in list(self.active_snippets):
            begin = snippet.begin_iter()
            end = snippet.end_iter()

            if begin.compare(end) >= 0:
                # Begin collapsed on end, just remove it
                self.deactivate_snippet(snippet)

        current = self.current_placeholder()

        if current:
            if not current in self.update_placeholders:
                self.update_placeholders.append(current)

            if self.timeout_update_id == 0:
                self.timeout_update_id = GLib.timeout_add(0, \
                        self.update_snippet_contents)

    def on_buffer_insert_text(self, buf, piter, text, length):
        ctx = helper.get_buffer_context(buf)

        # do nothing special if there is no context and no active
        # placeholder
        if (not ctx) and (not self.active_placeholder):
            return

        if not ctx:
            ctx = self.active_placeholder

        if not ctx in self.ordered_placeholders:
            return

        # move any marks that were incorrectly moved by this insertion
        # back to where they belong
        begin = ctx.begin_iter()
        end = ctx.end_iter()
        idx = self.ordered_placeholders.index(ctx)

        for placeholder in self.ordered_placeholders:
            if placeholder == ctx:
                continue

            ob = placeholder.begin_iter()
            oe = placeholder.end_iter()

            if ob.compare(begin) == 0 and ((not oe) or oe.compare(end) == 0):
                oidx = self.ordered_placeholders.index(placeholder)

                if oidx > idx and ob:
                    buf.move_mark(placeholder.begin, end)
                elif oidx < idx and oe:
                    buf.move_mark(placeholder.end, begin)
            elif ob.compare(begin) >= 0 and ob.compare(end) < 0 and (oe and oe.compare(end) >= 0):
                buf.move_mark(placeholder.begin, end)
            elif (oe and oe.compare(begin) > 0) and ob.compare(begin) <= 0:
                buf.move_mark(placeholder.end, begin)

    def on_notify_language(self, buf, spec):
        self.update_language()

    def on_view_key_press(self, view, event):
        library = Library()

        state = event.get_state()

        if not self.view.get_editable():
            return False

        if not (state & Gdk.ModifierType.CONTROL_MASK) and \
                not (state & Gdk.ModifierType.MOD1_MASK) and \
                event.keyval in self.TAB_KEY_VAL:
            if not state & Gdk.ModifierType.SHIFT_MASK:
                return self.run_snippet()
            else:
                return self.skip_to_previous_placeholder()
        elif not library.loaded and \
                library.valid_accelerator(event.keyval, state):
            library.ensure_files()
            library.ensure(self.language_id)
            self.accelerator_activate(event.keyval, \
                    state & Gtk.accelerator_get_default_mod_mask())

        return False

    def path_split(self, path, components=[]):
        head, tail = os.path.split(path)

        if not tail and head:
            return [head] + components
        elif tail:
            return self.path_split(head, [tail] + components)
        else:
            return components

    def apply_uri_snippet(self, snippet, mime, uri):
        # Remove file scheme
        gfile = Gio.file_new_for_uri(uri)

        environ = {'utf8': {'GEDIT_DROP_DOCUMENT_TYPE': mime.encode('utf-8')},
               'noenc': {'GEDIT_DROP_DOCUMENT_TYPE': mime}}

        self.env_add_for_location(environ, gfile, 'GEDIT_DROP_DOCUMENT')

        buf = self.view.get_buffer()
        location = buf.get_file().get_location()

        relpath = location.get_relative_path(gfile)

        # CHECK: what is the encoding of relpath?
        environ['utf8']['GEDIT_DROP_DOCUMENT_RELATIVE_PATH'] = relpath.encode('utf-8')
        environ['noenc']['GEDIT_DROP_DOCUMENT_RELATIVE_PATH'] = relpath

        mark = buf.get_mark('gtk_drag_target')

        if not mark:
            mark = buf.get_insert()

        piter = buf.get_iter_at_mark(mark)
        self.apply_snippet(snippet, piter, piter, environ)

    def in_bounds(self, x, y):
        rect = self.view.get_visible_rect()
        rect.x, rect.y = self.view.buffer_to_window_coords(Gtk.TextWindowType.WIDGET, rect.x, rect.y)

        return not (x < rect.x or x > rect.x + rect.width or y < rect.y or y > rect.y + rect.height)

    def on_drag_data_received(self, view, context, x, y, data, info, timestamp):
        if not self.view.get_editable():
            return

        uris = helper.drop_get_uris(data)
        if not uris:
            return

        if not self.in_bounds(x, y):
            return

        uris.reverse()
        stop = False

        for uri in uris:
            try:
                mime = Gio.content_type_guess(uri)
            except:
                mime = None

            if not mime:
                continue

            snippets = Library().from_drop_target(mime, self.language_id)

            if snippets:
                stop = True
                self.apply_uri_snippet(snippets[0], mime, uri)

        if stop:
            context.finish(True, False, timestamp)
            view.stop_emission('drag-data-received')
            view.get_toplevel().present()
            view.grab_focus()

    def find_uri_target(self, context):
        lst = Gtk.target_list_add_uri_targets((), 0)

        return self.view.drag_dest_find_target(context, lst)

    def on_proposal_activated(self, proposal, piter):
        if not self.view.get_editable():
            return False

        buf = self.view.get_buffer()
        bounds = buf.get_selection_bounds()

        if bounds:
            self.apply_snippet(proposal.snippet(), None, None)
        else:
            (word, start, end) = self.get_tab_tag(buf, piter)
            self.apply_snippet(proposal.snippet(), start, end)

        return True

    def on_default_activated(self, proposal, piter):
        buf = self.view.get_buffer()
        bounds = buf.get_selection_bounds()

        if bounds:
            buf.begin_user_action()
            buf.delete(bounds[0], bounds[1])
            buf.insert(bounds[0], proposal.props.label)
            buf.end_user_action()

            return True
        else:
            return False

    def iter_coords(self, piter):
        rect = self.view.get_iter_location(piter)
        rect.x, rect.y = self.view.buffer_to_window_coords(Gtk.TextWindowType.TEXT, rect.x, rect.y)

        return rect

    def placeholder_in_area(self, placeholder, area):
        start = placeholder.begin_iter()
        end = placeholder.end_iter()

        if not start or not end:
            return False

        # Test if start is before bottom, and end is after top
        start_rect = self.iter_coords(start)
        end_rect = self.iter_coords(end)

        return start_rect.y <= area.y + area.height and \
               end_rect.y + end_rect.height >= area.y

    def draw_placeholder_rect(self, ctx, placeholder):
        start = placeholder.begin_iter()
        start_rect = self.iter_coords(start)
        start_line = start.get_line()

        end = placeholder.end_iter()
        end_rect = self.iter_coords(end)
        end_line = end.get_line()

        line = start.copy()
        line.set_line_offset(0)
        geom = self.view.get_window(Gtk.TextWindowType.TEXT).get_geometry()

        ctx.translate(0.5, 0.5)

        while line.get_line() <= end_line:
            ypos, height = self.view.get_line_yrange(line)
            x_, ypos = self.view.window_to_buffer_coords(Gtk.TextWindowType.TEXT, 0, ypos)

            if line.get_line() == start_line and line.get_line() == end_line:
                # Simply draw a box, both are on the same line
                ctx.rectangle(start_rect.x, start_rect.y, end_rect.x - start_rect.x, start_rect.height - 1)
                ctx.stroke()
            elif line.get_line() == start_line or line.get_line() == end_line:
                if line.get_line() == start_line:
                    rect = start_rect
                else:
                    rect = end_rect

                ctx.move_to(0, rect.y + rect.height - 1)
                ctx.rel_line_to(rect.x, 0)
                ctx.rel_line_to(0, -rect.height + 1)
                ctx.rel_line_to(geom[2], 0)
                ctx.stroke()

            if not line.forward_line():
                break

    def draw_placeholder_bar(self, ctx, placeholder):
        start = placeholder.begin_iter()
        start_rect = self.iter_coords(start)

        ctx.translate(0.5, 0.5)
        extend_width = 2.5

        ctx.move_to(start_rect.x - extend_width, start_rect.y)
        ctx.rel_line_to(extend_width * 2, 0)

        ctx.move_to(start_rect.x, start_rect.y)
        ctx.rel_line_to(0, start_rect.height - 1)

        ctx.rel_move_to(-extend_width, 0)
        ctx.rel_line_to(extend_width * 2, 0)
        ctx.stroke()

    def draw_placeholder(self, ctx, placeholder):
        if isinstance(placeholder, PlaceholderEnd):
            return

        col = self.view.get_style_context().get_color(Gtk.StateFlags.INSENSITIVE)
        col.alpha = 0.5
        Gdk.cairo_set_source_rgba(ctx, col)

        if placeholder.tabstop > 0:
            ctx.set_dash([], 0)
        else:
            ctx.set_dash([2], 0)

        start = placeholder.begin_iter()
        end = placeholder.end_iter()

        if start.equal(end):
            self.draw_placeholder_bar(ctx, placeholder)
        else:
            self.draw_placeholder_rect(ctx, placeholder)

    def on_draw(self, view, ctx):
        window = view.get_window(Gtk.TextWindowType.TEXT)

        if not Gtk.cairo_should_draw_window(ctx, window):
            return False

        # Draw something
        ctx.set_line_width(1.0)

        Gtk.cairo_transform_to_window(ctx, view, window)

        clipped, clip = Gdk.cairo_get_clip_rectangle(ctx)

        if not clipped:
            return False

        for placeholder in self.ordered_placeholders:
            if not self.placeholder_in_area(placeholder, clip):
                continue

            ctx.save()
            self.draw_placeholder(ctx, placeholder)
            ctx.restore()

        return False

# ex:ts=4:et:
