# -*- coding: UTF-8 -*-
#
#       lists.py
#
#       Copyright 2009-2011 Giuseppe Penone <giuspen@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 gtk
import re
import cons


class ListsHandler:
    """Handler of Bulleted and Numbered Lists"""

    def __init__(self, dad):
        """Lists Handler boot"""
        self.dad = dad

    def list_todo_handler(self):
        """Handler of the ToDo List"""
        if self.dad.curr_buffer.get_has_selection():
            iter_start, sel_end = self.dad.curr_buffer.get_selection_bounds()
            end_offset = sel_end.get_offset() - 1
        else:
            end_offset = 0
            iter_start = self.dad.curr_buffer.get_iter_at_mark(self.dad.curr_buffer.get_insert())
        # get info about the paragraph starting with iter_start ([Number, Whether multiple line, List Start Iter Offset])
        list_info = self.get_paragraph_list_info(iter_start)
        if list_info[0] == None:
            # this is not a list
            first_iteration = True
            while first_iteration or new_par_offset < end_offset:
                first_iteration = False
                iter_start, iter_end = self.get_paragraph_iters(iter_start)
                if not iter_start:
                    # it's an empty paragraph
                    iter_start = self.dad.curr_buffer.get_iter_at_mark(self.dad.curr_buffer.get_insert())
                    self.dad.curr_buffer.insert(iter_start, "[ ] ")
                    break
                elif self.is_list_indented_continuation(iter_start):
                    new_par_offset = iter_end.get_offset() + 1
                else:
                    new_par_offset = iter_end.get_offset() + 4 + 1
                    self.dad.curr_buffer.insert(iter_start, "[ ] ")
                    end_offset += 4
                iter_start = self.dad.curr_buffer.get_iter_at_offset(new_par_offset)
                if not iter_start: break
        elif list_info[0] == 0:
            # this is a bulleted list paragraph and we turn it into todo list
            self.list_adjust_ahead(None, list_info[2]-1, "bul2tod")
        elif list_info[0] == -1:
            # this is a todo list and we turn it into normal text
            first_iteration = True
            while first_iteration or new_par_offset < end_offset:
                first_iteration = False
                iter_start, iter_end = self.get_paragraph_iters(iter_start)
                if self.is_list_indented_continuation(iter_start):
                    new_par_offset = iter_end.get_offset() + 1
                else:
                    new_par_offset = iter_end.get_offset() - 4 + 1
                    iter_end_deletion = iter_start.copy()
                    iter_end_deletion.forward_chars(4)
                    self.dad.curr_buffer.delete(iter_start, iter_end_deletion)
                    end_offset -= 4
                iter_start = self.dad.curr_buffer.get_iter_at_offset(new_par_offset)
                if not iter_start: break
        else:
            # this is a numbered list and we turn it into todo list
            self.list_adjust_ahead(None, list_info[2]-1, "num2tod")

    def list_bulleted_handler(self):
        """Handler of the Bulleted List"""
        if self.dad.curr_buffer.get_has_selection():
            iter_start, sel_end = self.dad.curr_buffer.get_selection_bounds()
            end_offset = sel_end.get_offset() - 1
        else:
            end_offset = 0
            iter_start = self.dad.curr_buffer.get_iter_at_mark(self.dad.curr_buffer.get_insert())
        # get info about the paragraph starting with iter_start ([Number, Whether multiple line, List Start Iter Offset])
        list_info = self.get_paragraph_list_info(iter_start)
        if list_info[0] == None:
            # this is not a list
            first_iteration = True
            while first_iteration or new_par_offset < end_offset:
                first_iteration = False
                iter_start, iter_end = self.get_paragraph_iters(iter_start)
                if not iter_start:
                    # it's an empty paragraph
                    iter_start = self.dad.curr_buffer.get_iter_at_mark(self.dad.curr_buffer.get_insert())
                    self.dad.curr_buffer.insert(iter_start, cons.CHAR_LISTBUL + cons.CHAR_SPACE)
                    break
                elif self.is_list_indented_continuation(iter_start):
                    new_par_offset = iter_end.get_offset() + 1
                else:
                    new_par_offset = iter_end.get_offset() + 2 + 1
                    self.dad.curr_buffer.insert(iter_start, cons.CHAR_LISTBUL + cons.CHAR_SPACE)
                    end_offset += 2
                iter_start = self.dad.curr_buffer.get_iter_at_offset(new_par_offset)
                if not iter_start: break
        elif list_info[0] == 0:
            # this is a bulleted list paragraph and we turn it into normal text
            first_iteration = True
            while first_iteration or new_par_offset < end_offset:
                first_iteration = False
                iter_start, iter_end = self.get_paragraph_iters(iter_start)
                if self.is_list_indented_continuation(iter_start):
                    new_par_offset = iter_end.get_offset() + 1
                else:
                    new_par_offset = iter_end.get_offset() -2 + 1
                    iter_end_deletion = iter_start.copy()
                    iter_end_deletion.forward_chars(2)
                    self.dad.curr_buffer.delete(iter_start, iter_end_deletion)
                    end_offset -= 2
                iter_start = self.dad.curr_buffer.get_iter_at_offset(new_par_offset)
                if not iter_start: break
        elif list_info[0] == -1:
            # this is a todo list and we turn it into bulleted list
            self.list_adjust_ahead(None, list_info[2]-1, "tod2bul")
        else:
            # this is a numbered list and we turn it into bulleted list
            self.list_adjust_ahead(None, list_info[2]-1, "num2bul")

    def list_numbered_handler(self):
        """Handler of the Numbered List"""
        if self.dad.curr_buffer.get_has_selection():
            iter_start, sel_end = self.dad.curr_buffer.get_selection_bounds()
            end_offset = sel_end.get_offset() - 1
        else:
            end_offset = 0
            iter_start = self.dad.curr_buffer.get_iter_at_mark(self.dad.curr_buffer.get_insert())
        # get info about the paragraph starting with iter_start ([Number, Whether multiple line, List Start Iter Offset])
        list_info = self.get_paragraph_list_info(iter_start)
        if list_info[0] == None:
            # this is not a list
            leading_num_count = 0
            while leading_num_count == 0 or new_par_offset < end_offset:
                leading_num_count += 1
                iter_start, iter_end = self.get_paragraph_iters(iter_start)
                if not iter_start:
                    # it's an empty paragraph
                    iter_start = self.dad.curr_buffer.get_iter_at_mark(self.dad.curr_buffer.get_insert())
                    self.dad.curr_buffer.insert(iter_start, "1. ")
                    break
                elif self.is_list_indented_continuation(iter_start):
                    new_par_offset = iter_end.get_offset() + 1
                else:
                    leading_str = "%s. " % leading_num_count
                    new_par_offset = iter_end.get_offset() + len(leading_str) + 1
                    self.dad.curr_buffer.insert(iter_start, leading_str)
                    end_offset += len(leading_str)
                iter_start = self.dad.curr_buffer.get_iter_at_offset(new_par_offset)
                if not iter_start: break
        elif list_info[0] == 0:
            # this is a bulleted list and we turn it into numbered list
            self.list_adjust_ahead(0, list_info[2]-1, "bul2num")
        elif list_info[0] == -1:
            # this is a todo list and we turn it into numbered list
            self.list_adjust_ahead(0, list_info[2]-1, "tod2num")
        else:
            # this is a numbered list paragraph and we turn it into normal text
            first_iteration = True
            while first_iteration or new_par_offset < end_offset:
                first_iteration = False
                iter_start, iter_end = self.get_paragraph_iters(iter_start)
                leading_str = "%s. " % list_info[0]
                if self.is_list_indented_continuation(iter_start):
                    new_par_offset = iter_end.get_offset() + 1
                else:
                    new_par_offset = iter_end.get_offset() -len(leading_str) + 1
                    iter_end_deletion = iter_start.copy()
                    iter_end_deletion.forward_chars(len(leading_str))
                    self.dad.curr_buffer.delete(iter_start, iter_end_deletion)
                    end_offset -= len(leading_str)
                iter_start = self.dad.curr_buffer.get_iter_at_offset(new_par_offset)
                if not iter_start: break

    def list_adjust_ahead(self, curr_num, offset_start, adj_type):
        """Adjust the Following Numbering"""
        if offset_start >= 0:
            iter_start = self.dad.curr_buffer.get_iter_at_offset(offset_start)
            while iter_start.get_char() != cons.CHAR_NEWLINE:
                if not iter_start.forward_char(): return # end of buffer
            if not iter_start.forward_char(): return
        else: iter_start = self.dad.curr_buffer.get_iter_at_offset(0)
        # we're at the beginning of a list item or subsequent indented line of a list item
        if iter_start.get_char() == cons.CHAR_SPACE:
            if iter_start.forward_char() and iter_start.get_char() == cons.CHAR_SPACE\
            and iter_start.forward_char() and iter_start.get_char() == cons.CHAR_SPACE:
                self.list_adjust_ahead(curr_num, iter_start.get_offset(), adj_type) # go on searching
            else: return # not an indentation
        elif adj_type[0:3] == "num":
            match = re.match('[1-9]', iter_start.get_char())
            if not match: return
            iter_end = iter_start.copy()
            while iter_end.forward_char() and re.match('[0-9]', iter_end.get_char()): pass
            if iter_end.get_char() != ".": return # something's wrong
            self.list_adjust_ahead_write_in(iter_start, iter_end, curr_num, adj_type)
        elif adj_type[0:3] == "bul":
            if iter_start.get_char() != cons.CHAR_LISTBUL: return
            iter_end = iter_start.copy()
            self.list_adjust_ahead_write_in(iter_start, iter_end, curr_num, adj_type)
        elif adj_type[0:3] == "tod":
            if iter_start.get_char() != cons.CHAR_SQ_BR_OPEN: return
            iter_end = iter_start.copy()
            iter_end.forward_chars(2)
            self.list_adjust_ahead_write_in(iter_start, iter_end, curr_num, adj_type)
        else: print "bad adj_type", adj_type

    def list_adjust_ahead_write_in(self, iter_start, iter_end, curr_num, adj_type):
        """Write a Replacement of List Point"""
        iter_end.forward_char()
        self.dad.curr_buffer.delete(iter_start, iter_end)
        if adj_type[-3:] == "num":
            curr_num += 1
            self.dad.curr_buffer.insert(iter_start, '%s.' % curr_num)
            self.list_adjust_ahead(curr_num, iter_start.get_offset(), adj_type)
        elif adj_type[-3:] == "bul":
            self.dad.curr_buffer.insert(iter_start, cons.CHAR_LISTBUL)
            self.list_adjust_ahead(None, iter_start.get_offset(), adj_type)
        elif adj_type[-3:] == "tod":
            self.dad.curr_buffer.insert(iter_start, "[ ]")
            self.list_adjust_ahead(None, iter_start.get_offset(), adj_type)
        else: print "bad adj_type", adj_type

    def list_get_number(self, iter_first_paragraph):
        """Returns a Number or None (0 fot the bulleted list)"""
        iter_start = iter_first_paragraph.copy()
        char = iter_start.get_char()
        if char == cons.CHAR_LISTBUL: return 0
        if char == cons.CHAR_SQ_BR_OPEN:
            iter_tmp = iter_start.copy()
            if iter_tmp.forward_chars(2) and iter_tmp.get_char() == ']':
                return -1
        match = re.match('[1-9]', char)
        if not match: return None
        number_str = char
        while iter_start.forward_char() and re.match('[0-9]', iter_start.get_char()):
            number_str += iter_start.get_char()
        if iter_start.get_char() == ".": return int(number_str)
        else: return None

    def get_paragraph_list_info(self, iter_start):
        """Returns [Number, Whether multiple line, List Start Iter Offset]
           Number == 0 if bulleted list, >=1 if numbered list or None if not a list"""
        if iter_start.get_char() == cons.CHAR_NEWLINE: iter_start.backward_char() # if we are exactly on the paragraph end
        # let's search for the paragraph start
        buffer_start = False
        while iter_start != None:
            if iter_start.get_char() == cons.CHAR_NEWLINE: break # we got the previous paragraph start
            elif not iter_start.backward_char():
                buffer_start = True
                break # we reached the buffer start
        if not buffer_start: iter_start.forward_char()
        # get the number of the paragraph starting with iter_start
        number = self.list_get_number(iter_start)
        if number != None: return [number, False, iter_start.get_offset()] # multiple line = False
        elif iter_start.get_char() == cons.CHAR_SPACE:
            if iter_start.forward_char() and iter_start.get_char() == cons.CHAR_SPACE\
            and iter_start.forward_char() and iter_start.get_char() == cons.CHAR_SPACE:
                # we are inside of a list paragraph but after a shift+return
                iter_start.backward_chars(4)
                list_info = self.get_paragraph_list_info(iter_start)
                return [list_info[0], True, list_info[2]] # multiple line = True
        return [None, None, None] # this paragraph is not a list

    def get_paragraph_iters(self, force_iter=None):
        """Generates and Returns two iters indicating the paragraph bounds"""
        if not force_iter and self.dad.curr_buffer.get_has_selection():
            iter_start, iter_end = self.dad.curr_buffer.get_selection_bounds() # there's a selection
        else:
            # There's not a selection/iter forced
            if not force_iter: iter_start = self.dad.curr_buffer.get_iter_at_mark(self.dad.curr_buffer.get_insert())
            else: iter_start = force_iter.copy()
            iter_end = iter_start.copy()
            if iter_start.get_char() == cons.CHAR_NEWLINE:
                # we're upon a row end
                if not iter_start.backward_char(): return (None, None)
                if iter_start.get_char() == cons.CHAR_NEWLINE: return (None, None)
        while iter_end != None:
            char = iter_end.get_char()
            if char == cons.CHAR_NEWLINE: break # we got it
            elif not iter_end.forward_char(): break # we reached the buffer end
        while iter_start != None:
            char = iter_start.get_char()
            if char == cons.CHAR_NEWLINE: # we got it
                iter_start.forward_char() # step forward to the beginning of the new line
                break
            elif not iter_start.backward_char(): break # we reached the buffer start
        return (iter_start, iter_end)

    def is_list_indented_continuation(self, iter_in):
        """The given iter is the beginning of an indented list item"""
        if not iter_in: return False
        iter_start = iter_in.copy()
        if iter_start.get_char() != cons.CHAR_SPACE: return False
        if not iter_start.forward_char() or iter_start.get_char() != cons.CHAR_SPACE: return False
        if not iter_start.forward_char() or iter_start.get_char() != cons.CHAR_SPACE: return False
        return True

    def is_list_todo_beginning(self, square_bracket_open_iter):
        """Check if it is [X] or [ ]"""
        text_iter = square_bracket_open_iter.copy()
        if text_iter.backward_char():
            if text_iter.get_char() == cons.CHAR_NEWLINE:
                text_iter.forward_chars(2)
            else: return False
        else: text_iter.forward_char()
        if text_iter.get_char() in [cons.CHAR_SPACE, cons.CHAR_X]\
        and text_iter.forward_char() and text_iter.get_char() == cons.CHAR_SQ_BR_CLOSE:
            return True
        return False

    def todo_list_invert_checked_status(self, square_bracket_open_iter):
        """From [ ] to [X] or vice-versa"""
        square_bracket_open_iter.forward_char()
        iter_offset = square_bracket_open_iter.get_offset()
        if self.dad.curr_buffer.get_iter_at_offset(iter_offset).get_char() == cons.CHAR_SPACE:
            self.dad.curr_buffer.delete(square_bracket_open_iter, self.dad.curr_buffer.get_iter_at_offset(iter_offset+1))
            self.dad.curr_buffer.insert(self.dad.curr_buffer.get_iter_at_offset(iter_offset), cons.CHAR_X)
        else:
            self.dad.curr_buffer.delete(square_bracket_open_iter, self.dad.curr_buffer.get_iter_at_offset(iter_offset+1))
            self.dad.curr_buffer.insert(self.dad.curr_buffer.get_iter_at_offset(iter_offset), cons.CHAR_SPACE)
        iter_offset += 3
        iter_start = self.dad.curr_buffer.get_iter_at_offset(iter_offset)
        if not iter_start: return
        while 1:
            iter_end = iter_start.copy()
            while 1:
                if not iter_end.forward_char(): break
                if iter_end.get_char() == cons.CHAR_NEWLINE: break
            if iter_start.equal(iter_end): return
            iter_offset = iter_end.get_offset()
            self.dad.curr_buffer.move_mark(self.dad.curr_buffer.get_insert(), iter_end)
            self.dad.curr_buffer.move_mark(self.dad.curr_buffer.get_selection_bound(), iter_start)
            self.dad.apply_tag_strikethrough()
            iter_start = self.dad.curr_buffer.get_iter_at_offset(iter_offset+1)
            if not (iter_start and iter_start.get_char() == cons.CHAR_SPACE\
            and iter_start.forward_char() and iter_start.get_char() == cons.CHAR_SPACE\
            and iter_start.forward_char() and iter_start.get_char() == cons.CHAR_SPACE):
                break # not an indentation
