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

    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/>.

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


#ifndef LIFEOGRAPH_WIDGETPICKER_HEADER
#define LIFEOGRAPH_WIDGETPICKER_HEADER


#include <gtkmm.h>

#include "../lifeograph.hpp"
#include "src/helpers_gtk.hpp"


namespace LoG
{

// WIDGET FOR SELECTING FROM DEFINED FILTERS IN THE DIARY ==========================================
template< class T >
class WidgetPicker : public Gtk::MenuButton
{
    public:
        WidgetPicker( BaseObjectType* o, const Glib::RefPtr< Gtk::Builder >& )
        : Gtk::MenuButton( o ) { init(); }
        WidgetPicker()
        { init(); }

        void
        set_map( MapUstringDiaryElem< T >* p2map,
                 MapUstringDiaryElem< T >* p2map_stock,
                 T* const *                p2p2active )
        {
            m_p2map = p2map;
            m_p2map_stock = p2map_stock;
            m_p2p2active = p2p2active;
        }

        void                        set_filter_slot( const sigc::slot< bool, T* >& handler )
        {
            handler_filter = handler;
            m_F_filter_slot_active = true;
        }

        void                        set_text( const Ustring& text )
        { m_L_name->set_text( text ); }

        void                        set_clearable( bool F_clearable )
        { m_F_clearable = F_clearable; }

        void
        set_show_rename_button( bool F_show_edit )
        { m_F_show_edit = F_show_edit; }
        void
        set_show_add_button( bool F_show_add )
        { m_F_show_add = F_show_add; }

        void                        set_select_only( bool F_select_only )
        {
            m_F_select_only = F_select_only;

            m_B_add->set_visible( m_F_show_add && !F_select_only );
        }

        void                        set_modal( bool F_modal )
        { m_Po->set_autohide( F_modal ); }

        void
        set_drag_source()
        {
            auto                drag_source { Gtk::DragSource::create() };
            typename T::GValue  value;

            value.init( value.value_type() );
            drag_source->set_content( Gdk::ContentProvider::create( value ) );
            add_controller( drag_source );
        }

        bool
        update_list()
        {
            if( !m_p2map )
                return false;

            bool         F_exact_match  { false };
            const auto&& filter_text    { STR::make_searchable( m_E_search->get_text() ) };

            m_F_internal++;

            m_F_show_dismiss = ( m_p2map->size() > 1 ||
                                 ( m_p2map_stock && !m_p2map_stock->empty() ) );

            clear_list();

            if( m_F_clearable && filter_text.empty() )
                add_none_item_to_list();

            add_map_to_list( m_p2map, filter_text, F_exact_match );

            if( m_p2map_stock )
                add_map_to_list( m_p2map_stock, filter_text, F_exact_match );

            center_selected_LB_row( m_LB );

            m_F_internal--;

            return F_exact_match;
        }

        void                        update_active_item( bool F_hide_Po )
        {
            if( *m_p2p2active )
            {
                set_text( ( *m_p2p2active )->get_name() );
                m_I_icon->set( ( *m_p2p2active )->get_icon() );
            }
            else
            {
                set_text( "" );
            }

            if( F_hide_Po )
                m_Po->hide();
        }

        sigc::signal< void( T* ) >  signal_active_changed()
        { return m_Sg_active_changed; }
        SignalVoid                  signal_add()
        { return m_Sg_add; }
        sigc::signal< void( T* ) >  signal_duplicate()
        { return m_Sg_duplicate; }
        sigc::signal< void( T* ) >  signal_dismiss()
        { return m_Sg_dismiss; }
        sigc::signal< bool( T*, const Ustring& ) >
                                    signal_rename()
        { return m_Sg_rename; }

    protected:
        void
        init()
        {
            Gtk::Box*            Bx_button;

            m_builder       = Lifeograph::create_gui( Lifeograph::SHAREDIR +
                                                      "/ui/widget_picker.ui" );

            m_Po            = m_builder->get_widget< Gtk::Popover >( "Po_picker" );
            m_LB            = m_builder->get_widget< Gtk::ListBox >( "LB_list" );
            m_B_add         = m_builder->get_widget< Gtk::Button >( "B_add" );
            m_E_search      = m_builder->get_widget< Gtk::SearchEntry >( "E_search" );
            Bx_button       = m_builder->get_widget< Gtk::Box >( "Bx_button" );
            m_L_name        = m_builder->get_widget< Gtk::Label >( "L_name" );
            m_I_icon        = m_builder->get_widget< Gtk::Image >( "I_icon" );

            set_child( *Bx_button );

            set_popover( *m_Po );
            set_create_popup_func(
                    [ this ]()
                    {
                        m_p2L_rename = nullptr;
                        m_p2E_rename = nullptr;

                        m_F_internal++;
                        m_E_search->set_text( "" );
                        update_list();
                        m_E_search->grab_focus();
                        m_F_internal--;
                    } );

            m_B_add->set_visible( m_F_show_add && !m_F_select_only );

            m_LB->signal_row_selected().connect(
                    [ this ]( Gtk::ListBoxRow* row )
                    {
                        ensure_selected_LB_row_in_view( m_LB, row );

                        if( m_F_internal ) return;
                        handle_item_rename_finish();
                        m_p2L_rename = nullptr;
                        m_p2E_rename = nullptr;
                    } );

            m_LB->signal_row_activated().connect(
                    [ this ]( Gtk::ListBoxRow* )
                    {
                        if( m_F_internal ) return;
                        m_Sg_active_changed.emit( get_selected_from_list() );
                        update_active_item( true );
                    } );

            m_B_add->signal_clicked().connect(
                    [ this ]()
                    {
                        if( !m_p2p2active ) return;
                        m_Sg_add.emit();
                        update_active_item( false );
                        update_list();
                        auto data { get_selected_row_data() }; // newly added row
                        handle_item_rename_start( data->p2L, data->p2E );
                    } );

            m_E_search->signal_changed().connect(
                    [ this ]()
                    {
                        if( m_F_internal ) return;
                        update_list();
                    } );
            m_E_search->signal_activate().connect(
                    [ this ]() { handle_key( GDK_KEY_Return ); } );

            // CONTROLLERS
            auto controller_key{ Gtk::EventControllerKey::create() };
            controller_key->signal_key_pressed().connect(
                    sigc::mem_fun( *this, &WidgetPicker::on_key_press_event ), false );
            add_controller( controller_key );

            auto controller_key_entry { Gtk::EventControllerKey::create() };
            controller_key_entry->signal_key_pressed().connect(
                    sigc::mem_fun( *this, &WidgetPicker::on_key_press_event ), false );
            m_E_search->add_controller( controller_key_entry );
        }

        Gtk::Button*                create_button( const String& icon_name )
        {
            auto button { Gtk::manage( new Gtk::Button ) };
            auto icon   { Gtk::manage( new Gtk::Image ) };

            icon->set_from_icon_name( icon_name );
            button->set_child( *icon );
            //button->set_relief( Gtk::RELIEF_NONE );

            return button;
        }

        void
        add_map_to_list( const MapUstringDiaryElem< T >* p2map, const Ustring& filter_text,
                         bool& F_exact_match )
        {
            for( auto& kv : *p2map )
            {
                T* p2elem { kv.second };

                if( m_F_filter_slot_active && !handler_filter( p2elem ) )
                    continue;

                if( !filter_text.empty() &&
                   STR::make_searchable( p2elem->get_name() ).find( filter_text ) == Ustring::npos )
                    continue;

//                if( m_F_select_only == false || m_F_ignore_entered_text ||
//                    STR::make_searchable( kv.first ).find( lowercase_filter ) != Ustring::npos )
                add_item_to_list( p2elem );
                if( !F_exact_match && p2elem == *m_p2p2active )
                {
                    m_F_internal++;
                    m_LB->select_row( *m_LB->get_row_at_index( m_items.size() - 1 ) );
                    F_exact_match = true;
                    m_F_internal--;
                }
            }
        }

        Gtk::Label*
        create_item_label( const Ustring& name, bool F_stock )
        {
            auto L_item_name { Gtk::manage( new Gtk::Label( F_stock ? "<i>&lt;" + name + "&gt;</i>"
                                                                    : name,
                                                            Gtk::Align::START ) ) };
            if( F_stock )
                L_item_name->set_use_markup( true );
            L_item_name->set_margin_end( 15 ); // this makes it look better
            L_item_name->set_ellipsize( Pango::EllipsizeMode::END );
            L_item_name->set_hexpand( true );
            return L_item_name;
        }

        void
        add_rename_finish_handlers( T* p2elem, Gtk::Entry* p2E )
        {
            p2E->signal_activate().connect(
                    [ this, p2elem, p2E ]()
                    {
                        if( handle_item_renamed( p2elem, p2E ) )
                            handle_item_rename_finish();
                    } );

            // create a key event controller
            auto key_controller { Gtk::EventControllerKey::create() };

            // connect to the key-pressed signal
            key_controller->signal_key_pressed().connect(
                    [ this ]( guint keyval, guint keycode, Gdk::ModifierType state ) -> bool
                    {
                        if( state == Gdk::ModifierType( 0 ) && ( keyval == GDK_KEY_Escape ) )
                        {
                            handle_item_rename_finish();
                            return true; // event handled, stop propagation
                        }
                        return false; // let other keys propagate
                    },
                    false ); // call before default handler

            p2E->add_controller( key_controller );
        }

        void
        add_item_to_list( T* p2elem )
        {
            auto Bx_item      { Gtk::manage( new Gtk::Box( Gtk::Orientation::HORIZONTAL, 0 ) ) };
            auto L_item_name  { create_item_label( p2elem->get_name(), p2elem->is_stock() ) };
            Gtk::Entry* E_item_name { nullptr };

            Bx_item->append( *L_item_name );

            m_LB->append( *Bx_item );

            if( !m_F_select_only )
            {
                auto&& B_dupli{ create_button( "edit-copy-symbolic" ) };
                B_dupli->set_tooltip_text( _( "Duplicate" ) );
                B_dupli->set_has_frame( false );
                Bx_item->append( *B_dupli );

                B_dupli->signal_clicked().connect(
                        sigc::bind( sigc::mem_fun( *this, &WidgetPicker::handle_item_duplicate ),
                                    p2elem ) );

                if( m_F_show_edit && !p2elem->is_stock() )
                {
                    auto&& B_rename{ create_button( "document-edit-symbolic" ) };

                    E_item_name = Gtk::make_managed< Gtk::Entry >();

                    E_item_name->hide();
                    E_item_name->set_text( p2elem->get_name() );
                    E_item_name->set_hexpand( true );
                    E_item_name->set_margin_end( 5 ); // this makes it look better

                    B_rename->set_tooltip_text( _( "Rename" ) );
                    B_rename->set_has_frame( false );
                    Bx_item->append( *B_rename );
                    B_rename->signal_clicked().connect(
                            sigc::bind( sigc::mem_fun( *this, &WidgetPicker::toggle_item_rename ),
                                        L_item_name, E_item_name ) );
                    Bx_item->prepend( *E_item_name );
                    add_rename_finish_handlers( p2elem, E_item_name );
                }

                if( m_F_show_dismiss && !p2elem->is_stock() )
                {
                    auto&& B_dismiss{ create_button( "edit-delete-symbolic" ) };
                    B_dismiss->set_tooltip_text( _( "Dismiss" ) );
                    B_dismiss->set_has_frame( false );
                    Bx_item->append( *B_dismiss );
                    B_dismiss->signal_clicked().connect(
                            sigc::bind( sigc::mem_fun( *this, &WidgetPicker::handle_item_remove ),
                                        p2elem ) );
                }
            }

            m_items.push_back( { p2elem, L_item_name, E_item_name } );
        }
        void                        add_none_item_to_list()
        {
            m_LB->append( *create_item_label( get_sstr_i( CSTR::NONE ), true ) );
            m_items.push_back( { nullptr, nullptr, nullptr } );
        }
        void                        clear_list()
        {
            remove_all_children_from_LBx( m_LB );
            m_items.clear();
        }

        void
        handle_item_duplicate( T* p2elem )
        {
            handle_item_rename_finish();
            m_Sg_duplicate.emit( p2elem );
            update_active_item( false );
            update_list();
        }

        void
        handle_item_rename_start( Gtk::Label* p2L, Gtk::Entry* p2E )
        {
            handle_item_rename_finish();

            m_p2L_rename = p2L;
            m_p2E_rename = p2E;
            p2L->hide();
            p2E->show();
            p2E->grab_focus();
        }

        void
        handle_item_rename_finish()
        {
            if( m_p2L_rename ) // safety check
            {
                m_p2E_rename->hide();
                m_p2L_rename->show();
                m_E_search->grab_focus();

                m_p2L_rename = nullptr;
                m_p2E_rename = nullptr;
            }
        }

        void
        toggle_item_rename( Gtk::Label* p2L, Gtk::Entry* p2E )
        {
            if( p2L != m_p2L_rename )
                handle_item_rename_start( p2L, p2E );
            else
                handle_item_rename_finish();
        }

        bool
        handle_item_renamed( T* p2elem, Gtk::Entry* p2E_name )
        {
            const Ustring& name_new { p2E_name->get_text() };

            if( m_Sg_rename.emit( p2elem, name_new ) )
            {
                m_p2L_rename->set_text( name_new );
                m_p2E_rename->remove_css_class( "error" );
                if( p2elem == *m_p2p2active )
                    m_L_name->set_text( name_new );
                return true;
            }
            else
                m_p2E_rename->add_css_class( "error" );

            return false;
        }

        void
        handle_item_remove( T* p2elem )
        {
            handle_item_rename_finish();
            m_Sg_dismiss.emit( p2elem );
            update_active_item( false );
            update_list();
        }

        bool
        on_key_press_event( guint keyval, guint, Gdk::ModifierType state )
        {
            if( state == Gdk::ModifierType( 0 ) )
            {
                if( m_Po->is_visible() && handle_key( keyval ) )
                    return true;
            }
            return false;
        }

        T*
        get_selected_from_list()
        {
            auto&& row { m_LB->get_selected_row() };
            return( row ? m_items[ row->get_index() ].p2elem : nullptr );
        }

        bool                        handle_key( guint keyval )
        {
            switch( keyval )
            {
                case GDK_KEY_Return:
                    m_Sg_active_changed.emit( get_selected_from_list() );
                    update_active_item( false );
                    return true;
                case GDK_KEY_Escape:
                    m_Po->hide();
                    return true;
                case GDK_KEY_F2:
                {
                    auto data { get_selected_row_data() };
                    if( data && data->p2E ) // data->p2E == not stock
                        toggle_item_rename( data->p2L, data->p2E );
                    return true;
                }
                case GDK_KEY_Down:
                    select_LB_item_next( m_LB );
                    return true;
                case GDK_KEY_Up:
                    select_LB_item_prev( m_LB );
                    return true;
            }

            return false;
        }

        struct ItemData
        {
            T*          p2elem  { nullptr };
            Gtk::Label* p2L     { nullptr };
            Gtk::Entry* p2E     { nullptr };
        };

        const ItemData*
        get_selected_row_data()
        {
            auto row { m_LB->get_selected_row() };
            return( row ? &m_items[ row->get_index() ] : nullptr );
        }

        Glib::RefPtr< Gtk::Builder >
                                    m_builder;
        Gtk::Popover*               m_Po;
        Gtk::ListBox*               m_LB;
        Gtk::Button*                m_B_add;
        Gtk::SearchEntry*           m_E_search;
        Gtk::Image*                 m_I_icon;
        Gtk::Label*                 m_L_name;
        Gtk::Label*                 m_p2L_rename;
        Gtk::Entry*                 m_p2E_rename;

        std::vector< ItemData >     m_items;
        std::function< bool( T* ) > handler_filter;

        sigc::signal< void( T* ) >  m_Sg_active_changed;
        SignalVoid                  m_Sg_add;
        sigc::signal< void( T* ) >  m_Sg_duplicate;
        sigc::signal< void( T* ) >  m_Sg_dismiss;
        sigc::signal< bool( T*, const Ustring& ) >
                                    m_Sg_rename;

        const MapUstringDiaryElem< T >*
                                    m_p2map                 { nullptr };
        const MapUstringDiaryElem< T >*
                                    m_p2map_stock           { nullptr };
        const T* const *            m_p2p2active            { nullptr };

        int                         m_F_internal            { 0 };
        bool                        m_F_clearable           { false };
        bool                        m_F_show_edit           { false };
        bool                        m_F_show_dismiss        { false };
        bool                        m_F_show_add            { true };
        bool                        m_F_select_only         { false };
        bool                        m_F_ignore_entered_text { false };
        bool                        m_F_filter_slot_active  { false };
};

} // end of namespace LoG

#endif
