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

    Copyright (C) 2007-2020 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 "../strings.hpp"
#include "../diarydata.hpp"
#include "../diary.hpp"
#include "../filtering.hpp"
#include "../lifeograph.hpp"
#include "chart.hpp"


using namespace LIFEO;

// CHART ===========================================================================================
const std::valarray< double > Chart::s_dash_pattern = { 3.0 };

Chart::Chart()
: m_data( nullptr )
{
    m_font_main.set_family( "Cantarell" );
    m_font_main.set_size( HEIGHT_LABEL * Pango::SCALE );

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

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

inline date_t
Chart::get_period_date( date_t date )
{
    switch( m_data.type & ChartData::PERIOD_MASK )
    {
        case ChartData::WEEKLY:
            Date::backward_to_week_start( date );
            return Date::get_pure( date );
        case ChartData::MONTHLY:
            return( Date::get_yearmonth( date ) + Date::make_day( 1 ) );
        case ChartData::YEARLY:
            return( ( date & Date::FILTER_YEAR ) + Date::make_month( 1 ) + Date::make_day( 1 ) );
    }

    return 0;
}

Ustring
Chart::get_date_str( date_t date ) const
{
    switch( m_data.type & ChartData::PERIOD_MASK )
    {
        case ChartData::WEEKLY:    return Date::format_string( date, "YMD" );
        case ChartData::MONTHLY:   return Date::format_string( date, "YM" );
        case ChartData::YEARLY:
        default:                   return std::to_string( Date::get_year( date ) );
    }
}

inline FiltererContainer*
Chart::get_filterer_stack()
{
    if( m_data.filter )
        return m_data.filter->get_filterer_stack();

    return nullptr;
}

void
Chart::calculate_points( double zoom_level )
{
    if( m_p2diary == nullptr )
        return;

    m_data.clear_points();

    auto        fc{ get_filterer_stack() };
    auto&       entries{ m_p2diary->get_entries() };
    Entry*      entry;
    Value       v{ 1.0 };
    Value       v_plan{ 0.0 };
    const int   y_axis{ m_data.get_y_axis() };
    struct      Values{ Value v[ 2 ]; };
    std::multimap< date_t, Values > map_values;

    for( auto kv_entry = entries.rbegin(); kv_entry != entries.rend(); ++kv_entry )
    {
        entry = kv_entry->second;

        if( y_axis == ChartData::TAG_VALUE_PARA )
        {
            if( m_data.tag && !entry->has_tag( m_data.tag ) )
                continue;
        }
        else
        {
            if( entry->is_ordinal() )
                continue;

            if( m_data.tag && m_data.is_tagged_only() && !entry->has_tag( m_data.tag ) )
                continue;
        }

        if( fc && fc->filter( entry ) == false )
            continue;

        switch( y_axis )
        {
            case ChartData::COUNT:
                break;
            case ChartData::TEXT_LENGTH:
                v = entry->get_size();
                break;
            case ChartData::MAP_PATH_LENGTH:
                v = entry->get_map_path_length();
                break;
            case ChartData::TAG_VALUE_ENTRY:
                if( m_data.tag != nullptr )
                {
                    v = entry->get_value_for_tag( &m_data );
                    v_plan = entry->get_value_planned_for_tag( &m_data );
                }
                break;
            case ChartData::TAG_VALUE_PARA:
                if( m_data.tag != nullptr )
                {
                    // first sort the values by date:
                    for( auto& para : *entry->get_paragraphs() )
                    {
                        date_t date{ para->get_date_broad() };
                        if( date == Date::NOT_SET || Date::is_ordinal( date ) )
                            continue;
                        if( !para->has_tag( m_data.tag ) )
                            continue;

                        int c{ 0 }; // dummy
                        v = para->get_value_for_tag( &m_data, c );
                        v_plan = para->get_value_planned_for_tag( &m_data, c );
                        Values values{ v, v_plan };

                        map_values.emplace( get_period_date( date ), values );
                    }
                }
                break;
        }

        if( y_axis != ChartData::TAG_VALUE_PARA ) // para values are added in their case
            m_data.add_value( get_period_date( kv_entry->first ), v, v_plan );
    }

    if( y_axis == ChartData::TAG_VALUE_PARA )
    {
        // feed the values in order:
        for( auto& kv : map_values )
            m_data.add_value( kv.first, kv.second.v[ 0 ], kv.second.v[ 1 ] );
    }

    m_data.update_min_max();

    m_p2diary->fill_up_chart_data( &m_data );

    if( zoom_level >= 0.0 )
        m_zoom_level = zoom_level;

    m_span = m_data.get_span();

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

    delete fc;
}

void
Chart::set_zoom( float level )
{
    if( level == m_zoom_level )
        return;

    m_zoom_level = ( level > 1.0f ? 1.0f : ( level < 0.0f ) ? 0.0f : level );
    if( m_width > 0 ) // if on_size_allocate is executed before
    {
        update_col_geom( false );
        refresh();
    }
}

bool
Chart::is_zoom_possible() const
{
    return( !( m_step_count == m_span && m_step_x >= WIDTH_COL_MIN ) );
}

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

    if( m_data.is_underlay_planned() )
    {
        m_v_min = std::min( m_data.v_min, m_data.v_plan_min );
        m_v_max = std::max( m_data.v_max, m_data.v_plan_max );
    }
    else
    {
        m_v_min = m_data.v_min;
        m_v_max = m_data.v_max;
    }

    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 ) * COEFF_OVERVIEW : 0.0;

    int mltp{ ( m_data.type & ChartData::PERIOD_MASK ) == ChartData::YEARLY ? 1 : 2 };
    m_y_max = m_height - mltp * HEIGHT_BAR - m_ov_height;
    m_y_mid = ( m_y_max + s_y_min ) / 2;
    m_amplitude = m_y_max - s_y_min;
    m_coefficient = ( m_v_max == m_v_min ) ? 0.0 : m_amplitude / ( m_v_max - m_v_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_v_max == m_v_min ) ? 0.5 : m_ampli_ov / ( m_v_max - m_v_min );
    m_step_x_ov = m_width - 2 * OFFSET_LABEL;
    if( m_span > 1 )
        m_step_x_ov /= m_span - 1;
}

void
Chart::resize( int w, int h )
{
    bool flag_first( m_width < 0 );

    m_width = w;
    m_height = h;

    m_x_max = m_width - BORDER_CURVE;
    m_length = m_x_max - s_x_min;

    update_col_geom( flag_first );
}

void
Chart::scroll( int offset )
{
    //if( m_points )
    {
        if( offset < 0 && m_step_start > 0 )
            m_step_start--;
        else
        if( offset > 0 && m_step_start < ( m_span - m_step_count ) )
            m_step_start++;
        else
            return;

        refresh();
    }
}

bool
Chart::draw( const Cairo::RefPtr< Cairo::Context >& cr )
{
    // 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_span < 2 )
    {
        cr->set_source_rgb( 0.0, 0.0, 0.0 );
        auto layout = Pango::Layout::create( cr );
        layout->set_text( _( "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_data.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
    const int   period{ m_data.get_period() };
    int         step_grid{ ( int ) ceil( WIDTH_COL_MIN / m_step_x ) };
    int         step_grid_first{ 0 };

    if( period == ChartData::YEARLY )
        cr->rectangle( 0.0, m_y_max, m_width, HEIGHT_BAR );
    else
    {
        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( m_data.dates[ m_step_start ] )
                            % step_grid ) + 1 ) % step_grid;
        cr->rectangle( 0.0, m_y_max, m_width, HEIGHT_BAR * 2 );
    }

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

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

    const double grid_step_y = ( m_y_max - s_y_min ) / 4;
    for( int i = 0; i < 4; i++ )
    {
        // + 0.5 offset needed to get crisp lines:
        cr->move_to( 0.0f, s_y_min + i * grid_step_y + 0.5 );
        cr->line_to( m_width, s_y_min + i * grid_step_y + 0.5 );
    }

    // y LABELS
    auto&& layout_ym{ Pango::Layout::create( cr ) };
    layout_ym->set_font_description( m_font_main );

    cr->set_source_rgb( 0.1, 0.1, 0.1 );
    cr->move_to( BORDER_LABEL, s_y_min - label_y - OFFSET_LABEL );
    layout_ym->set_text( STR::format_number( m_v_max ) + " " + m_data.unit );
    layout_ym->show_in_cairo_context( cr );

    cr->move_to( BORDER_LABEL, m_y_mid - label_y - OFFSET_LABEL );
    layout_ym->set_text(
            STR::format_number( ( m_v_max + m_v_min ) / 2 ) + " " + m_data.unit );
    layout_ym->show_in_cairo_context( cr );

    cr->move_to( BORDER_LABEL, m_y_max - label_y - OFFSET_LABEL );
    layout_ym->set_text( STR::format_number( m_v_min ) + " " + m_data.unit );
    layout_ym->show_in_cairo_context( cr );

    // YEAR & MONTH LABELS + VERTICAL LINES
    Date         date_hovered{ m_data.get_start_date() };
    unsigned int year_last{ 0 };

    if( ( m_step_start + m_hovered_step ) > 0 )
        m_data.forward_date( date_hovered.m_date, m_step_start + m_hovered_step );

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

    for( int i = step_grid_first; i < ( int ) m_step_count; i+=step_grid )
    {
        const date_t date{ m_data.dates[ m_step_start + i ] };

        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, m_y_max );

        if( period == ChartData::YEARLY )
        {
            cr->move_to( s_x_min + m_step_x * i + OFFSET_LABEL, m_y_max );
            layout_ym->set_text( std::to_string( Date::get_year( date ) ) );
            layout_ym->show_in_cairo_context( cr );
        }
        else
        {
            if( step_grid < 12 )
            {
                cr->move_to( s_x_min + m_step_x * i + OFFSET_LABEL, m_y_max );
                if( period == ChartData::MONTHLY )
                    layout_ym->set_text( std::to_string( Date::get_month( date ) ) );
                else // weekly
                    layout_ym->set_text( Date::format_string( date, "MD" ) );
                layout_ym->show_in_cairo_context( cr );
            }

            if( year_last != Date::get_year( date ) )
            {
                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( std::to_string( Date::get_year( date ) ) );
                layout_ym->show_in_cairo_context( cr );
                year_last = Date::get_year( date );
            }
        }
    }
    cr->set_source_rgb( 0.7, 0.7, 0.7 );
    cr->stroke();

    // GRAPH LINE
    cr->set_source_rgb( 0.7, 0.4, 0.4 );
    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_data.values[ m_step_start - pre_steps ] - m_v_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_data.values[ i + m_step_start - pre_steps ] - m_v_min ) );
    }
    cr->stroke();

    // UNDERLAY PREV YEAR
    if( m_data.is_underlay_prev_year() )
    {
        cr->set_source_rgb( 1.0, 0.7, 0.7 );
        cr->set_dash( s_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_data.values[ step_start_underlay ] - m_v_min ) );

        i++;

        for( ; i < m_step_count; i++ )
        {
            cr->line_to( s_x_min + m_step_x * i, m_y_max - m_coefficient *
                         ( m_data.values[ ++step_start_underlay ] - m_v_min ) );
        }
        cr->stroke();
        cr->unset_dash();
    }
    // UNDERLAY PLANNED VALUES
    else
    if( m_data.is_underlay_planned() )
    {
        cr->set_source_rgb( 1.0, 0.7, 0.7 );
        cr->set_dash( s_dash_pattern, 0 );
        cr->set_line_width( 2.0 );

        cr->move_to( s_x_min, m_y_max - m_coefficient *
                     ( m_data.values_plan[ m_step_start ] - m_v_min ) );

        for( unsigned int i = 1; i < m_step_count; i++ )
        {
            cr->line_to( s_x_min + m_step_x * i, m_y_max - m_coefficient *
                         ( m_data.values_plan[ m_step_start + i ] - m_v_min ) );
        }
        cr->stroke();
        cr->unset_dash();
    }

    // TOOLTIP
    if( m_flag_widget_hovered && m_hovered_step >= 0 )
    {
        const double tip_pt_x{ s_x_min + m_step_x * m_hovered_step };
        const double tip_pt_y{ m_y_max - m_coefficient *
            ( m_data.values[ m_hovered_step + m_step_start ] - m_v_min ) };
        auto layout{ Pango::Layout::create( cr ) };
        int w, h;

        layout->set_font_description( m_font_bold );

        cr->set_source_rgb( 0.8, 0.5, 0.5 );
        cr->set_line_width( 1.0 );
        cr->move_to( tip_pt_x, 0.0 );
        cr->line_to( tip_pt_x, m_height - m_ov_height );
        cr->move_to( 0.0, tip_pt_y );
        cr->line_to( m_width, tip_pt_y );
        cr->stroke();

        // Y RECT
        layout->set_text( STR::format_number( m_data.values[ m_hovered_step + m_step_start ] ) );
        layout->get_pixel_size( w, h );
        if( tip_pt_x < ( m_width / 2 ) )
            cr->rectangle( tip_pt_x, tip_pt_y - h - 4, w + 4, h + 4 );
        else
            cr->rectangle( tip_pt_x - w - 4, tip_pt_y - h - 4, w + 4, h + 4 );
        cr->set_source_rgba( 0.8, 0.5, 0.5, 0.6 );
        cr->fill();

        // Y VALUE
        cr->set_source_rgb( 1.0, 1.0, 1.0 );
        if( tip_pt_x < ( m_width / 2 ) )
            cr->move_to( tip_pt_x, tip_pt_y - h - 2 );
        else
            cr->move_to( tip_pt_x - w - 2, tip_pt_y - h - 2 );
        layout->show_in_cairo_context( cr );

        // X RECT
        layout->set_text( get_date_str( date_hovered.m_date ) );
        layout->get_pixel_size( w, h );
        cr->rectangle( tip_pt_x - w/2 - 2, m_height - m_ov_height - h, w + 4, h + 4 );
        cr->set_source_rgb( 0.8, 0.5, 0.5 );
        cr->fill();

        // X VALUE
        cr->set_source_rgb( 1.0, 1.0, 1.0 );
        cr->move_to( tip_pt_x - w/2, m_height - m_ov_height - h + 2 );
        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_overview_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_data.values[ 0 ] - m_v_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_data.values[ i ] - m_v_min ) );
        }
        cr->stroke();

        // DIVIDER
        if( m_flag_overview_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;
}
