# Document.py
#
# Moleskine: a source code editor for the GNOME desktop
#
# Copyright (c) 2000 - 2002   Michele Campeotto <micampe@micampe.it>
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 2 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU Library General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.

import os.path
import string
import re
import stat

from gtk import *
from gnome.ui import *
from gnome.util import *

import Moleskine
import gtkscintilla
import GTKSCINTILLA

# Dummy class used to cache the find options between searches
class FindFlags:
    match_case = 0
    whole_words = 0
    word_start = 0
    regexp = 0
    backwards = 0
    text = ''
    
    replace_prompt = 1
    replace_all = 0
    replace_selection = 0
    replace_text = ''
    
    found = 0
    
    in_open_files = 1
    in_directory_files = 0
    recursive_directory = 1
    in_all_files = 1
    in_pattern_files = 0
    in_files_pattern = ''
    in_directory = ''

class Document(gtkscintilla.GtkScintilla):
    def __init__(self, name = _('Unnamed')):
        gtkscintilla.GtkScintilla.__init__(self)
        
        self.connect('save_point_left', self.__save_point_left)
        self.connect('save_point_reached', self.__save_point_reached)
        self.connect('update_ui', self.highligh_braces)
        self.connect('char_added', self.char_added)
        self.connect('uri_dropped', self.uri_dropped)
        
        self.set_margin_width_n(1, 0)
        self.set_margin_width_n(2, 0)
        self.autoc_set_choose_single(TRUE)
        self.set_tab_indents(1)
        self.set_backspace_unindents(1)
        self.set_caret_policy(0x01 | 0x04 | 0x08, 0)
        self.set_visible_policy(0x01 | 0x04, 3)
        
        self.setup_default_style()
        self.setup_caret_style()
        self.setup_line_numbers_style()
        self.setup_braces_style()
        self.setup_line_numbers_digits()
        self.set_language(Moleskine.app.langs['Plain text'])
        
        self.is_modified = self.get_modify
        self.do_auto_indent = FALSE
        self.filepath = None
        self.filename = None
        self.mtime = 0
        self.title = _('Unnamed')
        
        self.label = GtkLabel('%i. %s' % (len(Moleskine.app.documents) + 1, name))
        
        self.set_usize(320, 240)
        
        #if we're using emacs keybindings then remove all scintilla command keys
        if int(Moleskine.app.prefs['keyboard/keybindings']) == 2:
            self.clear_all_cmd_keys()

    def setup_default_style(self):
        self.style_reset_default()
        self.style_set_font(GTKSCINTILLA.STYLE_DEFAULT,
            Moleskine.app.prefs['default style/font'])
        self.style_set_fore(GTKSCINTILLA.STYLE_DEFAULT,
            int(Moleskine.app.prefs['default style/fore']))
        self.style_set_back(GTKSCINTILLA.STYLE_DEFAULT,
            int(Moleskine.app.prefs['default style/back']))
        self.set_sel_back(TRUE,
            int(Moleskine.app.prefs['default style/selection']))
        self.marker_set_back(TRUE,
            int(Moleskine.app.prefs['default style/bookmark']))
        self.style_clear_all()
    
    def setup_caret_style(self):
        self.set_caret_fore(int(Moleskine.app.prefs['caret/fore']))
        self.set_caret_width(int(Moleskine.app.prefs['caret/width']))
        self.set_caret_period(int(Moleskine.app.prefs['caret/period']))
    
    def setup_line_numbers_style(self):
        self.style_set_font(GTKSCINTILLA.STYLE_LINE_NUMBER,
                            Moleskine.app.prefs['line numbers/font'])
        self.style_set_fore(GTKSCINTILLA.STYLE_LINE_NUMBER,
                            int(Moleskine.app.prefs['line numbers/fore']))
        self.style_set_back(GTKSCINTILLA.STYLE_LINE_NUMBER,
                            int(Moleskine.app.prefs['line numbers/back']))
    
    def setup_braces_style(self):
        from Languages import _merge_font
        
        self.style_set_font(GTKSCINTILLA.STYLE_BRACE_LIGHT,
                            _merge_font('-*-*-bold-*-*-*-*-*-*-*-*-*-*-*'))
        self.style_set_fore(GTKSCINTILLA.STYLE_BRACE_LIGHT,
                            int(Moleskine.app.prefs['braces style/fore_ok']))
        self.style_set_back(GTKSCINTILLA.STYLE_BRACE_LIGHT,
                            int(Moleskine.app.prefs['default style/back']))
        
        self.style_set_font(GTKSCINTILLA.STYLE_BRACE_BAD,
                            _merge_font('-*-*-bold-*-*-*-*-*-*-*-*-*-*-*'))
        self.style_set_fore(GTKSCINTILLA.STYLE_BRACE_BAD,
                            int(Moleskine.app.prefs['braces style/fore_bad']))
        self.style_set_back(GTKSCINTILLA.STYLE_BRACE_BAD,
                            int(Moleskine.app.prefs['default style/back']))
    
    def setup_line_numbers_digits(self):
        def digits_width(digits):
            try:
                font = load_font(Moleskine.app.prefs['line numbers/font'])
            except RuntimeError:
                font = load_font('fixed')
            return font.width('0' * (digits + 1))
        
        if int(Moleskine.app.prefs['line numbers/show']):
            if int(Moleskine.app.prefs['line numbers/auto']):
                digits = len(str(self.get_line_count())) + 1
            else:
                digits = int(Moleskine.app.prefs['line numbers/digits'])
            self.set_margin_width_n(0, digits_width(digits))
        else:
            self.set_margin_width_n(0, 0)
    
    def update_title(self):
        def cmp(a, b):
            if len(a[1]) > len(b[1]):
                return -1
            elif len(a[1]) == len(b[1]):
                return 0
            else:
                return 1
        
        if self.filepath is None:
            return
        self.title = self.filepath
        Moleskine.app.prefs.dirnames.sort(cmp)
        for dirname in Moleskine.app.prefs.dirnames:
            if string.find(self.title, dirname[1]) == 0:
                self.title = string.replace(self.title, dirname[1], dirname[0])
                break
    
    def load_file(self, filepath):
        def read(filepath):
            file = open(filepath, 'r')
            text = file.read()
            file.close()
            return text
        
        if os.access(filepath, os.F_OK) and not os.access(filepath, os.R_OK):
            gnome.ui.GnomeMessageBox(
                _('Cannot open `%s\': permission denied.') % \
                os.path.basename(filepath),
                gnome.uiconsts.MESSAGE_BOX_ERROR,
                gnome.uiconsts.STOCK_BUTTON_OK).run_and_close()
            return
        if os.path.exists(filepath):
            self.mtime = os.stat(filepath)[stat.ST_MTIME]
            text = read(filepath)
            #self.clear_all()
            #self.add_text(len(text), text)
            self.set_text(text)
        self.filepath = filepath
        self.filename = os.path.basename(filepath)
        self.label.set_text('%i. %s' %
                                (Moleskine.app.notebook.get_current_page() + 1,
                                 self.filename))
        self.update_title()
        
        self.setup_line_numbers_digits()
        self.check_file_types()
        self.empty_undo_buffer()
        self.set_save_point()
    
    def save_file(self, filepath=None):
        def write(filepath, text):
            file = open(filepath, 'w')
            file.write(text)
            file.close()
        
        if filepath is not None:
            checkfilepath = filepath
        else:
            checkfilepath = self.filepath
        file_ok = 0
        if not os.access(checkfilepath, os.F_OK):
            if os.access(os.path.dirname(checkfilepath), os.W_OK):
                file_ok = 1
        else:
            if os.access(checkfilepath, os.W_OK):
                file_ok = 1
        if not file_ok:
            gnome.ui.GnomeMessageBox(
                _('File can not be written: %s\n\nPermission denied.') %
                    checkfilepath,
                gnome.uiconsts.MESSAGE_BOX_ERROR,
                gnome.uiconsts.STOCK_BUTTON_OK).run_and_close()
            return
        if filepath is not None:
            self.filepath = filepath
            self.filename = os.path.basename(filepath)
            self.label.set_text('%i. %s' %
                                (Moleskine.app.notebook.get_current_page() + 1,
                                 self.filename))
            self.update_title()
        text = self.get_text()
        write(self.filepath, text)
        self.mtime = os.stat(self.filepath)[stat.ST_MTIME]
        self.check_file_types()
        self.set_save_point()
    
    def revert(self):
        if self.filepath is not None:
            #doc = Moleskine.app.get_current_document()
            pos = self.get_current_pos()
            self.load_file(self.filepath)
            self.goto_pos(pos)
    
    def matches(self, lang):
        from fnmatch import fnmatch
        
        first_line = self.get_line(0)
        for type, rule in lang.rules:
            if type == 0:
                if fnmatch(self.filename, rule):
                    return TRUE
            elif rule == 1:
                if re.match(rule, filename):
                    return TRUE
            else:
                if re.match(rule, first_line):
                    return TRUE
        return FALSE
    
    def check_file_types(self):
        for lang in Moleskine.app.langs.values():
            if self.matches(lang):
                self.set_language(lang)
                break
    
    def set_language(self, language):
        def set_font(style, self=self):
            from Languages import _merge_font
            
            value = self.language.styles[style]['font']
            if value is not None:
                self.style_set_font(style, _merge_font(value))
            else:
                self.style_set_font(style,
                        Moleskine.app.prefs['default style/font'])
        
        def set_fore(style, self=self):
            value = self.language.styles[style]['fore']
            if value is not None:
                self.style_set_fore(style, int(value))
            else:
                self.style_set_fore(style,
                        int(Moleskine.app.prefs['default style/fore']))
        
        def set_back(style, self=self):
            value = self.language.styles[style]['back']
            if value is not None:
                self.style_set_back(style, int(value))
            else:
                self.style_set_back(style,
                        int(Moleskine.app.prefs['default style/back']))
        
        self.language = language
        
        self.set_tab_width(language.tab_size)
        self.set_use_tabs(not language.soft_tabs)
        self.set_indent(language.indent_size)
        self.set_indentation_guides(language.show_indent)
        self.set_auto_indent(language.auto_indent)
        self.set_edge_mode(language.edge_indicator)
        self.set_edge_column(language.edge_column)
        self.set_edge_colour(language.edge_color)
        
        if language.lexer == GTKSCINTILLA.LEXER_HTML or \
           language.lexer == GTKSCINTILLA.LEXER_XML:
            self.set_style_bits(7)
        else:
            self.set_style_bits(5)
        self.set_lexer(language.lexer)
        self.clear_document_style()
        
        for kwclass in range(5):
            if language.keywords[kwclass] is not None:
                self.set_keywords(kwclass, language.keywords[kwclass])
        
        for i in language.styles.keys():
            set_font(i)
            set_fore(i)
            set_back(i)
        
        self.colourise(0, -1)
        self.load_api()
    
    def load_api(self):
        if self.language.api_file != '' and self.language.api_file is not None:
            self.api = {}
            api_file = open(self.language.api_file)
            for line in api_file.readlines():
                self.api[line[:string.find(line, '(')]] = line[:-1]
    
    def reload_language(self):
        self.set_language(self.language)
    
    def set_auto_indent(self, do_indent):
        self.do_auto_indent = do_indent
    
    def auto_indent(self):
        line = self.get_current_line() - 1
        if line > 0:
            indent = self.get_line_indentation(line)
            line = self.get_current_line()
            self.begin_undo_action()
            self.set_line_indentation(line, indent)
            self.goto_pos(self.get_line_indent_position(line))
            self.end_undo_action()
    
    def get_current_word(self):
        """Return the word the cursor is on"""
        pos = self.get_current_pos()
        self.word_left_extend()
        word = self.get_sel_text()
        self.goto_pos(pos)

        return word
    
    def get_function_name(self):
        pos = self.get_current_pos()
        self.goto_pos(pos - 1)
        self.word_left_extend()
        word = string.strip(self.get_sel_text())
        self.goto_pos(pos)
        return word
    
    def find_words(self, curword):
        """Find all words starting with a given prefix
        
        Find all words in the text starting with a given prefix.
        
        curword -- the prefix
        
        return -- a string with all the words separated by a space
        """
        text = self.get_text()
        expr = re.compile('(?:\A|\W)(' + curword + '\w*)')
        words = expr.findall(text)
        if len(words) > 0:
            d = {}                  # Remove duplicated items from words list
            for w in words:
                d[w] = None
            words = d.keys()
            try:
                words.extend(filter(expr.match, self.api.keys()))
            except AttributeError:
                pass
            words.sort()
            if curword in words:    # Remove the current word from the list
                words.remove(curword)
            return string.join(words)
        else:
            return None
    
    def complete_word(self):
        self.replace_sel('')
        curword = self.get_current_word()
        words = self.find_words(curword)
        if words:
            self.autoc_show(len(curword), words)
    
    def char_added(self, w, char, user_data=None):
        if self.do_auto_indent and (char == 10 or char == 13):
            self.auto_indent()
        
        if Moleskine.app.prefs['complete/auto'] == TRUE and \
           chr(char) in string.letters + '_' and not self.autoc_active():
            curword = self.get_current_word()
            words = self.find_words(curword)
            if words:
                curpos = self.get_current_pos()
                word = string.split(words)[0]
                the_rest = word[len(curword):]
                self.insert_text(curpos, the_rest)
                self.set_sel(curpos, curpos + len(the_rest))
        
        if chr(char) == '(':
            curpos = self.get_current_pos()
            try:
                function = self.get_function_name()
                description = self.api[function]
                description = string.replace(description, ') ', ')\n')
                self.call_tip_show(curpos - len(function), description)
            except:
                pass
        elif chr(char) == ')' and self.call_tip_active:
            self.call_tip_cancel()
    
    def uri_dropped(self, w, uri, user_data=None):
        uris = string.split(uri[:-1], '\n')
        for file in uris:
            Moleskine.app.open_document(file)
    
    def is_brace(self):
        position = self.get_current_pos()
        ch = self.get_char_at(position - 1)
        if ch in range(256) and chr(ch) in '[](){}':
            return position - 1
        ch = self.get_char_at(position)
        if ch in range(256) and chr(ch) in '[](){}':
            return position
        return -1

    def highligh_braces(self, w, user_data=None):
        brace_pos = self.is_brace()
        if brace_pos > 0:
            other_pos = self.brace_match(brace_pos)
            if other_pos > 0:
                self.brace_highlight(brace_pos, other_pos)
            else:
                self.brace_bad_light(brace_pos)
        else:
            self.brace_bad_light(-1)

    def can_close(self):
        return not self.is_modified()
    
    def is_empty(self):
        return self.get_text_length() == 0
    
    def get_current_line(self):
        return self.line_from_position(self.get_current_pos())
    
    def __save_point_left(self, w, user_data=None):
        if self.filename is not None:
            self.label.set_text('*%i. %s' %
                                (Moleskine.app.notebook.get_current_page() + 1,
                                 self.filename))
        else:
            self.label.set_text('*%i. %s' %
                                (Moleskine.app.notebook.get_current_page() + 1,
                                 _('Unnamed')))
        Moleskine.app.update_title()
    
    def __save_point_reached(self, w, user_data=None):
        if self.filename is not None:
            self.label.set_text('%i. %s' %
                                (Moleskine.app.notebook.get_current_page() + 1,
                                 self.filename))
        else:
            self.label.set_text('%i. %s' %
                                (Moleskine.app.notebook.get_current_page() + 1,
                                 _('Unnamed')))
        Moleskine.app.update_title()
    
    def search_text(self, text, match_case=0, whole_words=0,
                    word_start=0, regexp=0, backwards=0):
        """Search for a string in the active document
        
        The function starts searching from the current position. If the word
        is found the function selects the string and returns a tuple
        containing its starting and ending positions, or None if the string
        hasn't been found.
        """
        FindFlags.text = text
        FindFlags.match_case = match_case
        FindFlags.whole_words = whole_words
        FindFlags.word_start = word_start
        FindFlags.regexp = regexp
        FindFlags.backwards = backwards
        
        flags = backwards * GTKSCINTILLA.FIND_DOWN + \
                match_case * GTKSCINTILLA.FIND_MATCH_CASE + \
                whole_words * GTKSCINTILLA.FIND_WHOLE_WORDS + \
                word_start * GTKSCINTILLA.FIND_WORD_START + \
                regexp * GTKSCINTILLA.FIND_REGEXP
        if backwards:
            start = self.get_selection_start() - 1
            end = 0
        else:
            start = self.get_selection_end()
            end = self.get_length()
        result = self.find_text(flags, text, (start, end))
        if result:
            self.goto_pos(result[0])
            self.set_selection_start(result[0])
            self.set_selection_end(result[1])
            FindFlags.found = 1
        else:
            if (backwards):
                start = self.get_length ()
                end = self.get_selection_start() + 1
            else:
                start = 0
                end = self.get_selection_start() - 1
            result = self.find_text(flags, text, (start, end))
            if (result):
                self.goto_pos(result[0])
                self.set_selection_start(result[0])
                self.set_selection_end(result[1])
                FindFlags.found = 1
            else:
                self.set_selection_start(self.get_selection_end())
                FindFlags.found = 0
        return result
    
    def search(self):
        """Repeat the last performed search
        
        The function uses the same options and the same criterion used in the
        last search.
        """
        return self.search_text(text=FindFlags.text,
                                match_case=FindFlags.match_case,
                                whole_words=FindFlags.whole_words,
                                word_start=FindFlags.word_start,
                                regexp=FindFlags.regexp,
                                backwards=FindFlags.backwards)
    
    def search_next(self):
        """Search for the next string matching the last used criterion
        
        The function uses the same options and the same criterion used in the
        last search.
        """
        return self.search_text(text=FindFlags.text,
                                match_case=FindFlags.match_case,
                                whole_words=FindFlags.whole_words,
                                word_start=FindFlags.word_start,
                                regexp=FindFlags.regexp,
                                backwards=0)
    
    def search_previous(self):
        """Search for the previous string matching the last used criterion
        
        The function uses the same options and the same criterion used in the
        last search.
        """
        return self.search_text(text=FindFlags.text,
                                match_case=FindFlags.match_case,
                                whole_words=FindFlags.whole_words,
                                word_start=FindFlags.word_start,
                                regexp=FindFlags.regexp,
                                backwards=1)
    
    def replace_target(self, regexp, text):
        if regexp:
            return self.replace_target_re(len(text), text)
        else:
            return gtkscintilla.GtkScintilla.replace_target(self, len(text),
                                                            text)
