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

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

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


#include "../helpers_gtk.hpp"
#include "../fuzzyfinder.hpp"
#include "../lifeograph.hpp"
#include "po_completion_proto.hpp"
#include "widget_entrypicker.hpp"


using namespace LoG;


// COMPLETION POPOVER ==============================================================================
// this is always false for now but in the future there may be cases for true
// please always revert it to false after being done

TagPickerCompletion::TagPickerCompletion( Gtk::Widget& parent )
: PoCompletionProto( parent ), m_F_offer_new( s_F_offer_new )
{
    m_builder       = Lifeograph::create_gui( Lifeograph::SHAREDIR + "/ui/widget_entry_picker.ui" );

    auto Bx_main    = m_builder->get_widget< Gtk::Box >( "Bx_main" );
    m_LB            = m_builder->get_widget< Gtk::ListBox >( "LB_list" );
    m_Bx_new_under  = m_builder->get_widget< Gtk::Box >( "Bx_new_under" );

    m_LB->signal_row_activated().connect(
            [ this ]( Gtk::ListBoxRow* row )
            {
                DiaryElemTag* elem { m_items[ row->get_index() ] };

                if( elem ) // do nothing on the creat new line
                {
                    m_Sg_elem_activated.emit( elem );
                    popdown();
                }
            } );
    m_LB->signal_row_selected().connect(
            [ this ]( Gtk::ListBoxRow* row )
            {
                if( row )
                {
                    m_Bx_new_under->set_visible( !m_items[ row->get_index() ] );
                    ensure_selected_LB_row_in_view( m_LB, row );
                }
            } );

    if( m_F_offer_new )
    {
        s_F_offer_new = false; // must come before widget creation: blocks infinite recursion
        auto B_add      = m_builder->get_widget< Gtk::Button >( "B_add" );
        m_WTP_new_under = Gtk::Builder::get_widget_derived< WidgetTagPicker >( m_builder,
                                                                               "E_new_under" );
        m_WTP_new_under->set_name( "noncustom" );
        B_add->signal_clicked().connect( std::bind( &TagPickerCompletion::create_new_entry,
                                                    this ) );
    }

    set_child( *Bx_main );
}

void
TagPickerCompletion::set_diary( Diary* diary )
{
    m_p2diary = diary;
    if( m_WTP_new_under )
        m_WTP_new_under->set_diary( m_p2diary );
}

void
TagPickerCompletion::add_item( DiaryElemTag* entry )
{
    auto Bx_item{ Gtk::manage( new Gtk::Box( Gtk::Orientation::VERTICAL ) ) };
    auto L_item_name{ Gtk::manage( new Gtk::Label( entry->get_title_ancestral(),
                                                   Gtk::Align::START ) ) };
    auto L_item_info{ Gtk::manage( new Gtk::Label( entry->get_description(), Gtk::Align::START ) ) };

    L_item_name->set_margin_end( 15 ); // this makes it look better
    L_item_name->set_ellipsize( Pango::EllipsizeMode::START );
    L_item_name->set_use_markup( true );

    Pango::AttrList attrlist;
    auto&& attrscale{ Pango::Attribute::create_attr_scale( 0.8 ) };
    attrlist.insert( attrscale );
    L_item_info->set_attributes( attrlist );
    //L_item_info->set_sensitive( false );
    L_item_info->set_ellipsize( Pango::EllipsizeMode::END );

    Bx_item->append( *L_item_name );
    Bx_item->append( *L_item_info );

    m_LB->append( *Bx_item );

    m_items.push_back( entry );
}

void
TagPickerCompletion::add_new_item()
{
    auto&& text         { Ustring::compose(
                                    _( "Create New Entry: %1" ),
                                    STR::compose( "<b>", Glib::Markup::escape_text( m_filter ),
                                                  "</b>" ) ) };
    auto   Bx_item      { Gtk::manage( new Gtk::Box( Gtk::Orientation::VERTICAL ) ) };
    auto   L_item_name  { Gtk::manage( new Gtk::Label( text, Gtk::Align::START ) ) };
    auto   L_item_info  { Gtk::manage( new Gtk::Label( "\n\n\n", Gtk::Align::START ) ) };

    L_item_name->set_use_markup( true );

    Bx_item->append( *L_item_name );
    Bx_item->append( *L_item_info );
    // NOTE: the sole purpose of adding this blank label object is to make sure that the...
    // ...new entry options widgetry below does not obstruct the row. we use signal_idle...
    // ...to make sure that the scroll to selection happens after the resize but it fails

    m_LB->append( *Bx_item );

    m_items.push_back( nullptr );
}

void
TagPickerCompletion::clear()
{
    remove_all_children_from_LBx( m_LB );
    m_items.clear();
}

void
TagPickerCompletion::popup( const Ustring& filter )
{
    Gdk::Rectangle rect{ 0, 0,
                         get_parent()->get_width(),
                         get_parent()->get_height() };

    popup( rect, filter );
}

void
TagPickerCompletion::popup( const Gdk::Rectangle& rect, const Ustring& filter )
{
    if( !update_LB( filter ) ) // means: if empty
        return;

    set_pointing_to( rect );
    Gtk::Popover::popup();
}

struct FuncCmpTagsByRefs
{
    bool operator()( DiaryElemTag* const& l, DiaryElemTag* const& r ) const
    {
        return( l->get_reference_count() > r->get_reference_count() );
    }
};

bool
TagPickerCompletion::update_LB( const Ustring& filter )
{
    if( !m_p2diary ) return false;

    int         count { 0 };
    VecUstrings entry_names;
    VecTags     tags;

    clear();

    m_filter = filter;

    for( auto kv_tag : m_p2diary->get_tags() )
    {
        entry_names.push_back( kv_tag.second->get_name() );
        tags.push_back( kv_tag.second );
    }

    auto matches { FuzzyFinder::search( filter, entry_names ) };

    for( auto m : matches )
    {
        DiaryElemTag* item { tags[ m.idx ] };

        add_item( item );

        if( item->is_entry() && m.is_exact ) // add children of exact matches, too
        {
            Entry* entry { dynamic_cast< Entry* >( item ) };
            for( Entry* p2ec = entry->get_child_1st(); p2ec; p2ec = p2ec->get_next() )
                add_item( p2ec );
        }

        if( ++count > 20 ) break;
    }

    if( m_F_offer_new )
        add_new_item();

    if( !m_LB->get_first_child() ) // means: if empty
        return false;

    m_LB->select_row( * m_LB->get_row_at_index( 0 ) );
    return true;
}

bool
TagPickerCompletion::handle_key( guint keyval, bool F_ctrl, bool F_shift )
{
    if( !is_on_display() )
        return false;
    else if( F_ctrl )
    {
        if( keyval == GDK_KEY_Return )
        {
            create_new_entry();
            return true;
        }
    }
    else if( F_shift )
    {
        if( keyval == GDK_KEY_ISO_Left_Tab )
        {
            select_LB_item_prev( m_LB );
            return true;
        }
    }
    else
    {
        switch( keyval )
        {
            case GDK_KEY_Return:
            {
                auto e { get_selected() };
                if( e )
                {
                    m_Sg_elem_activated.emit( e );
                    popdown();
                }
                else // i.e. create new entry is secected
                    m_WTP_new_under->grab_focus();
                return true;
            }
            case GDK_KEY_Escape:
                popdown();
                return true;
            case GDK_KEY_Tab:
            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;
}

DiaryElemTag*
TagPickerCompletion::get_selected() const
{
    auto row { m_LB->get_selected_row() };
    return( row ? m_items[ row->get_index() ] : nullptr );
}

void
TagPickerCompletion::create_new_entry()
{
    auto p2entry_rel { m_WTP_new_under->get_entry() };
    bool F_parent    { Diary::guarantee_add_last_for_parent( p2entry_rel ) };

    auto entry { m_p2diary->create_entry( p2entry_rel,
                                          F_parent,
                                          Date::get_today(),
                                          m_filter,
                                          m_WTP_new_under->get_entry()
                                                ? VT::ETS::INHERIT::I
                                                : VT::ETS::NAME_ONLY::I ) };
    if( entry )
    {
        m_Sg_elem_activated.emit( entry );
        popdown();
    }
}

// ENTRY PICKER WIDGET =============================================================================
WidgetTagPicker::WidgetTagPicker( BaseObjectType* o, const Glib::RefPtr<Gtk::Builder>& b )
: EntryClear( o, b )
{
    initialize();
}

WidgetTagPicker::WidgetTagPicker( bool F_editable, const String& icon_name )
{
    TagPickerCompletion::s_F_offer_new = F_editable;

    if( not( icon_name.empty() ) )
        set_icon_from_icon_name( icon_name, Gtk::Entry::IconPosition::PRIMARY );

    initialize();
}

void
WidgetTagPicker::initialize()
{
    m_completion = new TagPickerCompletion( *this );

    if( get_icon_name( Gtk::Entry::IconPosition::PRIMARY ) == "" ) // if not set in the ui file
        set_icon_from_icon_name( "entry-16-symbolic", Gtk::Entry::IconPosition::PRIMARY );

    // does not seem to make a difference, why?
    set_icon_activatable( false, Gtk::Entry::IconPosition::PRIMARY );

    // SIGNALS
    m_completion->signal_elem_activated().connect(
            [ this ]( DiaryElemTag* t ){ handle_elem_selected( t ); } );

    // we couldn't detect enter key in on_key_press_event:
    signal_activate().connect(
            [ this ]()
            {
                if( m_completion && m_completion->is_on_display() )
                    m_completion->handle_key( GDK_KEY_Return, false, false );
            } );

    // the following is necessary to ensure timely destruction/unparenting
    signal_destroy().connect(
            [ this ]()
            {
                if( m_completion )
                {
                    delete m_completion;
                    m_completion = nullptr;
                }
            } );

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

    m_drop_target = Gtk::DropTarget::create( LoG::Entry::GValue::value_type(),
                                             Gdk::DragAction::COPY );
    m_drop_target->signal_drop().connect(
            sigc::mem_fun( *this, &WidgetTagPicker::handle_drop ), false );
    add_controller( m_drop_target );
}

void
WidgetTagPicker::set_elem( DiaryElemTag* elem )
{
    Lifeograph::START_INTERNAL_OPERATIONS();
    m_p2tag = elem;
    Gtk::Entry::set_text( elem ? elem->get_name() : "" );
    set_tooltip_markup( elem ? elem->get_title_ancestral() : "" );
    remove_css_class( "error" );
    Lifeograph::FINISH_INTERNAL_OPERATIONS();
}

/** creates a new entry when entered text doesn't correspond to one */
Entry*
WidgetTagPicker::get_entry_create( Diary* diary )
{
    if( m_p2tag && m_p2tag->is_entry() )
        return dynamic_cast< LoG::Entry* >( m_p2tag );

    if( !diary || get_text().empty() )
        return nullptr;

    return diary->create_entry( nullptr, true, Date::get_today(), get_text() );
}

void
WidgetTagPicker::on_changed()
{
    if( Lifeograph::is_internal_operations_ongoing() )
        return;

    const Ustring text_new{ get_text() };
    bool flag_ok{ false };

    // empty
    if( text_new.empty() )
    {
        m_p2tag = nullptr;
        m_completion->popdown();
        flag_ok = true;
    }
    else
    {
        m_completion->popup( text_new );

        DiaryElemTag* t { m_completion->m_F_entry_only ?
                            m_completion->get_diary()->get_entry_by_name( text_new ) :
                            m_completion->get_diary()->get_tag_by_name( text_new ) };
        if( t )
        {
            m_p2tag = t;
            flag_ok = true;
        }
        else
            m_p2tag = nullptr;
    }

    set_tooltip_markup( m_p2tag ? m_p2tag->get_title_ancestral() : "" );

    if( m_completion->m_F_entry_only )
        m_Sg_updated_entry.emit( dynamic_cast< LoG::Entry* >( m_p2tag ) );
    else
        m_Sg_updated.emit( m_p2tag );

    if( flag_ok )
        remove_css_class( "error" );
    else
        add_css_class( "error" );

    EntryClear::on_changed();
}

bool
WidgetTagPicker::on_key_press_event( guint keyval, guint, Gdk::ModifierType state )
{
    // if( ( state & ( Gdk::ModifierType::ALT_MASK|Gdk::ModifierType::SHIFT_MASK ) ) ==
    //     Gdk::ModifierType( 0 ) )
    {
        if( m_completion->handle_key( keyval, bool( state & Gdk::ModifierType::CONTROL_MASK ),
                                              bool( state & Gdk::ModifierType::SHIFT_MASK ) ) )
        {
            return true;
        }
    }
    return false;
}

void
WidgetTagPicker::handle_elem_selected( DiaryElemTag* tag )
{
    set_elem( tag );

    if( m_completion->m_F_entry_only )
        m_Sg_updated_entry.emit( dynamic_cast< LoG::Entry* >( tag ) );
    else
        m_Sg_updated.emit( tag );
}

bool
WidgetTagPicker::handle_drop( const Glib::ValueBase& value, double x, double y )
{
    if( G_VALUE_HOLDS( value.gobj(), LoG::Entry::GValue::value_type() ) )
    {
        LoG::Entry::GValue value_entry;
        value_entry.init( value.gobj() );

        handle_elem_selected( value_entry.get() );
        return true;
    }

    return false;
}

// SIMPLE POPOVER FOR SELECTING AN ELEMENT FROM A GROUP ============================================
VecDiaryElems   PoElemChooser::m_elems;
bool            PoElemChooser::m_F_para_mode;
Gtk::Popover*   PoElemChooser::m_Po { nullptr };
FuncVoidElem    PoElemChooser::handle_activate;

void
LoG::PoElemChooser::show( const SetTagsByNameID& elems,
                            std::function< void( DiaryElement* ) >&& select_handler,
                            Gtk::Widget& host,
                            const Gdk::Rectangle& rect )
{
    // clear the previsous values first:
    m_elems.clear();
    if( m_Po ) delete m_Po;
    m_Po = Gtk::make_managed< Gtk::Popover >();

    // new values:
    auto    SW          { Gtk::make_managed< Gtk::ScrolledWindow >() };
    auto    LV          { Gtk::make_managed< Gtk::ListView >() };
    auto    factory     { Gtk::SignalListItemFactory::create() };
    auto    stringlist  { Gtk::StringList::create() };
    int     size        { 0 };

    // fill the list model:
    for( auto& elem : elems )
    {
        if( ++size > 100 ) break; // limit to 100 elems

        stringlist->append( "" ); // just a placeholder TODO: 3.2: combine stringlist with m_elems
        m_elems.push_back( elem );
    }

    m_F_para_mode = ( size > 0 && m_elems[ 0 ]->get_type() == DiaryElement::ET_PARAGRAPH );

    handle_activate = select_handler;

    // set list model and selection model:
    auto selection_model { Gtk::SingleSelection::create( stringlist ) };
    selection_model->set_autoselect( false );
    // selection_model->set_can_unselect( true );
    LV->set_model( selection_model );
    LV->add_css_class( "data-table" ); // high density table
    LV->set_single_click_activate( true );
    LV->set_show_separators( true );

    factory->signal_setup().connect(
            []( const Glib::RefPtr< Gtk::ListItem >& list_item )
            {
                auto Bx       { Gtk::make_managed< Gtk::Box >( Gtk::Orientation::VERTICAL, 0 ) };
                auto L_entry  { Gtk::make_managed< Gtk::Label >() };

                L_entry->set_ellipsize( Pango::EllipsizeMode::MIDDLE );
                L_entry->set_halign( Gtk::Align::START );

                Bx->append( *L_entry );

                if( m_F_para_mode )
                {
                    auto L_para { Gtk::make_managed< Gtk::Label >() };

                    L_para->set_ellipsize( Pango::EllipsizeMode::END );
                    L_para->set_halign( Gtk::Align::START );

                    Bx->append( *L_para );
                }

                list_item->set_child( *Bx );
            } );

    factory->signal_bind().connect(
            []( const Glib::RefPtr< Gtk::ListItem >& list_item )
            {
                auto Bx       { dynamic_cast< Gtk::Box* >( list_item->get_child() ) };
                auto L_entry  { dynamic_cast< Gtk::Label* >( Bx->get_first_child() ) };
                auto elem     { m_elems[ list_item->get_position() ] };

                if( m_F_para_mode )
                {
                    auto L_para { dynamic_cast< Gtk::Label* >( L_entry->get_next_sibling() ) };
                    const auto para { dynamic_cast< Paragraph* >( elem ) };
                    L_entry->set_markup( para->m_host->get_title_ancestral() );
                    L_para->set_text( para->get_text() );
                }
                else
                {
                    const auto entry { dynamic_cast< Entry* >( elem ) };
                    L_entry->set_markup( entry->get_title_ancestral() );
                }

                // if( entry == AppWindow::p->UI_entry->get_cur_entry() ) menuitem->set_icon( Gio::Icon::create( "go-home-symbolic" ) );
            } );

    LV->set_factory( factory );
    LV->signal_activate().connect(
            []( guint i )
            {
                m_Po->hide();
                handle_activate( m_elems[ i ] );
            } );

    SW->set_propagate_natural_width( true );
    SW->set_propagate_natural_height( true );
    SW->set_child( *LV );
    m_Po->set_child( *SW );
    m_Po->signal_destroy().connect( [](){ m_Po->unparent(); } );
    m_Po->set_parent( host );
    m_Po->set_pointing_to( rect );
    m_Po->show();
}
