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

    Copyright (C) 2007-2026 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 "lifeograph.hpp"
#include "app_window.hpp"
#include "ui_entry.hpp"


using namespace LoG;


UIEntry::UIEntry()
{
    try
    {
        auto builder{ Lifeograph::get_builder() };
        m_Bx_navigation = builder->get_widget< Gtk::Box >( "Bx_navigation" );
        m_St_entry      = builder->get_widget< Gtk::Stack >( "St_entry" );
        m_TvDE          = Gtk::Builder::get_widget_derived< TextviewDiaryEdit >( builder,
                                                                                 "TV_entry" );
        m_TvDE->set_helper_widgets( builder->get_widget< Gtk::Box >( "Bx_entry_bg" ),
                                    builder->get_widget< Gtk::ScrolledWindow >( "SW_entry" ) );
    }
    catch( ... )
    {
        throw LoG::Error( "Failed to create the entry view" );
    }

    m_TvDE->set_editable( false );

    AppWindow::set_help_topic( m_TvDE, "Text editor" );

    m_TvDE->signal_changed_before_parse().connect(
            [ this ]() { AppWindow::p->UI_extra->remove_entry( m_p2entry ); } );

    m_TvDE->property_has_focus().signal_changed().connect(
            [ this ]()
            {
                if( Diary::d->is_ready() && m_TvDE->has_focus() )
                {
                    if( AppWindow::p->handle_entry_need_for_refresh( m_p2entry->get_id(),
                                                                     EditorType::MAIN ) )
                        m_TvDE->refresh_entry_simple();
                }
            } );

    m_TvDE->signal_changed().connect(
            [ this ]()
            {
                if( !m_F_extra_panel_update_queued ) // not sure if this is really needed
                {
                    m_F_extra_panel_update_queued = true;
                    Glib::signal_idle().connect_once(
                            [ this ]()
                            {
                                AppWindow::p->UI_extra->update_for_entry( m_p2entry );
                                m_F_extra_panel_update_queued = false;
                            } );
                }
                AppWindow::p->add_entry_to_be_refreshed( m_p2entry->get_id_full(),
                                                         EditorType::MAIN );

                m_F_bg_jobs_queued = true;
            } );

    m_dispatcher.connect( sigc::mem_fun( *this, &UIEntry::handle_bg_jobs_completed ) );

    // EDITOR ACTIONS
    m_A_go_back =    Lifeograph::p->add_action( "browse_back",    [ this ](){ go_back(); } );
    m_A_go_forward = Lifeograph::p->add_action( "browse_forward", [ this ](){ go_forward(); } );

    Lifeograph::p->set_accel_for_action( "app.browse_back",         "<Alt>Left" );
    Lifeograph::p->set_accel_for_action( "app.browse_forward",      "<Alt>Right" );
}
UIEntry::~UIEntry()
{
    m_TvDE->disband();
}

void
UIEntry::handle_login()
{
    m_Bx_navigation->show();
    m_A_go_back->set_enabled( false );
    m_A_go_forward->set_enabled( false );
    // m_TvDE->update_image_width(); // why was this needed?
}

void
UIEntry::handle_edit_enabled()
{
    if( !m_p2entry->is_locked() )
        m_TvDE->set_editable( true );

    m_connection_bg_jobs = Glib::signal_timeout().connect_seconds(
            [ this ]()
            {
                if( m_F_bg_jobs_queued )
                    start_bg_jobs();
                return true;
            },
            10 );
}

void
UIEntry::handle_logout()
{
    m_connection_bg_jobs.disconnect();
    destroy_bg_jobs_thread( true );
    m_F_bg_jobs_queued = false;

    m_Bx_navigation->hide();

    m_CPo_entry.handle_logout();

    clear_history();
    m_TvDE->unset_entry(); // also calls m_textview->set_editable( false );

    m_p2entry = nullptr;
}

void
UIEntry::show( Entry* entry )
{
    if( !entry )
        return;
    else if( is_cur_entry( entry ) )
    {
        refresh();
        return;
    }

    m_connection_bg_jobs.block();
    m_F_bg_jobs_queued = false;

    prepare_for_hiding(); // firstly, last element must be prepared for hiding

    m_p2entry = entry;

    add_entry_to_history( entry );

    // HEADER
    refresh_title();
    m_A_go_back->set_enabled( ( m_browsing_history.size() - m_bhist_offset ) > 1 );
    m_A_go_forward->set_enabled( m_bhist_offset > 0 );

    Lifeograph::START_INTERNAL_OPERATIONS();

    // BODY
    m_TvDE->set_entry( entry );
    if( Diary::d->is_in_edit_mode() )
        m_TvDE->set_editable( !entry->is_locked() );

    // word count can only be calculated after entry is set:
    start_bg_jobs();

    m_St_entry->set_visible_child( "entry_transition" );
    m_St_entry->set_visible_child( "entry_editor" );

    m_CPo_entry.set( entry, *m_TvDE, new Gdk::Rectangle( m_TvDE->PAGE_MARGIN -
                                                            TextviewDiary::MAIN_ICON_OFFSET_X,
                                                         TextviewDiary::MAIN_ICON_OFFSET_Y,
                                                         32.0, 0.0 ) );

    Lifeograph::FINISH_INTERNAL_OPERATIONS();

    // set the entry as the one to be saved as the last entry in the diary:
    Diary::d->set_current_entry( entry );

    PRINT_DEBUG( "entry shown: ", entry->get_id().get_raw() );

    m_TvDE->grab_focus();

    m_connection_bg_jobs.unblock();
}

void
UIEntry::refresh()
{
    prepare_for_hiding();

    // HEADER
    refresh_title();
    // refresh_icon();
    // refresh_overlay_icon();

    Diary::d->clear_chart_and_table_images(); // clear cached charts and tables

    Lifeograph::START_INTERNAL_OPERATIONS();

    // BODY
    m_TvDE->refresh_entry( true, true );

    Lifeograph::FINISH_INTERNAL_OPERATIONS();

    PRINT_DEBUG( "entry refreshed" );
}

void
UIEntry::show( Paragraph* para, const HiddenFormat* format )
{
    const bool F_was_invisible { !para->is_visible() };

    if( F_was_invisible )
        para->make_accessible();

    if( !is_cur_entry( para->m_host ) )
    {
        AppWindow::p->show_entry( para->m_host );
        Glib::signal_idle().connect_once( [ this, para, format ](){ show( para, format ); } );
        return;
    }

    if( F_was_invisible ) // do not refresh if entry is opened just now
        m_TvDE->refresh_entry( false, false ); // scroll_to and reset_visibs are done separately

    auto&& pos_bgn { para->get_bgn_offset_in_host() + ( format ? format->pos_bgn : 0 ) };
    auto&& pos_end { format ? ( pos_bgn - format->pos_bgn + format->pos_end )
                            : para->get_end_offset_in_host() };
    auto&& it_bgn  { m_TvDE->m_r2buffer->get_iter_at_offset( pos_bgn ) };
    auto&& it_end  { m_TvDE->m_r2buffer->get_iter_at_offset( pos_end ) };

    m_TvDE->m_r2buffer->select_range( it_bgn, it_end );
    m_TvDE->scroll_to_cursor_center();
    m_TvDE->grab_focus();
}

void
UIEntry::go_back()
{
    if( ( m_browsing_history.size() - m_bhist_offset ) > 1 )
    {
        auto i = m_browsing_history.begin();
        m_bhist_offset++;
        std::advance( i, m_bhist_offset );

        m_St_entry->set_transition_type( Gtk::StackTransitionType::SLIDE_RIGHT );

        m_F_index_locked = true;

        AppWindow::p->show_entry( *i );

        m_F_index_locked = false;

        m_St_entry->set_transition_type( Gtk::StackTransitionType::SLIDE_LEFT );

        m_A_go_back->set_enabled( ( m_browsing_history.size() - m_bhist_offset ) > 1 );
        m_A_go_forward->set_enabled( true );
    }
}

void
UIEntry::go_forward()
{
    if( m_bhist_offset > 0 )
    {
        auto i = m_browsing_history.begin();
        m_bhist_offset--;
        std::advance( i, m_bhist_offset );

        m_F_index_locked = true;

        AppWindow::p->show_entry( *i );

        m_F_index_locked = false;
    }
}

void
UIEntry::add_entry_to_history( Entry* entry )
{
    if( ! m_F_index_locked ) // locked while going back
    {
        if( m_bhist_offset > 0 )
        {
            auto&& i_end{ m_browsing_history.begin() };

            std::advance( i_end, m_bhist_offset );
            m_browsing_history.erase( m_browsing_history.begin(), i_end );
        }
        m_bhist_offset = 0;
        m_browsing_history.push_front( entry );
    }
}

void
UIEntry::remove_entry_from_history( Entry* entry )
{
    auto remove_element = [ & ]()
    {
        for( auto i = m_browsing_history.begin(); i != m_browsing_history.end(); ++i )
            if( *i == entry )
            {
                m_browsing_history.erase( i );
                return true;
            }

        return false;
    };

    while( remove_element() ) continue;
}

void
UIEntry::clear_history()
{
    m_browsing_history.clear();
    m_bhist_offset = 0;
}

void
UIEntry::refresh_title()
{
    if( m_p2entry )
        AppWindow::p->set_title( m_p2entry->get_name() );
    // TODO: 3.2: update the title of the poppup window, too (if any)
}

void
UIEntry::refresh_extra_info()
{
    Ustring text { STR::compose( Date::format_string_adv( m_p2entry->get_date(), "F, d  |  " ),
                                 m_parser_word_count.get_word_count(), " ", _( "word(s)" ),
                                 "  |  " ) };
    if( m_p2entry->get_workload() )
    {
        text += m_p2entry->get_completion_str();
        text += "  |  ";
    }

    text += Diary::d->get_name();
    AppWindow::p->set_subtitle( text );
}

void
UIEntry::prepare_for_hiding()
{
    if( m_p2entry )
    {
        PRINT_DEBUG( "prepare_for_hiding(): ENTRY VIEW SCROLL POS=",
                     m_TvDE->get_vadjustment()->get_value() );

        destroy_bg_jobs_thread( true );

        m_p2entry->set_cursor_pos( m_TvDE->m_pos_cursor );
        // m_p2entry->set_scroll_pos( m_TvDE->get_vadjustment()->get_value() );

        if( m_F_title_edited )
        {
            if( !m_p2entry->get_name_pure().empty() )
                Diary::d->update_tag_refs( m_p2entry->get_id(), m_p2entry->get_name_pure() );
            m_F_title_edited = false;
        }
        for( Paragraph* p = m_p2entry->get_paragraph_1st(); p; p = p->m_p2next )
        {
            if( p->get_tag_bound_changed() )
            {
                Diary::d->update_tag_refs( p->get_id(), p->get_name() );
                p->unset_tag_bound_changed();
            }
        }
    }
}

// INDEXING
void
UIEntry::start_bg_jobs()
{
    if( m_thread_bg_jobs )
    {
        print_info( "Background jobs are taking longer than expected!" );
    }
    else if( m_p2entry )
    {
        PRINT_DEBUG( "<BACKGROUND JOBS STARTED>" );
        m_F_bg_jobs_queued = false;
        m_parser_word_count.set_text( m_p2entry->get_text() );
        m_thread_bg_jobs = new std::thread{
                [ this ](){ m_parser_word_count.parse( m_dispatcher ); } };
    }
}

void
UIEntry::handle_bg_jobs_completed()
{
    PRINT_DEBUG( "<BACKGROUND JOBS COMPLETED>" );

    destroy_bg_jobs_thread( false );

    if( Diary::d->is_open() ) //if not logged out in the meantime
    {
        refresh_extra_info();
    }
}

void
UIEntry::destroy_bg_jobs_thread( bool F_force )
{
    if( !m_thread_bg_jobs ) return;

    if( F_force )
        m_parser_word_count.stop();

    if( m_thread_bg_jobs->joinable() )
        m_thread_bg_jobs->join();
    delete m_thread_bg_jobs;
    m_thread_bg_jobs = nullptr;
}
