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

    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 <string>

#include <gtkmm/eventcontrollerlegacy.h>

#include "strings.hpp"
#include "lifeograph.hpp"
#include "python/py_bindings.hpp"
#include "dialogs/dialog_preferences.hpp"
#include "printing.hpp"
#include "app_window.hpp"


using namespace LoG;

AppWindow*        AppWindow::p                        = nullptr;
int               AppWindow::parallel_thread_count    = 1;
FuncEntriesGetter AppWindow::p2get_entries            = nullptr;

// CONSTRUCTOR
AppWindow::AppWindow( BaseObjectType* cobject, const Glib::RefPtr<Gtk::Builder>& b )
:   Gtk::ApplicationWindow( cobject )
{
    p = this;

    Cipher::init();

    Diary::d = new Diary;

    const auto processor_count = std::thread::hardware_concurrency();
    PRINT_DEBUG( "Processor_count: ", processor_count );
    parallel_thread_count = std::max( processor_count - 2 , 1u );

#ifdef _WIN32
    set_default_icon_name( "net.sourceforge.Lifeograph" );
#endif

    // CONFIGURATION
    set_default_size( Lifeograph::settings.width, Lifeograph::settings.height );
    // gtkmm4: move is not available
    // if( Lifeograph::settings.position_x != POSITION_NOTSET &&
    //     Lifeograph::settings.position_y != POSITION_NOTSET )
    //     move( Lifeograph::settings.position_x, Lifeograph::settings.position_y );
    if( Lifeograph::settings.state_maximized )
        maximize();

    // PRINTING SETUP
    m_r2print_settings = Gtk::PrintSettings::create();
    m_r2page_setup = Gtk::PageSetup::create();

    // DRAW UI
    Gtk::Button* B_info_close;

    try
    {
        auto builder{ Lifeograph::get_builder() };
        m_Bx_main       = builder->get_widget< Gtk::Box >( "Bx_main" );
        m_Hb_main       = builder->get_widget< Gtk::HeaderBar >( "Hb_main" );
        m_L_title       = builder->get_widget< Gtk::Label >( "L_title" );
        m_L_subtitle    = builder->get_widget< Gtk::Label >( "L_subtitle" );
        m_St_main       = builder->get_widget< Gtk::Stack >( "St_main" );
        m_Rv_info       = builder->get_widget< Gtk::Revealer >( "Rv_info" );
        m_L_info        = builder->get_widget< Gtk::Label >( "L_info" );
        m_B_info        = builder->get_widget< Gtk::Button >( "B_info" );
        B_info_close    = builder->get_widget< Gtk::Button >( "B_info_close" );

        UI_login = new UILogin;
    }
    catch( ... )
    {
        throw LoG::Error( "Failed to create the login view" );
    }

#ifdef _WIN32
    m_Hb_main->set_decoration_layout( ":minimize,maximize,close" );
#elif GTK_CHECK_VERSION(4, 18, 0)
    m_Hb_main->set_use_native_controls( true );
#endif

    Lifeograph::icons->diary_32 = Gdk::Pixbuf::create_from_file(
            Lifeograph::get_icon_dir() + "/diary-32.png" );

    // SIGNALS
    m_B_info->signal_clicked().connect( [ this ](){ m_info_resp_handler(); } );
    B_info_close->signal_clicked().connect( [ this ](){ close_info(); } );

    // CONTROLLERS
    auto event_controller_legacy{ Gtk::EventControllerLegacy::create() };
    event_controller_legacy->signal_event().connect(
            sigc::mem_fun( *this, &AppWindow::on_event ), false );
    add_controller( event_controller_legacy );

    // ACTIONS
    Lifeograph::p->add_action( "context-help", [ this ](){ show_contextual_help(); } )
            ->set_enabled( !Lifeograph::settings.rtflag_single_file );
    Lifeograph::p->add_action( "webpage-contribute", [ this ](){ open_webpage_contribute(); } );
    Lifeograph::p->add_action( "close_diary", [ this ](){ check_finish_diary_session( true ); } );
    m_A_close_diary_wo_save = Lifeograph::p->add_action(
            "close_diary_wo_saving",
            [ this ]() { check_finish_diary_session( false ); } );
    m_A_disable_auto_close = Lifeograph::p->add_action_bool(
            "disable_auto_close",
            [ this ](){ handle_auto_logout_toggled(); },
            false );

    Lifeograph::p->add_action( "help", sigc::ptr_fun( &Lifeograph::show_manual ) )
            ->set_enabled( !Lifeograph::settings.rtflag_single_file );

    Lifeograph::p->add_action(
            "keyboard-shortcuts",
            [ this ](){ show_contextual_help_topic( "Keyboard Shortcuts" ); } )
                ->set_enabled( !Lifeograph::settings.rtflag_single_file );

    Lifeograph::p->add_action( "preferences", [](){ DialogPreferences::create(); } )
            ->set_enabled( !Lifeograph::settings.rtflag_read_only );

    Lifeograph::p->add_action( "reload-file-scripts", [ this ](){ reload_file_based_scripts(); } )
            ->set_enabled( !Lifeograph::settings.rtflag_single_file );

    Lifeograph::p->add_action( "move_focus_to_next_panel", [ this ](){ focus_next_panel(); } );
    Lifeograph::p->add_action( "refresh_panels", [ this ](){ refresh_panels(); } );

    Lifeograph::p->set_accel_for_action( "app.context-help",                "F1" );
    Lifeograph::p->set_accel_for_action( "app.keyboard-shortcuts",          "<Shift>F1" );
    Lifeograph::p->set_accel_for_action( "app.move_focus_to_next_panel",    "<Ctrl>Tab" );
    Lifeograph::p->set_accel_for_action( "app.refresh_panels",              "F5" );
    Lifeograph::p->set_accel_for_action( "app.close_diary",                 "<Ctrl>Escape" );

    UI_login->handle_start();

    refresh_title();

    load_file_based_scripts();
}

AppWindow::~AppWindow()
{
}

bool
AppWindow::on_event( const Glib::RefPtr< const Gdk::Event >& )
{
    m_F_block_backup_on_idle = false;

    if( m_auto_close_freeze_level == 0 )
    {
        // restart
        m_seconds_remaining = Lifeograph::settings.idletime;
        m_connection_timeout.disconnect();
        m_connection_timeout = Glib::signal_timeout().connect_seconds(
                sigc::mem_fun( *this, &AppWindow::handle_idle ),
                Lifeograph::settings.idletime - LOGOUT_COUNTDOWN );

        update_auto_logout_count_down();
    }

    return false;
}

bool
AppWindow::write_lock()
{
    if( m_F_block_backup_on_idle )
    {
        PRINT_DEBUG( "Saving backup blocked due to idleness" );
    }
    else
    {
        PRINT_DEBUG( "AppWindow::write_backup()" );

        if( Diary::d->write_lock() == SUCCESS )
            m_F_block_backup_on_idle = true;
    }

    return true;
}

void
AppWindow::focus_next_panel()
{
    if( Diary::d->is_open() )
    {
        if( UI_entry->get_textview()->has_focus() )
            UI_diary->get_list()->grab_focus();
        else
            UI_entry->get_textview()->grab_focus();
    }
}

void
AppWindow::refresh_panels()
{
    Glib::signal_idle().connect_once( // yields better results when is called from scripts
            [ this ]()
            {
                if( Diary::d->is_open() )
                {
                    UI_diary->update_all_entries_filter_status();
                    UI_diary->update_entry_list();
                    UI_extra->refresh_active_panel();
                    UI_entry->refresh();
                }
                else
                    UI_login->populate_diaries();
            } );
}

void
AppWindow::fix_paned_positions_if_needed()
{
    if( m_Pd_extra->get_position() > ( Lifeograph::settings.height * 0.9 ) ||
        m_Pd_extra->get_position() < ( Lifeograph::settings.height * 0.25 ) )
        m_Pd_extra->set_position( Lifeograph::settings.height * PANEPOS_EXTRA_DEFAULT );
}

void
AppWindow::init_textview_theme_classes()
{
    add_textview_theme_class( ThemeSystem::get() );

    for( auto kv_theme : *Diary::d->get_p2themes() )
        add_textview_theme_class( kv_theme.second );
}
void
AppWindow::add_textview_theme_class( Theme* theme )
{
    auto css_provider { Gtk::CssProvider::create() };
    css_provider->load_from_data( theme->get_css_class_def() );
    m_TV_css_providers[ theme->get_css_class_name() ] = css_provider;
    Gtk::StyleProvider::add_provider_for_display( Gdk::Display::get_default(),
                                                  css_provider,
                                                  GTK_STYLE_PROVIDER_PRIORITY_APPLICATION );
}
void
AppWindow::remove_textview_theme_class( Theme* theme )
{
    auto class_name   { theme->get_css_class_name() };
    auto css_provider { m_TV_css_providers.find( class_name ) };

    if( css_provider == m_TV_css_providers.end() )
    {
        PRINT_DEBUG( "Provider cannot be found: ", class_name );
        return;
    }

    Gtk::StyleProvider::remove_provider_for_display( Gdk::Display::get_default(),
                                                     css_provider->second );

    m_TV_css_providers.erase( class_name );
}
void
AppWindow::update_textview_theme_class( Theme* theme )
{
    auto class_name   { theme->get_css_class_name() };
    auto css_provider { m_TV_css_providers.find( class_name ) };

    if( css_provider == m_TV_css_providers.end() )
    {
        PRINT_DEBUG( "Provider cannot be found: ", class_name );
        return;
    }

    css_provider->second->load_from_data( theme->get_css_class_def() );
}
void
AppWindow::clear_textview_theme_classes()
{
    for( auto kv_css_provider : m_TV_css_providers )
    {
        Gtk::StyleProvider::remove_provider_for_display( Gdk::Display::get_default(),
                                                         kv_css_provider.second );
    }

    m_TV_css_providers.clear();
}

void
AppWindow::refresh_theme_in_TVDs( const Theme* p2theme )
{
    if( UI_entry->get_entry()->get_theme() == p2theme )
        UI_entry->get_textview()->refresh_theme();

    for( auto kv_dlg : m_popup_notes )
        if( kv_dlg.second->get_textview()->get_entry()->get_theme() == p2theme )
            kv_dlg.second->get_textview()->refresh_theme();
}

void
AppWindow::show_elem( DiaryElement* elem )
{
    switch( elem->get_type() )
    {
        case DiaryElement::ET_PARAGRAPH:
            UI_entry->show( dynamic_cast< Paragraph* >( elem ) );
            break;
        case DiaryElement::ET_ENTRY:
            show_entry( dynamic_cast< Entry* >( elem ) );
            break;
        case DiaryElement::ET_CHART:
            UI_extra->show_chart( elem->get_id() );
            break;
        case DiaryElement::ET_TABLE:
            UI_extra->show_table( elem->get_id() );
            break;
        default:
            break;
    }
}

void
AppWindow::show_elem( const D::DEID& id )
{
    DiaryElement* elem{ Diary::d->get_element( id ) };

    if( elem )
        show_elem( elem );
}

void
AppWindow::show_entry( Entry* entry )
{
    Lifeograph::START_INTERNAL_OPERATIONS();

    UI_extra->set_entry( entry );
    UI_diary->show_in_list( entry );
    UI_entry->show( entry );  // needs to come last to make sure that the text editor is focused

    Lifeograph::FINISH_INTERNAL_OPERATIONS();
}

void
AppWindow::handle_dismiss_entry( Entry* p2entry )
{
    Lifeograph::START_INTERNAL_OPERATIONS();

    if( p2entry->is_popup_note() )
    {
        auto dlg { get_popup_dlg( p2entry->get_id_full() ) };
        if( dlg )
            dlg->close();
    }

    PyBindings::update_for_entry( p2entry, false );
    UI_entry->remove_entry_from_history( p2entry );
    UI_extra->remove_entry_from_search( p2entry );

    Lifeograph::FINISH_INTERNAL_OPERATIONS();
}

void
AppWindow::print()
{
    if( !Diary::d->is_open() ) // only print when logged in
        return;

    Glib::RefPtr< PrintOpr > print = PrintOpr::create();
    print->set_default_page_setup( m_r2page_setup );
    print->set_print_settings( m_r2print_settings );
    print->set_hide_comments( UI_entry->get_entry()->get_comment_style() == VT::CS::HIDDEN::I );

    freeze_auto_close();
    try
    {
        print->run( Gtk::PrintOperation::Action::PRINT_DIALOG, *AppWindow::p );
    }
    catch( const Gtk::PrintError &ex )
    {
        // See documentation for exact Gtk::PrintError error codes:
        print_error( "An error occurred while trying to run a print operation:", ex.what() );
    }
    unfreeze_auto_close();
}

void
AppWindow::go_to_prev( bool F_include_invisibles )
{
    for( Entry* entry_prev = UI_entry->get_entry()->get_prev_straight();
         entry_prev;
         entry_prev = entry_prev->get_prev_straight() )
    {
        if( F_include_invisibles || !entry_prev->is_filtered_out() )
        {
            show_entry( entry_prev );
            break;
        }
    }
}
void
AppWindow::go_to_next( bool F_include_invisibles )
{
    for( Entry* entry_next = UI_entry->get_entry()->get_next_straight();
         entry_next;
         entry_next = entry_next->get_next_straight() )
    {
        if( F_include_invisibles || !entry_next->is_filtered_out() )
        {
            show_entry( entry_next );
            break;
        }
    }
}

// CONTEXTUAL help
void
AppWindow::show_contextual_help( Widget* W )
{
    // get the device:
    auto display  { get_display() };
    auto seat     { display->get_default_seat() };
    auto device   { seat->get_pointer() };

    // get coords: mostly taken from Gtk Inspector
    double x, y, nx, ny;
    auto surface  { device->get_surface_at_position() };
    if( !surface ) return;

    auto native   { Gtk::Native::get_for_surface( surface ) };
    if( !native ) return;

    Gdk::ModifierType mt;
    native->get_surface()->get_device_position( device, x, y, mt );
    native->get_surface_transform( nx, ny );
    x -= nx;
    y -= ny;

    if( !W ) W = this;

    // pick the widget at the cursor position:
    auto widget   { W->pick( x, y, Gtk::PickFlags::INSENSITIVE ) };

    while( widget )
    {
        auto help_data { widget->get_data( CONT_HELP_KEY ) };
        if( help_data )
        {
            show_contextual_help_topic( * static_cast< String* >( help_data ) );
            return;
        }
        PRINT_DEBUG( "pos : x=", x, ", y=", y, " | W: ", G_OBJECT_TYPE_NAME( widget->gobj() ) );
        widget = widget->get_parent();
    }

    Lifeograph::show_manual();
}
void
AppWindow::show_contextual_help_topic( const String& topic )
{
    if( get_user_manual() )
    {
        DiaryElemTag* p2related_entry { m_user_manual->get_tag_by_name_fuzzy( topic ) };
        if( p2related_entry )
            open_popup_dlg( p2related_entry );
        else
            Lifeograph::show_manual();
    }
}
Diary*
AppWindow::get_user_manual()
{
    if( m_user_manual ) return m_user_manual;

    m_user_manual = new Diary;

    if( m_user_manual->set_path( Lifeograph::MANUALPATH, Diary::SPT_READ_ONLY ) != SUCCESS ||
        m_user_manual->read_header() != SUCCESS ||
        m_user_manual->read_body() != SUCCESS  )
    {
        delete m_user_manual;
        m_user_manual = nullptr;
        show_info( _( "Cannot open the user manual" ) );
    }
    else
    {
        for( auto kv_theme : *m_user_manual->get_p2themes() )
            add_textview_theme_class( kv_theme.second );
    }

    return m_user_manual;
}
void
AppWindow::set_help_topic( Gtk::Widget* p2W, const String& key )
{
    p2W->set_data( CONT_HELP_KEY, new String( key ) );
}

void
AppWindow::setup_contextual_help_for_Po( Gtk::Popover* Po )
{
    auto EC_key = Gtk::EventControllerKey::create();
    EC_key->signal_key_pressed().connect(
        [ this, Po ]( guint keyval, guint, Gdk::ModifierType state )
        {
            if( state != Gdk::ModifierType( 0 ) || keyval != GDK_KEY_F1 ) return false;

            show_contextual_help( Po );
            Po->hide();
            return true;
        },
        false );
    Po->add_controller( EC_key );
}

void
open_webpage_finish( Glib::RefPtr< Gio::AsyncResult >& result,
                     Glib::RefPtr< Gtk::UriLauncher >& launcher )
{
    try
    {
        launcher->launch_finish( result );
    }
    catch( const Glib::Error& ex )
    {
        AppWindow::p->show_info( "Opening the contribute webpage failed" );
    }
}
void
AppWindow::open_webpage_contribute()
{
    auto launcher = Gtk::UriLauncher::create(
        "https://lifeograph.sourceforge.net/doku.php?id=contribute" );
    launcher->launch( *this, sigc::bind( sigc::ptr_fun( &open_webpage_finish ), launcher ) );
}

// POPUP NOTES (AKA STICKY NOTES)
DialogPopupNote*
AppWindow::get_popup_dlg( const D::DEIDF& id )
{
    auto kv_dlg { m_popup_notes.find( id ) };
    return( kv_dlg != m_popup_notes.end() ? kv_dlg->second : nullptr );
}
void
AppWindow::open_popup_dlg( DiaryElemTag* p2tag )
{
    auto dlg_id { p2tag->is_entry() ? p2tag->get_id_full()
                                    : dynamic_cast< Paragraph* >( p2tag )->m_host->get_id_full() };
    auto dlg    { get_popup_dlg( dlg_id ) };

    if( !dlg )
    {
        dlg = DialogPopupNote::create( p2tag, p2tag->get_diary() == m_user_manual );
        m_popup_notes.emplace( dlg_id, dlg );
        if( Diary::d->is_open() ) // contextual help can be shown before login
            AppWindow::p->UI_diary->set_B_popups_visible( true );
    }
    else
    {
        dlg->present();
    }
}
void
AppWindow::remove_popup_dlg( const D::DEIDF& id )
{
    if( get_popup_dlg( id ) )
        m_popup_notes.erase( id );
}
bool
AppWindow::get_popup_notes_visible() const
{
    return( m_popup_notes.size() > 0 );
}
void
AppWindow::hide_all_popup_notes()
{
    for( auto it = m_popup_notes.begin(); it != m_popup_notes.end(); )
    {
        auto dlg { it->second };
        it = m_popup_notes.erase( it ); // needed to prevent crash
        dlg->close();
    }
    m_popup_notes.clear();
}
void
AppWindow::toggle_popup_notes_visible()
{
    if( get_popup_notes_visible() )
        hide_all_popup_notes();
    else
    {
        for( Entry* e : *Diary::d )
            if( e->is_popup_note() )
                open_popup_dlg( e );
    }
}
void
AppWindow::update_popup_note_editabilities()
{
    for( auto& kv_dlg : m_popup_notes )
        kv_dlg.second->refresh_editability();
}
void
AppWindow::update_popup_note_editability( const D::DEIDF& id )
{
    DialogPopupNote* dlg { get_popup_dlg( id ) };
    if( dlg )
        dlg->refresh_editability();
}
void
AppWindow::unpopup_active_entry()
{
    auto entries { get_entries_per_context() };

    if( entries.size() == 1 )
    {
        Entry*  entry { *entries.begin() };
        auto    dlg   { get_popup_dlg( entry->get_id_full() ) };
        if( dlg )
        {
            dlg->close();
            entry->set_popup_note( false );
        }
    }
}
void
AppWindow::add_entry_to_be_refreshed( D::DEIDF id, EditorType et )
{
    if( et == EditorType::POPUP ||
        ( et == EditorType::MAIN && m_popup_notes.find( id ) != m_popup_notes.end() ) )
    {
        m_entries_to_be_refreshed[ id.get_lo() ] = et;
    }
}
void
AppWindow::remove_entry_to_be_refreshed( D::DEID id )
{
    m_entries_to_be_refreshed.erase( id );
}
bool
AppWindow::handle_entry_need_for_refresh( D::DEID id, EditorType et )
{
    auto it { m_entries_to_be_refreshed.find( id ) };
    if( it != m_entries_to_be_refreshed.end() && it->second != et )
    {
        m_entries_to_be_refreshed.erase( it ); // consume it
        return true;
    }
    return false;
}

void
AppWindow::show_about()
{
    static Gtk::AboutDialog* s_about_dialog{ nullptr };

    if( s_about_dialog == nullptr )
    {
        s_about_dialog = Lifeograph::get_builder2()->get_widget< Gtk::AboutDialog >(
                "aboutdialog" );
        s_about_dialog->set_name( PROGRAM_NAME );
        s_about_dialog->set_version( Lifeograph::PROGRAM_VERSION_STRING );
        s_about_dialog->set_transient_for( *p );
        s_about_dialog->set_hide_on_close( true );
        // TODO: 3.2: split into its own ui file to be able to destroy
    }

    s_about_dialog->show();
}

void
AppWindow::show_info( const Ustring& text, const Ustring& button_text, const FuncVoid& handler )
{
    m_L_info->set_markup( text );
    m_B_info->set_label( button_text );
    m_B_info->set_visible( !button_text.empty() );
    m_info_resp_handler = handler;
    m_Rv_info->set_reveal_child( true );
}
void
AppWindow::close_info()
{ m_Rv_info->set_reveal_child( false ); }

// putting win32 includes in the beginning causes problems
#ifdef _WIN32
#define WIN32_LEAN_AND_MEAN
#include <windows.h>
#include <shellapi.h>
#endif
void
AppWindow::open_uri_os( Diary* p2diary, const String& uri )
{
#ifndef _WIN32
    Gio::AppInfo::launch_default_for_uri( p2diary ? p2diary->convert_rel_uri( uri ) : uri );
#else
    ShellExecuteW( NULL, L"open", PATH2( p2diary ? p2diary->convert_rel_uri( uri ) : uri ),
                   NULL, NULL, SW_SHOWNORMAL );
#endif
}

void
AppWindow::handle_undo()
{
    UI_entry->undo();
}

void
AppWindow::handle_redo()
{
    UI_entry->redo();
}

void
AppWindow::handle_login()
{
    if( m_Pd_main == nullptr ) // first login
    {
        try
        {
            // PANES
            m_Pd_main   = Lifeograph::get_builder()->get_widget< Gtk::Paned >( "P_main" );
            m_Pd_extra  = Lifeograph::get_builder()->get_widget< Gtk::Paned >( "P_second" );

            // ICONS
            auto theme { Gtk::IconTheme::get_for_display( Gdk::Display::get_default() ) };
            theme->add_search_path( Lifeograph::get_icon_dir() );

            // is loaded upfront as a Pixbuf: Lifeograph::icons->diary_32 = theme->lookup_icon( "diary-32", 32 );

            Lifeograph::icons->entry_16 =           Gdk::Pixbuf::create_from_file(
                    Lifeograph::get_icon_dir() + "/entry-16.png" );
            Lifeograph::icons->entry_32 =           Gdk::Pixbuf::create_from_file(
                    Lifeograph::get_icon_dir() + "/entry-32.png" );
            Lifeograph::icons->entry_plus_16 =      Gdk::Pixbuf::create_from_file(
                    Lifeograph::get_icon_dir() + "/entry_plus-16.png" );
            Lifeograph::icons->entry_parent_16 =    Gdk::Pixbuf::create_from_file(
                    Lifeograph::get_icon_dir() + "/entry_parent-16.png" );
            Lifeograph::icons->entry_parent_32 =    Gdk::Pixbuf::create_from_file(
                    Lifeograph::get_icon_dir() + "/entry_parent-32.png" );
            Lifeograph::icons->milestone_16 =       Gdk::Pixbuf::create_from_file(
                    Lifeograph::get_icon_dir() + "/milestone-16.png" );
            Lifeograph::icons->milestone_32 =       Gdk::Pixbuf::create_from_file(
                    Lifeograph::get_icon_dir() + "/milestone-32.png" );

            Lifeograph::icons->tag_16 =             Gdk::Pixbuf::create_from_file(
                    Lifeograph::get_icon_dir() + "/tag-16.png" );
            Lifeograph::icons->tag_32 =             Gdk::Pixbuf::create_from_file(
                    Lifeograph::get_icon_dir() + "/tag-32.png" );
            Lifeograph::icons->entry_script_16 = lookup_theme_icon_pixbuf( theme,
                                                                           "entry-script", 16 )
                    ->scale_simple( 16, 16, Gdk::InterpType::BILINEAR );
            Lifeograph::icons->entry_script_32 = lookup_theme_icon_pixbuf( theme,
                                                                           "entry-script", 32 );

            Lifeograph::icons->favorite_16 =        Gdk::Pixbuf::create_from_file(
                    Lifeograph::get_icon_dir() + "/favorite-16.png" );

            // Lifeograph::icons->todo_auto_16 =       Gdk::Pixbuf::create_from_file(
            //         Lifeograph::get_icon_dir() + "/todo_auto-16.png" );
            // Lifeograph::icons->todo_auto_32 =       Gdk::Pixbuf::create_from_file(
            //         Lifeograph::get_icon_dir() + "/todo_auto-32.png" );
            Lifeograph::icons->todo_open_16 =       Gdk::Pixbuf::create_from_file(
                    Lifeograph::get_icon_dir() + "/todo_open-16.png" );
            Lifeograph::icons->todo_open_32 =       Gdk::Pixbuf::create_from_file(
                    Lifeograph::get_icon_dir() + "/todo_open-32.png" );
            Lifeograph::icons->todo_progressed_16 = Gdk::Pixbuf::create_from_file(
                    Lifeograph::get_icon_dir() + "/todo_progressed-16.png" );
            Lifeograph::icons->todo_progressed_32 = Gdk::Pixbuf::create_from_file(
                    Lifeograph::get_icon_dir() + "/todo_progressed-32.png" );
            Lifeograph::icons->todo_done_16 =       Gdk::Pixbuf::create_from_file(
                    Lifeograph::get_icon_dir() + "/todo_done-16.png" );
            Lifeograph::icons->todo_done_32 =       Gdk::Pixbuf::create_from_file(
                    Lifeograph::get_icon_dir() + "/todo_done-32.png" );
            Lifeograph::icons->todo_canceled_16 =   Gdk::Pixbuf::create_from_file(
                    Lifeograph::get_icon_dir() + "/todo_canceled-16.png" );
            Lifeograph::icons->todo_canceled_32 =   Gdk::Pixbuf::create_from_file(
                    Lifeograph::get_icon_dir() + "/todo_canceled-32.png" );

            Lifeograph::icons->trash_16 = lookup_theme_icon_pixbuf(
                    theme, "user-trash-symbolic", 16 );

            Lifeograph::icons->filter = lookup_theme_icon_pixbuf( theme, "filter-16-symbolic", 16 );
            Lifeograph::icons->filter_none = lookup_theme_icon_pixbuf(
                    theme, "filter-none-symbolic", 16 );
            Lifeograph::icons->table = lookup_theme_icon_pixbuf( theme, "table-16-symbolic", 16 );
            Lifeograph::icons->chart = lookup_theme_icon_pixbuf( theme, "chart-16-symbolic", 16 );

            Lifeograph::icons->map_point = lookup_theme_icon_pixbuf( theme, "map-point", 16 );

            Lifeograph::icons->go_prev = lookup_theme_icon_pixbuf( theme, "go-previous-symbolic",
                                                                   16 );
            Lifeograph::icons->go_next = lookup_theme_icon_pixbuf( theme, "go-next-symbolic", 16 );
            Lifeograph::icons->go_home = lookup_theme_icon_pixbuf( theme, "go-home-symbolic", 16 );

            Lifeograph::icons->comment = lookup_theme_icon_pixbuf( theme, "comment", 16 );

            Lifeograph::icons->lock = lookup_theme_icon_pixbuf(
                    theme, "changes-prevent-symbolic", 16 );

            // PANELS & VIEWS
            UI_diary = new UIDiary;
            UI_extra = new UIExtra;
            UI_entry = new UIEntry;
        }
        catch( std::exception &ex )
        {
            throw LoG::Error( ex.what() );
        }

        // GEOMETRY
        int width  { Lifeograph::settings.rtflag_open_directly ? Lifeograph::settings.width
                                                               : get_width() };
        int height { Lifeograph::settings.rtflag_open_directly ? Lifeograph::settings.height
                                                               : get_height() };
        // NOTE: get_width() and get_height() does not work in help mode (direct opening)
        m_Pd_main->set_position( Lifeograph::settings.position_paned_main * width );
        m_Pd_extra->set_position( Lifeograph::settings.position_paned_extra * height );

        Diary::d->m_dispatcher_postread_operations.connect(
                [ this ]()
                {
                    UI_diary->handle_diary_ready();
                    UI_extra->handle_diary_ready();
                    UI_entry->get_textview()->refresh_entry();
                } );

        // ACTIONS
        Lifeograph::p->add_action( "undo", [ this ](){ handle_undo(); } );
        Lifeograph::p->add_action( "redo", [ this ](){ handle_redo(); } );
        Lifeograph::p->add_action( "toggle_popups", [ this ](){ toggle_popup_notes_visible(); } );

        Lifeograph::p->set_accel_for_action( "app.undo",          "<Ctrl>Z" );
        Lifeograph::p->set_accel_for_action( "app.redo",          "<Ctrl><Shift>Z" );
        Lifeograph::p->set_accel_for_action( "app.toggle_popups", "<Ctrl>F12" );
        // Lifeograph::p->add_action_with_parameter(
        //         "show_elem",
        //         VInt::variant_type(),
	       //      []( const Glib::VariantBase& value )
	       //      {
        //             const DEID id{ Glib::VariantBase::cast_dynamic< VInt >( value ).get() };
        //             AppWindow::p->show_elem( id );
        //         } );
    }

    PyBindings::update_scripts_in_entries( false ); // update all scripts in entries

    init_textview_theme_classes();

    m_Rv_info->set_reveal_child( false );
    m_St_main->set_visible_child( *m_Pd_main );

    m_A_close_diary_wo_save->set_enabled( false );
    update_toggle_auto_close_gui( true );
}

void
AppWindow::handle_edit_enabled()
{
    m_connection_backup = Glib::signal_timeout().connect_seconds(
            sigc::mem_fun( *this, &AppWindow::write_lock ), BACKUP_INTERVAL );

    m_A_close_diary_wo_save->set_enabled( true );

    update_popup_note_editabilities();

    DiaryElemTag::s_F_edit_dates_updateable = true;
}

void
AppWindow::handle_logout()
{
    clear_textview_theme_classes();

    m_entries_to_be_refreshed.clear();

    if( Diary::d->is_logged_time_out() )
        show_info( _( STRING::ENTER_PASSWORD_TIMEOUT ) );
    else
        close_info();
}

// AUTO LOGOUT SYSTEM
bool
AppWindow::handle_idle()
{
    if( m_auto_close_freeze_level > 0 )
        return false;

    if( m_seconds_remaining > LOGOUT_COUNTDOWN )
        m_seconds_remaining = LOGOUT_COUNTDOWN;
    else
        m_seconds_remaining--;

    if( m_seconds_remaining > 0 )
    {
        m_connection_timeout.disconnect();
        m_connection_timeout = Glib::signal_timeout().connect_seconds(
                sigc::mem_fun( *this, &AppWindow::handle_idle ), 1 );

        update_auto_logout_count_down();
    }
    else
    {
        update_auto_logout_count_down();

        check_finish_diary_session( true );
        Diary::d->set_timed_out();
    }

    return false;
}

void
AppWindow::freeze_auto_close()
{
    if( m_auto_close_freeze_level == 0 )
    {
        m_connection_timeout.disconnect();
        m_seconds_remaining = Lifeograph::settings.idletime;
        update_auto_logout_count_down();
    }

    m_auto_close_freeze_level++;
}

void
AppWindow::unfreeze_auto_close()
{
    if( Lifeograph::settings.idletime > 0 && Diary::d->is_encrypted() &&
        m_auto_close_freeze_level == 1 )
    {
        m_connection_timeout = Glib::signal_timeout().connect_seconds(
                sigc::mem_fun( *this, &AppWindow::handle_idle ),
                Lifeograph::settings.idletime - LOGOUT_COUNTDOWN );
    }

    m_auto_close_freeze_level--;
}

void
AppWindow::update_auto_logout_count_down()
{
    static Gtk::Window* W_count_down( nullptr );
    static Gtk::Label* L_count_down( nullptr );

    if( m_seconds_remaining > 0  && m_seconds_remaining <= LOGOUT_COUNTDOWN )
    {
        if( !W_count_down )
        {
            auto builder { Lifeograph::create_gui( Lifeograph::SHAREDIR + "/ui/auto_logout.ui" ) };
            W_count_down = builder->get_widget< Gtk::Window >( "W_auto_logout" );
            L_count_down = builder->get_widget< Gtk::Label >( "L_auto_logout" );

            W_count_down->set_transient_for( *this );
        }

        L_count_down->set_label( Glib::ustring::compose(
                _( "%1 SECONDS TO CLOSE" ), m_seconds_remaining ) );

        W_count_down->show();
    }
    else if( W_count_down )
        W_count_down->hide();
}

void
AppWindow::handle_auto_logout_toggled()
{
    if( is_auto_close_in_use() )
        freeze_auto_close();
    else
        unfreeze_auto_close();

    update_toggle_auto_close_gui(); // the action's state does not change automatically
}

void
AppWindow::update_toggle_auto_close_gui( bool flag_availabilities_too )
{
    if( flag_availabilities_too )
    {
        m_auto_close_freeze_level =
                ( Lifeograph::settings.idletime && Diary::d->is_encrypted() ) ? 0 : 1;

        m_A_disable_auto_close->set_enabled( !m_auto_close_freeze_level );
    }

    m_A_disable_auto_close->change_state( ! is_auto_close_in_use() );
}

// LOG OUT
bool
AppWindow::check_finish_diary_session( bool opt_save )
{
    // hide all popup notes in advance to get a clean working space for logout attempt:
    hide_all_popup_notes();

    // should be reset to prevent logging in again:
    Lifeograph::settings.rtflag_open_directly = false;

    if( Diary::d->is_in_edit_mode() )
    {
        if( opt_save )
        {
            if( Diary::d->write() != SUCCESS )
            {
                String path { Diary::d->get_uri_unsaved() };

                // try to write the backup
                if( Diary::d->write( path ) != SUCCESS )
                {
                    path = ( Glib::get_tmp_dir() + "/" + Diary::d->get_name() );
                    Diary::d->write( path );
                }

                finish_diary_session();

                show_info( STR::compose(
                        _( "Diary couldn't be saved. A rescue copy is saved at: " ), path ) );
                // TODO: 3.2: add an open folder button

                return false;
            }
        }
        else
        {
            DialogMessage::init( AppWindow::p,
                                 _( "Are you sure you want to close the diary without saving?" ) )
                        ->add_extra_info(
                                Ustring::compose(
                                        _( "Your changes will be backed up in %1. "
                                           "If you exit normally, your diary is saved "
                                           "automatically." ),
                                           "<b>" + Diary::d->get_uri_unsaved() + "</b>" ),
                                true )
                        ->add_button(   _( "Close _without Saving" ),
                                        [ & ]()
                                        {
                                            Diary::d->write( Diary::d->get_uri_unsaved() );
                                            finish_diary_session();
                                        },
                                        "destructive-action" )
                        ->show();

            return false; // in this mode return value is not used anyway
        }
    }

    // below code is not executed when opt_save == false:
    // positions:
    Lifeograph::settings.position_paned_main = double( m_Pd_main->get_position() ) / get_width();
    Lifeograph::settings.position_paned_extra = double( m_Pd_extra->get_position() ) / get_height();

    finish_diary_session();
    return true;
}

void
AppWindow::finish_diary_session()
{
    if( Diary::d->is_in_edit_mode() )
        m_connection_backup.disconnect();

    m_connection_timeout.disconnect();

    PyBindings::remove_entry_based_scripts();

    Lifeograph::signal_close_diary().emit(); // only for DialogEvent
    Lifeograph::p->downgrade_cb_entry_text();

    Lifeograph::START_INTERNAL_OPERATIONS();

    UI_diary->handle_logout();
    UI_entry->handle_logout();
    UI_extra->handle_logout();
    this->handle_logout();

    Diary::d->remove_lock_if_necessary();
    Diary::d->clear();
    DiaryElemTag::s_F_edit_dates_updateable = false;

    UI_login->handle_logout();
    m_St_main->set_visible_child( "login" );

    Lifeograph::FINISH_INTERNAL_OPERATIONS();

    m_auto_close_freeze_level = 1;

    refresh_title();
}

void
AppWindow::confirm_dismiss_element( const Ustring& name,
                                    const FuncVoid& dismiss_handler,
                                    Gtk::Widget* W_extra )
{
    DialogMessage* dlg {
            DialogMessage::init( AppWindow::p,
                                 Ustring::compose( _( "Are you sure, you want to dismiss %1?" ),
                                                   Ustring::compose( "\"%1\"", name ) ) ) };

    if( W_extra ) dlg->add_extra_widget( W_extra ); // must come before add_extra_info

    dlg->add_extra_info( _( "This operation cannot be undone!" ) )
       ->add_button( _( "_Dismiss" ), dismiss_handler, "destructive-action" )->show();
}

void
AppWindow::refresh_title()
{
    if( Diary::d->is_open() )
        set_title( STR::compose( PROGRAM_NAME, " - ", Diary::d->get_name() ) );
        // in this case the headerbar title is set by entry view
    else
    {
        set_title( PROGRAM_NAME );
        set_subtitle( "" );
    }
}

void
AppWindow::login()
{
    Lifeograph::START_INTERNAL_OPERATIONS();

    this->handle_login();       // must come first
    UI_login->handle_login();
    UI_diary->handle_login();
    UI_extra->handle_login();
    UI_entry->handle_login();   // must come last

    show_entry( Diary::d->get_startup_entry() );

    Lifeograph::FINISH_INTERNAL_OPERATIONS();

    // NOTE: LOGGED_IN flag used to be set here, now it is set much earlier
}
