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

    Copyright (C) 2007-2021 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>

#ifdef _WIN32
#include <winsock2.h> // to silence warnings on Windows
#include <shlwapi.h>
#endif

#include "../lifeograph.hpp"
#include "../app_window.hpp"
#include "../ui_entry.hpp"
#include "chart_surface.hpp"
#include "widget_textviewedit.hpp"


using namespace LIFEO;

// UNDOEDIT ========================================================================================
void
UndoEdit::insert()
{
    m_ptr2buffer->insert( m_ptr2buffer->get_iter_at_offset( m_position ), m_text );
}

void
UndoEdit::erase()
{
    m_ptr2buffer->erase( m_ptr2buffer->get_iter_at_offset( m_position ),
                         m_ptr2buffer->get_iter_at_offset( m_position + m_text.length() ) );
}

// LINK CHECKBOX ===================================================================================
LinkCheck::LinkCheck( TextviewDiaryEdit* ptr2TvDE,
                      const Glib::RefPtr< Gtk::TextMark >& start,
                      const Glib::RefPtr< Gtk::TextMark >& end,
                      char state )
: Link( start, end, LT_CHECK ), m_state( state ), m_ptr2TvDE( ptr2TvDE )
{
}

void
LinkCheck::go()
{
    PRINT_DEBUG( "LinkCheck::go()" );
    m_ptr2TvDE->show_Po_check();
}

// TEXTBUFFERDIARY =================================================================================
TextbufferDiaryEdit::TextbufferDiaryEdit()
{
}

bool
TextbufferDiaryEdit::set_editable( bool flag_editable )
{
    if( m_p2entry == nullptr )
        return false;

    m_tag_markup->property_invisible() = !flag_editable;

    if( flag_editable )
    {
        set_language( m_p2entry->get_lang_final() ); // also sets the m_flag_check_word
        m_flag_settext_operation = true;
        reparse(); // in the future this may also be wanted when flag_editable == false
        m_flag_settext_operation = false;
    }
    else
    if( m_enchant_dict )
    {
        enchant_broker_free_dict( Lifeograph::s_enchant_broker, m_enchant_dict );
        m_enchant_dict = nullptr;
        m_flag_check_word = false;
    }

    return true;
}

void
TextbufferDiaryEdit::update_todo_status()
{
    if( m_p2entry &&
        ( m_p2entry->get_status() & ES::NOT_TODO ) &&
        m_p2entry->update_todo_status() )
    {
        AppWindow::p->UI_entry->refresh_icon();
        AppWindow::p->UI_diary->refresh_row( m_p2entry );
    }
}

// EVENT HANDLERS
void
TextbufferDiaryEdit::handle_space()
{
    Gtk::TextIter&& it_end{ get_iter_at_mark( get_insert() ) };
    Gtk::TextIter   it_bgn = it_end;
    if( ! it_bgn.backward_find_char( s_predicate_nl ) )
        return;

    it_bgn++;   // skip the new line char
    const auto&&    line{ get_text( it_bgn, it_end ) };
    char            char_lf{ '\t' };
    const auto      size{ line.length() };

    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;

                it_bgn++;   // indentation level
                break;
            case '*':
                if( char_lf != 'A' || i != ( size - 1 ) )
                    return;

                m_ongoing_operation_depth++;
                it_bgn = erase( it_bgn, it_end );
                m_ongoing_operation_depth--;
                insert( it_bgn, "•" );
                break;
            default:
                return;
        }
    }
}

bool
TextbufferDiaryEdit::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_ongoing_operation_depth++;
                    iter = erase( iter, get_iter_at_mark( get_insert() ) );
                    m_ongoing_operation_depth--;
                    insert( iter, "—" );
                    return true;
                }
                break;
            default:
                return false;
        }
    }
    return false;
}

bool
TextbufferDiaryEdit::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 += 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_ongoing_operation_depth++;
                        iter_start = erase( iter_start, iter_end );
                        m_ongoing_operation_depth--;
                        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
TextbufferDiaryEdit::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();

    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 += Ustring::compose< int,char >(
                        "%1%2", value, iter.get_char() );
                break;
            case ' ':
                if( char_lf != ' ' )
                    return false;
                m_ongoing_operation_depth++;
                iter_start = erase( iter_start, iter );
                m_ongoing_operation_depth--;
                iter = insert( iter_start, text );
                return true;
            default:
                return false;
        }
    }
    return false;
}

void
TextbufferDiaryEdit::on_insert( const Gtk::TextIter& iter, const Ustring& text, int bytes )
{
    std::string&& text2{ text }; // to drop constness

    if( m_flag_settext_operation )
    {
        Gtk::TextBuffer::on_insert( iter, text2, bytes );
        return;
    }
    else // get rid of unwanted chars that may lurk within copied texts
    if( m_flag_paste_operation )
    {
        std::replace( text2.begin(), text2.end(), '\r', '\n' );
        bytes -= STR::replace( text2, "\302\240", " " ); // nbr space (space is 1 byte less)
        m_flag_paste_operation = false;
    }

    PRINT_DEBUG( "TextbufferDiaryEdit::on_insert()" );

    const UstringSize offset_itr{ ( UstringSize ) iter.get_offset() };

    if( m_flag_undo_operation == false )
        m_ptr2TvDE->m_p2undo_manager->add_action( new UndoInsert( offset_itr, text2, this ) );

    if( m_p2entry )
        m_p2entry->insert_text( offset_itr, text2 );

    Gtk::TextBuffer::on_insert( iter, text2, bytes );

    // text2 does not have a length() method and using text is OK here:
    parse_damaged_region( get_iter_at_offset( offset_itr ),
                          get_iter_at_offset( offset_itr + text.length() ) );

    m_Sg_changed.emit();

    if( m_ptr2TvDE->m_completion && m_ptr2TvDE->m_completion->is_on_display() )
        m_ptr2TvDE->show_Po_completion(); // updates the completion
}

void
TextbufferDiaryEdit::on_erase( const Gtk::TextIter& it_bgn, const Gtk::TextIter& it_end )
{
    if( m_flag_settext_operation )
    {
        Gtk::TextBuffer::on_erase( it_bgn, it_end );
        return;
    }

    PRINT_DEBUG( "TextbufferDiary::on_erase()" );

    if( m_flag_undo_operation == false )
        m_ptr2TvDE->m_p2undo_manager->add_action(
                new UndoErase( it_bgn.get_offset(), get_slice( it_bgn, it_end ), this ) );

    if( m_p2entry )
        m_p2entry->erase_text( it_bgn.get_offset(), it_end.get_offset() );

    Gtk::TextBuffer::on_erase( it_bgn, it_end );

    parse_damaged_region( it_bgn, it_end );

    m_Sg_changed.emit();

    if( m_ptr2TvDE->m_completion && m_ptr2TvDE->m_completion->is_on_display() )
        m_ptr2TvDE->show_Po_completion(); // updates the completion
}

void
TextbufferDiaryEdit::on_mark_set( const Gtk::TextIter& iter,
                                  const Glib::RefPtr< TextBuffer::Mark >& mark )
{
    if( m_ptr2TvDE && m_max_thumbnail_w > 0 && m_ongoing_operation_depth == 0 &&
        m_flag_settext_operation == false &&
        m_ptr2TvDE->get_editable() && mark == get_insert() )
    {
        parse_damaged_region( iter, iter );

        if( m_ptr2TvDE->m_completion && m_ptr2TvDE->m_completion->is_on_display() )
            m_ptr2TvDE->m_completion->popdown();
    }
    Gtk::TextBuffer::on_mark_set( iter, mark );
}

// FORMATTING
void
TextbufferDiaryEdit::toggle_format( Glib::RefPtr< Tag > tag, const Ustring& markup )
{
    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;

        const int p_first_nl = get_first_nl_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_ongoing_operation_depth++;

            // 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_ongoing_operation_depth--;

            backspace( ++iter_end );
        }
        else
        if( iter_start.has_tag( m_tag_link ) == false ) // formatting is not allowed within links
        {
            // 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
TextbufferDiaryEdit::toggle_bold()
{
    toggle_format( m_tag_bold, "*" );
}

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

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

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

void
TextbufferDiaryEdit::set_justification( JustificationType justification )
{
    Gtk::TextIter iter_bgn, iter_end;
    calculate_sel_para_bounds( iter_bgn, iter_end );

    Paragraph* para;
    Ustring::size_type pos_para{ 0 };
    if( m_p2entry->get_paragraph( iter_bgn.get_offset(), para, pos_para ) )
    {
        switch( justification )
        {
            case JT_LEFT:
                remove_tag( m_tag_justify_center, iter_bgn, iter_end );
                remove_tag( m_tag_justify_right, iter_bgn, iter_end );
                apply_tag( m_tag_justify_left, iter_bgn, iter_end );
                break;
            case JT_CENTER:
                remove_tag( m_tag_justify_left, iter_bgn, iter_end );
                remove_tag( m_tag_justify_right, iter_bgn, iter_end );
                apply_tag( m_tag_justify_center, iter_bgn, iter_end );
                break;
            case JT_RIGHT:
                remove_tag( m_tag_justify_left, iter_bgn, iter_end );
                remove_tag( m_tag_justify_center, iter_bgn, iter_end );
                apply_tag( m_tag_justify_right, iter_bgn, iter_end );
                break;
        }

        para->m_justification = justification;
    }
}

void
TextbufferDiaryEdit::add_link_check( Gtk::TextIter& it_bgn, Gtk::TextIter& it_end, char state )
{
    m_list_links.push_back(
            new LinkCheck( m_ptr2TvDE, create_mark( it_bgn ), create_mark( it_end ), state ) );
}

void
TextbufferDiaryEdit::set_check( char new_status )
{
    if( Lifeograph::is_internal_operations_ongoing() )
        return;

    Gtk::TextIter it_bgn = get_iter_at_offset( m_ptr2TvDE->get_Po_check_pos() );
    Gtk::TextIter it_end = it_bgn;

    calculate_para_bounds( it_bgn, it_end );

    const int offset_bgn = it_bgn.get_offset();
    const int offset_end = it_end.get_offset();

    set_list_item_internal( it_bgn, it_end, new_status );

    parse( offset_bgn, offset_end );
}

void
TextbufferDiaryEdit::set_list_item( char type )
{
    if( Lifeograph::is_internal_operations_ongoing() )
        return;

    Gtk::TextIter it_bgn, it_end;
    calculate_sel_para_bounds( it_bgn, it_end );

    set_list_item_internal( it_bgn, it_end, type );

    calculate_sel_para_bounds( it_bgn, it_end );

    parse( it_bgn.get_offset() - 1, it_end.get_offset() );
}

void
TextbufferDiaryEdit::set_list_item_internal( Gtk::TextIter& it_bgn,
                                             Gtk::TextIter& it_end,
                                             char target_item_type )
{
    if ( it_bgn == it_end ) // empty line
    {
        switch( target_item_type )
        {
            case '*':
                it_bgn = insert( it_bgn, "\t• " );
                break;
            case '-':
                it_bgn = insert( it_bgn, "\t- " );
                break;
            case ' ':
                it_bgn = insert( it_bgn, "\t[ ] " );
                break;
            case '~':
                it_bgn = insert( it_bgn, "\t[~] " );
                break;
            case '+':
                it_bgn = insert( it_bgn, "\t[+] " );
                break;
            case 'x':
                it_bgn = insert( it_bgn, "\t[x] " );
                break;
        }
        return;
    }

    int     offset{ it_end.get_offset() };
    auto    it_erase_begin = it_bgn;
    char    item_type{ 0 };    // none
    char    char_lf{ 't' };    // tab
    // LEGEND for char_lf values
    //      a: any char
    //      t: tab
    //      s: space
    //      c: check mark such as + or x
    //      n: new line

    while( it_bgn.get_offset() <= offset )
    {
        switch( it_bgn.get_offset() == offset ? 0 : it_bgn.get_char() )
        {
            case '\t':
                if( char_lf == 't' || char_lf == '[' )
                {
                    char_lf = '[';  // opening bracket
                    it_erase_begin = it_bgn;
                }
                else
                    char_lf = 'n';
                break;
            case L'•':
                char_lf = ( char_lf == '[' ? 's' : 'n' );
                item_type = ( char_lf == 's' ? '*' : 0 );
                break;
            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 )
                    {
                        it_bgn++;
                        offset -= ( it_bgn.get_offset() - it_erase_begin.get_offset() );
                        it_bgn = erase( it_erase_begin, it_bgn );
                        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 = it_bgn.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 '+':
                            list_str = "\t[+] ";
                            break;
                        case 'x':
                            list_str = "\t[x] ";
                            break;
                    }
                    it_bgn = insert( it_bgn, list_str );
                    offset += list_str.length();
                }
                char_lf = 'n';
                break;
        }
        if( it_bgn.forward_char() == false )
            return;
    }
}

// REPLACE
void
TextbufferDiaryEdit::replace_itag_at_cursor( const Entry* entry )
{
    Gtk::TextIter iter_bgn, iter_end;
    get_selection_bounds( iter_bgn, iter_end );
    calculate_itag_bounds( iter_bgn, iter_end );

    if( entry == nullptr )
    {
        Ustring name{ get_slice( iter_bgn, iter_end ) };
        std::string::size_type i;
        while( ( i = name.find( ':' ) ) != std::string::npos )
            name.erase( i, 1 );

        date_t date{ m_ptr2diary->get_available_order_1st( true ) };
        entry = m_ptr2diary->create_entry( date, name );
        AppWindow::p->UI_diary->update_entry_list();
    }

    iter_bgn = erase( iter_bgn, iter_end );
    auto iter_prev = iter_bgn;
    iter_prev--;
    if( iter_prev.get_char() == ':' )
        insert( iter_bgn, entry->get_name() + ":" );
    else
        insert( iter_bgn, ":" + entry->get_name() + ":" );
}

void
TextbufferDiaryEdit::replace_word_at_cursor( const Ustring& new_text )
{
    Gtk::TextIter iter_bgn, iter_end;
    get_selection_bounds( iter_bgn, iter_end );

    if( ! iter_bgn.backward_find_char( s_predicate_blank ) )
        iter_bgn.set_offset( 0 );
    else
        iter_bgn++;

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

    iter_bgn = erase( iter_bgn, iter_end );
    insert( iter_bgn, new_text );
}

// CODE-EDITOR LIKE FORMATTING
void
TextbufferDiaryEdit::indent_paragraphs()
{
    if( ! m_ptr2TvDE->has_focus() )
        return;

    Gtk::TextIter iter, iter_end;
    calculate_sel_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
TextbufferDiaryEdit::unindent_paragraphs()
{
    if( ! m_ptr2TvDE->has_focus() )
        return;

    Gtk::TextIter iter, iter_end;
    calculate_sel_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;
    }
}

void
TextbufferDiaryEdit::add_empty_line_above()
{
    if( ! m_ptr2TvDE->has_focus() )
        return;

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

void
TextbufferDiaryEdit::remove_empty_line_above()
{
    if( ! m_ptr2TvDE->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
TextbufferDiaryEdit::open_paragraph_below()
{
    if( ! m_ptr2TvDE->has_focus() )
        return;

    Gtk::TextIter&& iter{ get_iter_at_mark( get_insert() ) };
    iter.forward_line();
    place_cursor( --iter );

    handle_new_line();
}

void
TextbufferDiaryEdit::move_line_up()
{
    if( ! m_ptr2TvDE->has_focus() )
        return;

    Gtk::TextIter iter_bgn, iter_end;
    bool flag_touches_end{ calculate_sel_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++;
    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
TextbufferDiaryEdit::move_line_down()
{
    if( ! m_ptr2TvDE->has_focus() )
        return;

    Gtk::TextIter iter_bgn, iter_end;
    if( calculate_sel_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++;

    Ustring text{ get_text( iter_bgn, iter_end ) };
    if( flag_is_near_end )
    {
        text.insert( 0, "\n" );
        text.erase( text.length() - 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
TextbufferDiaryEdit::delete_paragraphs()
{
    if( ! m_ptr2TvDE->has_focus() )
        return;

    Gtk::TextIter iter_bgn, iter_end;
    if( calculate_sel_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
TextbufferDiaryEdit::duplicate_paragraphs()
{
    if( ! m_ptr2TvDE->has_focus() )
        return;

    Gtk::TextIter iter_bgn, iter_end;
    if( calculate_sel_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() };
    Ustring text{ get_text( iter_bgn, iter_end ) };

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

// INSERTS
void
TextbufferDiaryEdit::insert_comment()
{
    if( get_has_selection() )
    {
        Gtk::TextIter it_bgn, it_end;
        get_selection_bounds( it_bgn, it_end );
        const auto pos_bgn = it_bgn.get_offset();
        const auto pos_end = it_end.get_offset() + 4;

        insert( it_end, "]]" );
        insert( get_iter_at_offset( pos_bgn ), "[[" );

        select_range( get_iter_at_offset( pos_bgn ), get_iter_at_offset( pos_end ) );
    }
    else
    {
        auto&& iter{ get_insert()->get_iter() };
        iter = insert( iter, "[[]]" );
        iter.backward_chars( 2 );
        place_cursor( iter );
    }
}

void
TextbufferDiaryEdit::insert_with_spaces( Ustring&& text )
{
    char c( get_char_at( m_insert_offset ) );
    if( c != ' ' && c != '\n' && c != '\t' )
        text += " ";

    if( m_insert_offset > 0 )
    {
        c = get_char_at( m_insert_offset - 1 );
        if( c != ' ' && c != '\n' && c != '\t' )
            text.insert( 0, " " );
    }

    insert( get_iter_at_offset( m_insert_offset ), text );
}

void
TextbufferDiaryEdit::insert_link( DiaryElement* element )
{
    insert_with_spaces(
            Ustring::compose( "<deid:%1\t%2>", element->get_id(), element->get_name() ) );

    place_cursor( get_iter_at_offset( m_insert_offset + 1 ) ); // to force parsing
}

void
TextbufferDiaryEdit::insert_tag( Entry* entry )
{
    insert_with_spaces(
            Ustring::compose( ":%1:", entry->get_name() ) );

    place_cursor( get_iter_at_offset( m_insert_offset + 1 ) ); // to force parsing
}

void
TextbufferDiaryEdit::insert_image()
{
    std::string uri;

    if( select_image_file( *AppWindow::p, uri ) )
    {
        Gtk::TextIter&& iter{ get_iter_at_mark( get_insert() ) };
        if( not( iter.ends_line() ) )
            iter.forward_to_line_end();
        iter++; // past the line end

        iter = insert( iter, uri + '\n' );
        place_cursor( --iter ); // to force parsing
    }
}

void
TextbufferDiaryEdit::insert_date_stamp( bool F_add_time )
{
    if( ! m_ptr2TvDE->has_focus() )
        return;

    m_insert_offset = get_iter_at_mark( get_insert() ).get_offset();

    insert_with_spaces( F_add_time ? Date::format_string_dt( time( nullptr ) ) :
                                     Date::format_string_d( time( nullptr ) ) );
}

void
TextbufferDiaryEdit::copy_image_file_to_rel()
{
#ifdef _WIN32
    m_ptr2TvDE->update_link_at_insert();
#endif
    if( m_ptr2TvDE->m_link_hovered == nullptr ) return;
    if( m_ptr2TvDE->m_link_hovered->type != Link::LT_IMAGE ) return;

    auto&& it_bgn        { get_iter_at_mark( m_ptr2TvDE->m_link_hovered->m_mark_start ) };
    auto&& it_end        { get_iter_at_mark( m_ptr2TvDE->m_link_hovered->m_mark_end ) };

    auto&& uri_src       { dynamic_cast< LinkUri* >( m_ptr2TvDE->m_link_hovered )->m_uri };
    auto&& path_dir_dest { Glib::build_filename( Glib::path_get_dirname( m_ptr2diary->get_path() ),
                                    "[" + STR::replace_spaces( m_ptr2diary->get_name() ) + "]" ) };
    auto&& path_dest     { Glib::build_filename( path_dir_dest,
                                                 Glib::path_get_basename( uri_src ) ) };

#ifndef _WIN32
    auto   file_src      { Gio::File::create_for_uri( uri_src ) };
    auto   file_dir_dest { Gio::File::create_for_path( path_dir_dest ) };
    auto   file_dest     { Gio::File::create_for_path( path_dest ) };

    try
    {
        // create the directory if needed
        if( file_dir_dest->query_exists() == false )
            if( file_dir_dest->make_directory() == false )
            {
                print_error( "Cannot create the relative directory" );
                return;
            }
        // copy the file if not already existed
        if( file_dest->query_exists() == false )
            file_src->copy( file_dest );
        else
        {
            print_error( "Target file already exists." );
            return;
        }
    }
    catch( Gio::Error& error )
    {
        print_error( "Copy failed: ", error.what() );
        return;
    }
#else
    CreateDirectory( PATH( path_dir_dest ).c_str(), nullptr );
    // following does not work for some reason:
    //char* fname_src = new char[ MAX_PATH ];
    //DWORD size;
    //PathCreateFromUrl( PATH( uri_src ).c_str(), fname_src, &size, 0 );
    // so we resort to a simpler alternative:
    std::string fname_src = uri_src.substr( 7, uri_src.size() - 7 );
    CopyFile( PATH( fname_src ).c_str(), PATH( path_dest ).c_str(), true );
#endif

    it_bgn = erase( it_bgn, it_end );
    insert( it_bgn,
            "rel://[" + STR::replace_spaces( m_ptr2diary->get_name() ) + "]/"
                      + Glib::path_get_basename( path_dest ) );
}

// OTHERS
Ustring
TextbufferDiaryEdit::get_text_to_save()
{
    return Gtk::TextBuffer::get_text();
}

void
TextbufferDiaryEdit::go_to_link()
{
    auto link = get_link( get_insert()->get_iter() );

    if( link )
        switch( link->type )
        {
            case Link::LT_DATE:
            case Link::LT_ID:
            case Link::LT_IMAGE:
            case Link::LT_TAG:
            case Link::LT_URI:
                link->go();
                break;
            default:
                break;
        }
}

// SPELL CHECKING BY DIRECT UTILIZATION OF ENCHANT (code partly copied from GtkSpell library)
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;
}

void
TextbufferDiaryEdit::set_language( std::string&& lang )
{
    if( m_enchant_dict )
        enchant_broker_free_dict( Lifeograph::s_enchant_broker, m_enchant_dict );

    if( lang == LANG_INHERIT_DIARY )
        lang = m_ptr2diary->get_lang();

    if( lang.empty() )  // empty means checking is turned off
    {
        m_enchant_dict = nullptr;
        m_flag_check_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_check_word = false;
        return;
    }

    m_flag_check_word = true;
    enchant_dict_describe( m_enchant_dict, set_lang_from_dict_cb, &lang );
}

void
TextbufferDiaryEdit::check_word()
{
    if( enchant_dict_check( m_enchant_dict, m_word_cur.c_str(), m_word_cur.length() ) != 0 )
    {
        auto&& iter_bgn{ get_iter_at_offset( m_pos_cur - int( m_word_cur.length() ) ) };
        auto&& iter_end{ get_iter_at_offset( m_pos_cur ) };
        apply_tag( m_tag_misspelled, iter_bgn, iter_end );
    }
}

void
TextbufferDiaryEdit::add_spell_suggestions( const Ustring& word, Gtk::Box* box )
{
    Gtk::ModelButton* mi;
    size_t n_suggs;
    char** suggestions;

    auto add_item = [ & ]( const Ustring& label )
            {
                mi = Gtk::manage( new Gtk::ModelButton );
                mi->property_text() = label;
                mi->show();
                box->pack_start( *mi, Gtk::PACK_SHRINK );
            };

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

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

    if ( suggestions == nullptr || n_suggs == 0 )
    {
        add_item( STR::compose( "(", _( "no suggestions" ), ")" ) );
        mi->set_sensitive( false );
    }
    else
    {
        //Gtk::Menu* menu_more( nullptr );

        for( size_t i = 0; i < n_suggs && i < 10; i++ )
        {
            add_item( suggestions[ i ] );
            mi->signal_clicked().connect( sigc::bind(
                    sigc::mem_fun( this, &TextbufferDiaryEdit::replace_misspelled_word ),
                    Ustring( suggestions[ i ] ) ) );

            /*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
    add_item( Ustring::compose( _( "Add \"%1\" to Dictionary" ), word ) );
    mi->signal_clicked().connect( sigc::bind(
            sigc::mem_fun( this, &TextbufferDiaryEdit::add_word_to_dictionary ), word ) );

    // IGNORE ALL
    add_item( _( "Ignore All" ) );
    mi->signal_clicked().connect( sigc::bind(
            sigc::mem_fun( this, &TextbufferDiaryEdit::ignore_misspelled_word ), word ) );
}

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

void
TextbufferDiaryEdit::replace_misspelled_word( const 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 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_ongoing_operation_depth++;
    int offset( iter_begin.get_offset() );
    erase( iter_begin, iter_end );
    iter_begin = get_iter_at_offset( offset );
    m_ongoing_operation_depth--;
    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
TextbufferDiaryEdit::add_word_to_dictionary( const Ustring& word )
{
    enchant_dict_add( m_enchant_dict, word.c_str(), word.size() );
    reparse();   // check entire buffer again
}

// TEXTVIEW ========================================================================================
TextviewDiaryEdit::TextviewDiaryEdit( BaseObjectType* cobject,
                                      const Glib::RefPtr< Gtk::Builder >& parent_builder )
:   TextviewDiary( cobject, parent_builder )
{
    m_buffer = m_buffer_edit = new TextbufferDiaryEdit;
    set_buffer( static_cast< Glib::RefPtr< TextbufferDiary > >( m_buffer ) );
    m_buffer_edit->m_ptr2TvD = m_buffer_edit->m_ptr2TvDE = this;

    drag_dest_get_target_list()->add( { Lifeograph::DTE_entry_row } );
    drag_dest_get_target_list()->add( { Lifeograph::DTE_theme } );
#ifdef _WIN32
    drag_dest_get_target_list()->add_uri_targets( 111111 );
#endif
    auto builder = Gtk::Builder::create();
    Lifeograph::load_gui( builder, Lifeograph::SHAREDIR + "/ui/tv_diary.ui" );

    builder->get_widget( "Po_context", m_Po_context );
    builder->get_widget( "Bx_context_edit_only_1", m_Bx_context_edit_1 );
    builder->get_widget( "Bx_context_edit_only_2", m_Bx_context_edit_2 );
    builder->get_widget( "B_expand_sel", m_B_expand_sel );
    builder->get_widget( "B_cut", m_B_cut );
    builder->get_widget( "B_copy", m_B_copy );
    builder->get_widget( "B_paste", m_B_paste );
    builder->get_widget( "B_insert_emoji", m_B_insert_emoji );
    builder->get_widget( "B_insert_image", m_B_insert_image );
    builder->get_widget( "MoB_copy_img_to_rel", m_MoB_copy_img_to_rel);
    builder->get_widget( "Bx_spell_separator", m_Bx_spell_separator );
    builder->get_widget( "Bx_spell", m_Bx_spell );

    builder->get_widget( "B_undo", m_B_undo );
    builder->get_widget( "B_redo", m_B_redo );
    builder->get_widget( "B_bold", m_B_bold );
    builder->get_widget( "B_italic", m_B_italic );
    builder->get_widget( "B_strikethrough", m_B_strkthru );
    builder->get_widget( "B_highlight", m_B_highlight );
    builder->get_widget( "L_highlight", m_L_highlight );
    builder->get_widget( "EB_entry_highlight", m_EB_highlight );
    builder->get_widget( "RB_para_justify_left", m_RB_justify_left );
    builder->get_widget( "RB_para_justify_center", m_RB_justify_center );
    builder->get_widget( "RB_para_justify_right", m_RB_justify_right );

    builder->get_widget( "RB_listitem_none", m_RB_list_none );
    builder->get_widget( "RB_listitem_bullet", m_RB_list_bllt );
    builder->get_widget( "RB_listitem_todo", m_RB_list_todo );
    builder->get_widget( "RB_listitem_progressed", m_RB_list_prog );
    builder->get_widget( "RB_listitem_done", m_RB_list_done );
    builder->get_widget( "RB_listitem_canceled", m_RB_list_cncl );

    builder->get_widget( "Po_checklist", m_Po_checklist );
    builder->get_widget( "RB_checklist_todo", m_RB_check_open );
    builder->get_widget( "RB_checklist_progressed", m_RB_check_prog );
    builder->get_widget( "RB_checklist_done", m_RB_check_done );
    builder->get_widget( "RB_checklist_canceled", m_RB_check_cncl );

    m_Po_context->set_relative_to( *this );
    m_Po_checklist->set_relative_to( *this );

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

    // PO CONTEXT
    m_B_cut->signal_clicked().connect(
            [ this ]()
            { m_buffer_edit->cut_clipboard( Gtk::Clipboard::get() ); m_Po_context->hide(); } );
    m_B_copy->signal_clicked().connect(
            [ this ]()
            { m_buffer_edit->copy_clipboard( Gtk::Clipboard::get() ); m_Po_context->hide(); } );
    m_B_paste->signal_clicked().connect(
            [ this ]()
            { m_buffer_edit->paste_clipboard2(); m_Po_context->hide(); } );
    m_B_insert_emoji->signal_clicked().connect(
            [ this ](){ g_signal_emit_by_name ( gobj(), "insert-emoji" ); } );
    m_B_insert_image->signal_clicked().connect(
            [ this ](){ m_Po_context->hide(); m_buffer_edit->insert_image(); } );
    m_MoB_copy_img_to_rel->signal_clicked().connect(
            [ this ](){ m_buffer_edit->copy_image_file_to_rel(); } );

    // PO TOOLBAR
    m_B_expand_sel->signal_clicked().connect( [ this ](){ m_buffer_edit->expand_selection(); } );
    m_B_bold->signal_clicked().connect( [ this ](){ m_buffer_edit->toggle_bold(); } );
    m_B_italic->signal_clicked().connect( [ this ](){ m_buffer_edit->toggle_italic(); } );
    m_B_strkthru->signal_clicked().connect( [ this ](){ m_buffer_edit->toggle_strikethrough(); } );
    m_B_highlight->signal_clicked().connect( [ this ](){ m_buffer_edit->toggle_highlight(); } );

    m_RB_justify_left->signal_toggled().connect(
            [ this ](){ if( m_RB_justify_left->get_active() &&
                            Lifeograph::is_internal_operations_ongoing() == false )
                            m_buffer_edit->set_justification( JT_LEFT ); } );
    m_RB_justify_center->signal_toggled().connect(
            [ this ](){ if( m_RB_justify_center->get_active() &&
                            Lifeograph::is_internal_operations_ongoing() == false )
                            m_buffer_edit->set_justification( JT_CENTER ); } );
    m_RB_justify_right->signal_toggled().connect(
            [ this ](){ if( m_RB_justify_right->get_active() &&
                            Lifeograph::is_internal_operations_ongoing() == false )
                            m_buffer_edit->set_justification( JT_RIGHT ); } );

    m_RB_list_none->signal_toggled().connect(
            [ this ](){ if( m_RB_list_none->get_active() ) m_buffer_edit->set_list_item( 0 ); } );
    m_RB_list_bllt->signal_toggled().connect(
            [ this ](){ if( m_RB_list_bllt->get_active() ) m_buffer_edit->set_list_item( '*' ); } );
    m_RB_list_todo->signal_toggled().connect(
            [ this ](){ if( m_RB_list_todo->get_active() ) m_buffer_edit->set_list_item( ' ' ); } );
    m_RB_list_prog->signal_toggled().connect(
            [ this ](){ if( m_RB_list_prog->get_active() ) m_buffer_edit->set_list_item( '~' ); } );
    m_RB_list_done->signal_toggled().connect(
            [ this ](){ if( m_RB_list_done->get_active() ) m_buffer_edit->set_list_item( '+' ); } );
    m_RB_list_cncl->signal_toggled().connect(
            [ this ](){ if( m_RB_list_cncl->get_active() ) m_buffer_edit->set_list_item( 'x' ); } );

    // PO COMPLETION
    m_completion = new EntryPickerCompletion( *this );
    m_completion->signal_entry_activated().connect(
            [ this ]( LIFEO::Entry* e ){ m_buffer_edit->replace_itag_at_cursor( e ); } );

    // PO CEHCK LIST
    m_RB_check_open->signal_toggled().connect(
            [ this ](){ if( m_RB_check_open->get_active() ) m_buffer_edit->set_check( ' ' ); } );
    m_RB_check_prog->signal_toggled().connect(
            [ this ](){ if( m_RB_check_prog->get_active() ) m_buffer_edit->set_check( '~' ); } );
    m_RB_check_done->signal_toggled().connect(
            [ this ](){ if( m_RB_check_done->get_active() ) m_buffer_edit->set_check( '+' ); } );
    m_RB_check_cncl->signal_toggled().connect(
            [ this ](){ if( m_RB_check_cncl->get_active() ) m_buffer_edit->set_check( 'x' ); } );
}

TextviewDiaryEdit::~TextviewDiaryEdit()
{
    if( m_completion )
        delete m_completion;
}

void
TextviewDiaryEdit::set_entry( Entry* entry )
{
    m_link_hovered = nullptr;

    auto kv_um = m_undo_managers.find( entry->get_id() );
    if( kv_um == m_undo_managers.end() )
    {
        m_p2undo_manager = new UndoManager;
        m_undo_managers.emplace( entry->get_id(), m_p2undo_manager );
    }
    else
        m_p2undo_manager = kv_um->second;

    // defer setting text to after the size allocation
    if( m_buffer_edit->m_max_thumbnail_w > 0 )
    {
        m_buffer_edit->set_entry( entry );
        update_highlight_button();
    }
    else
    {
        m_flag_set_text_queued = true;
        m_buffer_edit->m_p2entry = entry;
    }

    m_completion->set_diary( entry->get_diary() );
}

void
TextviewDiaryEdit::set_editable( bool flag_editable )
{
    Gtk::TextView::set_editable( flag_editable ); // must come first
    m_buffer_edit->set_editable( flag_editable );

    m_Bx_context_edit_1->set_visible( flag_editable );
    m_Bx_context_edit_2->set_visible( flag_editable );

    m_B_cut->set_sensitive( flag_editable );
    m_B_paste->set_sensitive( flag_editable );
}

void
TextviewDiaryEdit::show_Po_context( int x, int y )
{
    Gtk::TextIter   it_bgn, it_end;
    Gdk::Rectangle  rect;
    const bool      flag_has_sel { m_buffer->get_has_selection() };

    if( flag_has_sel || x < 0 ) // x<0 means force
    {
        m_buffer_edit->get_selection_bounds( it_bgn, it_end );
    }
    else
    {
        window_to_buffer_coords( Gtk::TEXT_WINDOW_TEXT, x, y, x, y );
        get_iter_at_location( it_bgn, x, y );
        it_end = it_bgn;
    }

    m_B_undo->set_sensitive( can_undo() );
    m_B_redo->set_sensitive( can_redo() );

    if( get_editable() )
    {
        m_B_cut->set_sensitive( flag_has_sel );

        Gtk::Clipboard::get()->request_targets(
                [ this ]( const std::vector< Ustring >& trgts )
                {
                    bool f_enable{ false };
                    for( auto& t : trgts )
                    {
                        f_enable = m_buffer_edit->get_paste_target_list()->find( t, nullptr );
                        if( f_enable ) break;
                    }

                    m_B_paste->set_sensitive( f_enable );
                } );

        m_MoB_copy_img_to_rel->set_visible(
                m_link_hovered && m_link_hovered->type == Link::LT_IMAGE &&
                dynamic_cast< LinkUri* >( m_link_hovered )->is_rel() == false );

        // PARA ALIGNMENT
        Paragraph* para;
        Ustring::size_type pos_para{ 0 };
        if( m_buffer_edit->m_p2entry->get_paragraph( it_bgn.get_offset(), para, pos_para ) )
        {
            Lifeograph::START_INTERNAL_OPERATIONS();
            switch( para->m_justification )
            {
                case JT_LEFT:
                    m_RB_justify_left->set_active( true );
                    break;
                case JT_CENTER:
                    m_RB_justify_center->set_active( true );
                    break;
                case JT_RIGHT:
                    m_RB_justify_right->set_active( true );
                    break;
            }
            Lifeograph::FINISH_INTERNAL_OPERATIONS();
        }

        // SPELL SUGGESTIONS (created by using GtkSpell as a template)
        if( m_buffer_edit->m_flag_check_word &&
            it_bgn.has_tag( m_buffer_edit->m_tag_misspelled ) )
        {
            m_buffer_edit->m_spell_suggest_offset = it_bgn.get_offset();
            m_Bx_spell->set_visible( true );
            m_Bx_spell_separator->set_visible( true );
            m_Bx_spell->foreach( [ this ]( Gtk::Widget& w ){ m_Bx_spell->remove( w ); } );
            it_bgn.backward_to_tag_toggle( m_buffer_edit->m_tag_misspelled );
            it_end.forward_to_tag_toggle( m_buffer_edit->m_tag_misspelled );
            m_buffer_edit->add_spell_suggestions(
                    m_buffer_edit->get_text( it_bgn, it_end ), m_Bx_spell );
        }
        else
        {
            m_Bx_spell->set_visible( false );
            m_Bx_spell_separator->set_visible( false );
        }
    }
    else if( m_link_hovered && m_link_hovered->type == Link::LT_IMAGE )
    {
        return;
    }

    m_B_copy->set_sensitive( flag_has_sel );

    if( flag_has_sel == false )
        m_buffer_edit->select_range( it_bgn, it_end );

    get_selection_rect( rect );
    m_Po_context->set_pointing_to( rect );
    m_Po_context->show();
}

void
TextviewDiaryEdit::update_highlight_button()
{
    const Theme* theme{ m_buffer_edit->get_theme() };
    m_L_highlight->override_color( theme->color_text, Gtk::STATE_FLAG_NORMAL );
    m_L_highlight->override_color( theme->color_text, Gtk::STATE_FLAG_PRELIGHT );
    m_L_highlight->override_color( theme->color_text, Gtk::STATE_FLAG_ACTIVE );

    m_EB_highlight->override_background_color( theme->color_highlight, Gtk::STATE_FLAG_NORMAL );
    m_EB_highlight->override_background_color( theme->color_highlight, Gtk::STATE_FLAG_PRELIGHT );
    m_EB_highlight->override_background_color( theme->color_highlight, Gtk::STATE_FLAG_ACTIVE );
}

void
TextviewDiaryEdit::show_Po_completion()
{
    if( !m_completion )
        return;

    Gdk::Rectangle rect;
    Gtk::TextIter it_bgn, it_end;
    m_buffer_edit->get_selection_bounds( it_bgn, it_end );
    get_selection_rect( rect );

    if( m_buffer_edit->calculate_itag_bounds( it_bgn, it_end ) )
    {   // take care of the colons, when within tag
        it_bgn.forward_char();
        it_end.backward_char();
    }
    m_completion->popup( rect, m_buffer_edit->get_slice( it_bgn, it_end ) );
}

void
TextviewDiaryEdit::show_Po_check()
{
    LinkCheck* link;
    try
    {
        link = dynamic_cast< LinkCheck* >( m_link_hovered );
    }
    catch( ... )
    {
        PRINT_DEBUG( "Unexpected link type!!!" );
        return;
    }

    Lifeograph::START_INTERNAL_OPERATIONS();
    switch( link->m_state )
    {
        case ' ':
            m_RB_check_open->set_active();
            break;
        case '~':
            m_RB_check_prog->set_active();
            break;
        case '+':
            m_RB_check_done->set_active();
            break;
        case 'x':
            m_RB_check_cncl->set_active();
            break;
    }
    Lifeograph::FINISH_INTERNAL_OPERATIONS();

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

    get_iter_location( iter, rect );
    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 );

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

void
TextviewDiaryEdit::replace_match( Match& match, const Ustring& new_str )
{
    if( match.para->m_host != m_buffer_edit->m_p2entry ) return;

    auto&& it_bgn{ m_buffer_edit->get_iter_at_offset( match.pos_entry ) };
    auto&& it_end{ m_buffer_edit->get_iter_at_offset(
            match.pos_entry + match.para->m_host->get_diary()->get_search_text().length() ) };

    m_buffer_edit->m_flag_settext_operation = true;

    it_bgn = m_buffer_edit->erase( it_bgn, it_end );
    m_buffer_edit->insert( it_bgn, new_str );

    m_buffer_edit->m_flag_settext_operation = false;
}

void
TextviewDiaryEdit::undo()
{
    if( m_p2undo_manager )
    {
        m_buffer_edit->m_flag_undo_operation = true;
        m_p2undo_manager->undo();
        m_buffer_edit->m_flag_undo_operation = false;
    }
}

void
TextviewDiaryEdit::redo()
{
    if( m_p2undo_manager )
    {
        m_buffer_edit->m_flag_undo_operation = true;
        m_p2undo_manager->redo();
        m_buffer_edit->m_flag_undo_operation = false;
    }
}

void
TextviewDiaryEdit::clear_undo_data()
{
    for( auto& kv_um : m_undo_managers )
        delete kv_um.second;

    m_undo_managers.clear();
}

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

    if( m_buffer_edit->update_thumbnail_width( allocation.get_width() ) )
    {
        if( m_flag_set_text_queued )
        {
            Lifeograph::START_INTERNAL_OPERATIONS();
            // this queuing should only happen while logging in, hence always internal
            m_buffer_edit->set_entry( m_buffer_edit->m_p2entry );
            // word count needs to be updated:
            AppWindow::p->UI_entry->refresh_extra_info();
            update_highlight_button();
            m_flag_set_text_queued = false;
            Lifeograph::FINISH_INTERNAL_OPERATIONS();
        }
        else
            m_buffer_edit->reparse();
    }
}

bool
TextviewDiaryEdit::on_button_press_event( GdkEventButton* event )
{
    if( event->button == 1 )
    {
        if( m_link_hovered != nullptr )
        {
            if( not( get_editable() ) || 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 )
    {
        show_Po_context( event->x, event->y );
        return true;
    }

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

bool
TextviewDiaryEdit::on_key_press_event( GdkEventKey* event )
{
    if( get_editable() )
    {
        switch( event->state & ( Gdk::CONTROL_MASK|Gdk::MOD1_MASK|Gdk::SHIFT_MASK ) )
        {
            case 0:
                if( m_completion && m_completion->is_on_display() &&
                    m_completion->handle_key( event->keyval ) )
                    return true;

                switch( event->keyval )
                {
                    case GDK_KEY_space: m_buffer_edit->handle_space(); break;
                    case GDK_KEY_Return:
                        if( m_buffer_edit->handle_new_line() )
                        {
                            // in this case we need to ensure visibility of the cursor ourselves
                            scroll_to( m_buffer_edit->get_insert(), 0.05 );
                            return true;
                        }
                        break;
                    case GDK_KEY_minus:
                        if( m_buffer_edit->handle_minus() )
                            return true;
                        break;
                    case GDK_KEY_Control_L:
                    case GDK_KEY_Control_R:
                        if( m_link_hovered )
                            update_link();
                        break;
                    case GDK_KEY_Escape:
                        m_Po_checklist->hide();
                        m_Po_context->hide();
                        break;
                }
                break;
            case Gdk::CONTROL_MASK:
                switch( event->keyval )
                {
                    case GDK_KEY_space:  show_Po_completion(); break;
                    case GDK_KEY_B:
                    case GDK_KEY_b:      m_buffer_edit->toggle_bold(); break;
                    case GDK_KEY_H:
                    case GDK_KEY_h:      m_buffer_edit->toggle_highlight(); break;
                    case GDK_KEY_Iabovedot: // Turkish İ
                    case GDK_KEY_idotless:  // Turkish ı
                    case GDK_KEY_I:
                    case GDK_KEY_i:      m_buffer_edit->toggle_italic(); break;
                    case GDK_KEY_S:
                    case GDK_KEY_s:      m_buffer_edit->toggle_strikethrough(); break;
                    case GDK_KEY_V:
                    case GDK_KEY_v:      m_buffer_edit->paste_clipboard2(); return true;
                }
                break;
//            case Gdk::SHIFT_MASK:
//                switch( event->keyval )
//                {
//                }
//                break;
            case Gdk::MOD1_MASK:
                switch( event->keyval )
                {
                    case GDK_KEY_Return:        m_buffer_edit->open_paragraph_below(); return true;
                    case GDK_KEY_0:             m_buffer_edit->set_list_item( 0 ); break;
                    case GDK_KEY_2:             m_buffer_edit->duplicate_paragraphs(); break;
                    case GDK_KEY_C:
                    case GDK_KEY_c:             m_buffer_edit->insert_comment(); break;
                    case GDK_KEY_D:
                    case GDK_KEY_d:             m_buffer_edit->insert_date_stamp(); break;
                    case GDK_KEY_E:
                    case GDK_KEY_e:             m_buffer_edit->delete_paragraphs(); break;
                    case GDK_KEY_Iabovedot: // Turkish İ
                    case GDK_KEY_idotless:  // Turkish ı
                    case GDK_KEY_I:
                    case GDK_KEY_i:             m_buffer_edit->indent_paragraphs(); break;
                    case GDK_KEY_L:
                    case GDK_KEY_l:             m_buffer_edit->add_empty_line_above(); break;
                    case GDK_KEY_U:
                    case GDK_KEY_u:             m_buffer_edit->unindent_paragraphs(); break;
                    case GDK_KEY_X:
                    case GDK_KEY_x:             m_buffer_edit->set_list_item( 'x' ); break;
                    case GDK_KEY_Up:            m_buffer_edit->move_line_up(); break;
                    case GDK_KEY_Down:          m_buffer_edit->move_line_down(); break;
                    case GDK_KEY_asterisk:      m_buffer_edit->set_list_item( '*' ); break;
                    case GDK_KEY_minus:         m_buffer_edit->set_list_item( '-' ); break;
                    case GDK_KEY_underscore:    m_buffer_edit->set_list_item( ' ' ); break;
                    case GDK_KEY_asciitilde:    m_buffer_edit->set_list_item( '~' ); break;
                    case GDK_KEY_plus:          m_buffer_edit->set_list_item( '+' ); break;
                    default: PRINT_DEBUG( "keyval: ",  event->keyval ); break;
                }
                break;
            case Gdk::CONTROL_MASK+Gdk::SHIFT_MASK:
                switch( event->keyval )
                {
                    case GDK_KEY_space: show_Po_context( -1 ); break;
                }
                break;
            case Gdk::MOD1_MASK+Gdk::SHIFT_MASK:
                switch( event->keyval )
                {
                    case GDK_KEY_D:
                    case GDK_KEY_d:             m_buffer_edit->insert_date_stamp( true ); break;
                    case GDK_KEY_L:
                    case GDK_KEY_l:             m_buffer_edit->remove_empty_line_above(); break;
                    // to support keyboard layouts where a Shift is needed for these characters:
                    case GDK_KEY_asterisk:      m_buffer_edit->set_list_item( '*' ); break;
                    case GDK_KEY_underscore:    m_buffer_edit->set_list_item( ' ' ); break;
                    case GDK_KEY_asciitilde:    m_buffer_edit->set_list_item( '~' ); break;
                    case GDK_KEY_plus:          m_buffer_edit->set_list_item( '+' ); break;
                }
                break;
        }
    }

    // key combinations that can be used in read-only mode, too:
    switch( event->state & ( Gdk::CONTROL_MASK|Gdk::MOD1_MASK|Gdk::SHIFT_MASK ) )
    {
//        case 0:
//            break;
//        case Gdk::CONTROL_MASK:
//            break;
        case Gdk::MOD1_MASK:
            switch( event->keyval )
            {
                case GDK_KEY_G:
                case GDK_KEY_g:             m_buffer_edit->go_to_link(); break;
            }
            break;
    }

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

bool
TextviewDiaryEdit::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
TextviewDiaryEdit::on_drag_data_received( const Glib::RefPtr< Gdk::DragContext >& dc,
                                          int x, int y, const Gtk::SelectionData& sel_data,
                                          guint info, guint time )
{
    if( drag_dest_find_target( dc ) == TE_ENTRY_ROW )
    {
        dc->drag_finish( true, false, time );

        int buf_x, buf_y;
        window_to_buffer_coords( Gtk::TEXT_WINDOW_TEXT, x, y, buf_x, buf_y );
        Gtk::TextIter iter;
        get_iter_at_location( iter, buf_x, buf_y );
        m_buffer_edit->m_insert_offset = iter.get_offset();

        //DiaryElement* elem = m_buffer->m_ptr2diary->get_element( sel_data.get_int() );
        Gdk::ModifierType modifiers;
        Gtk::Widget::get_window()->get_pointer( buf_x, buf_y, modifiers );
        if( modifiers & Gdk::CONTROL_MASK )
            m_buffer_edit->insert_link( Lifeograph::get_dragged_elem() );
        else
            m_buffer_edit->insert_tag( dynamic_cast< Entry* >( Lifeograph::get_dragged_elem() ) );
    }
    else
    if( drag_dest_find_target( dc ) == TE_THEME )
    {
        dc->drag_finish( true, false, time );

        AppWindow::p->UI_entry->set_theme(
                dynamic_cast< Theme* >( Lifeograph::get_dragged_elem() ) );
    }
#ifdef _WIN32
    else
    if( drag_dest_find_target( dc ) == "text/uri-list" )
    {
        dc->drag_finish( true, false, time );

        int buf_x, buf_y;
        window_to_buffer_coords( Gtk::TEXT_WINDOW_TEXT, x, y, buf_x, buf_y );
        Gtk::TextIter iter;
        get_iter_at_location( iter, buf_x, buf_y );
        m_buffer_edit->m_insert_offset = iter.get_offset();

        std::string str_uri = sel_data.get_data_as_string();
        str_uri = str_uri.substr( 0, str_uri.find( '\r' ) );

        m_buffer_edit->insert_with_spaces( "file://" + Glib::filename_from_uri( str_uri ) );
    }
#endif
    else
        Gtk::TextView::on_drag_data_received( dc, x, y, sel_data, info, time );
}
