/***********************************************************************************

    Copyright (C) 2007-2019 Ahmet Öztürk (aoz_2@yahoo.com)

    This file is part of Lifeograph.

    Lifeograph is free software: you can redistribute it and/or modify
    it under the terms of the GNU General Public License as published by
    the Free Software Foundation, either version 3 of the License, or
    (at your option) any later version.

    Lifeograph 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 Lifeograph.  If not, see <http://www.gnu.org/licenses/>.

***********************************************************************************/


#ifdef HAVE_CONFIG_H
#include <config.h>
#endif

#include <cmath>
#include <cassert>

#include "lifeograph.hpp"
#include "app_window.hpp"
#include "view_entry.hpp"
#include "widget_textview.hpp"


#define GTKSPELL_MISSPELLED_TAG "gtkspell-misspelled"


using namespace LIFEO;


// STATIC MEMBERS
Glib::RefPtr<Gtk::TextTag>  TextbufferDiary::m_tag_strikethrough;
Glib::RefPtr<Gtk::TextTag>  TextbufferDiary::m_tag_highlight;

Gtk::TextBuffer*            UndoEdit::m_ptr2buffer;

PredicateNL                 TextbufferDiary::s_predicate_nl;


int
TextbufferDiary::get_text_width( const Pango::FontDescription& fd, const Glib::ustring& str )
{
    int width, height;

    Glib::RefPtr< Pango::Context > context = m_ptr2textview->get_pango_context();

    Glib::RefPtr< Pango::Layout > layout = Pango::Layout::create( context );
    layout->set_text( str );
    layout->set_font_description( fd );
    layout->get_pixel_size( width, height );

    return width;
}

// UNDOEDIT
void
UndoEdit::insert()
{
    auto textbuffer = dynamic_cast< TextbufferDiary* >( m_ptr2buffer );

    textbuffer->m_flag_ongoing_operation = true;
    Gtk::TextIter iter_bgn = m_ptr2buffer->get_iter_at_offset( m_position );
    Gtk::TextIter iter_end = m_ptr2buffer->insert( iter_bgn, m_text );

    // handle reparsing internally
    iter_bgn = m_ptr2buffer->get_iter_at_offset( m_position );
    textbuffer->calculate_para_bounds( iter_bgn, iter_end );
    textbuffer->parse( iter_bgn.get_offset(), iter_end.get_offset() );

    textbuffer->m_flag_ongoing_operation = false;
}

// LINK
Link::Link( const Glib::RefPtr< Gtk::TextMark >& start,
            const Glib::RefPtr< Gtk::TextMark >& end,
            LinkType t )
:   m_mark_start( start ), m_mark_end( end ), type( t )
{

}

Link::~Link()
{
    // TODO: is this necessary?
    Glib::RefPtr< Gtk::TextBuffer > buffer = m_mark_start->get_buffer();
    if ( buffer )
    {
        buffer->delete_mark( m_mark_start );
        buffer->delete_mark( m_mark_end );
    }
}

// LINK TO ID
LinkID::LinkID( const Glib::RefPtr< Gtk::TextMark >& start,
                const Glib::RefPtr< Gtk::TextMark >& end,
                DEID id )
:   Link( start, end, LT_ID ), m_id( id )
{
}

void
LinkID::go()
{
    Diary::d->get_element( m_id )->show();
}

// LINK TO ENTRY
Gtk::Menu* LinkEntry::menu_link = nullptr;
LinkEntry::LinkEntry( const Glib::RefPtr< Gtk::TextMark >& start,
                      const Glib::RefPtr< Gtk::TextMark >& end,
                      Date date )
:   Link( start, end, LT_ENTRY ), m_date( date )
{
}

void
LinkEntry::go()
{
    EntryVector* entries = Diary::d->get_entries( m_date.get_pure() );

    // seek and destroy cyclic links
    for( EntryVectorIter iter = entries->begin(); iter != entries->end(); ++iter )
    {
        if( ( *iter ) == AppWindow::p->panel_main->get_cur_elem() )
        {
            entries->erase( iter );
            break;
        }
    }

    if( entries->size() > 1 )
    {
        if( menu_link )
            delete menu_link;
        menu_link = new Gtk::Menu;

        for( Entry* entry : *entries )
        {
            Gtk::MenuItem* mitem = Gtk::manage( new Gtk::MenuItem( entry->get_name() ) );
            mitem->signal_activate().connect( sigc::mem_fun( entry, &Entry::show ) );
            menu_link->append( *mitem );
        }
        menu_link->show_all_children();
        menu_link->popup( 0, gtk_get_current_event_time() );
    }
    else
    if( entries->size() == 1 )
        entries->at( 0 )->show();
    else
    {
        Entry* entry = Diary::d->create_entry( m_date.m_date );
        AppWindow::p->panel_diary->update_entry_list();
        entry->show();
    }

    delete entries;
}

// LINK TO URI
Gtk::TextBuffer* LinkUri::m_ptr2buffer;

LinkUri::LinkUri( const Glib::RefPtr< Gtk::TextMark >& start,
                  const Glib::RefPtr< Gtk::TextMark >& end,
                  const std::string& url )
:   Link( start, end, LT_URI ), m_url( url )
{
}

void
LinkUri::go()
{
    GError* err = nullptr;
    gtk_show_uri_on_window( static_cast< Gtk::Window* >( AppWindow::p )->gobj(),
                            m_url.c_str(), GDK_CURRENT_TIME, &err );
}

// LINK CHECKBOX
const Glib::ustring     LinkCheck::s_boxes = " ~+x";
int                     LinkCheck::s_pos_mark;

LinkCheck::LinkCheck( const Glib::RefPtr< Gtk::TextMark >& start,
                      const Glib::RefPtr< Gtk::TextMark >& end,
                      unsigned int state_index )
: Link( start, end, LT_CHECK ), m_state_index( state_index )
{
}

void
LinkCheck::go()
{
    PRINT_DEBUG( "LinkCheck::go()" );
    //m_state_index = ( m_state_index + 1 ) % 4; // the old way. may be added back as an alternative

    static Gtk::Popover* Po_check{ nullptr };
    static Gtk::RadioButton* RB_status[ 4 ];

    if( !Po_check )
    {
        auto builder{ Lifeograph::get_builder() };
        builder->get_widget( "Po_entry_checklist", Po_check );
        Lifeograph::get_builder()->get_widget( "RB_checklist_todo", RB_status[ 0 ] );
        RB_status[ 0 ]->signal_clicked().connect(
                sigc::bind( sigc::ptr_fun( &LinkCheck::set_check ), 0 ) );
        builder->get_widget( "RB_checklist_progressed", RB_status[ 1 ] );
        RB_status[ 1 ]->signal_clicked().connect(
                sigc::bind( sigc::ptr_fun( &LinkCheck::set_check ), 1) );
        builder->get_widget( "RB_checklist_done", RB_status[ 2 ] );
        RB_status[ 2 ]->signal_clicked().connect(
                sigc::bind( sigc::ptr_fun( &LinkCheck::set_check ), 2 ) );
        builder->get_widget( "RB_checklist_canceled", RB_status[ 3 ] );
        RB_status[ 3 ]->signal_clicked().connect(
                sigc::bind( sigc::ptr_fun( &LinkCheck::set_check ), 3 ) );

        Gtk::Image* img;
        builder->get_widget( "I_checklist_todo", img );
        img->set( Lifeograph::icons->todo_open_16 );
        builder->get_widget( "I_checklist_progressed", img );
        img->set( Lifeograph::icons->todo_progressed_16 );
        builder->get_widget( "I_checklist_done", img );
        img->set( Lifeograph::icons->todo_done_16 );
        builder->get_widget( "I_checklist_canceled", img );
        img->set( Lifeograph::icons->todo_canceled_16 );
    }

    Lifeograph::s_internaloperation++;
    RB_status[ m_state_index ]->set_active();
    Lifeograph::s_internaloperation--;

    Gdk::Rectangle rect;
    int win_x, win_y;
    Gtk::TextIter iter = m_mark_start->get_iter();
    iter++;

    Po_check->set_relative_to( *AppWindow::p->m_entry_view->get_textview() );
    AppWindow::p->m_entry_view->get_textview()->get_iter_location( iter, rect );
    AppWindow::p->m_entry_view->get_textview()->buffer_to_window_coords(
            Gtk::TEXT_WINDOW_TEXT, rect.get_x(), rect.get_y(), win_x, win_y );
    rect.set_x( win_x );
    rect.set_y( win_y );

    s_pos_mark = m_mark_start->get_iter().get_offset() + 1;

    Po_check->set_pointing_to( rect );
    Po_check->show();
}

void
LinkCheck::set_check( int new_index )
{
    if( Lifeograph::s_internaloperation )
        return;

    TextbufferDiary*    buffer{ AppWindow::p->m_entry_view->get_buffer() };
    Gtk::TextIter       iter_start{ buffer->get_iter_at_offset( s_pos_mark ) };
    Gtk::TextIter       iter_end{ buffer->get_iter_at_offset( s_pos_mark + 1 ) };


    // link should be preserved
    buffer->m_flag_ongoing_operation++;

    iter_start = buffer->erase( iter_start, iter_end );
    iter_start = buffer->insert( iter_start, s_boxes.substr( new_index, 1) );

    buffer->m_flag_ongoing_operation--;

    iter_end = iter_start;
    iter_start.backward_find_char( TextbufferDiary::s_predicate_nl );
    iter_end.forward_to_line_end();

    buffer->parse( iter_start.get_offset(), iter_end.get_offset() );
}

// TEXTBUFFERDIARY =================================================================================
TextbufferDiary::TextbufferDiary()
:   Gtk::TextBuffer()
{
    LinkUri::m_ptr2buffer = this;
    UndoEdit::m_ptr2buffer = this;

    // TAGS
    // NOTE: order is significant.
    // the later a tag is added the more dominant it is.
    Glib::RefPtr< TagTable > tag_table = get_tag_table();

    m_tag_heading = Tag::create( "heading" );
    m_tag_heading->property_weight() = Pango::WEIGHT_BOLD;
    m_tag_heading->property_scale() = 1.5;
    m_tags.push_back( m_tag_heading );

    m_tag_subheading = Tag::create( "subheading" );
    m_tag_subheading->property_weight() = Pango::WEIGHT_BOLD;
    m_tag_subheading->property_scale() = 1.2;
    m_tags.push_back( m_tag_subheading );

    m_tag_match = Tag::create( "match" );
    m_tags.push_back( m_tag_match );

    m_tag_markup = Tag::create( "markup" );
    m_tags.push_back( m_tag_markup );

    m_tag_hidden = Tag::create( "hidden" );
    m_tag_hidden->property_invisible() = true;
    m_tags.push_back( m_tag_hidden );

    m_tag_bold = Tag::create( "bold" );
    m_tag_bold->property_weight() = Pango::WEIGHT_BOLD;
    m_tags.push_back( m_tag_bold );

    m_tag_italic = Tag::create( "italic" );
    m_tag_italic->property_style() = Pango::STYLE_ITALIC;
    m_tags.push_back( m_tag_italic );

    m_tag_strikethrough = Tag::create( "strikethrough" );
    m_tag_strikethrough->property_strikethrough() = true;
    m_tags.push_back( m_tag_strikethrough );

    m_tag_highlight = Tag::create( "highlight" );
    m_tags.push_back( m_tag_highlight );

    m_tag_region = Tag::create( "region" );
    m_tags.push_back( m_tag_region );

    m_tag_link = Tag::create( "link" );
    m_tag_link->property_underline() = Pango::UNDERLINE_SINGLE;
    m_tags.push_back( m_tag_link );

    m_tag_link_broken = Tag::create( "link.broken" );
    m_tag_link_broken->property_underline() = Pango::UNDERLINE_SINGLE;
    m_tags.push_back( m_tag_link_broken );

    // this is just for keeping the boundaries:
    m_tag_link_hidden = Tag::create( "link.hidden" );
    m_tags.push_back( m_tag_link_hidden );

    m_tag_done = Tag::create( "done" );
    m_tags.push_back( m_tag_done );

    m_tag_checkbox_todo = Tag::create( "cb.todo" );
    m_tag_checkbox_todo->property_font() = "monospace";
    m_tag_checkbox_todo->property_weight() = Pango::WEIGHT_BOLD;
    m_tag_checkbox_todo->property_foreground_rgba() = Theme::s_color_todo;
    m_tags.push_back( m_tag_checkbox_todo );

    m_tag_checkbox_progressed = Tag::create( "cb.progressed" );
    m_tag_checkbox_progressed->property_font() = "monospace";
    m_tag_checkbox_progressed->property_weight() = Pango::WEIGHT_BOLD;
    m_tag_checkbox_progressed->property_foreground_rgba() = Theme::s_color_progressed;
    m_tags.push_back( m_tag_checkbox_progressed );

    m_tag_checkbox_done = Tag::create( "cb.done" );
    m_tag_checkbox_done->property_font() = "monospace";
    m_tag_checkbox_done->property_weight() = Pango::WEIGHT_BOLD;
    m_tag_checkbox_done->property_foreground_rgba() = Theme::s_color_done;
    m_tags.push_back( m_tag_checkbox_done );

    m_tag_checkbox_canceled = Tag::create( "cb.canceled" );
    m_tag_checkbox_canceled->property_font() = "monospace";
    m_tag_checkbox_canceled->property_weight() = Pango::WEIGHT_BOLD;
    m_tag_checkbox_canceled->property_foreground_rgba() = Theme::s_color_canceled;
    m_tags.push_back( m_tag_checkbox_canceled );

    m_tag_comment = Tag::create( "comment" );
    m_tag_comment->property_scale() = 0.8;
    m_tag_comment->property_rise() = 5000;
    m_tag_comment->property_strikethrough() = false; // for comments in canceled check list items
    m_tags.push_back( m_tag_comment );

    m_tag_image = Tag::create( "image" );
    m_tag_image->property_justification() = Gtk::JUSTIFY_CENTER;
    m_tags.push_back( m_tag_image );

    m_tag_misspelled = Tag::create( GTKSPELL_MISSPELLED_TAG );
    m_tag_misspelled->property_underline() = Pango::UNDERLINE_ERROR;
    m_tags.push_back( m_tag_misspelled );

    for( VecTags::iterator iter_tag = m_tags.begin(); iter_tag != m_tags.end(); ++iter_tag )
        tag_table->add( *iter_tag );
}

void
TextbufferDiary::handle_login()
{
    // SPELL CHECKING
    if( Diary::d->is_read_only() )
        m_flag_handle_word = false;

    m_tag_markup->property_invisible() = Diary::d->is_read_only();
}

void
TextbufferDiary::handle_logout()
{
    EntryParser::set_search_str( "" );

    if( m_enchant_dict )
    {
        enchant_broker_free_dict( Lifeograph::s_enchant_broker, m_enchant_dict );
        m_enchant_dict = nullptr;
    }
}

void
TextbufferDiary::set_search_str( const Glib::ustring& str )
{
    m_flag_updating_search_text = true;
    EntryParser::set_search_str( str );
    if( AppWindow::p->panel_main->get_cur_elem_type() == DiaryElement::ET_ENTRY )
    {
        m_flag_ongoing_operation++;
        place_cursor( begin() );
        m_flag_ongoing_operation--;
        if( m_ptr2entry )
            reparse();
    }
    m_flag_updating_search_text = false;
}

bool
TextbufferDiary::select_searchstr_previous()
{
    if( m_search_str.size() <= 0 )
        return false;

    Gtk::TextIter iter_start, iter_end, iter_bound_start, iter_bound_end;
    get_selection_bounds( iter_bound_start, iter_bound_end );

    if( iter_bound_start.is_start() )
        return false;

    if( ! iter_bound_start.backward_search( m_search_str,
                                            Gtk::TEXT_SEARCH_CASE_INSENSITIVE,
                                            iter_start,
                                            iter_end,
                                            begin() ) )
        return false;

    m_ptr2textview->scroll_to( iter_start );
    select_range( iter_start, iter_end );
    return true;
}

bool
TextbufferDiary::select_searchstr_next()
{
    if( m_search_str.size() <= 0 )
        return false;

    Gtk::TextIter iter_start, iter_end, iter_bound_start, iter_bound_end;
    get_selection_bounds( iter_bound_start, iter_bound_end );

    if( iter_bound_end.is_end() )
        return false;

    if( ! iter_bound_end.forward_search( m_search_str,
                                         Gtk::TEXT_SEARCH_CASE_INSENSITIVE,
                                         iter_start,
                                         iter_end,
                                         end() ) )
        return false;

    m_ptr2textview->scroll_to( iter_start );
    select_range( iter_start, iter_end );
    return true;
}

void
TextbufferDiary::parse( int o_start, int o_end )
{
    m_flag_parsing = true;
    m_flag_ongoing_operation++;

    // COMPLETELY CLEAR THE PARSING REGION
    if( ! m_flag_settextoperation )
    {
        if( ! m_flag_updating_search_text )
        {
            clear_images( o_start, o_end ); // alters the buffer
            clear_links( o_start, o_end );
        }

        remove_all_tags( get_iter_at_offset( o_start ), get_iter_at_offset( o_end ) );

        m_ptr2entry->set_text( get_text() );
    }

    update_todo_status();

    // DO THE PARSING
    EntryParser::parse( o_start, o_end );

    m_flag_ongoing_operation--;
    m_flag_parsing = false;

    if( !Diary::d->is_read_only() )
        reset_markup_visibilities();
}

Wchar
TextbufferDiary::get_char_at( int i )
{
    return get_iter_at_offset( i ).get_char();
}

bool
TextbufferDiary::check_cursor_is_in_para()
{
    return( ( m_pos_current > m_parser_pos_cur_para_begin ) &&
            ( m_pos_current <= m_parser_pos_cur_para_end ) );
}

// PARSING APPLIERS
void
TextbufferDiary::apply_heading( bool flag_non_empty )
{
    Gtk::TextIter iter_start{ get_iter_at_offset( 0 ) };
    Gtk::TextIter iter_end{ iter_start };

    if( flag_non_empty )
    {
        if( ! iter_start.ends_line() )
            iter_end.forward_to_line_end();
        apply_tag( m_tag_heading, iter_start, iter_end );
    }

    if( ! m_flag_settextoperation )
    {
        m_ptr2entry->set_name( iter_end.get_offset() > 0 ?
                get_text( iter_start, iter_end ) :
                get_char_count() < 1 ? STRING::EMPTY_ENTRY_TITLE : "" );

        AppWindow::p->panel_diary->handle_elem_title_changed( m_ptr2entry );
        AppWindow::p->panel_main->refresh_title();
    }
}

void
TextbufferDiary::apply_subheading()
{
    Gtk::TextIter iter_start( get_iter_at_offset( m_pos_start ) );
    Gtk::TextIter iter_current( iter_start );
    iter_current.forward_to_line_end();
    apply_tag( m_tag_subheading, iter_start, iter_current );
}

void
TextbufferDiary::apply_bold()
{
    apply_markup( m_tag_bold );
}

void
TextbufferDiary::apply_italic()
{
    apply_markup( m_tag_italic );
}

void
TextbufferDiary::apply_strikethrough()
{
    apply_markup( m_tag_strikethrough );
}

void
TextbufferDiary::apply_highlight()
{
    apply_markup( m_tag_highlight );
}

void
TextbufferDiary::apply_comment()
{
    Gtk::TextIter iter_start( get_iter_at_offset( m_pos_start ) );
    Gtk::TextIter iter_end( get_iter_at_offset( m_pos_current ) );
    iter_end++;
    apply_tag( m_tag_comment, iter_start, iter_end );
}

void
TextbufferDiary::apply_ignore()
{
    Gtk::TextIter iter_start( get_iter_at_offset( m_pos_start ) );
    Gtk::TextIter iter_end( iter_start );
    iter_end.forward_to_line_end();
    apply_tag( m_tag_region, iter_start, iter_end );
}

void
TextbufferDiary::apply_hidden_link_tags( Gtk::TextIter& iter_end,
                                         const Glib::RefPtr< Tag >& tag_link )
{
    Gtk::TextIter iter_start( get_iter_at_offset( m_pos_start ) );
    Gtk::TextIter iter_current( get_iter_at_offset( m_pos_current ) );
    Gtk::TextIter iter_label( get_iter_at_offset( pos_tab + 1 ) );

    apply_tag( m_tag_markup, iter_start, iter_label );
    apply_tag( m_tag_markup, iter_current, iter_end );
    remove_tag( m_tag_misspelled, iter_start, iter_label );

    apply_tag( tag_link, iter_label, iter_current );
    apply_tag( m_tag_link_hidden, iter_start, iter_end );

    // hide link markup if cursor is not in it:
    if( !check_cursor_is_in_para() )
    {
        apply_tag( m_tag_hidden, iter_start, iter_label );
        apply_tag( m_tag_hidden, iter_current, iter_end );
    }
}

void
TextbufferDiary::apply_link()
{
    // HANDLE RELATIVE PATHS
    if( m_word_last.find( "rel://" ) == 0 )
        m_word_last.replace( 0, 5, "file://" + Glib::path_get_dirname( Diary::d->get_path() ) );

    Gtk::TextIter iter_current( get_iter_at_offset( m_pos_current ) );

    if( m_flag_hidden_link )
    {
        Gtk::TextIter iter_end( get_iter_at_offset( m_pos_current + 1 ) );
        Gtk::TextIter iter_url_start( get_iter_at_offset( m_pos_start + 1 ) );
        Gtk::TextIter iter_tab( get_iter_at_offset( pos_tab ) );
        Gtk::TextIter iter_label( get_iter_at_offset( pos_tab + 1 ) );
        m_list_links.push_back( new LinkUri( create_mark( iter_label ),
                                             create_mark( iter_current ),
                                             get_slice( iter_url_start, iter_tab ) ) );

        apply_hidden_link_tags( iter_end, m_tag_link );
    }
    else
    {
        PRINT_DEBUG( "url: " + m_word_last );

        Gtk::TextIter iter_start( get_iter_at_offset( m_pos_start ) );
        remove_tag( m_tag_misspelled, iter_start, iter_current );

        // check if link is to an image file:
        if( m_word_last.find( "file:///" ) == 0  &&
            // there can be no text on the same line as the image:
            iter_start.starts_line() && iter_current.ends_line() )
        {
            try
            {
                Glib::RefPtr< Gdk::Pixbuf > buf(
                        Lifeograph::get_thumbnail( Glib::filename_from_uri( m_word_last ),
                                                   m_max_thumbnail_width ) );

                iter_current = insert( iter_current, "\n" );
                iter_current = insert_pixbuf( iter_current, buf );
                iter_start = get_iter_at_offset( m_pos_start );

                m_pos_current += 2;
                m_pos_end += 2;

                auto iter_img_start{ iter_current };
                iter_img_start.backward_cursor_positions( 2 );
                apply_tag( m_tag_link, iter_start, iter_current );
                apply_tag( m_tag_markup, iter_start, iter_img_start );
                apply_tag( m_tag_image, iter_img_start, iter_current );

            }
            catch( Glib::FileError& er )
            {
                print_error( "Link target not found" );
                apply_tag( m_tag_link_broken, iter_start, iter_current );
            }
            catch( Gdk::PixbufError& er )
            {
                PRINT_DEBUG( "Link is not an image" );
                apply_tag( m_tag_link, iter_start, iter_current );
            }
        }
        else
        {
            apply_tag( m_tag_link, iter_start, iter_current );
        }

        m_list_links.push_back( new LinkUri( create_mark( iter_start ),
                                             create_mark( iter_current ),
                                             m_word_last ) );
    }
}

void
TextbufferDiary::apply_link_id()
{
    DiaryElement* element( Diary::d->get_element( id_last ) );

    if( element != nullptr )
    {
        if( element->get_type() == DiaryElement::ET_ENTRY )
        {
            Gtk::TextIter iter_label( get_iter_at_offset( pos_tab + 1 ) );
            Gtk::TextIter iter_current( get_iter_at_offset( m_pos_current ) );
            Gtk::TextIter iter_end( get_iter_at_offset( m_pos_current + 1 ) );

            m_list_links.push_back( new LinkID( create_mark( iter_label ),
                                                create_mark( iter_current ),
                                                id_last ) );

            apply_hidden_link_tags( iter_end, m_tag_link );
            return;
        }
    }
    // indicate dead links
    Gtk::TextIter iter_start( get_iter_at_offset( m_pos_start ) );
    Gtk::TextIter iter_end( get_iter_at_offset( m_pos_current + 1 ) );
    apply_tag( m_tag_link_broken, iter_start, iter_end );
}

void
TextbufferDiary::apply_link_date()
{
    LinkStatus status( LS_OK );
    Entry* ptr2entry( Diary::d->get_entry( date_last.m_date + 1 ) ); // + 1 fixes order
    if( ptr2entry == nullptr )
    {
        if( Diary::d->is_read_only() )
            return;
        status = LS_ENTRY_UNAVAILABLE;
    }
    else if( date_last.get_pure() == m_ptr2entry->get_date().get_pure() )
        status = Diary::d->get_day_has_multiple_entries( date_last ) ? LS_OK : LS_CYCLIC;

    Gtk::TextIter iter_start( get_iter_at_offset( m_pos_start ) );
    Gtk::TextIter iter_current( get_iter_at_offset( m_pos_current ) );
    Gtk::TextIter iter_label( get_iter_at_offset( pos_tab + 1 ) );

    if( status < LS_INVALID )
    {
        Gtk::TextIter iter_end( iter_current );
        iter_end++;

        if( m_flag_hidden_link )
        {
            m_list_links.push_back( new LinkEntry( create_mark( iter_label ),
                                                   create_mark( iter_current ),
                                                   date_last ) );
            apply_hidden_link_tags( iter_end, status == LS_OK ? m_tag_link : m_tag_link_broken );
        }
        else
        {
            m_list_links.push_back( new LinkEntry( create_mark( iter_start ),
                                                   create_mark( iter_end ),
                                                   date_last ) );

            apply_tag( status == LS_OK ? m_tag_link : m_tag_link_broken, iter_start, iter_end );
        }
    }
}

void
TextbufferDiary::apply_check( Glib::RefPtr< Gtk::TextTag >* tag_box,
                              Glib::RefPtr< Gtk::TextTag >* tag,
                              int c )
{
    Gtk::TextIter iter_start = get_iter_at_offset( m_pos_current - 3 );
    Gtk::TextIter iter_box = get_iter_at_offset( m_pos_current );
    Gtk::TextIter iter_end = iter_box;
    iter_end.forward_to_line_end();
    if( ! Diary::d->is_read_only() )
        m_list_links.push_back( new LinkCheck( create_mark( iter_start ),
                                               create_mark( iter_box ),
                                               c ) );

    // MAYBE: apply_tag( m_tag_hidden, iter_start, iter_box );
    apply_tag( *tag_box, iter_start, iter_box );
    if( tag )
        apply_tag( *tag, ++iter_box, iter_end ); // ++ to skip separating space char

    // MAYBE: m_list_images.insert( new InlineImage( create_mark( iter_start ) ) );
    // MAYBE: insert_pixbuf( iter_start, pixbuf );
}

void
TextbufferDiary::apply_check_unf()
{
    apply_check( &m_tag_checkbox_todo, &m_tag_bold, 0 );
}

void
TextbufferDiary::apply_check_prg()
{
    apply_check( &m_tag_checkbox_progressed, nullptr, 1 );
}

void
TextbufferDiary::apply_check_fin()
{
    apply_check( &m_tag_checkbox_done, &m_tag_done, 2 );
}

void
TextbufferDiary::apply_check_ccl()
{
    apply_check( &m_tag_checkbox_canceled, &m_tag_strikethrough, 3 );
}

void
TextbufferDiary::apply_match()
{
    Gtk::TextIter iter_start( get_iter_at_offset( pos_search ) );
    Gtk::TextIter iter_end( get_iter_at_offset( m_pos_current+1 ) );
    apply_tag( m_tag_match, iter_start, iter_end );
}

void
TextbufferDiary::apply_indent()
{
    Glib::RefPtr< Gtk::TextTag > tag_indent = Tag::create();
    int tab_width = get_text_width( m_ptr2entry->get_theme()->font, "\t" );
    tab_width *=  ( m_pos_current - m_pos_start );
    tag_indent->property_left_margin() = tab_width; // FIXME: indents more than expected
    tag_indent->property_indent() = -tab_width;

    get_tag_table()->add( tag_indent );

    Gtk::TextIter iter_start( get_iter_at_offset( m_pos_start ) );
    Gtk::TextIter iter_end( get_iter_at_offset( m_pos_current ) );
    apply_tag( tag_indent, iter_start, iter_end );
}

//#define CALL_MEMBER_FN(object,ptrToMember)  ((object).*(ptrToMember))

inline void
TextbufferDiary::apply_markup( const Glib::RefPtr< Tag >& tag )
{
    Gtk::TextIter iter_start( get_iter_at_offset( m_pos_start ) );
    Gtk::TextIter iter_current( get_iter_at_offset( m_pos_current ) );
    Gtk::TextIter iter2( iter_start );
    iter2++;
    apply_tag( m_tag_markup, iter_start, iter2 );
    apply_tag( tag, iter2, iter_current );
    iter2 = iter_current;
    iter2++;
    apply_tag( m_tag_markup, iter_current, iter2 );
}

void
TextbufferDiary::update_todo_status()
{
    if( ( m_ptr2entry->get_status() & ES::NOT_TODO ) && m_ptr2entry->calculate_todo_status() )
    {
        AppWindow::p->panel_main->set_icon( m_ptr2entry->get_icon32() );
        AppWindow::p->panel_diary->handle_elem_changed( m_ptr2entry );
    }
}

// EVENT HANDLERS
void
TextbufferDiary::handle_space()
{
    Gtk::TextIter iter_end = get_iter_at_mark( get_insert() );
    Gtk::TextIter iter_start( iter_end );
    if( ! iter_start.backward_find_char( s_predicate_nl ) )
        return;

    iter_start++;   // skip the new line char
    Glib::ustring line = get_text( iter_start, iter_end );
    char char_lf = '\t';
    unsigned int size = line.size();

    for( unsigned int i = 0; i < size; i++ )
    {
        switch( line[ i ] )
        {
            case '\t':
                if( char_lf == '\t' )
                    char_lf = 'A';  // any list char like [ or *
                else
                if( char_lf != 'A' )    // multiple tabs are possible (indentation)
                    return;
                iter_start++;   // indentation level
                break;
            case '[':
                if( char_lf != 'A' )
                    return;
                char_lf = ']';
                break;
            case ']':
                if( char_lf != ']' || i != ( size - 1 ) )
                    return;
                {
                    m_flag_ongoing_operation++;
                    iter_start = erase( iter_start, iter_end );
                    m_flag_ongoing_operation--;
                    insert( iter_start, "[ ]" );
                }
                break;
            case '*':
                if( char_lf != 'A' || i != ( size - 1 ) )
                    return;
                {
                    m_flag_ongoing_operation++;
                    iter_start = erase( iter_start, iter_end );
                    m_flag_ongoing_operation--;
                    insert( iter_start, "•" );
                }
                break;
            default:
                return;
        }
    }
}

bool
TextbufferDiary::handle_minus() // replaces 3 minus signs with an em dash
{
    Gtk::TextIter iter = get_iter_at_mark( get_insert() );
    for( int count{ 0 }; iter.backward_char(); )
    {
        switch( iter.get_char() )
        {
            case '-':
                if( ++count == 2 )
                {
                    m_flag_ongoing_operation++;
                    iter = erase( iter, get_iter_at_mark( get_insert() ) );
                    m_flag_ongoing_operation--;
                    insert( iter, "—" );
                    return true;
                }
                break;
            default:
                return false;
        }
    }
    return false;
}

bool
TextbufferDiary::handle_new_line()
{
    Gtk::TextIter iter_end = get_iter_at_mark( get_insert() );
    Gtk::TextIter iter_start( iter_end );
    if( ! iter_start.backward_find_char( s_predicate_nl ) ||
        iter_end.get_line_offset() < 3 )
        return false;

    iter_start++;   // get rid of the new line char
    const int offset_start{ iter_start.get_offset() };   // save for future

    if( iter_start.get_char() == '\t' )
    {
        Ustring text( "\n\t" );
        int value = 0;
        char char_lf = '*';
        iter_start++;   // first tab is already handled, so skip it

        for( ; iter_start != iter_end; ++iter_start )
        {
            switch( iter_start.get_char() )
            {
                // BULLETED LIST
                case L'•':
                    if( char_lf != '*' )
                        return false;
                    char_lf = ' ';
                    text += "• ";
                    break;
                // CHECK LIST
                case '[':
                    if( char_lf != '*' )
                        return false;
                    char_lf = 'c';
                    break;
                case '~':
                case '+':
                case 'x':
                case 'X':
                    if( char_lf != 'c' )
                        return false;
                    char_lf = ']';
                    break;
                case ']':
                    if( char_lf != ']' )
                        return false;
                    char_lf = ' ';
                    text += "[ ] ";
                    break;
                // NUMBERED LIST
                case '0': case '1': case '2': case '3': case '4':
                case '5': case '6': case '7': case '8': case '9':
                    if( char_lf != '*' && char_lf != '1' )
                        return false;
                    char_lf = '1';
                    value *= 10;
                    value += iter_start.get_char() - '0';
                    break;
                case '-':
                    if( char_lf == '*' )
                    {
                        char_lf = ' ';
                        text += "- ";
                        break;
                    }
                    // no break
                case '.':
                case ')':
                    if( char_lf != '1' )
                        return false;
                    char_lf = ' ';
                    text += Glib::ustring::compose< int,char >(
                            "%1%2 ", ++value, iter_start.get_char() );
                    break;
                case '\t':
                    if( char_lf != '*' )
                        return false;
                    text += '\t';
                    break;
                case ' ':
                    if( char_lf == 'c' )
                    {
                        char_lf = ']';
                        break;
                    }
                    else if( char_lf != ' ' )
                        return false;
                    // remove the last list item if no text follows it:
                    if( iter_start.get_offset() == iter_end.get_offset() - 1 )
                    {
                        iter_start = get_iter_at_offset( offset_start );
                        m_flag_ongoing_operation++;
                        iter_start = erase( iter_start, iter_end );
                        m_flag_ongoing_operation--;
                        insert( iter_start, "\n" );
                        return true;
                    }
                    else
                    {
                        iter_start = insert( iter_end, text );
                        if( value > 0 )
                        {
                            iter_start++;
                            while( increment_numbered_line( iter_start, value++ ) )
                            {
                                iter_start.forward_to_line_end();
                                iter_start++;
                            }
                        }
                        return true;
                    }
                    break;
                default:
                    return false;
            }
        }
    }
    return false;
}

bool
TextbufferDiary::increment_numbered_line( Gtk::TextIter& iter, int expected_value )
{
    Gtk::TextIter iter_start = iter;
    Gtk::TextIter iter_end = iter;
    iter_end.forward_to_line_end();

    Glib::ustring text( "" );
    int value = 0;
    char char_lf = 't';

    for( ; iter != iter_end; ++iter )
    {
        switch( iter.get_char() )
        {
            case '\t':
                if( char_lf != 't' && char_lf != '1' )
                    return false;
                char_lf = '1';
                text += '\t';
                break;
            case '0': case '1': case '2': case '3': case '4':
            case '5': case '6': case '7': case '8': case '9':
                if( char_lf != '1' && char_lf != '-' )
                    return false;
                char_lf = '-';
                value *= 10;
                value += iter.get_char() - '0';
                break;
            case '-':
            case '.':
            case ')':
                if( char_lf != '-' || value != expected_value )
                    return false;
                char_lf = ' ';
                value++;
                text += Glib::ustring::compose< int,char >(
                        "%1%2", value, iter.get_char() );
                break;
            case ' ':
                if( char_lf != ' ' )
                    return false;
                m_flag_ongoing_operation++;
                iter_start = erase( iter_start, iter );
                m_flag_ongoing_operation--;
                iter = insert( iter_start, text );
                return true;
            default:
                return false;
        }
    }
    return false;
}

void
TextbufferDiary::on_insert( const Gtk::TextIter& iterator,
                            const Glib::ustring& text,
                            int bytes )
{
    PRINT_DEBUG( "TextbufferDiary::on_insert()" );

    // REPLACE \r chars
    std::string text2( text ); // to drop constness
    std::replace( text2.begin(), text2.end(), '\r', '\n' );

    const Glib::ustring::size_type offset_itr = iterator.get_offset();

    // UNDO
    if( UndoManager::m->is_freezed() )
    {
        Gtk::TextIter iter_scroll( iterator );  // to remove constness
        m_ptr2textview->scroll_to( iter_scroll );
    }
    else
    {
        UndoInsert* undo_insert = new UndoInsert( offset_itr, text2 );
        UndoManager::m->add_action( undo_insert );
    }

    if( text == "-" && handle_minus() )
    {   // curly bracket is necessary for non-debug build
        PRINT_DEBUG( "em dash inserted" );
    }
    else
        Gtk::TextBuffer::on_insert( iterator, text2, bytes );

    m_flag_outstanding_change = true;
}

void
TextbufferDiary::on_erase( const Gtk::TextIter& iter_start,
                           const Gtk::TextIter& iter_end )
{
    PRINT_DEBUG( "TextbufferDiary::on_erase()" );

    const int offset_itr = iter_start.get_offset();

    if( UndoManager::m->is_freezed() )
    {
        Gtk::TextIter iter_scroll( iter_start );    // to remove constness
        m_ptr2textview->scroll_to( iter_scroll );
    }
    else
    {
        UndoErase* undo_erase = new UndoErase( offset_itr, get_slice( iter_start, iter_end ) );
        UndoManager::m->add_action( undo_erase );
    }

    Gtk::TextBuffer::on_erase( iter_start, iter_end );

    m_flag_outstanding_change = true;
}

void
TextbufferDiary::on_mark_set( const TextBuffer::iterator& iter,
                              const Glib::RefPtr< TextBuffer::Mark >& mark )
{
    if( m_ptr2textview && m_max_thumbnail_width > 0 && mark == get_insert() )
    {
        PRINT_DEBUG( "TextbufferDiary::on_mark_set()" );
        m_flag_outstanding_change = true;
    }
    Gtk::TextBuffer::on_mark_set( iter, mark );
}

void
TextbufferDiary::handle_event_after( GdkEvent* )
{
    if( Lifeograph::loginstatus == Lifeograph::LOGGED_IN &&
        m_flag_outstanding_change && ! m_flag_ongoing_operation &&
        ! Diary::d->is_read_only() )
    {
#if LIFEOGRAPH_DEBUG_BUILD
        PRINT_DEBUG( "*** TextbufferDiary::handle_event_after() *** ",
                     "para: (", m_parser_pos_cur_para_begin,
                     ", ", m_parser_pos_cur_para_end, ")" );
#endif

        // UPDATE PARA BOUNDARIES
        Gtk::TextIter   iter_bgn    { get_insert()->get_iter() };
        Gtk::TextIter   iter_end    { iter_bgn };

        m_pos_cursor = iter_bgn.get_offset();

        calculate_para_bounds( iter_bgn, iter_end );

        if( iter_bgn.has_tag( m_tag_image ) )
            iter_bgn.backward_to_tag_toggle( m_tag_link );

        if( iter_end.has_tag( m_tag_image ) )
            iter_end.forward_to_tag_toggle( m_tag_image );

        m_parser_pos_cur_para_begin = iter_bgn.get_offset();
        m_parser_pos_cur_para_end = iter_end.get_offset();

        // PARSE
        parse( m_parser_pos_cur_para_begin, m_parser_pos_cur_para_end );

        m_flag_outstanding_change = false;
    }
}

Link*
TextbufferDiary::get_link( int offset ) const
{
    for ( ListLinks::const_iterator iter = m_list_links.begin();
          iter != m_list_links.end();
          ++iter )
    {
        if( offset >= ( *iter )->m_mark_start->get_iter().get_offset() &&
            offset <= ( *iter )->m_mark_end->get_iter().get_offset() )
            return( *iter );
    }

    return nullptr;
}

Link*
TextbufferDiary::get_link( const Gtk::TextIter& iter ) const
{
    for( Link* link : m_list_links )
    {
        if( iter.in_range( link->m_mark_start->get_iter(), link->m_mark_end->get_iter() ) )
            return( link );
    }
    return nullptr;
}

void
TextbufferDiary::clear_links( int start, int end )
{
    ListLinks::iterator iter_tmp;
    for( ListLinks::iterator iter = m_list_links.begin(); iter != m_list_links.end(); )
        if( start <= ( *iter )->m_mark_start->get_iter().get_offset() &&
            end >= ( *iter )->m_mark_end->get_iter().get_offset() )
        {
            iter_tmp = iter;
            ++iter;
            delete( *iter_tmp );
            m_list_links.erase( iter_tmp );
        }
        else
            ++iter;
}

void
TextbufferDiary::clear_links()
{
    for( Link* link : m_list_links )
        delete( link );

    m_list_links.clear();
}

void
TextbufferDiary::clear_images( int start, int& end )
{
    PRINT_DEBUG( "TextbufferDiary::clear_images( ", start, ", ", end, " )" );

    m_flag_ongoing_operation++;

    for( auto iter = get_iter_at_offset( start ); iter.forward_to_tag_toggle( m_tag_image ); )
    {
        if( iter.get_offset() > end )
            break;

        if( iter.starts_tag( m_tag_image ) )
            iter.forward_to_tag_toggle( m_tag_image );

        auto iter_begin{ iter };
        iter_begin.backward_cursor_positions( 2 );
        iter = erase( iter_begin, iter );
        end--;
    }

    m_flag_ongoing_operation--;
}

bool
TextbufferDiary::update_thumbnail_width( int width )
{
    // only multitudes of 100 are accepted
    if( width > 200 )
    {
        width = width / 2;
        width -= ( width % 100 );
    }
    else
        width = 100;

    if( width == m_max_thumbnail_width )
        return false;
    else
    {
        m_max_thumbnail_width = width;
        return true;
    }
}

void
TextbufferDiary::reset_markup_visibilities()
{
    auto iter{ begin() };
    auto iter_end{ iter };
    int offset;

    while( iter.forward_to_tag_toggle( m_tag_markup ) )
    {
        iter_end = iter;
        iter_end.forward_to_tag_toggle( m_tag_markup );
        offset = iter.get_offset();

        if( ! iter.has_tag( m_tag_hidden ) &&
            ( offset < m_parser_pos_cur_para_begin ||
              offset > m_parser_pos_cur_para_end ) )
            apply_tag( m_tag_hidden, iter, iter_end );

        iter = iter_end;
    }
}

void
TextbufferDiary::handle_menu( Gtk::Menu* menu )
{
    if( Diary::d->is_read_only() )
        return;

    // SEPARATOR
    Gtk::SeparatorMenuItem* separator( Gtk::manage( new Gtk::SeparatorMenuItem ) );
    separator->show();
    menu->prepend( *separator );

    if( !m_ptr2entry->get_lang_final().empty() )
    {
        // below code is created by using GtkSpell as a template
        // SUGGESTIONS
        Gtk::TextIter iter_begin = get_iter_at_offset( m_spell_suggest_offset );
        if( iter_begin.has_tag( m_tag_misspelled ) )
        {
            Gtk::TextIter iter_end( iter_begin );
            iter_begin.backward_to_tag_toggle( m_tag_misspelled );
            iter_end.forward_to_tag_toggle( m_tag_misspelled );
            add_suggestion_menus( get_text( iter_begin, iter_end ), menu );
        }
    }

    // LANGUAGE SELECTION SUBMENU
    Gtk::MenuItem* mi_langs( Gtk::manage( new Gtk::MenuItem( _( "Spell Checking" ) ) ) );
    mi_langs->set_submenu( *build_languages_menu() );
    mi_langs->show_all();
    menu->prepend( *mi_langs );
}

// FORMATTING
void
TextbufferDiary::toggle_format( Glib::RefPtr< Tag > tag, const Glib::ustring& markup )
{
    if( ! m_ptr2textview->has_focus() )
        return;

    Gtk::TextIter iter_start, iter_end;
    if( get_has_selection() )
    {
        int start( -2 ), end( -1 );
        bool properly_separated( false );

        get_selection_bounds( iter_start, iter_end );
        int p_start = iter_start.get_offset();
        int p_end = iter_end.get_offset() - 1;

        Gtk::TextIter firstnl = begin();
        if( ! firstnl.forward_find_char( s_predicate_nl ) ) // there is only heading
            return;
        int p_first_nl = firstnl.get_offset();

        if( p_end <= p_first_nl )
            return;
        else if( p_start > p_first_nl )
            p_start--;   // also evaluate the previous character
        else // p_start <= p_first_nl
        {
            p_start = p_first_nl + 1;
            properly_separated = true;
            start = -1;
        }

        for( ; ; p_start++ )
        {
            if( get_iter_at_offset( p_start ).has_tag( m_tag_bold ) ||
                get_iter_at_offset( p_start ).has_tag( m_tag_italic ) ||
                get_iter_at_offset( p_start ).has_tag( m_tag_highlight ) ||
                get_iter_at_offset( p_start ).has_tag( m_tag_strikethrough ) )
                return;
            switch( get_iter_at_offset( p_start ).get_char() )
            {
                case '\n': // selection spreads over more than one line
                    if( start >= 0 )
                    {
                        if( properly_separated )
                        {
                            insert( get_iter_at_offset( start ), markup );
                            end += 2;
                            p_start += 2;
                            p_end += 2;
                        }
                        else
                        {
                            insert( get_iter_at_offset( start ), " " + markup );
                            end += 3;
                            p_start += 3;
                            p_end += 3;
                        }

                        insert( get_iter_at_offset( end ), markup );

                        properly_separated = true;
                        start = -1;
                        break;
                    }
                    /* else no break */
                case ' ':
                case '\t':
                    if( start == -2 )
                    {
                        properly_separated = true;
                        start = -1;
                    }
                    break;
                default: // non-space
                    if( start == -2 )
                        start = -1;
                    else
                    if( start == -1 )
                        start = p_start;
                    end = p_start;
                    break;
            }
            if( p_start == p_end )
                break;
        }
        // add markup chars to the beginning and end:
        if( start >= 0 )
        {
            if( properly_separated )
            {
                insert( get_iter_at_offset( start ), markup );
                end += 2;
            }
            else
            {
                insert( get_iter_at_offset( start ), " " + markup );
                end += 3;
            }

            insert( get_iter_at_offset( end ), markup );
            place_cursor( get_iter_at_offset( end ) );
        }
    }
    else    // no selection case
    {
        Glib::RefPtr< Gtk::TextMark > mark = get_insert();
        iter_start = mark->get_iter();
        if( Glib::Unicode::isspace( iter_start.get_char() ) || iter_start.is_end() )
        {
            if( iter_start.starts_line() )
                return;
            iter_start--;
            if( iter_start.has_tag( TextbufferDiary::m_tag_markup ) )
                iter_start--;
        }
        else if( iter_start.has_tag( TextbufferDiary::m_tag_markup ) )
        {
            if( iter_start.starts_line() )
                return;
            iter_start--;
            if( Glib::Unicode::isspace( iter_start.get_char() ) )
            {
                iter_start.forward_chars( 2 );
            }
        }
        if( iter_start.has_tag( tag ) ) // if already has the tag, remove it
        {
            m_flag_ongoing_operation++;

            // necessary when cursor is between a space char and non-space char:
            if( iter_start.starts_word() )
                iter_start++;

            iter_end = iter_start;

            iter_start.backward_to_tag_toggle( tag );
            iter_end.forward_to_tag_toggle( TextbufferDiary::m_tag_markup );
            int offset_end( iter_end.get_offset() );
            backspace( iter_start );

            iter_end = get_iter_at_offset( offset_end );
            iter_end--;

            m_flag_ongoing_operation--;

            backspace( ++iter_end );
        }
        else
        if( iter_start.has_tag( m_tag_bold ) == false &&
            iter_start.has_tag( m_tag_italic ) == false &&
            iter_start.has_tag( m_tag_strikethrough ) == false &&
            iter_start.has_tag( m_tag_highlight ) == false &&
            iter_start.has_tag( m_tag_heading ) == false &&
            iter_start.has_tag( m_tag_link ) == false ) // formatting tags cannot be nested
        {
            // find word boundaries:
            if( !( iter_start.starts_word() || iter_start.starts_line() ) )
                iter_start.backward_word_start();
            insert( iter_start, markup );

            iter_end = mark->get_iter();
            if( !( iter_end.ends_word() || iter_end.ends_line() ) )
            {
                iter_end.forward_word_end();
                insert( iter_end, markup );
            }
            else
            {
                int offset = iter_end.get_offset();
                insert( iter_end, markup );
                place_cursor( get_iter_at_offset( offset ) );
            }
        }
    }
}

void
TextbufferDiary::toggle_bold()
{
    toggle_format( m_tag_bold, "*" );
}

void
TextbufferDiary::toggle_italic()
{
    toggle_format( m_tag_italic, "_" );
}

void
TextbufferDiary::toggle_strikethrough()
{
    toggle_format( m_tag_strikethrough, "=" );
}

void
TextbufferDiary::toggle_highlight()
{
    toggle_format( m_tag_highlight, "#" );
}

void
TextbufferDiary::set_comment_visibility( bool visible )
{
    m_tag_comment->property_invisible() = visible;
}

void
TextbufferDiary::calculate_para_bounds( Gtk::TextIter& iter_begin, Gtk::TextIter& iter_end )
{
    PRINT_DEBUG( "TextbufferDiary::calculate_para_bounds()" );
    if( ! iter_begin.backward_find_char( s_predicate_nl ) )
        iter_begin.set_offset( 0 );

    if( !iter_end.ends_line() )
        if( !iter_end.forward_find_char( s_predicate_nl ) )
            iter_end.forward_to_end();
}

bool    // returns true if boundaries reach the end of text
TextbufferDiary::calculate_multi_para_bounds( Gtk::TextIter& iter_bgn, Gtk::TextIter& iter_end )
{
    if( get_has_selection() )
        get_selection_bounds( iter_bgn, iter_end );
    else
        iter_bgn = iter_end = get_iter_at_mark( get_insert() );

    if( ! iter_bgn.backward_find_char( s_predicate_nl ) )
        iter_bgn = get_iter_at_offset( 0 );
    else
        iter_bgn++;

    if( iter_end.get_char() != '\n' )
        if( ! iter_end.forward_find_char( s_predicate_nl ) )
        {
            iter_end.forward_to_end();
            iter_end++;
            return true;    // end of text case
        }

    return false;
}

void
TextbufferDiary::handle_indent()
{
    if( ! m_ptr2textview->has_focus() )
        return;

    Gtk::TextIter iter, iter_end;
    calculate_multi_para_bounds( iter, iter_end );

    if( iter == iter_end ) // empty line
    {
        insert( iter, "\t" );
        return;
    }

    int offset{ iter_end.get_offset() + 1 };    // +1 for the new \t char

    iter = insert( iter, "\t" );    // first line

    while( iter != get_iter_at_offset( offset ) )
    {
        if( iter.get_char() == '\n' )
        {
            ++iter;
            iter = insert( iter, "\t" );
            offset++;
        }
        else
            ++iter;
    }
}

void
TextbufferDiary::handle_unindent()
{
    if( ! m_ptr2textview->has_focus() )
        return;

    Gtk::TextIter iter, iter_end;
    calculate_multi_para_bounds( iter, iter_end );

    if ( iter == iter_end ) // empty line
        return;

    int offset{ iter_end.get_offset() };

    if( iter.get_char() == '\t' )
    {
        iter_end = iter;
        iter = erase( iter, ++iter_end ); // first line
        offset--;
    }

    while( iter != get_iter_at_offset( offset ) )
    {
        if( iter.get_char() == '\n' )
        {
            ++iter;
            if( iter.get_char() == '\t' )
            {
                iter_end = iter;
                iter = erase( iter, ++iter_end );    // first line
                offset--;
            }
        }
        else
            ++iter;
    }
}

// LEGEND for char_lf values in add_bullet() and add_checkbox()
//      a: any char
//      t: tab
//      s: space
//      c: check mark such as + or x
//      n: new line

void
TextbufferDiary::set_list_item_mark( char target_item_type )
{
    Gtk::TextIter iter, iter_end;
    calculate_multi_para_bounds( iter, iter_end );

    if ( iter == iter_end ) // empty line
    {
        switch( target_item_type )
        {
            case '*':
                iter = insert( iter, "\t• " );
                break;
            case ' ':
                iter = insert( iter, "\t[ ] " );
                break;
            case '~':
                iter = insert( iter, "\t[~] " );
                break;
            case '+':
                iter = insert( iter, "\t[+] " );
                break;
            case 'x':
                iter = insert( iter, "\t[x] " );
                break;
        }
        return;
    }

    int offset{ iter_end.get_offset() };
    Gtk::TextIter iter_erase_begin( iter );
    char item_type( 0 );    // none
    char char_lf( 't' );    // tab
    while( iter != get_iter_at_offset( offset ) )
    {
        switch( iter.get_char() )
        {
            case '\t':
                if( char_lf == 't'  || char_lf == '[' )
                {
                    char_lf = '[';  // opening bracket
                    iter_erase_begin = iter;
                }
                else
                    char_lf = 'n';
                break;
            case L'•':
            case '-':
                char_lf = ( char_lf == '[' ? 's' : 'n' );
                item_type = ( char_lf == 's' ? '*' : 0 );
                break;
            case '[':
                char_lf = ( char_lf == '[' ? 'c' : 'n' );
                break;
            case ' ':
                if( char_lf == 's' ) // separator space
                {
                    if( item_type != target_item_type )
                    {
                        iter++;
                        offset -= ( iter.get_offset() - iter_erase_begin.get_offset() );
                        iter = erase( iter_erase_begin, iter );
                        char_lf = 'a';
                        continue;
                    }
                    else
                    {
                        char_lf = 'n';
                        break;
                    }
                }
                // no break: process like other check box chars:
            case '~':
            case '+':
            case 'x':
            case 'X':
                char_lf = ( char_lf == 'c' ? ']' : 'n' );
                item_type = iter.get_char();
                break;
            case ']':
                char_lf = ( char_lf == ']' ? 's' : 'n' );
                break;
            case '\n':
                item_type = 0;
                char_lf = 't';  // tab
                break;
            case 0: // end
            default:
                if( char_lf == 'a' || char_lf == 't' || char_lf == '[' )
                {
                    Ustring list_str;
                    switch( target_item_type )
                    {
                        case '*':
                            list_str = "\t• " ;
                            break;
                        case ' ':
                            list_str = "\t[ ] ";
                            break;
                        case '~':
                            list_str = "\t[~] ";
                            break;
                        case '+':
                            list_str = "\t[+] ";
                            break;
                        case 'x':
                            list_str = "\t[x] ";
                            break;
                    }
                    iter = insert( iter, list_str );
                    offset+= list_str.length();
                }
                char_lf = 'n';
                break;
        }
        ++iter;
    }
}

void
TextbufferDiary::add_empty_line_above()
{
    if( ! m_ptr2textview->has_focus() )
        return;

    Gtk::TextIter iter( get_iter_at_mark( get_insert() ) );
    if( iter.backward_line() )
        iter.forward_line();
    insert( iter, "\n" );
}

void
TextbufferDiary::remove_empty_line_above()
{
    if( ! m_ptr2textview->has_focus() )
        return;

    Gtk::TextIter iter( get_iter_at_mark( get_insert() ) );
    if( iter.backward_line() )
        iter.forward_line();

    if( iter.get_line() < 1 )
        return;

    Gtk::TextIter iter_begin( --iter );
    iter_begin--;
    if( iter_begin.get_char() == '\n' )
        erase( iter_begin, iter );
}

void
TextbufferDiary::move_line_up()
{
    if( ! m_ptr2textview->has_focus() )
        return;

    Gtk::TextIter iter_bgn, iter_end;
    bool flag_touches_end{ calculate_multi_para_bounds( iter_bgn, iter_end ) };
    if( iter_bgn.is_start() )
        return;

    Gtk::TextIter iter_insert{ iter_bgn };
    iter_insert.backward_line();
    int offset_insert{ iter_insert.get_offset() - int( flag_touches_end ) };
    int offset_line{ get_iter_at_mark( get_insert() ).get_line_offset() };

    // when the selection touches end of text the [new line] before the start is used,
    // otherwise the one after the end is used
    if( flag_touches_end )
        iter_bgn--;
    else
        iter_end++;
    Glib::ustring text{ get_text( iter_bgn, iter_end ) };

    erase( iter_bgn, iter_end );

    iter_insert = get_iter_at_offset( offset_insert );
    insert( iter_insert, text );

    place_cursor( get_iter_at_offset( offset_insert + offset_line + int( flag_touches_end ) ) );
}

void
TextbufferDiary::move_line_down()
{
    if( ! m_ptr2textview->has_focus() )
        return;

    Gtk::TextIter iter_bgn, iter_end;
    if( calculate_multi_para_bounds( iter_bgn, iter_end ) )
        return;
    iter_end++;

    bool flag_is_near_end{ iter_end.get_line() == ( get_line_count() - 1 ) };
    Gtk::TextIter iter_insert{ iter_end };
    if( flag_is_near_end )
        iter_insert.forward_to_end();
    else if( iter_insert.get_char() != '\n' )
        iter_insert.forward_find_char( s_predicate_nl );
    iter_insert++;

    Glib::ustring text{ get_text( iter_bgn, iter_end ) };
    if( flag_is_near_end )
    {
        text.insert( 0, "\n" );
        text.erase( text.size() - 1 );
    }

    int offset_insert{ iter_insert.get_offset() - int( text.length() ) };
    int offset_cursor{ get_iter_at_mark( get_insert() ).get_offset()
                       +( iter_insert.get_offset() - iter_end.get_offset() )
                       + int( flag_is_near_end ) };

    erase( iter_bgn, iter_end );

    iter_insert = get_iter_at_offset( offset_insert );
    insert( iter_insert, text );

    place_cursor( get_iter_at_offset( offset_cursor ) );
}

void
TextbufferDiary::delete_paragraphs()
{
    if( ! m_ptr2textview->has_focus() )
        return;

    Gtk::TextIter iter_bgn, iter_end;
    if( calculate_multi_para_bounds( iter_bgn, iter_end ) )
        iter_end++;

    if( ! iter_bgn.is_start() )
        iter_bgn--;

    int offset{ iter_bgn.get_offset() + 1 };

    erase( iter_bgn, iter_end );
    place_cursor( get_iter_at_offset( offset ) );
}

void
TextbufferDiary::duplicate_paragraphs()
{
    if( ! m_ptr2textview->has_focus() )
        return;

    Gtk::TextIter iter_bgn, iter_end;
    if( calculate_multi_para_bounds( iter_bgn, iter_end ) )
        iter_end++;

    if( ! iter_bgn.is_start() )
        iter_bgn--;

    int offset{ get_iter_at_mark( get_insert() ).get_offset() };
    Glib::ustring text{ get_text( iter_bgn, iter_end ) };

    insert( iter_end, text );
    place_cursor( get_iter_at_offset( offset + text.length() ) );
}

void
TextbufferDiary::insert_link( DiaryElement* element )
{
    // TODO: implement a custom insert function that adds spaces where necessary
    Gtk::TextIter iter( get_iter_at_mark( get_insert() ) );
    if( iter.get_tags().size() > 0 )
        return;
    if( iter.get_offset() > 0 )
        iter--;
    char c( iter.get_char() );
    if( c != ' ' && c != '\n' && c != '\t' )
        insert( get_iter_at_mark( get_insert() ), " ");
    insert( get_iter_at_mark( get_insert() ),
                Glib::ustring::compose( "<deid:%1\t%2>", element->get_id(), element->get_name() ) );
}

void
TextbufferDiary::insert_time_stamp()
{
    if( ! m_ptr2textview->has_focus() )
        return;

    // TODO: implement a custom insert function that adds spaces where necessary
    Gtk::TextIter iter( get_iter_at_mark( get_insert() ) );
    //if( iter.get_tags().size() > 0 )
        //return;
    if( iter.get_offset() > 0 )
        iter--;
    char c( iter.get_char() );
    if( c != ' ' && c != '\n' && c != '\t' )
        insert( get_iter_at_mark( get_insert() ), " ");

    insert( get_iter_at_mark( get_insert() ), Date::format_string_dt( time( nullptr ) ) );
}

void
TextbufferDiary::set_richtext( Entry* entry )
{
    m_flag_settextoperation = true;
    clear_links();
    m_parser_pos_cur_para_begin = -1;
    m_parser_pos_cur_para_end = -1;
    UndoManager::m->clear();
    UndoManager::m->freeze();
    m_ptr2entry = entry;
    m_word_count = 0;
    set_language( Diary::d->is_read_only() ? "" : entry->get_lang_final() );
    set_theme( entry->get_theme() );
    Gtk::TextBuffer::set_text( entry->get_text() );
    place_cursor( begin() );
    UndoManager::m->thaw();
    reparse();
    m_flag_settextoperation = false;
}

Glib::ustring
TextbufferDiary::get_selected_text() const
{
    if( get_has_selection() )
    {
        Gtk::TextIter iter_start, iter_end;
        get_selection_bounds( iter_start, iter_end );
        return get_text( iter_start, iter_end );
    }
    else
        return "";
}

inline Gdk::RGBA
midcontrast( const Theme* theme, const Gdk::RGBA& color_target )
{
    //Gdk::RGBA color_lt = midtone( theme->color_base, color_target, 0.8 );
    //Gdk::RGBA color_dk = midtone( theme->color_text, color_target, 0.8 );
    //return contrast2( color_target, color_lt, color_dk );

    Gdk::RGBA color_lt = midtone( Gdk::RGBA( "white" ), color_target, 0.8 );
    return midtone( color_lt, theme->color_base, 0.85 );
}

void
TextbufferDiary::set_theme( const Theme* theme )
{
    //TODO: check in Gtk+3.12
    static Glib::RefPtr< Gtk::CssProvider > css_provider;

    int size{ theme->font.get_size() };
    if( ! theme->font.get_size_is_absolute() )
        size /= PANGO_SCALE;

    PRINT_DEBUG( "theme size=", theme->font.get_size(),
                 "  size=", size,
                 "  PANGO_SCLAE=", PANGO_SCALE );

    const Glib::ustring data{
            STR::compose(
                "textview { font-family: ", theme->font.get_family(), "; "
                    "font-size: ", size, "pt }\n"
                "text { color: ", theme->color_text.to_string(), "; "
                    "background-color: ", theme->color_base.to_string(), " }\n"
                "text:selected, text selection { color: ", theme->color_base.to_string(), "; "
                    "background: ", theme->color_heading.to_string(), " }"
                ) };

    if( css_provider )
        m_ptr2textview->get_style_context()->remove_provider( css_provider );
    css_provider = Gtk::CssProvider::create();
    if( css_provider->load_from_data( data ) )
        m_ptr2textview->get_style_context()->add_provider(
                css_provider, GTK_STYLE_PROVIDER_PRIORITY_APPLICATION );

    m_tag_heading->property_foreground_rgba() = theme->color_heading;
    m_tag_subheading->property_foreground_rgba() = theme->color_subheading;
    m_tag_highlight->property_background_rgba() = theme->color_highlight;
    m_tag_comment->property_background_rgba() = theme->color_base; // to disable highlighting

    Gdk::RGBA color_mid( midtone( theme->color_base, theme->color_text ) );

    m_tag_comment->property_foreground_rgba() = color_mid;
    m_tag_comment->property_background_rgba() = theme->color_base;
    m_tag_region->property_paragraph_background_rgba() = midtone(
            theme->color_base, theme->color_text, 0.9 );
    m_tag_match->property_foreground_rgba() = theme->color_base;
    m_tag_match->property_background_rgba() = contrast2(
            theme->color_base, Theme::s_color_match1, Theme::s_color_match2 );
    m_tag_markup->property_foreground_rgba() = color_mid;
    m_tag_link->property_foreground_rgba() = contrast2(
            theme->color_base, Theme::s_color_link1, Theme::s_color_link2 );
    m_tag_link_broken->property_foreground_rgba() = contrast2(
            theme->color_base, Theme::s_color_broken1, Theme::s_color_broken2 );

    m_tag_done->property_background_rgba() = contrast2(
            theme->color_text, Theme::s_color_done1, Theme::s_color_done2 );
    m_tag_checkbox_todo->property_background_rgba() =
            midcontrast( theme, m_tag_checkbox_todo->property_foreground_rgba() );
    m_tag_checkbox_progressed->property_background_rgba() =
            midcontrast( theme, m_tag_checkbox_progressed->property_foreground_rgba() );
    m_tag_checkbox_done->property_background_rgba() =
            midcontrast( theme, m_tag_checkbox_done->property_foreground_rgba() );
    m_tag_checkbox_canceled->property_background_rgba() =
            midcontrast( theme, m_tag_checkbox_canceled->property_foreground_rgba() );
}

// SPELL CHECKING BY DIRECT UTILIZATION OF ENCHANT (code partly copied from GtkSpell library)
void
TextbufferDiary::handle_word()
{
    const gchar* word = alpha_last.c_str();

    if( enchant_dict_check( m_enchant_dict, word, strlen( word ) ) != 0 )
    {
        Gtk::TextIter iter_start( get_iter_at_offset( pos_alpha ) );
        Gtk::TextIter iter_end( get_iter_at_offset( m_pos_current ) );
        apply_tag( m_tag_misspelled, iter_start, iter_end );
    }
}

static void
set_lang_from_dict_cb( const char* const lang_tag, const char* const provider_name,
                       const char* const provider_desc, const char* const provider_file,
                       void* user_data )
{
    std::string* language = ( std::string* ) user_data;
    ( *language ) = lang_tag;
}

std::string
TextbufferDiary::set_language( std::string lang )
{
    if ( m_enchant_dict )
        enchant_broker_free_dict( Lifeograph::s_enchant_broker, m_enchant_dict );

    if( lang.empty() )  // empty means checking is turned off
    {
        m_enchant_dict = nullptr;
        m_flag_handle_word = false;
        return "";
    }

    m_enchant_dict = enchant_broker_request_dict( Lifeograph::s_enchant_broker, lang.c_str() );

    if( !m_enchant_dict )
    {
        print_error( "Enchant error for language: ", lang );
        m_flag_handle_word = false;
        return "";
    }

    m_flag_handle_word = true;
    enchant_dict_describe( m_enchant_dict, set_lang_from_dict_cb, &lang );

    return lang;
}

Gtk::Menu*
TextbufferDiary::build_languages_menu()
{
    Gtk::Menu* menu = Gtk::manage( new Gtk::Menu );
    Gtk::RadioButtonGroup group;

    Gtk::RadioMenuItem* mi_lang =
            Gtk::manage( new Gtk::RadioMenuItem( group, _( "Diary Default" ) ) );
    if( m_ptr2entry->get_lang() == LANG_INHERIT_DIARY )
        mi_lang->set_active( true );
    else
        mi_lang->signal_activate().connect(
                sigc::bind( sigc::mem_fun( *this, &TextbufferDiary::handle_language_changed ),
                            LANG_INHERIT_DIARY, mi_lang ) );
    menu->append( *mi_lang );

    mi_lang = Gtk::manage( new Gtk::RadioMenuItem( group, _( STRING::OFF ) ) );
    if( !m_ptr2entry->get_lang().empty() )
        mi_lang->signal_activate().connect(
                sigc::bind( sigc::mem_fun( *this, &TextbufferDiary::handle_language_changed ),
                            "", mi_lang ) );
    else
        mi_lang->set_active( true );
    menu->append( *mi_lang );

    for( LanguageList::iterator iter = Lifeograph::s_lang_list.begin();
         iter != Lifeograph::s_lang_list.end(); ++iter )
    {
        mi_lang = Gtk::manage( new Gtk::RadioMenuItem( group, *iter ) );
        if( m_ptr2entry->get_lang().compare( *iter ) == 0 )
            mi_lang->set_active( true );
        else
            mi_lang->signal_activate().connect(
                    sigc::bind( sigc::mem_fun( *this, &TextbufferDiary::handle_language_changed ),
                                *iter, mi_lang ) );
        menu->append( *mi_lang );
    }

    // SOME OTHER LANGUAGE THAT IS NOT SUPPORTED BY THE CURRENT SYSTEM
    if( !m_ptr2entry->get_lang().empty() && m_ptr2entry->get_lang() != LANG_INHERIT_DIARY &&
        Lifeograph::s_lang_list.find( m_ptr2entry->get_lang() ) == Lifeograph::s_lang_list.end() )
    {
        mi_lang = Gtk::manage( new Gtk::RadioMenuItem( group, m_ptr2entry->get_lang() ) );
        mi_lang->set_active( true );
        menu->append( *mi_lang );
    }

    return menu;
}

void
TextbufferDiary::add_suggestion_menus( const Glib::ustring& word, Gtk::Menu* topmenu )
{
    Gtk::MenuItem* mi;
    char** suggestions;
    size_t n_suggs;
    int menu_position( 0 );

//    if (!spell->speller)
//        return;

    suggestions = enchant_dict_suggest( m_enchant_dict, word.c_str(), word.size(), & n_suggs );

    if ( suggestions == nullptr || !n_suggs )
    {
        Gtk::Label* label( Gtk::manage( new Gtk::Label ) );
        label->set_markup( _( "<i>(no suggestions)</i>" ) );

        mi = Gtk::manage( new Gtk::MenuItem );
        mi->add( *label );
        mi->show_all();
        topmenu->insert( *mi, menu_position++ );
    }
    else
    {
        Gtk::Menu* menu_more( nullptr );

        for( size_t i = 0; i < n_suggs && i < 20; i++ )
        {
            mi = Gtk::manage( new Gtk::MenuItem( suggestions[ i ] ) );
            mi->show();
            mi->signal_activate().connect( sigc::bind(
                    sigc::mem_fun( this, &TextbufferDiary::replace_misspelled_word ),
                    mi->get_label() ) );

            if( i < 5 )
            {
                topmenu->insert( *mi, menu_position++ );
            }
            else
            {
                if( i == 5 )
                {
                    Gtk::MenuItem* mi_more = Gtk::manage( new Gtk::MenuItem( _("More...") ) );
                    mi_more->show();
                    topmenu->insert( *mi_more, menu_position++ );
                    menu_more = Gtk::manage( new Gtk::Menu );
                    mi_more->set_submenu( *menu_more );
                }
                menu_more->append( *mi );
            }
        }

        enchant_dict_free_string_list( m_enchant_dict, suggestions );
    }

    // ADD TO DICTIONARY
    mi = Gtk::manage( new Gtk::MenuItem(
            Glib::ustring::compose( _("Add \"%1\" to Dictionary" ), word ) ) );
    mi->show();
    topmenu->insert( *mi, menu_position++ );
    mi->signal_activate().connect( sigc::bind(
            sigc::mem_fun( this, &TextbufferDiary::add_word_to_dictionary ), word ) );

    // IGNORE ALL
    mi = Gtk::manage( new Gtk::MenuItem( _("Ignore All" ) ) );
    mi->show();
    topmenu->insert( *mi, menu_position++ );
    mi->signal_activate().connect( sigc::bind(
            sigc::mem_fun( this, &TextbufferDiary::ignore_misspelled_word ), word ) );
}

void
TextbufferDiary::handle_language_changed( const std::string& lang,
                                          const Gtk::RadioMenuItem* mi_lang )
{
    if( mi_lang->get_active() )
    {
        if( lang.empty() )
        {
            m_ptr2entry->set_lang( "" );
            m_flag_handle_word = false;
            remove_tag( m_tag_misspelled, begin(), end() );
        }
        else if( lang == LANG_INHERIT_DIARY )
        {
            m_ptr2entry->set_lang( LANG_INHERIT_DIARY );
            if( set_language( Diary::d->get_lang() ).empty() )
                remove_tag( m_tag_misspelled, begin(), end() );
            else
                reparse();
        }
        else
        {
            m_ptr2entry->set_lang( set_language( lang ) );
            reparse();
        }
    }
}

void
TextbufferDiary::ignore_misspelled_word( const Glib::ustring& word )
{
    enchant_dict_add_to_session( m_enchant_dict, word.c_str(), word.size() );
    reparse();   // check entire buffer again
}

void
TextbufferDiary::replace_misspelled_word( const Glib::ustring& new_word )
{
//    if (!spell->speller)
//        return;

    Gtk::TextIter iter_begin = get_iter_at_offset( m_spell_suggest_offset );

    if( !iter_begin.has_tag( m_tag_misspelled ) )
    {
        PRINT_DEBUG( "No misspelled word found at suggestion offset" );
        return;
    }

    Gtk::TextIter iter_end( iter_begin );
    iter_begin.backward_to_tag_toggle( m_tag_misspelled );
    iter_end.forward_to_tag_toggle( m_tag_misspelled );
    const Glib::ustring old_word( get_text( iter_begin, iter_end ) );

    PRINT_DEBUG( "Old word: \"" + old_word + "\"" );
    PRINT_DEBUG( "New word: \"" + new_word + "\"" );

    // TODO: combine in undo history
    m_flag_ongoing_operation++;
    int offset( iter_begin.get_offset() );
    erase( iter_begin, iter_end );
    iter_begin = get_iter_at_offset( offset );
    m_flag_ongoing_operation--;
    insert( iter_begin, new_word );

    // why?
    enchant_dict_store_replacement( m_enchant_dict,
                                    old_word.c_str(), old_word.size(),
                                    new_word.c_str(), new_word.size() );
}

void
TextbufferDiary::add_word_to_dictionary( const Glib::ustring& word )
{
    enchant_dict_add( m_enchant_dict, word.c_str(), word.size() );
    reparse();   // check entire buffer again
}

// TEXTVIEW ========================================================================================
// STATIC MEMBERS
TextviewDiary::TextviewDiary( BaseObjectType* cobject, const Glib::RefPtr< Gtk::Builder >& )
:   Gtk::TextView( cobject ),
    m_cursor_hand( Gdk::Cursor::create( Gdk::HAND2 ) ),
    m_cursor_xterm( Gdk::Cursor::create( Gdk::XTERM ) ),
    m_ptr2cursor_last( &m_cursor_xterm )
{
    m_buffer = new TextbufferDiary;
    set_buffer( static_cast< Glib::RefPtr< TextbufferDiary > >( m_buffer ) );
    m_buffer->set_textview( this );
    set_wrap_mode( Gtk::WRAP_WORD );
    set_left_margin( TextbufferDiary::LEFT_MARGIN );

    signal_populate_popup().connect( sigc::mem_fun( m_buffer, &TextbufferDiary::handle_menu ) );
    signal_event_after().connect( sigc::mem_fun( m_buffer, &TextbufferDiary::handle_event_after ) );
    signal_query_tooltip().connect( sigc::mem_fun( this, &TextviewDiary::handle_query_tooltip ) );

    set_has_tooltip();
}

inline void
TextviewDiary::update_link()
{
    Gtk::TextIter       iter;
    const Glib::RefPtr< Gdk::Cursor >
                        *ptr2cursor = &m_cursor_xterm;
    int                 pointer_x, pointer_y;
    int                 trailing, buffer_x, buffer_y;
    Gdk::ModifierType   modifiers;

    Gtk::Widget::get_window()->get_pointer( pointer_x, pointer_y, modifiers );
    window_to_buffer_coords( Gtk::TEXT_WINDOW_WIDGET,
                             pointer_x, pointer_y,
                             buffer_x, buffer_y );
    get_iter_at_position( iter, trailing, buffer_x, buffer_y );

    // FIX ITER IF NEEDED:
    if( iter.has_tag( m_buffer->m_tag_hidden ) )
        iter.forward_to_tag_toggle( m_buffer->m_tag_hidden );
    Gtk::TextIter iter2( iter );
    iter2++;
    if( ( iter2.ends_tag( m_buffer->m_tag_link ) ||
          iter2.ends_tag( m_buffer->m_tag_link_broken ) ) && trailing > 0 )
        iter++;

    m_link_hovered = m_buffer->get_link( iter );

    if( m_link_hovered != nullptr )
    {
        if( Diary::d->is_read_only() || m_link_hovered->type == Link::LT_CHECK )
        {
            if( !( modifiers & Gdk::CONTROL_MASK ) )
                ptr2cursor = &m_cursor_hand;
        }
        else
        {
            if( modifiers & Gdk::CONTROL_MASK )
                ptr2cursor = &m_cursor_hand;
        }
    }

    if( ptr2cursor != m_ptr2cursor_last )
    {
        m_ptr2cursor_last = ptr2cursor;
        get_window( Gtk::TEXT_WINDOW_TEXT )->set_cursor( *ptr2cursor );
    }
}

void
TextviewDiary::set_richtext( Entry* entry )
{
    m_link_hovered = nullptr;

    // defer setting text to after the size allocation
    if( m_buffer->m_max_thumbnail_width > 0 )
        m_buffer->set_richtext( entry );
    else
    {
        m_flag_set_text_queued = true;
        m_buffer->m_ptr2entry = entry;
    }
}

bool
TextviewDiary::on_motion_notify_event( GdkEventMotion* event )
{
    update_link();
    return Gtk::TextView::on_motion_notify_event( event );
}

bool
TextviewDiary::on_button_press_event( GdkEventButton* event )
{
    if( event->button == 1 )
    {
        if( m_link_hovered != nullptr )
        {
            if( Diary::d->is_read_only() || m_link_hovered->type == Link::LT_CHECK )
            {
                if( !( event->state & Gdk::CONTROL_MASK ) )
                    return true;
            }
            else
            {
                if( event->state & Gdk::CONTROL_MASK )
                    return true;
            }
        }
    }
    else if( event->button == 3)
    {
        int x, y;
        Gtk::TextIter iter;

        window_to_buffer_coords( Gtk::TEXT_WINDOW_TEXT, event->x, event->y, x, y );
        get_iter_at_location( iter, x, y );
        m_buffer->m_spell_suggest_offset = iter.get_offset();
        //gtk_text_buffer_move_mark(spell->buffer, spell->mark_click, &iter);
    }

    return Gtk::TextView::on_button_press_event( event );
}

bool
TextviewDiary::on_button_release_event( GdkEventButton* event )
{
    if( m_link_hovered != nullptr && event->button == 1 )
    {
        if( Diary::d->is_read_only() || m_link_hovered->type == Link::LT_CHECK )
        {
            if( !( event->state & Gdk::CONTROL_MASK ) )
            {
                m_link_hovered->go();
                return true;
            }
        }
        else
        {
            if( event->state & Gdk::CONTROL_MASK )
            {
                m_link_hovered->go();
                return true;
            }
        }
    }

    return Gtk::TextView::on_button_release_event( event );
}

bool
TextviewDiary::on_key_press_event( GdkEventKey* event )
{
    if( ! Diary::d->is_read_only() )
    {
        if( ( event->state & ( Gdk::CONTROL_MASK|Gdk::MOD1_MASK|Gdk::SHIFT_MASK ) ) == 0 )
        {
            switch( event->keyval )
            {
                case GDK_KEY_space:
                    m_buffer->handle_space();
                    break;
                case GDK_KEY_Return:
                    if( m_buffer->handle_new_line() )
                    {
                        // in this case we need to ensure visibility of the cursor ourselves
                        scroll_to( m_buffer->get_insert() );
                        return true;
                    }
                    break;
                case GDK_KEY_Control_L:
                case GDK_KEY_Control_R:
                    if( m_link_hovered )
                        update_link();
                    break;
            }
        }
    }

    return Gtk::TextView::on_key_press_event( event );
}

bool
TextviewDiary::on_key_release_event( GdkEventKey* event )
{
    if( event->keyval == GDK_KEY_Control_L || event->keyval == GDK_KEY_Control_R )
        if( m_link_hovered )
            update_link();

    return Gtk::TextView::on_key_release_event( event );
}

void
TextviewDiary::on_size_allocate( Gtk::Allocation& allocation )
{
    Gtk::TextView::on_size_allocate( allocation );

    if( m_buffer->update_thumbnail_width( allocation.get_width() ) )
    {
        if( m_flag_set_text_queued )
        {
            Lifeograph::s_internaloperation++;
            // this queuing should only happen while logging in, hence always internal
            m_buffer->set_richtext( m_buffer->m_ptr2entry );
            AppWindow::p->panel_main->refresh_extra_info(); // word count needs to be updated
            m_flag_set_text_queued = false;
            Lifeograph::s_internaloperation--;
        }
        else
            m_buffer->reparse();
    }
}

bool
TextviewDiary::on_drag_motion( const Glib::RefPtr< Gdk::DragContext >& context,
                               int x, int y, guint time )
{
    if( Lifeograph::s_flag_dragging && Lifeograph::s_elem_dragged )
    {
        switch( Lifeograph::s_elem_dragged->get_type() )
        {
            case DiaryElement::ET_ENTRY:
                break;
            case DiaryElement::ET_TAG:
            case DiaryElement::ET_UNTAGGED:
                context->drag_status( Gdk::ACTION_COPY, time );
                return true;
            default:
                return false;
        }
    }

    return Gtk::TextView::on_drag_motion( context, x, y, time );
}

//void
//TextviewDiary::on_style_changed( const Glib::RefPtr< Gtk::Style > &style_prev )
//{
//  Gtk::TextView::on_style_changed( style_prev );
////    TextbufferDiary = get_pango_context()->get_font_description();
//  TextbufferDiary::m_theme_font = get_style()->get_font();
//}

bool
TextviewDiary::handle_query_tooltip( int x, int y, bool keyboard_mode,
                                     const Glib::RefPtr< Gtk::Tooltip > &tooltip )
{
/*    Gtk::TextIter iter;

    if( keyboard_mode )
    {
        int offset = m_buffer->property_cursor_position().get_value();
        iter = m_buffer->get_iter_at_offset( offset );
    }
    else
    {
        int mouse_x, mouse_y, trailing;
        window_to_buffer_coords( Gtk::TEXT_WINDOW_TEXT, x, y, mouse_x, mouse_y );
        get_iter_at_position( iter, trailing, mouse_x, mouse_y );
    }*/

    if( m_link_hovered != nullptr ) //m_buffer->get_link( iter ) != nullptr )
    {
        if( m_link_hovered->type == Link::LT_CHECK )
            return false;

        Glib::ustring tooltip_text( Diary::d->is_read_only() ?
                _( "Press Ctrl to select" ) :
                _( "Press Ctrl to follow the link" ) );

        if( m_link_hovered->type == Link::LT_URI )
        {
            tooltip_text += "\n";
            tooltip_text += dynamic_cast< LinkUri* >( m_link_hovered )->m_url;
        }

        tooltip->set_text( tooltip_text );
        //tooltip->set_icon_from_icon_name( "dialog-information", Gtk::ICON_SIZE_MENU );
    }
    else
        return false;

    return true;
}
