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

    Copyright (C) 2007-2018 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 <cmath>
#include <cairomm/context.h>

#include "helpers.hpp"
#include "lifeograph.hpp"
#include "app_window.hpp"
#include "widget_entrytags.hpp"


using namespace LIFEO;


// TAG WIDGET ======================================================================================
WidgetTagEdit::WidgetTagEdit( BaseObjectType* cobject, const Glib::RefPtr<Gtk::Builder>& b )
:   EntryClear( cobject, b ), m_completion( Gtk::EntryCompletion::create() )
{
    m_completion->set_popup_completion( true );
    m_completion->set_popup_single_match( true );
    m_completion->set_match_func( sigc::mem_fun( *this, &WidgetTagEdit::compare_text ) );

    m_liststore = Gtk::ListStore::create( colrec );
    m_completion->set_model( m_liststore );
    m_completion->set_text_column( colrec.name );

    set_completion( m_completion );

    signal_event_after().connect( sigc::mem_fun( this, &WidgetTagEdit::handle_event_after) );
}

bool
WidgetTagEdit::compare_text( const Ustring& text, const Gtk::TreeModel::const_iterator& iter )
{
    Ustring tagname = iter->get_value( colrec.name );
    if( text.size() == 1 )
        return( Glib::Unicode::tolower( tagname[ 0 ] ) ==
                Glib::Unicode::tolower( text[ 0 ] ) );
    else
        return( tagname.lowercase().find( text.lowercase() ) != std::string::npos );
}

void
WidgetTagEdit::set_entry( LIFEO::Entry* entry )
{
    m_ptr2entry = entry;
}

void
WidgetTagEdit::populate( const TagSet* set )
{
    m_liststore->clear();

    Gtk::TreeRow row;

    for( Tag* tag : *set )
    {
        row = *( m_liststore->append() );
        row[ colrec.name ] = Tag::escape_name( tag->get_name() );
    }
}

void
WidgetTagEdit::populate()
{
    m_liststore->clear();

    Gtk::TreeRow row;

    for( auto& kv_tag : *Diary::d->get_tags() )
    {
        row = *( m_liststore->append() );
        row[ colrec.name ] = Tag::escape_name( kv_tag.second->get_name() );
    }
}

void
WidgetTagEdit::on_changed()
{
    static bool flag_eq_append_in_progress = false;

    EntryClear::on_changed();

    if( flag_eq_append_in_progress )
        return;

    Ustring text_new{ get_text() };
    m_nav = NameAndValue::parse( text_new );

    // empty
    if( !( m_nav.status & NameAndValue::HAS_NAME ) )
    {
        m_signal_updated.emit( m_tag_op_cur = TO_NONE, nullptr );
    }
    else
    {
        Tag* tag{ Diary::d->get_tags()->get_tag( m_nav.name ) };
        if( tag == nullptr )
        {
            if( m_nav.status & NameAndValue::HAS_VALUE )
                m_signal_updated.emit( m_tag_op_cur = TO_CREATE_CUMULATIVE, nullptr );
            else
                m_signal_updated.emit( m_tag_op_cur = TO_CREATE_BOOLEAN, nullptr );
        }
        else if( !m_ptr2entry ) // for generic use case
            m_tag_op_cur = TO_ADD;
        else if( tag->is_boolean() && m_nav.value != 1 )
            m_signal_updated.emit( m_tag_op_cur = TO_INVALID, nullptr );
        else
        {
            if( m_ptr2entry->get_tags().check_for_member( tag ) )
            {
                if( tag->get_value( m_ptr2entry ) != m_nav.value )
                    m_signal_updated.emit( m_tag_op_cur = TO_CHANGE_VALUE, tag );
                else
                    m_signal_updated.emit( m_tag_op_cur = TO_REMOVE, tag );
            }
            else
            {
                m_signal_updated.emit( m_tag_op_cur = TO_ADD, tag );
            }

            if( !( m_nav.status & NameAndValue::HAS_EQUAL ) && !tag->is_boolean() &&
                ( text_new + '=' ) != m_text_last )
            {
                text_new += '=';
                flag_eq_append_in_progress = true;
                EntryClear::set_text( text_new );
                m_flag_append_op = true;
                flag_eq_append_in_progress = false;
            }
        }
    }

    if( m_tag_op_cur == TO_INVALID )
        set_icon_from_icon_name( "dialog-warning-symbolic" );
    else
        unset_icon();

    m_text_last = text_new;
}

// this is necassary for set_position to work. within the same event handler it does not work:
void
WidgetTagEdit::handle_event_after( GdkEvent* event )
{
    if( m_flag_append_op )
    {
        set_position( -1 );
        m_flag_append_op = false;
    }
}

// TAG LIST WIDGET =================================================================================
WidgetEntryTags::WidgetEntryTags( BaseObjectType* cobject, const Glib::RefPtr< Gtk::Builder >& )
:   DrawingArea( cobject )
{
    set_events( Gdk::BUTTON_PRESS_MASK | Gdk::BUTTON_RELEASE_MASK |
                Gdk::POINTER_MOTION_MASK | Gdk::LEAVE_NOTIFY_MASK | Gdk::SCROLL_MASK );

    try
    {
        auto builder{ Lifeograph::get_builder() };
        builder->get_widget( "Po_entry_tag_edit", m_Po_tag_edit );
        builder->get_widget( "Bx_entry_tag_name", m_Bx_tag_name );
        builder->get_widget_derived( "E_entry_tag", m_W_tag_edit );
        builder->get_widget( "B_entry_tag", m_B_tag_operation );

        builder->get_widget( "Bx_entry_tag_value", m_Bx_tag_value );
        builder->get_widget( "E_entry_tag_value", m_E_tag_value );
        builder->get_widget( "L_entry_tag_unit", m_L_tag_unit );
        builder->get_widget( "B_entry_tag_value", m_B_tag_value );

        builder->get_widget( "B_entry_tag_remove", m_B_tag_remove );
        builder->get_widget( "B_entry_tag_theme", m_B_set_theme );
    }
    catch( ... )
    {
        throw LIFEO::Error( "Failed to create the entry tags widget" );
    }

    m_W_tag_edit->signal_updated().connect(
            sigc::mem_fun( this, &WidgetEntryTags::handle_W_tag_updated ) );
    m_W_tag_edit->signal_activate().connect(
            sigc::mem_fun( this, &WidgetEntryTags::handle_button_tag_clicked ) );
    m_B_tag_operation->signal_clicked().connect(
            sigc::mem_fun( this, &WidgetEntryTags::handle_button_tag_clicked ) );
    m_E_tag_value->signal_activate().connect(
            sigc::mem_fun( this, &WidgetEntryTags::handle_change_value ) );
    m_B_tag_value->signal_clicked().connect(
            sigc::mem_fun( this, &WidgetEntryTags::handle_change_value ) );
    m_B_set_theme->signal_clicked().connect(
            sigc::mem_fun( this, &WidgetEntryTags::handle_set_theme ) );
    m_B_tag_remove->signal_clicked().connect(
            sigc::mem_fun( this, &WidgetEntryTags::handle_remove ) );

    // DEFAULT FONT
    Gtk::Button* button = new Gtk::Button;
    button->show();
    m_font_system = Cairo::ToyFontFace::create( button->get_style_context()->get_font().to_string(),
                                                Cairo::FONT_SLANT_NORMAL,
                                                Cairo::FONT_WEIGHT_NORMAL );
    m_color_text_default = button->get_style_context()->get_color( Gtk::STATE_FLAG_ACTIVE );
    delete button;

    // TAG ICON
    //ICON_SIZE = Lifeograph::icons->tag_16->get_width(); // we assume that icon is a square
    /*Cairo::Format format( image_ptr_->get_has_alpha() ?
            Cairo::FORMAT_ARGB32 : Cairo::FORMAT_RGB24 );*/
    /*m_image_surface = Cairo::ImageSurface::create( Cairo::FORMAT_ARGB32,
                                                   ICON_SIZE, ICON_SIZE );
    m_image_context = Cairo::Context::create( m_image_surface );
    Gdk::Cairo::set_source_pixbuf( m_image_context, Lifeograph::icons->tag_theme_16, 0.0, 0.0 );
    m_image_context->paint();*/

    m_image_surface_add = Cairo::ImageSurface::create( Cairo::FORMAT_ARGB32,
                                                       ICON_SIZE, ICON_SIZE );
    m_image_context = Cairo::Context::create( m_image_surface_add );
    Gdk::Cairo::set_source_pixbuf(
            m_image_context,
            Gtk::IconTheme::get_default()->load_icon( "list-add", Gtk::ICON_SIZE_MENU ),
            0.0, 0.0 );
    m_image_context->paint();
}

void
WidgetEntryTags::set_entry( Entry* ptr2entry )
{
    m_ptr2entry = ptr2entry;
    m_W_tag_edit->set_entry( ptr2entry );
    m_W_tag_edit->populate();

    m_pos_x = MARGIN;

    if( Glib::RefPtr< Gdk::Window > window = get_window() )
        window->invalidate( false );
}

void
WidgetEntryTags::redraw()
{
    if( Glib::RefPtr< Gdk::Window > window = get_window() )
        window->invalidate( false );
}

void
WidgetEntryTags::hide_popover()
{
    m_Po_tag_edit->hide();
}

void
WidgetEntryTags::handle_W_tag_updated( WidgetTagEdit::TagOperation to, Tag* tag )
{
    m_B_tag_operation->show();

    switch( to )
    {
        case WidgetTagEdit::TO_INVALID:
        case WidgetTagEdit::TO_NONE:
            m_B_tag_operation->hide();
            break;
        case WidgetTagEdit::TO_CREATE_BOOLEAN:
        case WidgetTagEdit::TO_CREATE_CUMULATIVE:
            m_B_tag_operation->set_label( _( "Create Tag" ) );
            break;
        case WidgetTagEdit::TO_REMOVE:
            m_B_tag_operation->set_label( _( "Remove Tag" ) );
            break;
        case WidgetTagEdit::TO_ADD:
            m_B_tag_operation->set_label( _( "Add Tag" ) );
            break;
        case WidgetTagEdit::TO_CHANGE_VALUE:
            m_B_tag_operation->set_label( _( "Change Value" ) );
            break;
        default:
            break;
    }
}

void
WidgetEntryTags::handle_button_tag_clicked()
{
    Tag* tag{ nullptr };
    NameAndValue&& nav{ m_W_tag_edit->get_nav() };

    switch( m_W_tag_edit->get_status() )
    {
        case WidgetTagEdit::TO_NONE:
        case WidgetTagEdit::TO_INVALID:
            return; // don't even clear
        case WidgetTagEdit::TO_REMOVE:
            tag = Diary::d->get_tags()->get_tag( nav.name );
            m_ptr2entry->remove_tag( tag );
            break;
        case WidgetTagEdit::TO_CREATE_BOOLEAN:
        case WidgetTagEdit::TO_CREATE_CUMULATIVE:
            if( m_W_tag_edit->get_status() == WidgetTagEdit::TO_CREATE_CUMULATIVE )
                tag = Diary::d->create_tag( nav.name, nullptr,
                                            ChartPoints::MONTHLY|ChartPoints::CUMULATIVE );
            else
                tag = Diary::d->create_tag( nav.name, nullptr );

            m_ptr2entry->add_tag( tag, nav.value );
            AppWindow::p->panel_extra->populate();
            m_W_tag_edit->populate();
            break;
        case WidgetTagEdit::TO_ADD:
            tag = Diary::d->get_tags()->get_tag( nav.name );
            m_ptr2entry->add_tag( tag, nav.value );
            break;
        case WidgetTagEdit::TO_CHANGE_VALUE:
            tag = Diary::d->get_tags()->get_tag( nav.name );
            m_ptr2entry->remove_tag( tag );
            m_ptr2entry->add_tag( tag, nav.value );
            break;
    }

    if( tag && tag->get_has_own_theme() )
        m_signal_theme_changed.emit();

    hide_popover();
}

void
WidgetEntryTags::handle_change_value()
{
    if( ! m_hovered_tag->is_boolean() )
    {
        Tag* tag{ const_cast< Tag* >( m_hovered_tag ) };
        Value value = convert_string_d( m_E_tag_value->get_text() );
        m_ptr2entry->remove_tag( tag );
        m_ptr2entry->add_tag( tag, value );

        hide_popover();
    }
}

void
WidgetEntryTags::handle_set_theme()
{
    if( m_hovered_tag->get_has_own_theme() )
    {
        m_ptr2entry->set_theme_tag( m_hovered_tag );
        m_signal_theme_changed.emit();

        hide_popover();
    }
}

void
WidgetEntryTags::handle_remove()
{
    m_ptr2entry->remove_tag( const_cast< Tag* >( m_hovered_tag ) );

    if( m_hovered_tag->get_has_own_theme() )
        m_signal_theme_changed.emit();

    hide_popover();
}

void
WidgetEntryTags::start_editing()
{
    if( !m_hovered_tag ) // this is necessary for when used from keyboard shortcut
        m_hovered_tag = &m_add_tag_item;

    if( AppWindow::p->panel_main->get_cur_elem_type() == DiaryElement::ET_ENTRY &&
        !Diary::d->is_read_only() )
    {
        auto iter{ m_items.find( m_hovered_tag ) };
        if( iter == m_items.end() )
            return;
        TagItem ti{ iter->second };

        Gdk::Rectangle rect{ ti.xl, ti.yl, ti.xr - ti.xl, ti.yr - ti.yl };
        m_Po_tag_edit->set_relative_to( *this );
        m_Po_tag_edit->set_pointing_to( rect );
        m_Po_tag_edit->show();

        if( m_hovered_tag == &m_add_tag_item )
        {
            m_Bx_tag_name->show();
            m_Bx_tag_value->hide();
            m_B_tag_remove->hide();

            m_W_tag_edit->clear();
            m_W_tag_edit->grab_focus();
        }
        else if( m_hovered_tag->is_boolean() )
        {
            m_Bx_tag_name->hide();
            m_Bx_tag_value->hide();
            m_B_tag_remove->show();

            m_B_set_theme->grab_focus();
        }
        else
        {
            m_Bx_tag_name->hide();
            m_Bx_tag_value->show();
            m_B_tag_remove->show();

            m_L_tag_unit->set_text( m_hovered_tag->get_unit() );
            m_E_tag_value->set_text( STR::compose( m_hovered_tag->get_value( m_ptr2entry ) ) );
            m_E_tag_value->grab_focus();
        }

        if( m_hovered_tag->get_has_own_theme() && m_ptr2entry->get_theme_tag() != m_hovered_tag )
            m_B_set_theme->show();
        else
            m_B_set_theme->hide();
    }
}

bool
WidgetEntryTags::on_scroll_event( GdkEventScroll* event )
{
    /*if( m_points )
    {
        if( event->direction == GDK_SCROLL_UP && m_col_start > 0 )
            m_col_start--;
        else
        if( event->direction == GDK_SCROLL_DOWN &&
            m_col_start < ( m_points->size() - m_col_count ) )
            m_col_start++;
        else
            return true;

        get_window()->invalidate( false );
    }*/
    return true;
}

bool
WidgetEntryTags::check_point( int x, int y )
{
    for( auto& kv_ti : m_items )
    {
        const TagItem ti{ kv_ti.second };
        if( ti.xl < x && ti.xr > x && ti.yl < y && ti.yr > y )
        {
            if( m_hovered_tag != kv_ti.first )
            {
                m_hovered_tag = kv_ti.first;
                get_window()->invalidate( false );  // TODO: clip
            }
            return true;
        }
    }

    if( m_hovered_tag )
    {
        m_hovered_tag = nullptr;
        get_window()->invalidate( false );  // TODO: clip
    }

    return false;
}


bool
WidgetEntryTags::on_button_press_event( GdkEventButton* event )
{
    if( m_ptr2entry == nullptr )
        return false;

    if( ( event->button == 1 || event->button == 3 ) && check_point( event->x, event->y ) )
    {
        if( event->type == GDK_DOUBLE_BUTTON_PRESS && m_hovered_tag != &m_add_tag_item )
        {
            m_signal_tag_double_clicked.emit( m_hovered_tag );
        }
        else
        {
            //m_signal_tag_selected.emit( m_hovered_tag );
            start_editing();
            PRINT_DEBUG( "Pressed tag: ", m_hovered_tag->get_name() );
        }

        //m_flag_button_pressed = true;
    }

    return true;
}

/*bool
WidgetTagList::on_button_release_event( GdkEventButton* event )
{
    if( event->button == 1 )
    {
        m_flag_button_pressed = false;
    }

    return true;
}*/

bool
WidgetEntryTags::on_motion_notify_event( GdkEventMotion* event )
{
    if( m_ptr2entry != nullptr )
        check_point( event->x, event->y );

    return Gtk::DrawingArea::on_motion_notify_event( event );
}

bool
WidgetEntryTags::on_leave_notify_event( GdkEventCrossing* event )
{
    /*if( m_points )
    {
        m_flag_pointer_hovered = false;
        get_window()->invalidate( false );  // TODO: limit to scrollbar only
    }*/

    return true;
}

void
WidgetEntryTags::on_size_allocate( Gtk::Allocation& allocation )
{
    Gtk::Widget::on_size_allocate( allocation );
    m_width = allocation.get_width();
    m_height = allocation.get_height();
}

void
WidgetEntryTags::add_item( const Cairo::RefPtr< Cairo::Context >& cr, const Tag* tag )
{
    TagItem titem;
    Cairo::TextExtents te;
    const bool flag_hovered{ tag == m_hovered_tag };
    const Glib::ustring label{ tag->get_name_and_value( m_ptr2entry, false, true ) };

    if( tag && tag->get_has_own_theme() )
    {
        m_font_theme.clear();    // is this necessary?
        m_font_theme = Cairo::ToyFontFace::create( tag->get_theme()->font.get_family(),
                                                   Cairo::FONT_SLANT_NORMAL,
                                                   Cairo::FONT_WEIGHT_NORMAL );
        cr->set_font_face( m_font_theme );
    }
    else
        cr->set_font_face( m_font_system );

    cr->get_text_extents( label, te );
    if( m_pos_x + ICON_SIZE + LABEL_OFFSET + te.width + MARGIN > m_width )
    {
        m_pos_x = MARGIN;
        m_pos_y += ( ICON_SIZE + VSPACING );
    }

    titem.xl = m_pos_x - ITEM_BORDER;
    titem.xr = m_pos_x + te.width + HALF_HEIGHT;
    titem.yl = m_pos_y - ITEM_BORDER;
    titem.yr = titem.yl + ITEM_HEIGHT;

    if( tag == &m_add_tag_item )
        titem.xr += ( ICON_SIZE + LABEL_OFFSET );

    m_items[ tag ] = titem;

    // BACKGROUND
    if( flag_hovered || tag != &m_add_tag_item )
    {
        int width( titem.xr - titem.xl - HALF_HEIGHT );

        cr->move_to( titem.xl + 0.5, titem.yl + 0.5 );
        cr->rel_line_to( width, 0 );
        cr->rel_line_to( HALF_HEIGHT, HALF_HEIGHT );
        cr->rel_line_to( HALF_HEIGHT * -1, HALF_HEIGHT );
        cr->rel_line_to(-width, 0);
        cr->close_path();

        if( flag_hovered && m_flag_editable )
            cr->set_line_width( 2.0 );

        if( tag && tag->get_has_own_theme() )
        {
            Gdk::Cairo::set_source_rgba( cr, tag->get_theme()->color_base );
            cr->fill_preserve();

            Gdk::Cairo::set_source_rgba( cr, tag->get_theme()->color_highlight );
            cr->stroke_preserve();

            std::vector< double > dashes;
            dashes.push_back( 10.0 );
            dashes.push_back( 10.0 );

            cr->set_dash( dashes, 0.0 );

            Gdk::Cairo::set_source_rgba( cr, tag->get_theme()->color_heading );
            cr->stroke();
            cr->unset_dash();
        }
        else
        {
            Gdk::Cairo::set_source_rgba( cr, m_color_text_default );
            cr->stroke();
        }

        if( flag_hovered && m_flag_editable )
            cr->set_line_width( 1.0 );
    }

    // ICON
    if( tag == &m_add_tag_item )
    {
        cr->set_source( m_image_surface_add, m_pos_x, m_pos_y );
        cr->rectangle( m_pos_x, m_pos_y, ICON_SIZE, ICON_SIZE );
        cr->clip();
        cr->paint();
        cr->reset_clip();

        m_pos_x += ( ICON_SIZE + LABEL_OFFSET );
    }

    // LABEL
    if( tag && tag->get_has_own_theme() )
        Gdk::Cairo::set_source_rgba( cr, tag->get_theme()->color_text );
    else
        Gdk::Cairo::set_source_rgba( cr, m_color_text_default );

    cr->move_to( m_pos_x + 0.5, m_pos_y + TEXT_HEIGHT + 0.5 );
    cr->show_text( label );

    m_pos_x += ( te.width + HALF_HEIGHT + HSPACING );
}

bool
WidgetEntryTags::on_draw( const Cairo::RefPtr< Cairo::Context >& cr )
{
    // after we started to use Gtk::Stack this widget gets on_draw event during log out
    if( m_ptr2entry == nullptr || Lifeograph::loginstatus != Lifeograph::LOGGED_IN ||
        Lifeograph::s_internaloperation )
        return false;

    /* TODO
    if( event )
    {
        // clip to the area indicated by the expose event so that we only
        // redraw the portion of the window that needs to be redrawn
        cr->rectangle( event->area.x, event->area.y,
                event->area.width, event->area.height );
        cr->clip();
    }*/

    m_pos_x = m_pos_y = MARGIN;
    m_items.clear();
    cr->set_font_size( TEXT_HEIGHT );
    cr->set_line_width( 1.0 );
    cr->set_line_join( Cairo::LINE_JOIN_ROUND );

    const TagSet& tags = m_ptr2entry->get_tags();

    if( tags.empty() && !m_flag_editable )
    {
        cr->set_font_face( m_font_system );
        Gdk::Cairo::set_source_rgba( cr, m_color_text_default );
        cr->move_to( m_pos_x, m_pos_y + TEXT_HEIGHT );
        cr->show_text( _( "Not tagged" ) );
    }

    for( Tag* tag : tags )
        add_item( cr, tag );

    if( m_flag_editable )
        add_item( cr, &m_add_tag_item );

    set_size_request( -1, m_pos_y + TEXT_HEIGHT + MARGIN );

    return true;
}
