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

    Copyright (C) 2007-2020 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 "../ui_entry.hpp"
#include "chart_surface.hpp"
#include "widget_textview.hpp"


#define GTKSPELL_MISSPELLED_TAG "gtkspell-misspelled"


using namespace LIFEO;

// STATIC MEMBERS
PredicateNL                     TextbufferDiary::s_predicate_nl;
PredicateBlank                  TextbufferDiary::s_predicate_blank;

// LINKS ===========================================================================================
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()
{
    AppWindow::p->show_entry( Diary::d->get_entry_by_id( m_id ) );
}

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

void
LinkDate::go()
{
    AppWindow::p->UI_extra->show_date_in_cal( m_date.get_pure() );
}

// LINK TO CHART
LinkChart::LinkChart( const Glib::RefPtr< Gtk::TextMark >& start,
                      const Glib::RefPtr< Gtk::TextMark >& end,
                      const std::string& uri )
:   Link( start, end, LT_TAG ), m_uri( uri )
{
}

void
LinkChart::go()
{
    AppWindow::p->UI_extra->set_view( "chart" );
    AppWindow::p->UI_extra->set_active_chart( m_uri.substr( 6, m_uri.size() - 6 ) );
}

// LINK TO URI
LinkUri::LinkUri( const Glib::RefPtr< Gtk::TextMark >& start,
                  const Glib::RefPtr< Gtk::TextMark >& end,
                  const std::string& uri, LinkType lt, Diary* ptr2diary)
:   Link( start, end, lt ), m_uri( uri ), m_ptr2diary( ptr2diary )
{
}

void
LinkUri::go()
{
#ifndef _WIN32
    GError* err = nullptr;
    gtk_show_uri_on_window( static_cast< Gtk::Window* >( AppWindow::p )->gobj(),
                            m_ptr2diary->convert_rel_uri( m_uri ).c_str(),
                            GDK_CURRENT_TIME, &err );
#else
    ShellExecute( NULL, "open", PATH( m_ptr2diary->convert_rel_uri( m_uri ) ).c_str(),
                  NULL, NULL, SW_SHOWNORMAL );
#endif
}

// TEXTBUFFERDIARY =================================================================================
TextbufferDiary::TextbufferDiary()
{
    // 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.6;
    tag_table->add( m_tag_heading );

    m_tag_subheading = Tag::create( "subheading" );
    m_tag_subheading->property_weight() = Pango::WEIGHT_BOLD;
    m_tag_subheading->property_scale() = 1.3;
    tag_table->add( m_tag_subheading );

    m_tag_subsubheading = Tag::create( "subsubheading" );
    m_tag_subsubheading->property_weight() = Pango::WEIGHT_BOLD;
    m_tag_subsubheading->property_scale() = 1.15;
    tag_table->add( m_tag_subsubheading );

    m_tag_bold = Tag::create( "bold" );
    m_tag_bold->property_weight() = Pango::WEIGHT_BOLD;
    tag_table->add( m_tag_bold );

    m_tag_italic = Tag::create( "italic" );
    m_tag_italic->property_style() = Pango::STYLE_ITALIC;
    tag_table->add( m_tag_italic );

    m_tag_strikethrough = Tag::create( "strikethrough" );
    m_tag_strikethrough->property_strikethrough() = true;
    tag_table->add( m_tag_strikethrough );

    m_tag_highlight = Tag::create( "highlight" );
    tag_table->add( m_tag_highlight );

    m_tag_region = Tag::create( "region" );
    tag_table->add( m_tag_region );

    m_tag_hidable = Tag::create( "hidable" );
    tag_table->add( m_tag_hidable );

    m_tag_done = Tag::create( "done" );
    tag_table->add( 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;
    tag_table->add( 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;
    tag_table->add( 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;
    tag_table->add( 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;
    tag_table->add( m_tag_checkbox_canceled );

    m_tag_link = Tag::create( "link" );
    m_tag_link->property_underline() = Pango::UNDERLINE_SINGLE;
    tag_table->add( m_tag_link );

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

    // this is just for keeping the boundaries:
    m_tag_link_hidden = Tag::create( "link.hidden" );
    tag_table->add( m_tag_link_hidden );

    m_tag_inline_tag = Tag::create( "inline tag" );
    m_tag_inline_tag->property_scale() = 0.85;
    m_tag_inline_tag->property_rise() = 1400;
    m_tag_inline_tag->property_letter_spacing() = 1000;
    m_tag_inline_tag->property_weight() = Pango::WEIGHT_NORMAL;
    m_tag_inline_tag->property_strikethrough() = false;
    tag_table->add( m_tag_inline_tag );

    m_tag_inline_tag_bg = Tag::create( "inline tag bg" );
    tag_table->add( m_tag_inline_tag_bg );

    m_tag_inline_value = Tag::create( "inline value" );
    m_tag_inline_value->property_scale() = 0.85;
    m_tag_inline_value->property_rise() = 1400;
    m_tag_inline_value->property_letter_spacing() = 1000;
    m_tag_inline_value->property_weight() = Pango::WEIGHT_NORMAL;
    m_tag_inline_value->property_strikethrough() = false;
    tag_table->add( m_tag_inline_value );

    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
    tag_table->add( m_tag_comment );

    m_tag_pixbuf = Tag::create( "image" );
    tag_table->add( m_tag_pixbuf );

    m_tag_markup = Tag::create( "markup" );
    m_tag_markup->property_invisible() = true;
    m_tag_markup->property_scale() = 0.6;
    m_tag_comment->property_strikethrough() = false; // for comments in canceled check list items
    tag_table->add( m_tag_markup );

    m_tag_hidden = Tag::create( "hidden" );
    m_tag_hidden->property_invisible() = true;
    tag_table->add( m_tag_hidden );

    m_tag_hidden_virt = Tag::create( "hidden virt" );
    tag_table->add( m_tag_hidden_virt );

    m_tag_misspelled = Tag::create( GTKSPELL_MISSPELLED_TAG );
    m_tag_misspelled->property_underline() = Pango::UNDERLINE_ERROR;
    tag_table->add( m_tag_misspelled );

    m_tag_justify_left = Tag::create( "justify_left" );
    m_tag_justify_left->property_justification() = Gtk::JUSTIFY_LEFT;
    tag_table->add( m_tag_justify_left );

    m_tag_justify_center = Tag::create( "justify_center" );
    m_tag_justify_center->property_justification() = Gtk::JUSTIFY_CENTER;
    tag_table->add( m_tag_justify_center );

    m_tag_justify_right = Tag::create( "justify_right" );
    m_tag_justify_right->property_justification() = Gtk::JUSTIFY_RIGHT;
    tag_table->add( m_tag_justify_right );

    m_tag_match = Tag::create( "match" ); // highest priority
    tag_table->add( m_tag_match );
}

void
TextbufferDiary::set_static_text( const Ustring& text, bool flag_apply_heading )
{
    m_flag_apply_heading = flag_apply_heading;
    m_flag_settext_operation = true;
    clear_links();
    m_parser_pos_cursor_para_bgn = -1;
    m_parser_pos_cursor_para_end = -1;
    m_ptr2TvD->set_editable( false );
    m_p2entry = nullptr;
    m_word_count = 0;
    Gtk::TextBuffer::set_text( text );
    reparse();
    m_flag_settext_operation = false;
}

void
TextbufferDiary::set_entry( Entry* entry )
{
    m_flag_settext_operation = true;
    clear_links();
    m_parser_pos_cursor_para_bgn = -1;
    m_parser_pos_cursor_para_end = -1;
    m_p2entry = entry;
    m_ptr2diary = entry->get_diary();
    m_word_count = 0;
    set_language( m_ptr2TvD->get_editable() ? entry->get_lang_final() : "" );
    set_theme( entry->get_theme() );
    Gtk::TextBuffer::set_text( entry->get_text() );
    place_cursor( begin() );
    reparse();
    m_flag_settext_operation = false;
}

void
TextbufferDiary::unset_entry()
{
    ParserText::set_search_str( "" );

    m_p2entry = nullptr;
}

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

    Glib::RefPtr< Pango::Layout > layout{ m_ptr2TvD->create_pango_layout( str ) };
    layout->set_font_description( fd );
    layout->get_pixel_size( width, height );

    return width;
}

Wchar
TextbufferDiary::check_and_get_char_at( int i )
{
    return( i >= end().get_offset() ? 0 : get_iter_at_offset( i ).get_char() );
}
Wchar
TextbufferDiary::get_char_at( int i )
{
    return( get_iter_at_offset( i ).get_char() );
}

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 "";
}

void
TextbufferDiary::expand_selection()
{
    Gtk::TextIter it_bgn, it_end;
    if( !calculate_sel_word_bounds( it_bgn, it_end ) )
        calculate_sel_para_bounds( it_bgn, it_end );
    select_range( it_bgn, it_end );
}

void
TextbufferDiary::set_theme( const Theme* theme )
{
    // when theme == nullptr, it works in refresh mode:
    m_p2theme = theme ? theme : m_p2entry->get_theme();

    static Glib::RefPtr< Gtk::CssProvider > css_provider;

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

    const auto font =
            Pango::FontDescription( STR::compose( m_p2theme->font.get_family(), " ", size ) );
    m_text_width_tab = get_text_width( font, "\t" );
    m_text_width_checkbox =
            get_text_width(
                    Pango::FontDescription( STR::compose( "monospace ", size ) ), "[*]" ) +
            get_text_width( font, " " );
    m_text_width_dash = get_text_width( font, "- " );
    m_text_width_dot = get_text_width( font, "• " );

    Ustring data{
            STR::compose(
                "textview { "
                    "font-family: ",
                        m_p2theme->font.get_family().empty() ?
                            "sans" : m_p2theme->font.get_family(), "; "
                    "font-size: ", size, "pt; "
                    "caret-color: ", m_p2theme->color_text.to_string(), "; }\n"
                "text:selected, text selection { "
                    "color: ", m_p2theme->color_base.to_string(), "; "
                    "background: ", m_p2theme->color_heading.to_string(), " }\n"
                "text { "
                    "color: ", m_p2theme->color_text.to_string(), "; "
                    "background-color: ", m_p2theme->color_base.to_string(), "; "
                    "background-image: ",
                        m_p2theme->image_bg.empty() ?
                            "none }" : STR::compose( "url(\"", m_p2theme->image_bg, "\") }" )
                    ) };

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

    m_tag_heading->property_foreground_rgba() = m_p2theme->color_heading;
    m_tag_subheading->property_foreground_rgba() = m_p2theme->color_subheading;
    m_tag_subsubheading->property_foreground_rgba() = m_p2theme->get_color_subsubheading();

    m_tag_highlight->property_background_rgba() = m_p2theme->color_highlight;

    m_tag_inline_tag_bg->property_background_rgba() =  m_p2theme->get_color_inline_tag();
    m_tag_inline_value->property_background_rgba() = m_p2theme->get_color_inline_tag();

    m_tag_comment->property_foreground_rgba() = m_p2theme->get_color_mid();
    m_tag_comment->property_background_rgba() = m_p2theme->color_base; // to disable highlighting

    m_tag_region->property_paragraph_background_rgba() = m_p2theme->get_color_region_bg();

    m_tag_match->property_foreground_rgba() = m_p2theme->color_base;
    m_tag_match->property_background_rgba() = m_p2theme->get_color_match_bg();

    m_tag_hidden_virt->property_foreground_rgba() = m_p2theme->color_base;
    m_tag_hidable->property_foreground_rgba() = m_p2theme->get_color_mid();

    m_tag_link->property_foreground_rgba() = m_p2theme->get_color_link();
    m_tag_link_broken->property_foreground_rgba() = m_p2theme->get_color_link_broken();

    m_tag_checkbox_todo->property_foreground_rgba() = m_p2theme->get_color_open();
    m_tag_checkbox_todo->property_background_rgba() = m_p2theme->get_color_open_bg();

    m_tag_checkbox_progressed->property_foreground_rgba() = m_p2theme->get_color_progressed();
    m_tag_checkbox_progressed->property_background_rgba() = m_p2theme->get_color_progressed_bg();

    m_tag_done->property_foreground_rgba() = m_p2theme->get_color_done_text();
    m_tag_done->property_background_rgba() = m_p2theme->get_color_done_bg();
    m_tag_checkbox_done->property_foreground_rgba() = m_p2theme->get_color_done();
    m_tag_checkbox_done->property_background_rgba() = m_p2theme->get_color_done_bg();

    m_tag_checkbox_canceled->property_foreground_rgba() = m_p2theme->get_color_canceled();
    m_tag_checkbox_canceled->property_background_rgba() = m_p2theme->get_color_canceled_bg();
}

void
TextbufferDiary::set_search_str( const Ustring& str )
{
    ParserText::set_search_str( str );
    m_flag_settext_operation = true;
    reparse();
    m_flag_settext_operation = false;
}

void
TextbufferDiary::set_comments_hidden( bool hidden )
{
    m_tag_comment->property_invisible() = hidden;
}

// PARSING
void
TextbufferDiary::parse( int p_bgn, int p_end )
{
    m_flag_parsing = true;
    m_ongoing_operation_depth++;

    // SET CURSOR POS AND CURSOR PARA BOUNDS
    auto&& it_bgn{ get_insert()->get_iter() };
    auto   it_end = it_bgn;

    m_pos_cursor = it_bgn.get_offset();
    calculate_para_bounds( it_bgn, it_end );
    m_parser_pos_cursor_para_bgn = it_bgn.get_offset();
    m_parser_pos_cursor_para_end = it_end.get_offset();

    // FIX START if it goes beyond start while trying to include the proceeding \n
    if( p_bgn < 0 )
        p_bgn = 0;

    update_todo_status();

    // DO THE PARSING
    ParserText::parse( p_bgn, p_end );

    m_ongoing_operation_depth--;
    m_flag_parsing = false;

    reset_markup_visibilities();
}

void
TextbufferDiary::parse_damaged_region( Gtk::TextIter it_bgn, Gtk::TextIter it_end )
{
    calculate_para_bounds( it_bgn, it_end );
    parse( it_bgn.get_offset(), it_end.get_offset() );
}

void
TextbufferDiary::reset( UstringSize bgn, UstringSize end )
{
    ParserText::reset( bgn, end );

    // COMPLETELY CLEAR THE PARSING REGION
    clear_links( bgn, end );
    remove_all_tags( get_iter_at_offset( bgn ), get_iter_at_offset( end ) );

    // Following must come after reset as m_pos_para_bgn is reset there
    if( m_p2entry )
    {
        // when bgn != 0, 1 added to the m_pos_para_bgn to skip the \n at the beginning
        m_p2para_cur = m_p2entry->get_paragraph( m_pos_para_bgn > 0 ? m_pos_para_bgn + 1 : 0 );
        m_p2entry->clear_paragraph_data( m_pos_para_bgn, end );
    }
}

void
TextbufferDiary::process_paragraph()
{
    if( m_p2entry == nullptr || m_p2para_cur == nullptr )
        return;

    auto&& it_bgn{ get_iter_at_offset( m_pos_para_bgn ) };
    auto&& it_end{ get_iter_at_offset( m_pos_cur ) };

    // From para to textview
    switch( m_p2para_cur->m_justification )
    {
        case JustificationType::JT_LEFT:
            apply_tag( m_tag_justify_left, it_bgn, it_end );
            break;
        case JustificationType::JT_CENTER:
            apply_tag( m_tag_justify_center, it_bgn, it_end );
            break;
        case JustificationType::JT_RIGHT:
            apply_tag( m_tag_justify_right, it_bgn, it_end );
            break;
    }

    PRINT_DEBUG( "TbD::process_paragraph(): ", m_p2para_cur->get_text() );
    m_p2para_cur = m_p2para_cur->get_next();
}

void
TextbufferDiary::apply_heading()
{
    if( !m_flag_apply_heading )
        return;

    auto&& it_bgn{ get_iter_at_offset( 0 ) };
    auto   it_end = it_bgn;

    if( it_bgn.ends_line() )
        it_end++;
    else
        it_end.forward_to_line_end();
    apply_tag( m_tag_heading, it_bgn, it_end );

    if( ! m_flag_settext_operation && m_p2entry && m_ptr2diary )
    {
        AppWindow::p->UI_diary->handle_entry_title_changed( m_p2entry );
        AppWindow::p->UI_entry->refresh_title();
    }

    if( m_p2para_cur )
        m_p2para_cur->m_heading_level = 3;
}

void
TextbufferDiary::apply_subheading()
{
    auto&& iter_start{ get_iter_at_offset( m_recipe_cur->m_pos_bgn ) };
    auto iter_end = iter_start;
    iter_end.forward_to_line_end();
    apply_tag( m_tag_subheading, iter_start, iter_end );

    if( m_p2para_cur )
        m_p2para_cur->m_heading_level = 2;
}

void
TextbufferDiary::apply_subsubheading()
{
    auto&& iter_start{ get_iter_at_offset( m_recipe_cur->m_pos_bgn ) };
    auto iter_end = iter_start;
    iter_end.forward_to_line_end();
    apply_tag( m_tag_subsubheading, iter_start, iter_end );

    if( m_p2para_cur )
        m_p2para_cur->m_heading_level = 1;
}

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()
{
    apply_tag( m_tag_comment, get_iter_at_offset( m_recipe_cur->m_pos_bgn ),
                              get_iter_at_offset( m_pos_cur + 1 ) );
}

void
TextbufferDiary::apply_ignore()
{
    auto&& it_bgn { get_iter_at_offset( m_recipe_cur->m_pos_bgn ) };
    auto   it_end = it_bgn;
    it_end.forward_to_line_end();
    apply_tag( m_tag_region, it_bgn, it_end );
}

void
TextbufferDiary::apply_hidden_link_tags( Gtk::TextIter& it_end, const Glib::RefPtr< Tag >& tag )
{
    auto&& it_bgn   { get_iter_at_offset( m_recipe_cur->m_pos_bgn ) };
    auto&& it_cur   { get_iter_at_offset( m_pos_cur ) };
    auto&& it_label { get_iter_at_offset( m_recipe_cur->m_pos_mid + 1 ) };

    apply_tag( m_tag_hidable, it_bgn, it_label );
    apply_tag( m_tag_hidable, it_cur, it_end );

    if( tag != m_tag_hidden ) // m_tag_hidden is set when link is not wanted in this function
        apply_tag( tag, it_label, it_cur );
    apply_tag( m_tag_link_hidden, it_bgn, it_end );
}

void
TextbufferDiary::apply_link_hidden()
{
    bool   flag_broken { false };
    auto&& it_cur      { get_iter_at_offset( m_pos_cur ) };
    auto&& it_end      { get_iter_at_offset( m_pos_cur + 1 ) };
    auto&& it_uri_bgn  { get_iter_at_offset( m_recipe_cur->m_pos_bgn + 1 ) };
    auto&& it_tab      { get_iter_at_offset( m_recipe_cur->m_pos_mid ) };
    auto&& it_label    { get_iter_at_offset( m_recipe_cur->m_pos_mid + 1 ) };

    remove_tag( m_tag_misspelled, it_uri_bgn, it_tab );

    switch( m_recipe_cur->m_id )
    {
        case RID_URI:
        {
            const auto&& uri{ get_slice( it_uri_bgn, it_tab ) };
            PRINT_DEBUG( "URI -> ", uri );
            m_list_links.push_back( new LinkUri( create_mark( it_label ),
                                                 create_mark( it_cur ),
                                                 uri,
                                                 Link::LT_URI,
                                                 m_ptr2diary ) );
            break;
        }
        case RID_ID:
        {
            DiaryElement* element{ Diary::d->get_element( m_recipe_cur->m_int_value ) };

            if( element != nullptr && element->get_type() == DiaryElement::ET_ENTRY )
                m_list_links.push_back( new LinkID( create_mark( it_label ),
                                                    create_mark( it_cur ),
                                                    m_recipe_cur->m_int_value ) );
            else // indicate dead links
                flag_broken = true;
            break;
        }
    }

    apply_hidden_link_tags( it_end, flag_broken ? m_tag_link_broken : m_tag_link );
}

void
TextbufferDiary::apply_link()
{
    bool   flag_broken { false };
    bool   flag_img    { false };
    auto&& it_bgn      { get_iter_at_offset( m_recipe_cur->m_pos_bgn ) };
    auto&& it_cur      { get_iter_at_offset( m_pos_cur ) };

    remove_tag( m_tag_misspelled, it_bgn, it_cur );

    switch( m_recipe_cur->m_id )
    {
        case RID_DATE:
            apply_date();
            break;
        case RID_LINK_AT:
        {
            const auto&& uri{ "mailto:" + get_slice( it_bgn, it_cur ) };
            apply_tag( m_tag_link, it_bgn, it_cur );

            m_list_links.push_back( new LinkUri( create_mark( it_bgn ),
                                                 create_mark( it_cur ),
                                                 uri,
                                                 Link::LT_URI,
                                                 m_ptr2diary ) );
            break;
        }
        case RID_URI:
        {
            const auto&& uri{ get_slice( it_bgn, it_cur ) };
            PRINT_DEBUG( "URI -> ", uri );

            // check if link is to an image file if there is no text on the same with it:
            if( it_bgn.starts_line() && it_cur.ends_line() &&
                ( uri.find( "file://" ) == 0 || uri.find( "rel://" ) == 0 ) )
            {
                try
                {
                    auto&& buf{ m_ptr2diary->get_image( m_ptr2diary->convert_rel_uri( uri ),
                                                        m_max_thumbnail_w ) };

                    apply_tag( m_tag_link, it_bgn, it_cur );
                    apply_tag( m_tag_pixbuf, it_bgn, it_cur );
                    apply_tag( m_tag_hidable, it_bgn, it_cur );

                    const auto&& tag_name   { STR::compose( "height.", buf->get_height() ) };
                    auto         tag_height { get_tag_table()->lookup( tag_name ) };
                    if( !tag_height )
                    {
                        tag_height = Tag::create( tag_name );
                        tag_height->property_pixels_below_lines() = buf->get_height();
                        get_tag_table()->add( tag_height );
                    }
                    apply_tag( tag_height, it_bgn, it_cur );

                    if( m_p2para_cur )
                        m_p2para_cur->set_as_image_para( true );

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

            m_list_links.push_back( new LinkUri( create_mark( it_bgn ),
                                                 create_mark( it_cur ),
                                                 uri,
                                                 flag_img ? Link::LT_IMAGE : Link::LT_URI,
                                                 m_ptr2diary ) );
            break;
        }
        case RID_ID:
        {
            PRINT_DEBUG( "********** m_int_value: ", m_recipe_cur->m_int_value );
            DiaryElement* element{ Diary::d->get_element( m_recipe_cur->m_int_value ) };

            if( element != nullptr && element->get_type() == DiaryElement::ET_ENTRY )
                m_list_links.push_back( new LinkID( create_mark( it_bgn ),
                                                    create_mark( it_cur ),
                                                    m_recipe_cur->m_int_value ) );
            else // indicate dead links
                flag_broken = true;

            apply_tag( flag_broken ? m_tag_link_broken : m_tag_link, it_bgn, it_cur );

            break;
        }
    }
}

void
TextbufferDiary::apply_date()
{
    // DO NOT FORGET TO COPY UPDATES HERE TO TextbufferDiarySearch::apply_date()
    auto&& it_bgn{ get_iter_at_offset( m_recipe_cur->m_pos_bgn ) };
    auto&& it_end{ get_iter_at_offset( m_pos_cur + 1 ) };

    m_list_links.push_back( new LinkDate( create_mark( it_bgn ),
                                          create_mark( it_end ),
                                          m_date_last ) );

    apply_tag( m_tag_link, it_bgn, it_end );

    if( m_p2para_cur )
        m_p2para_cur->set_date( m_date_last.m_date );
}

void
TextbufferDiary::apply_check( Glib::RefPtr< Gtk::TextTag >* tag_box,
                              Glib::RefPtr< Gtk::TextTag >* tag,
                              char c )
{
    auto&& it_bgn{ get_iter_at_offset( m_pos_cur - 3 ) };
    auto&& it_box{ get_iter_at_offset( m_pos_cur ) };
    auto   it_end = it_box;
    it_end.forward_to_line_end();
    if( m_ptr2TvD->is_editor() && m_ptr2TvD->get_editable() )
        add_link_check( it_bgn, it_box, c );

    apply_tag( *tag_box, it_bgn, it_box );
    if( tag )
        apply_tag( *tag, ++it_box, it_end ); // ++ to skip separating space char
}

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

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

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

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

void
TextbufferDiary::apply_chart()
{
    auto&&       it_bgn { get_iter_at_offset( m_recipe_cur->m_pos_bgn ) };
    auto&&       it_cur { get_iter_at_offset( m_pos_cur ) };
    const auto&& uri    { get_slice( it_bgn, it_cur ) };

    remove_tag( m_tag_misspelled, it_bgn, it_cur );

    try
    {
        auto&& buf{ m_ptr2diary->get_image( uri, m_max_thumbnail_w ) };

        apply_tag( m_tag_link, it_bgn, it_cur );
        apply_tag( m_tag_pixbuf, it_bgn, it_cur );
        apply_tag( m_tag_hidable, it_bgn, it_cur );

        const auto&& tag_name   { STR::compose( "height.", buf->get_height() ) };
        auto         tag_height { get_tag_table()->lookup( tag_name ) };
        if( !tag_height )
        {
            tag_height = Tag::create( tag_name );
            tag_height->property_pixels_below_lines() = buf->get_height();
            get_tag_table()->add( tag_height );
        }
        apply_tag( tag_height, it_bgn, it_cur );

        if( m_p2para_cur )
            m_p2para_cur->set_as_image_para( true );

        m_list_links.push_back( new LinkChart( create_mark( it_bgn ),
                                               create_mark( it_cur ),
                                               uri ) );
    }
    catch( ... )
    {
        print_error( "Could not add chart pixbuf" );
        apply_tag( m_tag_link_broken, it_bgn, it_cur );
    }
}

void
TextbufferDiary::apply_match()
{
    apply_tag( m_tag_match, get_iter_at_offset( m_pos_search ),
                            get_iter_at_offset( m_pos_cur + 1 ) );
}

void
TextbufferDiary::apply_indent()
{
    const int tab_count{ int( m_pos_cur - m_recipe_cur->m_pos_bgn ) - 1 };
    int margin_width = m_text_width_checkbox;
    int indent_width = -tab_count * m_text_width_tab;

    switch( m_char_cur )
    {
        case '-':
            indent_width -= m_text_width_dash;
            margin_width = m_text_width_checkbox - m_text_width_dash;
            break;
        case L'•':
            indent_width -= m_text_width_dot;
            margin_width = m_text_width_checkbox - m_text_width_dot;
            break;
        case '[':
            if( check_and_get_char_at( m_pos_cur + 2 ) == ']' )
            {
                indent_width -= m_text_width_checkbox;
                margin_width = 0;
            }
            break;
    }

    // NAME
    const std::string&& tag_name{ STR::compose( "indent.", indent_width ) };
    auto&& tag_indent{ get_tag_table()->lookup( tag_name ) };
    if( !tag_indent )
    {
        tag_indent = Tag::create( tag_name );
        tag_indent->property_indent() = indent_width;
        tag_indent->property_left_margin() = LEFT_MARGIN + margin_width;
        get_tag_table()->add( tag_indent );
    }

    auto&& it_bgn{ get_iter_at_offset( m_recipe_cur->m_pos_bgn + 1 ) };
    auto&& it_end{ get_iter_at_offset( m_pos_cur ) };
    it_end.forward_to_line_end();
    apply_tag( tag_indent, it_bgn, it_end );
}

void
TextbufferDiary::apply_inline_tag()
{
    // m_pos_mid is used to determine if a value is assigned to the tag
    auto&&       it_bgn{ get_iter_at_offset( m_recipe_cur->m_pos_bgn ) };
    auto&&       it_end{ get_iter_at_offset( m_recipe_cur->m_pos_mid > 0 ?
                                                 m_recipe_cur->m_pos_mid : m_pos_cur ) };
    auto         it_name_bgn = it_bgn;
    auto         it_name_end = it_end;

    const auto&& tag_name{ get_slice( ++it_name_bgn, --it_name_end ) };

    auto&&       entries{ Diary::d->get_entries_by_name( tag_name ) };

    if( entries.empty() )
        apply_tag( m_tag_link_broken, it_bgn, it_end );
    else
    {
        if( m_recipe_cur->m_pos_mid == 0 )
        {
            apply_tag( m_tag_hidable, it_bgn, it_name_bgn );
            apply_tag( m_tag_inline_tag, it_bgn, it_end );
            apply_tag( m_tag_inline_tag_bg, it_name_bgn, it_name_end );
            apply_tag( m_tag_hidable, it_name_end, it_end );

            m_list_links.push_back( new LinkID( create_mark( it_bgn ),
                                                create_mark( it_end ),
                                                entries[ 0 ]->get_id() ) );

            if( m_p2para_cur )
                m_p2para_cur->set_tag( tag_name, 1.0 );
        }
        // the value (BEAWARE that the last reference overrides previous ones within a paragraph):
        else
        {
            it_bgn = get_iter_at_offset( m_recipe_cur->m_pos_mid + 1 );
            it_end = get_iter_at_offset( m_pos_extra_2 + 1 );
            apply_tag( m_tag_inline_value, it_bgn, it_end );

            if( m_p2para_cur == nullptr )
                return;

            if( m_pos_extra_1 > m_recipe_cur->m_pos_bgn ) // has planned value
            {
                auto&& it_sep{ get_iter_at_offset( m_pos_extra_1 ) };
                const Value v_real{ STR::get_d( get_slice( it_bgn, it_sep ) ) };
                const Value v_plan{ STR::get_d( get_slice( ++it_sep, it_end ) ) };
                m_p2para_cur->set_tag( tag_name, v_real, v_plan );
            }
            else
                m_p2para_cur->set_tag( tag_name, STR::get_d( get_slice( it_bgn, it_end ) ) );
        }
    }
}

inline void
TextbufferDiary::apply_markup( const Glib::RefPtr< Tag >& tag )
{
    auto&& it_bgn { get_iter_at_offset( m_recipe_cur->m_pos_bgn ) };
    auto&& it_cur { get_iter_at_offset( m_pos_cur ) };
    auto   it_mid = it_bgn;
    it_mid++;
    apply_tag( m_tag_markup, it_bgn, it_mid );
    apply_tag( m_tag_hidable, it_bgn, it_mid );
    apply_tag( tag, it_mid, it_cur );
    it_mid = it_cur;
    it_mid++;
    apply_tag( m_tag_markup, it_cur, it_mid );
    apply_tag( m_tag_hidable, it_cur, it_mid );
}

// LINKS & IMAGES
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 )
{
    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() )
        {
            delete( *iter );
            iter = m_list_links.erase( iter );
        }
        else
            ++iter;

    m_ptr2TvD->m_link_hovered = nullptr;
}

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

    m_list_links.clear();
}

bool
TextbufferDiary::update_thumbnail_width( int width )
{
    // only multitudes of 100 are accepted
    width *= 0.75;
    width -= ( width % 100 );

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

void
TextbufferDiary::reset_markup_visibilities()
{
    auto&&      it_bgn{ begin() };
    auto        it_end = it_bgn;
    UstringSize offset;
    UstringSize pos_itag_bgn{ 0 };
    UstringSize pos_itag_end{ 0 };

    auto&&      it_cursor{ get_insert()->get_iter() };
    if( not( it_cursor.has_tag( m_tag_inline_tag ) ) && not( it_cursor.starts_line() ) )
        it_cursor--;
    if( it_cursor.has_tag( m_tag_inline_tag ) )
    {
        if( not( it_cursor.begins_tag( m_tag_inline_tag ) ) )
            it_cursor.backward_to_tag_toggle( m_tag_inline_tag );
        pos_itag_bgn = it_cursor.get_offset();
        it_cursor.forward_to_tag_toggle( m_tag_inline_tag );
        pos_itag_end = it_cursor.get_offset();
    }

    while( it_bgn.forward_to_tag_toggle( m_tag_hidable ) )
    {
        it_end = it_bgn;
        it_end.forward_to_tag_toggle( m_tag_hidable );
        offset = it_bgn.get_offset();

        if( it_bgn.has_tag( m_tag_inline_tag ) &&
            ( offset < pos_itag_bgn || offset > pos_itag_end ) )
        {
            apply_tag( m_tag_hidden_virt, it_bgn, it_end );
        }
        else
        if( ! it_bgn.has_tag( m_tag_hidden ) &&
            ( offset < m_parser_pos_cursor_para_bgn || offset > m_parser_pos_cursor_para_end ) )
        {
            if( it_bgn.has_tag( m_tag_markup ) || it_bgn.has_tag( m_tag_pixbuf ) )
                apply_tag( m_tag_hidden_virt, it_bgn, it_end );
            else
                apply_tag( m_tag_hidden, it_bgn, it_end );
        }

        it_bgn = it_end;
    }
}

// HELPER FUCNTIONS
bool
TextbufferDiary::check_cursor_is_in_para()
{
    return( ( m_pos_cur > m_parser_pos_cursor_para_bgn ) &&
            ( m_pos_cur <= m_parser_pos_cursor_para_end ) );
}

void
TextbufferDiary::calculate_para_bounds( Gtk::TextIter& iter_begin, Gtk::TextIter& iter_end )
{
    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 change at the end
TextbufferDiary::calculate_sel_word_bounds( Gtk::TextIter& iter_bgn, Gtk::TextIter& iter_end )
{
    bool flag_iters_moved{ true };

    get_selection_bounds( iter_bgn, iter_end );

    if( iter_bgn.starts_word() )
        flag_iters_moved = false;
    else
        iter_bgn.backward_word_start();

    if( iter_end.ends_word() )
        return flag_iters_moved;
    else
        iter_end.forward_word_end();

    return true;
}

bool    // returns true if boundaries reach the end of text
TextbufferDiary::calculate_sel_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;
}

Ustring
TextbufferDiary::calculate_word_bounds( Gtk::TextIter& iter_bgn, Gtk::TextIter& iter_end )
{
    if( ! iter_bgn.backward_find_char( s_predicate_blank ) )
        iter_bgn.set_offset( 0 );
    else
        iter_bgn++;

    iter_end--;
    if( !iter_end.forward_find_char( s_predicate_blank ) )
        iter_end.forward_to_end();

    return get_slice( iter_bgn, iter_end );
}

bool // returns true if itag format is detected
TextbufferDiary::calculate_itag_bounds( Gtk::TextIter& iter_bgn, Gtk::TextIter& iter_end )
{
    auto expand_to_tag_boundaries = [ & ]( Glib::RefPtr< Tag > tag )
    {
        if( not( iter_bgn.starts_tag( tag ) ) )
            iter_bgn.backward_to_tag_toggle( tag );

        if( iter_end.has_tag( tag ) )
            iter_end.forward_to_tag_toggle( tag );
    };

    if( iter_bgn.has_tag( m_tag_inline_tag ) || iter_bgn.ends_tag( m_tag_inline_tag ) )
        expand_to_tag_boundaries( m_tag_inline_tag );
    else if( iter_bgn.has_tag( m_tag_link_broken ) || iter_bgn.ends_tag( m_tag_link_broken ) )
        expand_to_tag_boundaries( m_tag_link_broken );
    else // if not formatted as tag word boundaries are used but initial colon will be dropped
    {
        Ustring word{ calculate_word_bounds( iter_bgn, iter_end ) };
        if( word.find( ':' ) == 0 )
            iter_bgn.forward_char();

        return false;
    }

    return true;
}

// TEXTVIEW ========================================================================================
TextviewDiary::TextviewDiary()
{
    init();
}

TextviewDiary::TextviewDiary( BaseObjectType* cobject, const Glib::RefPtr< Gtk::Builder >& )
:   Gtk::TextView( cobject )
{
    init();
}

inline void
TextviewDiary::init()
{
    if( not is_derived() )
    {
        m_buffer = new TextbufferDiary();
        set_buffer( static_cast< Glib::RefPtr< TextbufferDiary > >( m_buffer ) );
        m_buffer->m_ptr2TvD = this;
    }
    set_wrap_mode( Gtk::WRAP_WORD );
    set_left_margin( TextbufferDiary::LEFT_MARGIN );
    set_has_tooltip();

    signal_query_tooltip().connect( sigc::mem_fun( this, &TextviewDiary::handle_query_tooltip ) );
}

bool
TextviewDiary::get_selection_rect( Gdk::Rectangle& rect )
{
    int w_x, w_y;
    Gtk::TextIter iter_bgn, iter_end;
    m_buffer->get_selection_bounds( iter_bgn, iter_end );

    get_iter_location( iter_bgn, rect );
    buffer_to_window_coords( Gtk::TEXT_WINDOW_TEXT, rect.get_x(), rect.get_y(), w_x, w_y );
    rect.set_x( w_x );
    rect.set_y( w_y < 0 ? 0 : w_y ); // ensure that y is within the visible portion

    return true; // reserved
}

inline void
TextviewDiary::update_link()
{
    const static auto&& s_Cu_hand       { Gdk::Cursor::create( Gdk::Display::get_default(),
                                                               Gdk::HAND2 ) };
    const auto&&        s_Cu_default    { Gdk::Cursor::create( Gdk::Display::get_default(),
                                                               m_cursor_default ) };

    Gtk::TextIter       iter;
    auto                ptr2cursor      { &s_Cu_default };
    int                 x_pointer, y_pointer, x_buf, y_buf;
    int                 trailing;
    Gdk::ModifierType   modifiers;

    Gtk::Widget::get_window()->get_pointer( x_pointer, y_pointer, modifiers );
    window_to_buffer_coords( Gtk::TEXT_WINDOW_WIDGET, x_pointer, y_pointer, x_buf, y_buf );
    get_iter_at_position( iter, trailing, x_buf, y_buf );

    // special treatment for inline images:
    if( iter.get_char() == '\n' )
    {
        if( iter-- )
            if( iter.has_tag( m_buffer->m_tag_pixbuf ) == false )
                iter++;  // revert
    }

    // special checks are needed for the inline tags at the end of a paragraph:
    if( trailing > 0 &&
        ( iter.has_tag( m_buffer->m_tag_inline_tag ) || iter.has_tag( m_buffer->m_tag_link ) ) )
    {
        if( iter++ )
        {
            if( iter.ends_line() || iter.ends_tag( m_buffer->m_tag_link ) )
                goto bypass_link_check; // may not be halal/kosher but extremely practical here
            else
                iter--;  // revert
        }
    }

    m_link_hovered = m_buffer->get_link( iter );

    if( m_link_hovered != nullptr )
    {
        if( get_editable() == false || m_link_hovered->type == Link::LT_CHECK )
        {
            if( !( modifiers & Gdk::CONTROL_MASK ) )
                ptr2cursor = &s_Cu_hand;
        }
        else
        {
            if( modifiers & Gdk::CONTROL_MASK )
                ptr2cursor = &s_Cu_hand;
        }
    }

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

void
TextviewDiary::update_link_at_insert()
{
    auto&& iter{ m_buffer->get_insert()->get_iter() };

    // special treatment for inline images:
    if( iter.get_char() == '\n' )
    {
        if( iter-- )
            if( iter.has_tag( m_buffer->m_tag_pixbuf ) == false )
                iter++;  // revert
    }

    m_link_hovered = m_buffer->get_link( iter );
    PRINT_DEBUG( "link is: ", m_link_hovered );
}

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

bool
TextviewDiary::on_button_release_event( GdkEventButton* event )
{
    if( m_link_hovered != nullptr && event->button == 1 )
    {
        if( get_editable() == false || 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_draw( const Cairo::RefPtr< Cairo::Context >& cr )
{
    Gtk::TextView::on_draw( cr );

    if( Diary::d->is_open() && m_buffer && m_buffer->m_p2theme )
    {
        int unused, y_Tv_top, y_Tv_btm, y_Pb_top, y_Pb_btm{ 0 }; // treeview and pixbuf coords
        int xTL, yTL, xBR, yBR;// top-left & bottom-right coordinates
        const auto pos_insert{ m_buffer->get_insert()->get_iter().get_offset() };
        Gdk::Rectangle rect_Tv, rect;

        get_visible_rect( rect_Tv );
        buffer_to_window_coords( Gtk::TEXT_WINDOW_TEXT, 0, rect_Tv.get_y(), unused, y_Tv_top );
        y_Tv_btm = y_Tv_top + rect_Tv.get_height();

        //cr->get_clip_extents( clip_x1, clip_y1, clip_x2, clip_y2 );
        Gdk::Cairo::set_source_rgba( cr, m_buffer->m_p2theme->color_highlight );
        cr->set_line_width( 1.0 );
        cr->set_line_join( Cairo::LINE_JOIN_ROUND );

        for( auto&& iter = m_buffer->begin();
             iter.forward_to_tag_toggle( m_buffer->m_tag_inline_tag ); )
        {
            auto it_end = iter;
            it_end.forward_to_tag_toggle( m_buffer->m_tag_inline_tag );
            iter++;
            it_end--;

            const bool flag_full_frame{
                    pos_insert < iter.get_offset() - 1 || pos_insert > it_end.get_offset() + 1 };

            get_string_coordinates( iter, it_end, xTL, yTL, xBR, yBR );

            const int half_height{ ( yBR - yTL ) / 2 };

            if( yTL >= y_Tv_btm )
                break;
            else if( yBR > y_Tv_top && yTL < y_Tv_btm )
            {
                int width{ xBR - xTL };

                cr->move_to( xTL - 1, double( yTL + 0.5 ) );
                cr->rel_line_to( width, 0 );
                if( flag_full_frame )
                {
                    cr->rel_line_to( half_height / 2, double( half_height - 0.5 ) );
                    cr->rel_line_to( half_height / -2, double( half_height - 0.5 ) );
                }
                else
                    cr->rel_move_to( 0.0, double( 2 * half_height - 1 ) );

                cr->rel_line_to( -width, 0 );
                if( flag_full_frame )
                    cr->close_path();
                cr->stroke();
            }

            iter = ++it_end;
        }

        for( auto&& iter = m_buffer->begin();
             iter.forward_to_tag_toggle( m_buffer->m_tag_pixbuf ); )
        {
            auto iter_end = iter;
            iter_end.forward_to_tag_toggle( m_buffer->m_tag_pixbuf );

            get_iter_location( iter_end, rect );
            buffer_to_window_coords( Gtk::TEXT_WINDOW_TEXT, 0,
                                     rect.get_y() + rect.get_height(), unused, y_Pb_top );
            auto&& tags{ iter.get_tags() };
            for( auto& tag : iter.get_tags() )
                if( tag->property_pixels_below_lines_set() )
                    y_Pb_btm = y_Pb_top + tag->property_pixels_below_lines();

            if( y_Pb_top >= y_Tv_btm )
                break;
            else if( y_Pb_btm > y_Tv_top && y_Pb_top < y_Tv_btm )
            {
                using namespace Cairo;
                const std::string&& uri{ m_buffer->get_slice( iter, iter_end ) };
                Icon buf{ m_buffer->m_ptr2diary->get_image( uri, m_buffer->m_max_thumbnail_w ) };
                const int    w_buf   { buf->get_width() };
                const int    h_buf   { buf->get_height() };
                auto&&       IS_img  { ImageSurface::create( FORMAT_ARGB32, w_buf, h_buf ) };
                auto&&       IC_img  { Context::create( IS_img ) };
                const double y1_clip { ( double ) std::max( y_Tv_top, y_Pb_top ) };
                double       h_clip  { ( double ) h_buf };

                if( y_Tv_top > y_Pb_top ) h_clip -= ( y_Tv_top - y_Pb_top );
                if( y_Pb_btm > y_Tv_btm ) h_clip -= ( y_Pb_btm - y_Tv_btm );

                Gdk::Cairo::set_source_pixbuf( IC_img, buf, 0.0, 0.0 );
                IC_img->paint();

                cr->set_source( IS_img, ( rect_Tv.get_width() - w_buf ) / 2, y_Pb_top );
                cr->rectangle( ( rect_Tv.get_width() - w_buf ) / 2, y1_clip, w_buf, h_clip );
                cr->clip();
                cr->paint();
                cr->reset_clip();
            }

            iter = iter_end;
        }
    }

    return true;
}

bool
TextviewDiary::handle_query_tooltip( int x, int y, bool keyboard_mode,
                                     const Glib::RefPtr< Gtk::Tooltip >& tooltip )
{
    if( m_link_hovered != nullptr )
    {
        if( m_link_hovered->type == Link::LT_CHECK )
            return false;

        Ustring tooltip_text;

        switch( m_link_hovered->type)
        {
            case Link::LT_DATE:
                tooltip_text = dynamic_cast< LinkDate* >( m_link_hovered )->
                                                          m_date.get_weekday_str() + "\n";
                break;
            case Link::LT_URI:
            case Link::LT_IMAGE:
                tooltip_text = dynamic_cast< LinkUri* >( m_link_hovered )->m_uri + "\n";
                break;
            case Link::LT_ID:
            {
                auto id{ dynamic_cast< LinkID* >( m_link_hovered )->m_id };
                DiaryElement* elem{ Diary::d->get_element( id ) };
                if( elem && elem->get_type() == DiaryElement::ET_ENTRY )
                    tooltip_text = dynamic_cast< Entry* >( elem )->get_description() + "\n";
                break;
            }
            default:
                break;
        }

        tooltip_text += ( get_editable() ? _( "Press Ctrl to follow the link" ) :
                                           _( "Press Ctrl to select" ) );

        tooltip->set_text( tooltip_text );
    }
    else
        return false;

    return true;
}

void
TextviewDiary::get_string_coordinates( const Gtk::TextIter& it_bgn, const Gtk::TextIter& it_end,
                                       int& xTL, int& yTL, int& xBR, int& yBR )
{
    Gdk::Rectangle rect_bgn;
    Gdk::Rectangle rect_end;
    // FIXME: Gtk does not calculate the iter coordinates correctly when there are invisible
    //        chars in the line. we tried to address this in the below code but failed.
    //        Best solution might be forgoing the invisibility tag altogether...
    /*auto           iter = it_bgn;
    Gtk::TextIter  it_invisib_end;
    int            diff{ 0 };

    backward_display_line_start( iter );

    while( true )
    {
        if( iter.forward_to_tag_toggle( m_buffer->m_tag_hidden ) )
        {
            if( iter.get_offset() < it_bgn.get_offset() )
            {
                it_invisib_end = iter;
                it_invisib_end.forward_to_tag_toggle( m_buffer->m_tag_hidden );
                if( it_invisib_end.get_offset() < it_bgn.get_offset() )
                {
                    get_iter_location( iter, rect );
                    get_iter_location( it_invisib_end, rect2 );
                    diff += ( rect2.get_x() - rect.get_x() );
                }
                else break;
            }
            else break;
        }
        else break;

        iter = it_invisib_end;
    }*/

    get_iter_location( it_bgn, rect_bgn );
    buffer_to_window_coords( Gtk::TEXT_WINDOW_TEXT, rect_bgn.get_x(), rect_bgn.get_y(), xTL, yTL );
    get_iter_location( it_end, rect_end );
    // the following tries to address the case when the string spans more than one line
    // this is not an ideal solution, but we will stick to this for 2.0
    if( rect_end.get_y() != rect_bgn.get_y() )
        buffer_to_window_coords( Gtk::TEXT_WINDOW_TEXT, get_width(),
                                 rect_bgn.get_y() + rect_bgn.get_height(),
                                 xBR, yBR );
    else
        buffer_to_window_coords( Gtk::TEXT_WINDOW_TEXT, rect_end.get_x(),
                                 rect_end.get_y() + rect_end.get_height(),
                                 xBR, yBR );
}
