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

    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 "widget_chart.hpp"
#include "strings.hpp"

#include "diarydata.hpp"
#include "entry.hpp"

using namespace LIFEO;


// WIDGETCHART =====================================================================================

WidgetChart::WidgetChart( 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 );

    m_font_main.set_family( "Cantarell" );
    m_font_main.set_size( label_height * Pango::SCALE );

    m_font_bold.set_family( "Cantarell" );
    m_font_bold.set_weight( Pango::WEIGHT_BOLD );
    m_font_bold.set_size( label_height * Pango::SCALE );

    m_font_big.set_family( "Cantarell" );
    m_font_big.set_weight( Pango::WEIGHT_BOLD );
    m_font_big.set_size( 1.5 * label_height * Pango::SCALE );
}

void
WidgetChart::set_points( ChartPoints* points, float zoom_level )
{
    if( m_points )
        delete m_points;
    m_points = points;
    if( zoom_level >= 0.0 )
        m_zoom_level = zoom_level;

    m_span = points ? points->get_span() : 0;

    if( m_width > 0 ) // if on_size_allocate is executed before
    {
        update_col_geom( zoom_level >= 0.0 );
        update();
    }
}

void
WidgetChart::set_zoom( float level )
{
    m_zoom_level = level;
    if( m_width > 0 ) // if on_size_allocate is executed before
    {
        update_col_geom( false );
        update();
    }
}

bool
WidgetChart::is_zoom_possible()
{
    if( ! m_points )
        return false;
    return( !( m_step_count == m_span && m_step_x >= COLUMN_WIDTH_MIN ) );
}

void
WidgetChart::update_col_geom( bool flag_new )
{
    if( m_points )
    {
        // 100% zoom:
        const unsigned int step_count_nominal{ unsigned( m_length / COLUMN_WIDTH_MIN ) + 1 };
        const unsigned int step_count_min{ m_span > step_count_nominal ?
                                                   step_count_nominal : m_span };

        m_step_count = ( m_zoom_level * ( m_span - step_count_min ) ) + step_count_min;
        m_step_x = ( m_step_count < 3 ? m_length : m_length / ( m_step_count - 1 ) );

        m_ov_height = m_step_count < m_span ? log10( m_height ) * OVERVIEW_COEFFICIENT : 0.0;

        int mltp{ ( m_points->type & ChartPoints::PERIOD_MASK ) == ChartPoints::YEARLY ? 1 : 2 };
        m_y_max = m_height - mltp * bar_height - m_ov_height;
        m_y_mid = ( m_y_max + s_y_min ) / 2;
        m_amplitude = m_y_max - s_y_min;
        m_coefficient = m_points->value_max == m_points->value_min ? 0.0 :
                m_amplitude / ( m_points->value_max - m_points->value_min );

        const unsigned int col_start_max{ m_span - m_step_count };
        if( flag_new || m_step_start > col_start_max )
            m_step_start = col_start_max;

        // OVERVIEW PARAMETERS
        m_ampli_ov = m_ov_height - 2 * offset_label;
        m_coeff_ov = m_points->value_max == m_points->value_min ? 0.5 :
                m_ampli_ov / ( m_points->value_max - m_points->value_min );
        m_step_x_ov = m_width - 2 * offset_label;
        if( m_span > 1 )
            m_step_x_ov /= m_span - 1;
    }
}

bool
WidgetChart::on_scroll_event( GdkEventScroll *event )
{
    if( m_points )
    {
        if( event->direction == GDK_SCROLL_UP && m_step_start > 0 )
            m_step_start--;
        else
        if( event->direction == GDK_SCROLL_DOWN &&
            m_step_start < ( m_span - m_step_count ) )
            m_step_start++;
        else
            return true;

        update();
    }
    return true;
}

bool
WidgetChart::on_button_press_event( GdkEventButton* event )
{
    if( m_points && event->button == 1 && event->y > m_height - m_ov_height )
    {
        int col_start{ int( ( event->x / m_width ) * m_span - m_step_count / 2 ) };
        const unsigned int col_start_max{ m_span - m_step_count };
        if( col_start > int( col_start_max ) )
            col_start = col_start_max;
        else
        if( col_start < 0 )
            col_start = 0;
        m_step_start = col_start;

        update();

        m_flag_button_pressed = true;
    }

    return true;
}

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

    return true;
}

bool
WidgetChart::on_motion_notify_event( GdkEventMotion* event )
{
    if( m_points )
    {
        if( m_flag_button_pressed )
        {
            int col_start = ( event->x / m_width ) * m_span - m_step_count / 2;
            if( col_start > int( m_span - m_step_count ) )
                col_start = m_span - m_step_count;
            else
            if( col_start < 0 )
                col_start = 0;

            if( col_start != ( int ) m_step_start )
            {
                m_step_start = col_start;
                update();
            }
        }
        int hovered_step{ int( round( ( event->x - s_x_min ) / m_step_x ) ) };

        if( hovered_step < 0 )
            hovered_step = 0;
        else if( hovered_step >= int( m_step_count ) )
            hovered_step = m_step_count - 1;

        bool flag_pointer_hovered = ( event->y > m_height - m_ov_height );
        if( flag_pointer_hovered != m_flag_pointer_hovered )
        {
            m_flag_pointer_hovered = flag_pointer_hovered;
            update();  // TODO: limit to scrollbar only
        }
        else if( hovered_step != m_hovered_step )
        {
            m_hovered_step = hovered_step;
            update();  // TODO: limit to tooltip only
        }
    }

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

bool
WidgetChart::on_leave_notify_event( GdkEventCrossing* event )
{
    if( m_points )
    {
        m_flag_pointer_hovered = false;
        update();  // TODO: limit to scrollbar only
    }

    return true;
}

void
WidgetChart::on_size_allocate( Gtk::Allocation& allocation )
{
    bool flag_first( m_width < 0 );

    Gtk::Widget::on_size_allocate( allocation );
    m_width = allocation.get_width();
    m_height = allocation.get_height();

    m_x_max = m_width - border_curve;
    m_length = m_x_max - s_x_min;

    update_col_geom( flag_first );
}

bool
WidgetChart::on_draw( const Cairo::RefPtr< Cairo::Context >& cr )
{
    /* 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();
    }*/

    // BACKGROUND
    cr->rectangle( 0.0, 0.0, m_width, m_height );
    cr->set_source_rgb( 1.0, 1.0, 1.0 );
    cr->fill();

    // HANDLE THERE-IS-TOO-FEW-ENTRIES-CASE SPECIALLY
    if( ! m_points || m_span < 2 )
    {
        cr->set_source_rgb( 0.0, 0.0, 0.0 );

        auto layout = create_pango_layout( _( "INSUFFICIENT DATA" ) );
        layout->set_font_description( m_font_big );
        int w, h;
        layout->get_pixel_size( w, h );
        cr->move_to( ( m_width - w ) / 2 , m_height / 2 );
        layout->show_in_cairo_context( cr );

        return true;
    }

    // NUMBER OF STEPS IN THE PRE AND POST BORDERS
    double pre_steps{ ceil( s_x_min / m_step_x ) };
    if( pre_steps > m_step_start )
        pre_steps = m_step_start;

    double post_steps{ ceil( border_curve / m_step_x ) };
    if( post_steps > m_span - m_step_count - m_step_start )
        post_steps = m_span - m_step_count - m_step_start;

    // CHAPTER BACKGROUNDS
    double pos_chapter_last{ -FLT_MAX };
    double pos_chapter_new{ 0.0 };
    Color chapter_color_last{ "#FFFFFF" };
    for( auto& pc_chapter : m_points->chapters )
    {
        pos_chapter_new = s_x_min + m_step_x * ( pc_chapter.first - m_step_start );
        if( pos_chapter_last != -FLT_MAX )
        {
            if( pos_chapter_new > 0 )
            {
                if( pos_chapter_new > m_width )
                    pos_chapter_new = m_width;

                Gdk::Cairo::set_source_rgba( cr, chapter_color_last );
                cr->rectangle( pos_chapter_last, 0.0, pos_chapter_new - pos_chapter_last, m_y_max );
                cr->fill();
            }

            if( pos_chapter_new >= m_width )
                break;
        }

        pos_chapter_last = pos_chapter_new;
        chapter_color_last = pc_chapter.second;
    }

    // YEAR & MONTH BAR
    Date date( m_points->start_date );
    int period{ m_points->type & ChartPoints::PERIOD_MASK };
    int step_grid = ceil( COLUMN_WIDTH_MIN / m_step_x );
    int step_grid_first = 0;

    if( period == ChartPoints::MONTHLY )
    {
        date.forward_months( m_step_start );

        if( step_grid > 12 )
            step_grid += 12 - ( step_grid % 12 );
        else if( step_grid > 6 )
            step_grid = 12;
        else if( step_grid > 4 )
            step_grid = 6;
        else if( step_grid > 3 )
            step_grid = 4;
        else if( step_grid > 2 )
            step_grid = 3;

        step_grid_first = ( step_grid - ( date.get_month() % step_grid ) + 1 ) % step_grid;
        date.forward_months( step_grid_first );
        cr->rectangle( 0.0, m_y_max, m_width, bar_height * 2 );
    }
    else
    {
        date.forward_years( m_step_start );
        cr->rectangle( 0.0, m_y_max, m_width, bar_height );
    }

//PRINT_DEBUG( "m_step_x=", m_step_x, " - step_grid=", step_grid, " - step_grid_first=",
//             step_grid_first, " - month=",date.get_month()  );

    cr->set_source_rgb( 0.85, 0.85, 0.85 );
    cr->fill();

    // HORIZONTAL LINES
    cr->set_line_width( 1.0 );

    // + 0.5 offset needed to get crisp lines:
    cr->move_to( 0.0f, s_y_min + 0.5 );
    cr->line_to( m_width, s_y_min + 0.5 );
    cr->move_to( 0.0f, m_y_mid + 0.5 );
    cr->line_to( m_width, m_y_mid + 0.5 );

    // YEAR & MONTH LABELS + VERTICAL LINES
    Date date_hovered( m_points->start_date );
    if( period == ChartPoints::MONTHLY )
        date_hovered.forward_months( m_step_start + m_hovered_step );
    else
        date_hovered.forward_years( m_step_start + m_hovered_step );

    unsigned int year_last{ 0 };
    auto layout_ym = create_pango_layout( "" );
    layout_ym->set_font_description( m_font_main );

    cr->set_source_rgb( 0.0, 0.0, 0.0 );

    for( int i = step_grid_first; i < ( int ) m_step_count; i+=step_grid )
    {
        cr->move_to( s_x_min + m_step_x * i, m_y_max + label_y );
        cr->line_to( s_x_min + m_step_x * i, 0.0f );

        if( period == ChartPoints::MONTHLY )
        {
            if( step_grid < 12 )
            {
                cr->move_to( s_x_min + m_step_x * i + offset_label, m_y_max );
                layout_ym->set_text( date.format_string( "M" ) );
                layout_ym->show_in_cairo_context( cr );
            }

            if( year_last != date.get_year() )
            {
                cr->move_to( s_x_min + m_step_x * i + offset_label,
                             step_grid < 12 ? m_y_max + label_y : m_y_max );
                layout_ym->set_text( date.format_string( "Y" ) );
                layout_ym->show_in_cairo_context( cr );
                year_last = date.get_year();
            }

            date.forward_months( step_grid );
        }
        else
        {
            cr->move_to( s_x_min + m_step_x * i + offset_label, m_y_max );
            layout_ym->set_text( date.format_string( "Y" ) );
            layout_ym->show_in_cairo_context( cr );

            date.forward_years( step_grid );
        }
    }
    cr->set_source_rgb( 0.6, 0.6, 0.6 );
    cr->stroke();

    // GRAPH LINE
    cr->set_source_rgb( 0.9, 0.3, 0.3 );
    cr->set_line_join( Cairo::LINE_JOIN_BEVEL );
    cr->set_line_width( 3.0 );

    cr->move_to( s_x_min - m_step_x * pre_steps, m_y_max - m_coefficient *
            ( m_points->values[ m_step_start - pre_steps ] - m_points->value_min ) );

    for( unsigned int i = 1; i < m_step_count + pre_steps + post_steps; i++ )
    {
        cr->line_to( s_x_min + m_step_x * ( i - pre_steps ), m_y_max - m_coefficient *
                ( m_points->values[ i + m_step_start - pre_steps ] - m_points->value_min ) );
    }
    //cr->save();
    cr->stroke();

    // GRAPH LINE PREVIOUS
    if( period == ChartPoints::MONTHLY && ( m_points->type & ChartPoints::UNDERLAY_PREV_YEAR ) )
    {
        const static std::valarray< double > dash_pattern{ 3.0 };
        cr->set_source_rgb( 1.0, 0.5, 0.5 );
        cr->set_dash( dash_pattern, 0 );
        cr->set_line_width( 2.0 );
        int step_start_underlay = ( m_step_start ) > 12 ? m_step_start - 12 : 0 ;
        unsigned int i = m_step_start < 12 ? 12 - m_step_start : 0 ;

        cr->move_to( s_x_min + m_step_x * i, m_y_max - m_coefficient *
                     ( m_points->values[ step_start_underlay ] - m_points->value_min ) );

        i++;

        for( ; i < m_step_count; i++ )
        {
            cr->line_to( s_x_min + m_step_x * i, m_y_max - m_coefficient *
                         ( m_points->values[ ++step_start_underlay ] - m_points->value_min ) );
        }
        cr->stroke();
        cr->unset_dash();
    }

    // y LABELS
    cr->set_source_rgb( 0.0, 0.0, 0.0 );
    cr->move_to( border_label, s_y_min - label_y );
    layout_ym->set_text( STR::compose( m_points->value_max, " ", m_points->unit ) );
    layout_ym->show_in_cairo_context( cr );
    cr->move_to( border_label, m_y_max - label_y );
    layout_ym->set_text( STR::compose( m_points->value_min, " ", m_points->unit ) );
    layout_ym->show_in_cairo_context( cr );

    // TOOLTIP
    {
        const double tip_pt_x{ s_x_min + m_step_x * m_hovered_step };
        const double tip_pt_y{ 5.0 };

        cr->set_source_rgb( 0.2, 0.2, 0.7 );
        cr->set_line_width( 1.5 );
        cr->move_to( tip_pt_x, 0.0 );
        cr->line_to( tip_pt_x, m_height - m_ov_height );
        cr->stroke();

        auto layout = create_pango_layout(
                STR::compose( date_hovered.format_string( period == ChartPoints::MONTHLY ?
                                                          "YM" : "Y" ),
                              "\n", m_points->values[ m_hovered_step + m_step_start ],
                              " ", m_points->unit )  );
        layout->set_font_description( m_font_bold );

        if( m_hovered_step < int( m_step_count / 2 ) )
        {
            layout->set_alignment( Pango::ALIGN_LEFT );
            cr->move_to( tip_pt_x + offset_label, tip_pt_y );
        }
        else
        {
            layout->set_alignment( Pango::ALIGN_RIGHT );
            int w, h;
            layout->get_pixel_size( w, h );
            cr->move_to( tip_pt_x - w - offset_label, tip_pt_y );
        }

        layout->show_in_cairo_context( cr );
    }

    // OVERVIEW
    if( m_step_count < m_span )
    {
        // OVERVIEW REGION
        cr->set_source_rgb( 0.7, 0.7, 0.7 );
        cr->rectangle( 0.0, m_height - m_ov_height, m_width, m_ov_height );
        cr->fill();

        if( m_flag_pointer_hovered )
            cr->set_source_rgb( 1.0, 1.0, 1.0 );
        else
            cr->set_source_rgb( 0.95, 0.95, 0.95 );
        cr->rectangle( offset_label + m_step_start * m_step_x_ov, m_height - m_ov_height,
                       ( m_step_count - 1 ) * m_step_x_ov, m_ov_height );
        cr->fill();
        //cr->restore();

        // OVERVIEW LINE
        cr->set_source_rgb( 0.9, 0.3, 0.3 );
        cr->set_line_join( Cairo::LINE_JOIN_BEVEL );
        cr->set_line_width( 2.0 );

        //date.m_date = m_points->begin()->first;
        cr->move_to( offset_label, m_height - offset_label - m_coeff_ov *
                     ( m_points->values[ 0 ] - m_points->value_min ) );
        for( unsigned int i = 1; i < m_span; ++i )
        {
            //date.forward_month();
            cr->line_to( offset_label + m_step_x_ov * i, m_height - offset_label - m_coeff_ov *
                         ( m_points->values[ i ] - m_points->value_min ) );
        }
        cr->stroke();

        // DIVIDER
        if( m_flag_pointer_hovered )
            cr->set_source_rgb( 0.2, 0.2, 0.2 );
        else
            cr->set_source_rgb( 0.45, 0.45, 0.45 );
        cr->rectangle( 1.0, m_height - m_ov_height, m_width - 2.0, m_ov_height - 1.0 );
        cr->stroke();
    }

    return true;
}

//bool
//WidgetChart::handle_query_tooltip( int x, int y, bool keyboard_mode,
//                                   const Glib::RefPtr< Gtk::Tooltip >& tooltip )
//{
//    if( !m_points || keyboard_mode || m_span < 2 )
//        return false;
//
//    if( y > m_y_max ) // skip the label bars and overview area
//        return false;
//
//    int i{ int( round( ( x - s_x_min ) / m_step_x ) + m_step_start ) };
//
//    if( i < 0 )
//        i = 0;
//    else if( i >= int( m_span ) )
//        i = m_span - 1;
//
//    if( m_points->unit.empty() )
//        tooltip->set_text( STR::compose( m_points->values[ i ] ) );
//    else
//        tooltip->set_text( STR::compose( m_points->values[ i ], " ", m_points->unit ) );
//
//    return true;
//}
