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

    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 <cmath>
#include <cairomm/context.h>

#include "chart.hpp"


using namespace LoG;

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

Chart::Chart( const Pango::FontDescription& fd, bool F_printing_mode )
: ChartCommon( F_printing_mode )
{
    m_font_main = fd;
    m_font_bold = fd;
    m_font_big = fd;

    m_font_bold.set_weight( Pango::Weight::BOLD );

    m_font_big.set_weight( Pango::Weight::BOLD );
    m_font_big.set_size( fd.get_size() * 1.5 );
}

bool
Chart::update_label_size()
{
    if( !m_layout ) return false;

    m_layout->set_font_description( m_font_main );
    m_layout->set_text( "LLL" );

    m_label_size      = m_layout->get_pixel_logical_extents().get_height();
    return true;
}

inline void
Chart::draw_line( const Cairo::RefPtr< Cairo::Context >& cr )
{
    cr->set_source_rgb( 0.7, 0.4, 0.4 );
    cr->set_line_join( Cairo::Context::LineJoin::BEVEL );
    cr->set_line_width( m_unit_line_thk * 3 );

    draw_line2( cr, false );

    // UNDERLAY
    if( m_data.m_tcidu != ChartData::COLUMN_NONE )
    {
        cr->set_source_rgb( 1.0, 0.7, 0.7 );
        cr->set_dash( s_dash_pattern, 0 );
        cr->set_line_width( m_unit_line_thk * 2 );

        draw_line2( cr, true );

        cr->unset_dash();
    }
}
inline void
Chart::draw_line2( const Cairo::RefPtr< Cairo::Context >& cr, bool F_underlay )
{
    switch( m_data.get_type() )
    {
        case ChartData::TYPE_STRING:
            draw_line3( cr, m_data.values_str, F_underlay );
            break;
        case ChartData::TYPE_NUMBER:
            draw_line3( cr, m_data.values_num, F_underlay );
            break;
        case ChartData::TYPE_DATE:
            draw_line3( cr, m_data.values_date, F_underlay );
            break;
    }
}

inline void
Chart::draw_bars( const Cairo::RefPtr< Cairo::Context >& cr )
{
    cr->set_source_rgb( 0.7, 0.4, 0.4 );
    cr->set_line_join( Cairo::Context::LineJoin::BEVEL );

    draw_bars2( cr, false );

    // UNDERLAY
    if( m_data.m_tcidu != ChartData::COLUMN_NONE )
    {
        cr->set_source_rgb( 1.0, 0.7, 0.7 );
        cr->set_dash( s_dash_pattern, 0 );
        cr->set_line_width( m_unit_line_thk * 3 );

        draw_bars2( cr, true );

        cr->unset_dash();
    }
}
inline void
Chart::draw_bars2( const Cairo::RefPtr< Cairo::Context >& cr, bool F_underlay )
{
    switch( m_data.get_type() )
    {
        case ChartData::TYPE_STRING:
            draw_bars3( cr, m_data.values_str, F_underlay );
            break;
        case ChartData::TYPE_NUMBER:
            draw_bars3( cr, m_data.values_num, F_underlay );
            break;
        case ChartData::TYPE_DATE:
            draw_bars3( cr, m_data.values_date, F_underlay );
            break;
    }
}

inline void
Chart::draw_x_values_date( const Cairo::RefPtr< Cairo::Context >& cr )
{
    // YEAR & MONTH BAR
    const int   period          { m_data.get_period() };
    int         step_grid       { ( int ) ceil( m_width_col_min / m_step_x ) };
    int         step_grid_first { 0 };
    const auto  bar_top         { m_y_offset + m_height - m_ov_height - m_h_x_values };
    auto&&      it_date         { m_data.values_date.begin() };
    int         year_last       { 0 };

    std::advance( it_date, m_step_start );

    if( period != ChartData::PERIOD_YEARLY )
    {
        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;

        const auto sg_reduced{ step_grid > 12 ? 12 : step_grid };
        step_grid_first = ( sg_reduced - ( Date::get_month( it_date->first ) % sg_reduced ) + 1 )
                          % sg_reduced;
    }

    cr->rectangle( m_x_offset, bar_top, m_width, m_h_x_values );
    cr->set_source_rgb( 0.9, 0.9, 0.9 );
    cr->fill();

    m_layout->set_font_description( m_font_main );
    m_layout->set_alignment( Pango::Alignment::LEFT );

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

    std::advance( it_date, step_grid_first );

    for( int i = step_grid_first; ;  )
    {
        const DateV date{ it_date->first };

        if( m_data.get_style() == ChartData::STYLE_BARS )
            cr->move_to( m_x_offset + m_x_min + m_step_x * ( i + 0.5 ), bar_top );
        else
            cr->move_to( m_x_offset + m_x_min + m_step_x * i, bar_top );
        cr->rel_line_to( 0, m_label_height );
        cr->rel_move_to( m_border_label, -m_label_height );

        if( period == ChartData::PERIOD_YEARLY )
        {
            m_layout->set_text( Date::get_year_str( date ) );
            m_layout->show_in_cairo_context( cr );
        }
        else
        {
            if( step_grid < 12 )
            {
                if( period == ChartData::PERIOD_MONTHLY )
                    m_layout->set_text( Date::get_month_str( date ) );
                else // weekly
                    m_layout->set_text( Date::format_string( date, "MD" ) );
                m_layout->show_in_cairo_context( cr );
            }

            if( year_last != int( Date::get_year( date ) ) )
            {
                cr->rel_move_to( 0, m_label_height );
                m_layout->set_text( Date::get_year_str( date ) );
                m_layout->show_in_cairo_context( cr );
                year_last = Date::get_year( date );
            }
        }

        i += step_grid;
        if( i < ( int ) m_step_count )
            std::advance( it_date, step_grid );
        else
            break;
    }
}

inline void
Chart::draw_x_values( const Cairo::RefPtr< Cairo::Context >& cr )
{
    switch( m_data.get_type() )
    {
        case ChartData::TYPE_STRING: draw_x_values_str( cr ); break;
        case ChartData::TYPE_NUMBER: draw_x_values_num( cr ); break;
        case ChartData::TYPE_DATE:   draw_x_values_date( cr ); break;
    }

    cr->set_source_rgb( 0.7, 0.7, 0.7 );
    cr->stroke();
}

inline void
Chart::draw_y_levels( const Cairo::RefPtr< Cairo::Context >& cr )
{
    const auto  step_y { m_data.v_grid_step * m_coefficient };

    cr->set_source_rgb( 0.6, 0.6, 0.6 );
    cr->set_line_width( m_unit_line_thk );
    m_layout->set_font_description( m_font_main );
    m_layout->set_width( 150 * Pango::SCALE );
    m_layout->set_alignment( Pango::Alignment::LEFT );

    for( int i = 0; i < 5; ++i )
    {
        // horizontal lines:
        cr->move_to( m_x_offset + m_width, m_y_offset + m_y_max - i * step_y );
        cr->rel_line_to( -m_width, 0.0 );

        // labels:
        cr->rel_move_to( 4 * m_border_label, -m_label_size );
        m_layout->set_text( STR::format_number( m_data.v_grid_min + m_data.v_grid_step * i )
                            + " " + m_data.m_unit );
        m_layout->show_in_cairo_context( cr );

        cr->stroke(); // comes after label to reuse line coordinates
    }
}

inline void
Chart::draw_milestones( const Cairo::RefPtr< Cairo::Context >& cr )
{
    auto&&  it_vd   { m_data.values_date.begin() };
    std::advance( it_vd, m_step_start );
    DateV   date_b  { it_vd->first };
    std::advance( it_vd, m_step_count - 1 );
    DateV   date_e  { it_vd->first };
    double  day_x   { m_length / Date::calculate_days_between_abs( date_b, date_e ) };

    for( auto& vd : m_data.values_date )
    {
        for( auto& elem : vd.second.elems )
        {
            if( elem->is_entry() )
            {
                Entry* e{ dynamic_cast< Entry* >( elem ) };
                if( e->get_title_style() == VT::ETS::MILESTONE::I )
                {
                    Gdk::Cairo::set_source_rgba( cr, Color( e->get_color() ) );
                    cr->rectangle( m_x_offset + m_x_min
                                              + day_x * Date::calculate_days_between_abs(
                                                                date_b, e->get_date() ),
                                   m_y_offset,
                                   day_x * e->get_duration(),
                                   m_y_max );
                    cr->fill();
                }
            }
        }
    }
}

inline void
Chart::draw_tooltip( const Cairo::RefPtr< Cairo::Context >& cr )
{
    const bool      F_line      { m_data.get_style() == ChartData::STYLE_LINE };
    const Value     v_hov       { get_value_at( m_hovered_step + m_step_start ) };
    const double    tip_pt_x    { m_x_min + m_step_x * ( m_hovered_step + ( F_line ? 0 : 0.5 ) ) };
    const double    tip_pt_y    { m_y_max - m_coefficient * ( v_hov - m_data.v_grid_min ) };
    int             w, h;

    m_layout->set_font_description( m_font_bold );
    m_layout->set_text( STR::format_number( v_hov ) );
    m_layout->get_pixel_size( w, h );

    cr->set_source_rgb( 0.8, 0.5, 0.5 );
    cr->set_line_width( m_unit_line_thk );
    cr->move_to( m_x_offset, m_y_offset + tip_pt_y );
    cr->rel_line_to( m_width, 0.0 );
    if( F_line )
    {
        cr->move_to( m_x_offset + tip_pt_x, m_y_offset );
        cr->rel_line_to( 0.0, m_height - m_ov_height );
    }
    cr->stroke();

    if( F_line )
    {
        // Y RECT
        if( F_line ) // right aligned
            cr->rectangle( m_x_offset + tip_pt_x - w - 4,
                           m_y_offset + tip_pt_y - h - 4,
                           w + 4, h + 4 );
        else // centered
            cr->rectangle( m_x_offset + tip_pt_x - w/2.0 - 2,
                           m_y_offset + 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( F_line )
            cr->move_to( m_x_offset + tip_pt_x - w - 2, m_y_offset + tip_pt_y - h - 2 );
        else
            cr->move_to( m_x_offset + tip_pt_x - w/2.0, m_y_offset + tip_pt_y - h - 2 );
        m_layout->show_in_cairo_context( cr );

        // X RECT
        m_layout->set_text( get_x_label_at( m_step_start + m_hovered_step ) );
        m_layout->get_pixel_size( w, h );
        cr->rectangle( m_x_offset + tip_pt_x - w/2.0 - 2,
                       m_y_offset + 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( m_x_offset + tip_pt_x - w/2.0, m_y_offset + m_height - m_ov_height - h + 2 );
        m_layout->show_in_cairo_context( cr );
    }
}

inline void
Chart::draw_overview( const Cairo::RefPtr< Cairo::Context >& cr )
{
    // OVERVIEW REGION
    cr->set_source_rgb( 0.7, 0.7, 0.7 );
    cr->rectangle( m_x_offset, m_y_offset + m_height - m_ov_height, m_width, m_ov_height );
    cr->fill();

    if( m_F_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( m_x_offset + m_border_label + m_step_start * m_step_x_ov,
                    m_y_offset + 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::Context::LineJoin::BEVEL );
    cr->set_line_width( m_unit_line_thk * 2 );

    cr->move_to( m_x_offset + m_border_label,
                    m_y_offset + m_height
                            - m_border_label
                            - m_coeff_ov * ( get_value_at( 0 ) - m_data.v_min ) );
    for( int i = 1; i < m_data.m_span; ++i )
    {
        cr->rel_line_to( m_step_x_ov,
                            - m_coeff_ov * ( get_value_at( i ) - get_value_at( i - 1 ) ) );
    }
    cr->stroke();

    // DIVIDER
    if( m_F_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( m_x_offset + 1.0,
                    m_y_offset + m_height - m_ov_height,
                    m_width - 2.0, m_ov_height - 1.0 );
    cr->stroke();
}

bool
Chart::draw( const Cairo::RefPtr< Cairo::Context >& cr, int w, int h )
{
    // TODO: 3.2: we may consider using the supplied w and h
    // BACKGROUND
    if( !m_F_printing_mode )
    {
        cr->rectangle( m_x_offset, m_y_offset, 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_data.m_span < 2 )
    {
        cr->set_source_rgb( 0.0, 0.0, 0.0 );
        //auto layout = Pango::Layout::create( cr );
        m_layout->set_width( - 1 );
        m_layout->set_text( _( "INSUFFICIENT DATA" ) );
        m_layout->set_font_description( m_font_big );
        int w_layout, h_layout;
        m_layout->get_pixel_size( w_layout, h_layout );
        cr->move_to( m_x_offset + ( m_width - w_layout ) / 2.0 , m_y_offset + m_height / 2.0 );
        m_layout->show_in_cairo_context( cr );

        return true;
    }

    // NUMBER OF STEPS IN THE PRE AND POST BORDERS
    update_pre_and_post_steps();

    // ENTRY BACKGROUNDS
    if( m_data.get_type() == ChartData::TYPE_DATE )
        draw_milestones( cr );

    // YEAR & MONTH BAR
    draw_x_values( cr );

    // Y LEVELS
    draw_y_levels( cr );

    // GRAPH LINE OR BARS
    if( m_data.get_style() == ChartData::STYLE_LINE )
        draw_line( cr );
    else
        draw_bars( cr );
    // TODO: 3.2: add pie

    // TOOLTIP
    if( m_F_widget_hovered && m_hovered_step >= 0 )
        draw_tooltip( cr );

    // OVERVIEW
    if( m_step_count < m_data.m_span )
        draw_overview( cr );

    return true;
}
