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

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


using namespace LIFEO;


// DIARYELEMENT ====================================================================================
// STATIC MEMBERS
ListData::Colrec*                   ListData::colrec;
const Icon                          DiaryElement::s_pixbuf_null;
bool                                DiaryElement::FLAG_ALLOCATE_GUI_FOR_DIARY = true;
const Ustring                       DiaryElement::s_type_names[] =
{
    "", _( "Chapter Category" ), _( "Theme" ), _( "Filter" ), _( "Chart" ), _( "Table" ),
    _( "Diary" ), "Multiple Entries", _( "Entry" ), _( "Chapter" ),
    "Header"
};

DiaryElement::DiaryElement() : NamedElement( "" ) {}

DiaryElement::DiaryElement( Diary* const ptr2diary,
                            const Ustring& name,
                            ElemStatus status )
:   NamedElement( name ), m_ptr2diary( ptr2diary ),
    m_status( status ),
    m_id( ptr2diary ? ptr2diary->create_new_id( this ) :  DEID_UNSET )
{
    if( FLAG_ALLOCATE_GUI_FOR_DIARY )
        m_list_data = new ListData;
}

DiaryElement::DiaryElement( Diary* const ptr2diary, DEID id, ElemStatus status )
:   NamedElement( "" ), m_ptr2diary( ptr2diary ), m_status( status ), m_id( id )
{
    if( FLAG_ALLOCATE_GUI_FOR_DIARY )
        m_list_data = new ListData;
}

DiaryElement::~DiaryElement()
{
    if( m_ptr2diary != nullptr )
        m_ptr2diary->erase_id( m_id );

    if( m_list_data != nullptr )
        delete m_list_data;
}

void
DiaryElement::set_todo_status( ElemStatus s )
{
    m_status -= ( m_status & ES::FILTER_TODO );
    m_status |= s;
}

SI
DiaryElement::get_todo_status_si() const
{
    switch( get_todo_status() )
    {
        case ES::TODO:
            return SI::TODO;
        case ES::PROGRESSED:
            return SI::PROGRESSED;
        case ES::DONE:
            return SI::DONE;
        case ES::CANCELED:
            return SI::CANCELED;
        case ( ES::NOT_TODO | ES::TODO ):
        case ( ES::NOT_TODO | ES::PROGRESSED ):
        case ( ES::NOT_TODO | ES::DONE ):
        case ( ES::NOT_TODO | ES::CANCELED ):
            return SI::AUTO;
        case ES::NOT_TODO:
        default:    // should never be the case
            return SI::_NONE_;
    }
}

// PARAGRAPH =======================================================================================
bool
FuncCmpParagraphs::operator()( const Paragraph* l, const Paragraph* r ) const
{
    return( l->m_host == r->m_host ?
            l->m_para_no > r->m_para_no :
            l->m_host->get_id() > r->m_host->get_id() );
}

Paragraph*
Paragraph::get_prev() const
{
    if( !m_host || m_para_no == 0 )
        return nullptr;

    auto paragraphs{ m_host->get_paragraphs() };

    auto&& it_para{ paragraphs->begin() };
    std::advance( it_para, m_para_no-1 );

    return( *it_para );
}

Paragraph*
Paragraph::get_next() const
{
    if( !m_host )
        return nullptr;

    auto paragraphs{ m_host->get_paragraphs() };

    if( ( unsigned int ) m_para_no == ( paragraphs->size() - 1 ) )
        return nullptr;

    auto&& it_para{ paragraphs->begin() };
    std::advance( it_para, m_para_no+1 );

    return( *it_para );
}

Paragraph*
Paragraph::get_parent() const
{
    if( !m_host || m_para_no < 2 )
        return nullptr;

    auto&& it_para{ m_host->get_paragraphs()->begin() };
    std::advance( it_para, m_para_no-1 );

    for( ; ; it_para-- )
    {
        auto previous{ *it_para };

        if( previous->m_para_no == 0 )
            return nullptr;

        if( previous->m_heading_level > this->m_heading_level )
            return previous;

        if( previous->m_indentation_level < this->m_indentation_level && !previous->is_empty() )
            return previous;

    }

    throw LIFEO::Error( "Unexpected point reached while searching for parent para" );
    return nullptr;
}

bool
Paragraph::has_tag( const Entry* tag ) const
{
    return( m_tags.find( tag->get_name() ) != m_tags.end() );
}

bool
Paragraph::has_tag_broad( const Entry* tag ) const
{
    if( m_tags.find( tag->get_name() ) != m_tags.end() )
        return true;

    VecEntries&& children{ tag->get_descendants() };

    for( auto& child_tag : children )
    {
        if( has_tag_broad( child_tag ) ) // recursion
            return true;
    }

    auto parent_para = get_parent();
    if( parent_para )
        return parent_para->has_tag_broad( tag ); // recursion
    else
        return false;
}

Value
Paragraph::get_value_for_tag( const Entry* tag, int& count ) const
{
    if( !tag || !has_tag( tag ) )
        return 0.0;
    else
    {
        count++;
        return m_tags.get_value_for_tag( tag->get_name() );
    }
}

Value
Paragraph::get_value_planned_for_tag( const Entry* tag, int& count ) const
{
    if( !tag || !has_tag( tag ) )
        return 0.0;
    else
    {
        count++;
        return m_tags_planned.get_value_for_tag( tag->get_name() );
    }
}

Value
Paragraph::get_value_remaining_for_tag( const Entry* tag, int& count ) const
{
    if( !tag || !has_tag( tag ) )
        return 0.0;
    else
    {
        count++;
        return( m_tags_planned.get_value_for_tag( tag->get_name() ) -
                m_tags.get_value_for_tag( tag->get_name() ) );
    }
}

Value
Paragraph::get_value_for_tag( const ChartData* chart_data, int& count ) const
{
    if( chart_data->para_filter_tag && !has_tag_broad( chart_data->para_filter_tag ) )
        return 0.0;
    else
    if( !has_tag( chart_data->tag ) )
        return 0.0;
    else
    {
        count++;
        return m_tags.get_value_for_tag( chart_data->tag->get_name() );
    }
}

Value
Paragraph::get_value_planned_for_tag( const ChartData* chart_data, int& count ) const
{
    if( chart_data->para_filter_tag && !has_tag_broad( chart_data->para_filter_tag ) )
        return 0.0;
    else
    if( !has_tag( chart_data->tag ) )
        return 0.0;
    else
    {
        count++;
        return m_tags_planned.get_value_for_tag( chart_data->tag->get_name() );
    }
}

Entry*
Paragraph::get_sub_tag_first( const Entry* parent_tag ) const
{
    for( auto& tag_name : m_tags_in_order )
    {
        auto tag{ m_host->get_diary()->get_entry_by_name( tag_name ) };

        if( tag && Date::is_descendant_of( tag->get_date_t(), parent_tag->get_date_t() ) )
            return tag;
    }

    return nullptr;
}
Entry*
Paragraph::get_sub_tag_last( const Entry* parent_tag ) const
{
    for( auto iter = m_tags_in_order.rbegin(); iter != m_tags_in_order.rend(); iter++ )
    {
        auto tag{ m_host->get_diary()->get_entry_by_name( *iter ) };

        if( tag && Date::is_descendant_of( tag->get_date_t(), parent_tag->get_date_t() ) )
            return tag;
    }

    return nullptr;
}
Entry*
Paragraph::get_sub_tag_lowest( const Entry* parent ) const
{
    Entry* sub_tag_lowest{ nullptr };

    if( parent != nullptr )
    {
        for( auto& tag_name : m_tags_in_order )
        {
            auto sub_tag{ m_host->get_diary()->get_entry_by_name( tag_name ) };
            if( sub_tag && Date::is_descendant_of( sub_tag->get_date_t(), parent->get_date_t() ) )
            {
                if( sub_tag_lowest )
                {
                    if( sub_tag->get_date_t() < sub_tag_lowest->get_date_t() )
                        sub_tag_lowest = sub_tag;
                }
                else
                    sub_tag_lowest = sub_tag;
            }
        }
    }

    return sub_tag_lowest;
}
Entry*
Paragraph::get_sub_tag_highest( const Entry* parent ) const
{
    Entry* sub_tag_highest{ nullptr };

    if( parent != nullptr )
    {
        for( auto& tag_name : m_tags_in_order )
        {
            auto sub_tag{ m_host->get_diary()->get_entry_by_name( tag_name ) };
            if( sub_tag && Date::is_descendant_of( sub_tag->get_date_t(), parent->get_date_t() ) )
            {
                if( sub_tag_highest )
                {
                    if( sub_tag->get_date_t() > sub_tag_highest->get_date_t() )
                        sub_tag_highest = sub_tag;
                }
                else
                    sub_tag_highest = sub_tag;
            }
        }
    }

    return sub_tag_highest;
}

double
Paragraph::get_completion() const
{
    if( m_host == nullptr ) return 0.0;

    const double wl{ get_workload() };

    if( wl == 0.0 )
        return 0.0;

    return( get_completed() / wl );
}

double
Paragraph::get_completed() const
{
    if( m_host == nullptr ) return 0.0;

    int c{ 0 }; // dummy
    Entry* tag_comp{ m_host->get_diary()->get_completion_tag() };

    if( tag_comp == nullptr )
        return 0.0;

    return get_value_for_tag( tag_comp, c );
}

double
Paragraph::get_workload() const
{
    if( m_host == nullptr ) return 0.0;

    int c{ 0 }; // dummy
    Entry* tag_comp{ m_host->get_diary()->get_completion_tag() };

    if( tag_comp == nullptr )
        return 0.0;

    return get_value_planned_for_tag( tag_comp, c );
}

date_t
Paragraph::get_date_broad() const
{
    if( m_date != Date::NOT_SET )
        return m_date;
    else
    if( m_host )
        return m_host->get_date_t();

    return Date::NOT_SET;
}

void
Paragraph::update_per_text()
{
    m_indentation_level = 0;
    for( UstringSize i = 0; i < m_text.length() && m_text[ i ] == '\t'; i++ )
        m_indentation_level++;
}

// COORDS ==========================================================================================
double
Coords::get_distance( const Coords& p1, const Coords& p2 )
{
#if __GNUC__ > 9
    const static double D{ 6371 * 2 } ;      // mean diameter of Earth in kilometers
    const static double to_rad{ HELPERS::PI/180 }; // degree to radian conversion multiplier
    const double φ1{ p1.latitude * to_rad };  // in radians
    const double φ2{ p2.latitude * to_rad };  // in radians
    const double Δφ{ φ2 - φ1 };
    const double Δλ{ ( p2.longitude - p1.longitude ) * to_rad };

    const double a = pow( sin( Δφ / 2 ), 2 ) + cos( φ1 ) * cos( φ2 ) * pow( sin( Δλ / 2 ), 2 );

    return( D * atan2( sqrt( a ), sqrt( 1 - a ) ) );
    // per Wikipedia article about haversine formula, asin( sqrt( a ) ) should also work
#else // Unicode identifier support was added in GCC 10!
    const static double D{ 6371 * 2 } ;
    const static double to_rad{ HELPERS::PI/180 };
    const double phi1{ p1.latitude * to_rad };
    const double phi2{ p2.latitude * to_rad };
    const double dp{ phi2 - phi1 };
    const double dl{ ( p2.longitude - p1.longitude ) * to_rad };

    const double a = pow( sin( dp / 2 ), 2 ) + cos( phi1 ) * cos( phi2 ) * pow( sin( dl / 2 ), 2 );

    return( D * atan2( sqrt( a ), sqrt( 1 - a ) ) );
#endif
}

// NAME AND VALUE ==================================================================================
NameAndValue
NameAndValue::parse( const Ustring& text )
{
    NameAndValue nav;
    char lf{ '=' }; // =, \, #, $(unit)
    int divider{ 0 };
    int trim_length{ 0 };
    int trim_length_unit{ 0 };
    bool negative{ false };
    Wchar c;

    for( Ustring::size_type i = 0; i < text.size(); i++ )
    {
        c = text.at( i );
        switch( c )
        {
            case '\\':
                if( lf == '#' || lf == '$' )
                {
                    nav.unit += c;
                    trim_length_unit = 0;
                    lf = '$';
                }
                else if( lf == '\\' )
                {
                    nav.name += c;
                    trim_length = 0;
                    lf = '=';
                }
                else // i.e. ( lf == '=' )
                    lf = '\\';
                break;
            case '=':
                if( nav.name.empty() || lf == '\\' )
                {
                    nav.name += c;
                    trim_length = 0;
                    lf = '=';
                }
                else if( lf == '#' || lf == '$' )
                {
                    nav.unit += c;
                    trim_length_unit = 0;
                    lf = '$';
                }
                else // i.e. ( lf == '=' )
                {
                    nav.status |= NameAndValue::HAS_EQUAL;
                    lf = '#';
                }
                break;
            case ' ':
            case '\t':
                // if( lf == '#' ) just ignore
                if( lf == '=' || lf == '\\' )
                {
                    if( !nav.name.empty() ) // else ignore
                    {
                        nav.name += c;
                        trim_length++;
                    }
                }
                else if( lf == '$' )
                {
                    nav.unit += c;
                    trim_length_unit++;
                }
                break;
            case ',':
            case '.':
                if( divider || lf == '$' ) // note that if divider, lf must be #
                {
                    nav.unit += c;
                    trim_length_unit = 0;
                    lf = '$';
                }
                else if( lf == '#' )
                    divider = 1;
                else
                {
                    nav.name += c;
                    trim_length = 0;
                    lf = '=';
                }
                break;
            case '-':
                if( negative || lf == '$' ) // note that if negative, lf must be #
                {
                    nav.unit += c;
                    trim_length_unit = 0;
                    lf = '$';
                }
                else if( lf == '#' )
                    negative = true;
                else
                {
                    nav.name += c;
                    trim_length = 0;
                    lf = '=';
                }
                break;
            case '0': case '1': case '2': case '3': case '4':
            case '5': case '6': case '7': case '8': case '9':
                if( lf == '#' )
                {
                    nav.status |= NameAndValue::HAS_VALUE;
                    nav.value *= 10;
                    nav.value += ( c - '0' );
                    if( divider )
                        divider *= 10;
                }
                else if( lf == '$' )
                {
                    nav.unit += c;
                    trim_length_unit = 0;
                }
                else
                {
                    nav.name += c;
                    trim_length = 0;
                    lf = '='; // reset ( lf == \ ) case
                }
                break;
            default:
                if( lf == '#' || lf == '$' )
                {
                    nav.unit += c;
                    trim_length_unit = 0;
                    lf = '$';
                }
                else
                {
                    nav.name += c;
                    trim_length = 0;
                    lf = '=';
                }
                break;
        }
    }

    if( lf == '$' )
        nav.status |= ( NameAndValue::HAS_NAME | NameAndValue::HAS_UNIT );
    else if( ! nav.name.empty() )
        nav.status |= NameAndValue::HAS_NAME;

    if( trim_length )
        nav.name.erase( nav.name.size() - trim_length, trim_length );
    if( trim_length_unit )
        nav.unit.erase( nav.unit.size() - trim_length_unit, trim_length_unit );

    if( lf == '=' && ! nav.name.empty() ) // implicit boolean tag
        nav.value = 1;
    else
    {
        if( divider > 1 )
            nav.value /= divider;
        if( negative )
            nav.value *= -1;
    }

    PRINT_DEBUG( "tag parsed | name: ", nav.name, "; value: ", nav.value, "; unit: ", nav.unit );

    return nav;
}

// THEMES ==========================================================================================
// STATIC MEMBERS
const Color Theme::s_color_match1( "#33FF33" );
const Color Theme::s_color_match2( "#009900" );
const Color Theme::s_color_link1( "#6666FF" );
const Color Theme::s_color_link2( "#000099" );
const Color Theme::s_color_broken1( "#FF3333" );
const Color Theme::s_color_broken2( "#990000" );

const Color Theme::s_color_todo( "#FF0000" );
const Color Theme::s_color_progressed( "#FF8811" );
const Color Theme::s_color_done( "#66BB00" );
const Color Theme::s_color_canceled( "#AA8855" );

Theme::Theme( Diary* const d, const Ustring& name )
:   DiaryElement( d, name )
{
}

Theme::Theme( Diary* const d,
              const Ustring& name,
              const Ustring& str_font,
              const std::string& str_base,
              const std::string& str_text,
              const std::string& str_heading,
              const std::string& str_subheading,
              const std::string& str_highlight )
:   DiaryElement( d, name ),
    font( str_font ), color_base( str_base ), color_text( str_text ),
    color_heading( str_heading ), color_subheading( str_subheading ),
    color_highlight( str_highlight )
{
    calculate_derived_colors();
}

Theme::Theme( Diary* const d, const Ustring& name, const Theme* theme )
:   DiaryElement( d, name ),
    font( theme->font ),
    color_base( theme->color_base ),
    color_text( theme->color_text ),
    color_heading( theme->color_heading ),
    color_subheading( theme->color_subheading ),
    color_highlight( theme->color_highlight )
{
    calculate_derived_colors();
}

bool
Theme::is_default() const
{
    return( m_ptr2diary ? ( m_ptr2diary->get_theme_default() == this ) : false );
}

void
Theme::copy_to( Theme* target ) const
{
    target->font = font;
    target->color_base = color_base;
    target->color_text = color_text;
    target->color_heading = color_heading;
    target->color_subheading = color_subheading;
    target->color_highlight = color_highlight;

    target->calculate_derived_colors();
}

void
Theme::calculate_derived_colors()
{
    m_color_subsubheading = midtone( color_base, color_subheading, 0.8 );
    m_color_inline_tag =    midtone( color_base, color_highlight, 0.2 );
    m_color_mid =           midtone( color_base, color_text );
    m_color_region_bg =     midtone( color_base, color_text, 0.1 );
    m_color_match_bg =      contrast2( color_base, s_color_match1, s_color_match2 );
    m_color_link =          contrast2( color_base, s_color_link1, s_color_link2 );
    m_color_link_broken =   contrast2( color_base, s_color_broken1, s_color_broken2 );

    // TODO: we may change the coefficients below depending on the difference between the...
    // ... contrasting colors using get_color_diff( Theme::s_color_done, theme->color_base )...
    // ... generally, when get_color_diff is < 1.0 contrast is not satisfactory
    m_color_open =          midtone( s_color_todo, color_text );
    m_color_open_bg =       midtone( s_color_todo, color_base, 0.7 );

    m_color_progressed =    midtone( s_color_progressed, color_text );
    m_color_progressed_bg = midtone( s_color_progressed, color_base, 0.7 );

    m_color_done =          midtone( s_color_done, color_text );
    m_color_done_text =     midtone( s_color_done, color_text, 0.7 );
    m_color_done_bg =       midtone( s_color_done, color_base, 0.7 );

    m_color_canceled =      midtone( s_color_canceled, color_text );
    m_color_canceled_bg =   midtone( s_color_canceled, color_base, 0.7 );
}

ThemeSystem::ThemeSystem( const Ustring& f,
                          const std::string& cb,
                          const std::string& ct,
                          const std::string& ch,
                          const std::string& csh,
                          const std::string& chl )
:   Theme( nullptr, "Lifeograph", f, cb, ct, ch, csh, chl )
{
}

ThemeSystem*
ThemeSystem::get()
{
    static ThemeSystem *s_theme{ nullptr };

    if( s_theme == nullptr )
        s_theme = new ThemeSystem( "Sans 10", "white", "black", "#B72525", "#963F3F", "#FFBBBB" );

    return s_theme;
}

// CHARTS ==========================================================================================
void
ChartData::clear()
{
    type = 0;
    tag = nullptr;
    para_filter_tag = nullptr;
    filter = nullptr;
    unit.clear();
}

std::string
ChartData::get_as_string() const
{
    std::string chart_def;

    switch( type & Y_AXIS_MASK )
    {
        case COUNT: // entry count
            chart_def += "Gyc\n";
            break;
        case TEXT_LENGTH: // text length
            chart_def += "Gyl\n";
            break;
        case MAP_PATH_LENGTH: // map path length
            chart_def += "Gym\n";
            break;
        case TAG_VALUE_ENTRY: // tag value
            chart_def += STR::compose( "Gyt", tag ? tag->get_id() : DEID_UNSET, '\n' ) ;
            chart_def += STR::compose( "Gp", para_filter_tag ? para_filter_tag->get_id() :
                                                               DEID_UNSET, '\n' ) ;
            break;
        case TAG_VALUE_PARA: // tag value
            chart_def += STR::compose( "Gyp", tag ? tag->get_id() : DEID_UNSET, '\n' ) ;
            chart_def += STR::compose( "Gp", para_filter_tag ? para_filter_tag->get_id() :
                                                               DEID_UNSET, '\n' ) ;
            break;
    }

    if( filter )
        chart_def += ( "Gf" + filter->get_name() + '\n' );

    chart_def += STR::compose( "Go", ( type & TAGGED_ONLY ) ? 'T' : '-' );

    switch( type & UNDERLAY_MASK )
    {
        case UNDERLAY_PREV_YEAR:    chart_def += 'Y'; break;
        case UNDERLAY_PLANNED:      chart_def += 'P'; break;
        case 0:                     chart_def += '_'; break;
    }
    switch( type & PERIOD_MASK )
    {
        case WEEKLY:                chart_def += 'W'; break;
        case MONTHLY:               chart_def += 'M'; break;
        case YEARLY:                chart_def += 'Y'; break;
    }
    switch( type & VALUE_TYPE_MASK )
    {
        case CUMULATIVE_PERIODIC:   chart_def += 'P'; break;
        case CUMULATIVE_CONTINUOUS: chart_def += 'C'; break;
        case AVERAGE:               chart_def += 'A'; break;
    }

    return chart_def;
}

void
ChartData::set_from_string( const Ustring& chart_def )
{
    std::string             line( "" );
    std::string::size_type  line_offset{ 0 };

    clear();

    while( STR::get_line( chart_def, line_offset, line ) )
    {
        if( line.size() < 2 )   // should never occur
            continue;

        switch( line[ 1 ] )
        {
            case 'y':   // y axis
            {
                switch( line[ 2 ] )
                {
                    case 'c':   // count
                        type |= COUNT;
                        break;
                    case 'l':   // text length
                        type |= TEXT_LENGTH;
                        break;
                    case 'm':   // map path length
                        type |= MAP_PATH_LENGTH;
                        break;
                    case 't':   // tag value
                        type |= ChartData::TAG_VALUE_ENTRY;
                        tag = m_ptr2diary->get_entry_by_id( std::stoul( line.substr( 3 ) ) );
                        break;
                    case 'p':   // tag value
                        type |= ChartData::TAG_VALUE_PARA;
                        tag = m_ptr2diary->get_entry_by_id( std::stoul( line.substr( 3 ) ) );
                        break;
                }
                break;
            }
            case 'p': // para filter tag
                para_filter_tag = m_ptr2diary->get_entry_by_id( std::stoul( line.substr( 2 ) ) );
                break;
            case 'f':   // filter
                filter = m_ptr2diary->get_filter( line.substr( 2 ) );
                break;
            case 'o':
                if( line[ 2 ] == 'T' )
                    type |= ChartData::TAGGED_ONLY;

                switch( line[ 3 ] )
                {
                    case 'Y': type |= ChartData::UNDERLAY_PREV_YEAR; break;
                    case 'P': type |= ChartData::UNDERLAY_PLANNED; break;
                }
                switch( line[ 4 ] )
                {
                    case 'W': type |= WEEKLY; break;
                    case 'M': type |= MONTHLY; break;
                    case 'Y': type |= YEARLY; break;
                }
                switch( line[ 5 ] )
                {
                    case 'P': type |= CUMULATIVE_PERIODIC; break;
                    case 'C': type |= CUMULATIVE_CONTINUOUS; break;
                    case 'A': type |= AVERAGE; break;
                }
                break;
            default:
                PRINT_DEBUG( "Unrecognized chart string: ", line );
                break;
        }
    }

    refresh_unit();
}

unsigned int
ChartData::calculate_distance( const date_t d1, const date_t d2 ) const
{
    switch( type & PERIOD_MASK )
    {
        case WEEKLY:
            return Date::calculate_weeks_between( d1, d2 );
        case MONTHLY:
            return Date::calculate_months_between( d1, d2 );
        case YEARLY:
        default:
            return labs( int( Date::get_year( d1 ) ) - int( Date::get_year( d2 ) ) );
    }
}

void
ChartData::forward_date( date_t& date, unsigned int n ) const
{
    switch( type & PERIOD_MASK )
    {
        case WEEKLY:
            Date::forward_days( date, 7 * n );
            break;
        case MONTHLY:
            Date::forward_months( date, n );
            break;
        case YEARLY:
            Date::forward_months( date, 12 * n );
            break;
    }
}

void
ChartData::clear_points()
{
    values.clear();
    values_plan.clear();
    dates.clear();
    counts.clear();
    chapters.clear();

    v_min = std::numeric_limits< double >::max();
    v_max = -std::numeric_limits< double >::max();
    v_plan_min = std::numeric_limits< double >::max();
    v_plan_max = -std::numeric_limits< double >::max();
}

void
ChartData::add_value( const date_t date, const Value value, const Value value_plan )
{
    if( values.empty() )
        push_back_value( value, value_plan );
    else
    {
        Value v_last{ values.back() };
        Value v_plan_last{ values_plan.back() };

        if( date == dates.back() )
        {
            switch( get_type() )
            {
                case ChartData::CUMULATIVE_PERIODIC:
                case ChartData::CUMULATIVE_CONTINUOUS:
                    values.pop_back();
                    values_plan.pop_back();
                    push_back_value( v_last + value, v_plan_last + value_plan );
                    counts.back()++;
                    break;
                case ChartData::AVERAGE:
                    v_last *= counts.back();
                    v_last += value;
                    v_plan_last *= counts.back();
                    v_plan_last += value_plan;
                    counts.back()++;
                    values.pop_back();
                    values_plan.pop_back();
                    push_back_value( v_last / counts.back(), v_plan_last / counts.back() );
                    break;
            }
            return;
        }
        else
        {
            const auto steps_between{ calculate_distance( date, dates.back() ) };
            const Value value_offset{ ( value - v_last ) / steps_between };
            const Value value_plan_offset{ ( value_plan - v_plan_last ) / steps_between };
            date_t date_inter{ dates.back() };

            for( unsigned int i = 1; i < steps_between; i++ )
            {
                forward_date( date_inter, 1 );
                switch( get_type() )
                {
                    case ChartData::CUMULATIVE_PERIODIC:
                        push_back_value( 0.0, 0.0 );
                        break;
                    case ChartData::CUMULATIVE_CONTINUOUS:
                        push_back_value( v_last, v_plan_last );
                        break;
                    case ChartData::AVERAGE:
                        push_back_value( v_last + ( i * value_offset ),
                                         v_plan_last + ( i * value_plan_offset ) );
                        break;
                }

                dates.push_back( date_inter );
                counts.push_back( 0 );
            }
        }
        if( get_type() == ChartData::CUMULATIVE_CONTINUOUS )
            push_back_value( value + v_last, value_plan + v_plan_last );
        else
            push_back_value( value, value_plan );
    }

    dates.push_back( date );
    counts.push_back( 1 );
}

void
ChartData::update_min_max()
{
    for( auto& v : values )
    {
        if( v < v_min )
            v_min = v;
        if( v > v_max )
            v_max = v;
    }

    for( auto& v : values_plan )
    {
        if( v < v_plan_min )
            v_plan_min = v;
        if( v > v_plan_max )
            v_plan_max = v;
    }
}

void
ChartData::set_type_sub( int t )
{
    switch( t )
    {
        case TAGGED_ONLY:
            type |= TAGGED_ONLY;
            break;
        case -TAGGED_ONLY:
            type &= ( ~TAGGED_ONLY );
            break;
        default:
            if( 0 == ( t & PERIOD_MASK ) )          t |= ( type & PERIOD_MASK );
            if( 0 == ( t & VALUE_TYPE_MASK ) )      t |= ( type & VALUE_TYPE_MASK );
            if( 0 == ( t & Y_AXIS_MASK ) )          t |= ( type & Y_AXIS_MASK );
            if( 0 == ( t & UNDERLAY_MASK ) )        t |= ( type & UNDERLAY_MASK );
            if( 0 == ( t & TAGGED_ONLY ) )          t |= ( type & TAGGED_ONLY );

            type = t;
            break;
    }

    refresh_unit();
}

void
ChartData::refresh_unit()
{
    switch( type & Y_AXIS_MASK )
    {
        case MAP_PATH_LENGTH:
            unit = ( Lifeograph::settings.use_imperial_units ? "mi" : "km" );
            break;
        case TAG_VALUE_ENTRY:
        case TAG_VALUE_PARA:
            unit = ( tag ? tag->get_unit() : "" );
            break;
        default:
            unit.clear();
    }
}

const Ustring ChartElem::DEFINITION_DEFAULT{ "Gyc\nGo--MP" };
const Ustring ChartElem::DEFINITION_DEFAULT_Y{ "Gyc\nGo--YP" };

// TABLES ==========================================================================================
bool
TableColumn::is_numeric() const
{
    switch( m_type )
    {
        case TCT_COUNT:
        case TCT_TAG_V:
        case TCT_COMPLETION:
        case TCT_PATH_LENGTH:
            return true;
        case TCT_SUB:
            return m_flag_conditional;
        case TCT_NAME:
        case TCT_DATE:
        default:
            return false;
    }
}

//bool
//TableColumn::is_fraction() const
//{
//    switch( m_type )
//    {
//        case TCT_COMPLETION:
//            return true;
//        default:
//            return false;
//    }
//}

bool
TableColumn::is_percentage() const
{
    switch( m_type )
    {
        case TCT_COMPLETION:
            return true;
        default:
            return false;
    }
}

Value
TableColumn::get_entry_v_num( const Entry* entry ) const
{
    switch( m_type )
    {
        case TCT_COUNT:
            return 1;
        case TCT_COMPLETION:
            return entry->get_completed();
        case TCT_PATH_LENGTH:
            return entry->get_map_path_length();
        case TCT_TAG_V:
            if( m_p2tag )
            {
                const auto  f_avg{ bool( m_value_type & VT_AVERAGE ) };
                const Value v
                {
                    ( m_value_type & VT_REMAINING ) ?
                        entry->get_value_remaining_for_tag( m_p2tag, f_avg ) :
                        ( m_value_type & VT_PLANNED ) ?
                            entry->get_value_planned_for_tag( m_p2tag, f_avg ) :
                            entry->get_value_for_tag( m_p2tag, f_avg )
                };

                if( m_flag_conditional )
                {
                    bool ret_value{ false };
                    switch( m_condition_comparison_lo )
                    {
                        case TableColumn::C_LESS:
                            ret_value = ( m_condition_num_lo < v ? true : false );
                            break;
                        case TableColumn::C_LESS_OR_EQUAL:
                            ret_value = ( m_condition_num_lo <= v ? true : false );
                            break;
                        default: // IGNORE
                            ret_value = true;
                            break;
                    }

                    if( ret_value == false )
                        return 0.0;

                    switch( m_condition_comparison_hi )
                    {
                        case C_EQUAL:         return( v == m_condition_num_hi ? 1.0 : 0.0 );
                        case C_LESS:          return( v < m_condition_num_hi ? 1.0 : 0.0 );
                        case C_LESS_OR_EQUAL: return( v <= m_condition_num_hi ? 1.0 : 0.0 );
                        default:              break; // IGNORE
                    }
                    return 1.0;
                }
                else // not conditional
                    return v;
            }
            else
                return 0.0;
        case TCT_SUB:
            if( m_p2tag && m_flag_conditional )
            {
                Entry* tag_sub{ nullptr };
                switch( m_value_type )
                {
                    case VT_FIRST:   tag_sub = entry->get_sub_tag_first( m_p2tag ); break;
                    case VT_LAST:    tag_sub = entry->get_sub_tag_last( m_p2tag ); break;
                    case VT_LOWEST:  tag_sub = entry->get_sub_tag_lowest( m_p2tag ); break;
                    case VT_HIGHEST: tag_sub = entry->get_sub_tag_highest( m_p2tag ); break;
                    default:         break;
                }

                if( tag_sub )
                {
                    if( m_condition_sub_filter &&
                        m_condition_sub_filter->get_filterer_stack()->filter( tag_sub ) )
                        return 1;
                }
                else if( m_condition_sub_filter == nullptr )
                    return 1;

            }
            return 0.0;
        default:
            throw LIFEO::Error( "Illegal request!" );
    }

    return 0.0;
}

Value
TableColumn::get_entry_weight( const Entry* entry ) const
{
    switch( m_type )
    {
        case TCT_COMPLETION:
            return entry->get_workload();
        default:
            return 0.0;
    }
}

Ustring
TableColumn::get_entry_v_txt( const Entry* entry ) const
{
    switch( m_type )
    {
        case TCT_NAME:
            return entry->get_name();
        case TCT_DATE:
            return entry->get_date().format_string();
        case TCT_SUB:
            if( m_p2tag && not( m_flag_conditional ) )
            {
                Entry* tag_sub{ nullptr };
                switch( m_value_type )
                {
                    case VT_FIRST:   tag_sub = entry->get_sub_tag_first( m_p2tag ); break;
                    case VT_LAST:    tag_sub = entry->get_sub_tag_last( m_p2tag ); break;
                    case VT_LOWEST:  tag_sub = entry->get_sub_tag_lowest( m_p2tag ); break;
                    case VT_HIGHEST: tag_sub = entry->get_sub_tag_highest( m_p2tag ); break;
                    default:         break;
                }
                if( tag_sub )
                    return tag_sub->get_name();
            }
            return "";
        default:
            break;
    }
    // numeric
    return STR::compose( get_entry_v_num( entry ) );
}

Value
TableColumn::get_para_v_num( const Paragraph* para ) const
{
    switch( m_type )
    {
        case TCT_COUNT:
            return 1.0;
        case TCT_COMPLETION:
            return para->get_completed();
        case TCT_PATH_LENGTH:
            return 0.0; // not applicable for now, in the future we may associate paths with paras
        case TCT_TAG_V:
            if( m_p2tag )
            {
                int         c{0}; // dummy;
                const Value v
                {
                    ( m_value_type & VT_REMAINING ) ?
                        para->get_value_remaining_for_tag( m_p2tag, c ) :
                        ( m_value_type & VT_PLANNED ) ?
                            para->get_value_planned_for_tag( m_p2tag, c ) :
                            para->get_value_for_tag( m_p2tag, c )
                };

                if( m_flag_conditional )
                {
                    bool ret_value{ false };
                    switch( m_condition_comparison_lo )
                    {
                        case TableColumn::C_LESS:
                            ret_value = ( m_condition_num_lo < v ? true : false );
                            break;
                        case TableColumn::C_LESS_OR_EQUAL:
                            ret_value = ( m_condition_num_lo <= v ? true : false );
                            break;
                        default: // IGNORE
                            ret_value = true;
                            break;
                    }

                    if( ret_value == false )
                        return 0.0;

                    switch( m_condition_comparison_hi )
                    {
                        case C_EQUAL:         return( v == m_condition_num_hi ? 1.0 : 0.0 );
                        case C_LESS:          return( v < m_condition_num_hi ? 1.0 : 0.0 );
                        case C_LESS_OR_EQUAL: return( v <= m_condition_num_hi ? 1.0 : 0.0 );
                        default:              break; // IGNORE
                    }
                    return 1.0;
                }
                else // not conditional
                    return v;
            }
            return 0.0;
        case TCT_SUB:
            if( m_p2tag && m_flag_conditional )
            {
                Entry* tag_sub{ nullptr };
                switch( m_value_type )
                {
                    case VT_FIRST:   tag_sub = para->get_sub_tag_first( m_p2tag ); break;
                    case VT_LAST:    tag_sub = para->get_sub_tag_last( m_p2tag ); break;
                    case VT_LOWEST:  tag_sub = para->get_sub_tag_lowest( m_p2tag ); break;
                    case VT_HIGHEST: tag_sub = para->get_sub_tag_highest( m_p2tag ); break;
                    default:         break;
                }
                if( tag_sub )
                {
                    if( m_condition_sub_filter &&
                        m_condition_sub_filter->get_filterer_stack()->filter( tag_sub ) )
                        return 1.0;
                }
                else if( m_condition_sub_filter == nullptr )
                    return 1.0;
            }
            return 0.0;
        default:
            throw LIFEO::Error( "Illegal request!" );
    }

    return 0.0;
}

Value
TableColumn::get_para_weight( const Paragraph* para ) const
{
    return( m_type == TCT_COMPLETION ? para->get_workload() : 0.0 );
}

Ustring
TableColumn::get_para_v_txt( const Paragraph* para ) const
{
    switch( m_type )
    {
        case TCT_NAME:
            return( para->m_host ? para->m_host->get_name() : para->get_text() );
        case TCT_DATE:
            return Date::format_string( para->get_date_broad() );
        case TCT_SUB:
            if( m_p2tag && not( m_flag_conditional ) )
            {
                Entry* tag_sub{ nullptr };
                switch( m_value_type )
                {
                    case VT_FIRST:   tag_sub = para->get_sub_tag_first( m_p2tag ); break;
                    case VT_LAST:    tag_sub = para->get_sub_tag_last( m_p2tag ); break;
                    case VT_LOWEST:  tag_sub = para->get_sub_tag_lowest( m_p2tag ); break;
                    case VT_HIGHEST: tag_sub = para->get_sub_tag_highest( m_p2tag ); break;
                    default:         break;
                }
                if( tag_sub )
                    return tag_sub->get_name();
            }
            break;
        default:
            break;
    }
    return "";
}

// TABLE LINE
void
TableLine::add_col_v_num( const int i, Value v, Value w )
{
    for( int i2 = m_values_num.size(); i2 <= i; i2++ )
        m_values_num.push_back( 0.0 );
    for( int i2 = m_weights.size(); i2 <= i; i2++ )
        m_weights.push_back( 0.0 );

    m_values_num[ i ] += v;
    m_weights[ i ] += w;
}

void
TableLine::add_col_v_txt( int i, const Ustring& v )
{
    for( int i2 = m_values_txt.size(); i2 <= i; i2++ )
        m_values_txt.push_back( "" );

    if( m_values_txt[ i ] != v ) // do not repeatedly add same value
    {
        if( m_values_txt[ i ].empty() )
            m_values_txt[ i ] = v;
        else
            m_values_txt[ i ] += ( "; " + v );
    }
}

Value
TableLine::get_col_v_num( int i ) const
{
    if( i >= int( m_values_num.size() ) )
        return 0.0;
    else if( m_weights[ i ] == 0.0 ) // 0.0 means not-a-fraction
        return m_values_num[ i ];
    else
        return( m_values_num[ i ] / m_weights[ i ] );
}

Ustring
TableLine::get_col_v_txt( int i ) const
{
    if( i >= int( m_values_txt.size() ) )
        return "";
    else
        return m_values_txt[ i ];
}

// TABLE DATA
void
TableData::clear()
{
    m_filter = nullptr;
    m_tag_filter = nullptr;
    m_flag_group_by_first = false;
    m_flag_para_based = false;

    clear_lines();

    for( auto& col : m_columns ) delete col;
    m_columns.clear();

    m_i_col_sort = -1;
    m_f_sort_desc = false;
}

void
TableData::clear_lines()
{
    m_lines_unsorted.clear();

    if( m_lines_sorted )
    {
        for( auto& kv_line : m_lines_unsorted ) delete kv_line.second;
        delete m_lines_sorted;
        m_lines_sorted = nullptr;
    }

    if( m_line_total )
    {
        delete m_line_total;
        m_line_total = nullptr;
    }
}

std::string
TableData::get_as_string() const
{
    std::string table_def;

    for( auto& col : m_columns )
    {
        table_def += STR::compose( "Mcn", col->get_name(), '\n' );

        table_def += "Mco"; // options

        switch( col->get_type() )
        {
            case TableColumn::TCT_NAME:        table_def += 'N'; break;
            case TableColumn::TCT_DATE:        table_def += 'D'; break;
            case TableColumn::TCT_COUNT:       table_def += 'C'; break;
            case TableColumn::TCT_SUB:         table_def += 'S'; break;
            case TableColumn::TCT_TAG_V:       table_def += 'T'; break;
            case TableColumn::TCT_COMPLETION:  table_def += 'P'; break;
            case TableColumn::TCT_PATH_LENGTH: table_def += 'L'; break;
        }

        switch( col->get_value_type() )
        {
            case TableColumn::VT_FIRST:           table_def += 'F'; break;
            case TableColumn::VT_LAST:            table_def += 'L'; break;
            case TableColumn::VT_LOWEST:          table_def += 'O'; break;
            case TableColumn::VT_HIGHEST:         table_def += 'H'; break;
            case TableColumn::VT_TOTAL_REALIZED:  table_def += 'R'; break;
            case TableColumn::VT_TOTAL_PLANNED:   table_def += 'P'; break;
            case TableColumn::VT_TOTAL_REMAINING: table_def += 'B'; break; // B for balance
            case TableColumn::VT_AVG_REALIZED:    table_def += 'r'; break;
            case TableColumn::VT_AVG_PLANNED:     table_def += 'p'; break;
            case TableColumn::VT_AVG_REMAINING:   table_def += 'b'; break;
            default: break;
        }

        table_def += ( col->get_conditional() ? 'C' : '-' );

        table_def += '\n'; // end of options

        if( col->get_tag() )
            table_def += STR::compose( "Mct", col->get_tag()->get_id(), '\n' );

        if( col->get_condition_sub_filter() != nullptr )
            table_def += STR::compose( "Mccf", col->get_condition_sub_filter()->get_name(), '\n' );

        table_def += "Mccl"; // numeric condition lo

        switch( col->get_condition_rel_lo() )
        {
            case TableColumn::C_LESS:
                table_def += STR::compose( '<', col->get_condition_num_lo(), '\n' );
                break;
            case TableColumn::C_LESS_OR_EQUAL:
                table_def += STR::compose( '[', col->get_condition_num_lo(), '\n' );
                break;
            default: // IGNORE
                table_def += "_\n";
                break;
        }

        table_def += "Mcch"; // numeric condition hi

        switch( col->get_condition_rel_hi() )
        {
            case TableColumn::C_LESS:
                table_def += STR::compose( '<', col->get_condition_num_hi(), '\n' );
                break;
            case TableColumn::C_LESS_OR_EQUAL:
                table_def += STR::compose( '[', col->get_condition_num_hi(), '\n' );
                break;
            case TableColumn::C_EQUAL:
                table_def += STR::compose( '=', col->get_condition_num_hi(), '\n' );
                break;
            default: // IGNORE
                table_def += "_\n";
                break;
        }
    }

    if( m_filter != nullptr )
        table_def += STR::compose( "Mf", m_filter->get_name(), '\n' );

    if( m_tag_filter != nullptr )
        table_def += STR::compose( "Mt", m_tag_filter->get_id(), '\n' );

    table_def += STR::compose( "Mo", m_flag_group_by_first ? 'G' : '_',
                                     m_flag_para_based ? 'P' : 'E' );
    // this comes last to guarantee lack of \n at the end

    return table_def;
}

void
TableData::set_from_string( const Ustring& def )
{
    std::string             line( "" );
    std::string::size_type  line_offset{ 0 };
    TableColumn*            col{ nullptr };

    clear();

    while( STR::get_line( def, line_offset, line ) )
    {
        if( line.size() < 2 )   // should never occur
            continue;

        switch( line[ 1 ] )
        {
            case 'c':   // column
            {
                switch( line[ 2 ] )
                {
                    case 'n':   // name
                        col = add_column();
                        col->set_name( line.substr( 3 ) );
                        break;
                    case 'o':   // options
                        switch( line[ 3 ] )
                        {
                            case 'N': col->set_type( TableColumn::TCT_NAME ); break;
                            case 'D': col->set_type( TableColumn::TCT_DATE ); break;
                            case 'C': col->set_type( TableColumn::TCT_COUNT ); break;
                            case 'S': col->set_type( TableColumn::TCT_SUB ); break;
                            case 'T': col->set_type( TableColumn::TCT_TAG_V ); break;
                            case 'P': col->set_type( TableColumn::TCT_COMPLETION ); break;
                            case 'L': col->set_type( TableColumn::TCT_PATH_LENGTH ); break;
                        }
                        switch( line[ 4 ] )
                        {
                            case 'F': col->set_value_type( TableColumn::VT_FIRST ); break;
                            case 'L': col->set_value_type( TableColumn::VT_LAST ); break;
                            case 'O': col->set_value_type( TableColumn::VT_LOWEST ); break;
                            case 'H': col->set_value_type( TableColumn::VT_HIGHEST ); break;
                            case 'R': col->set_value_type( TableColumn::VT_TOTAL_REALIZED); break;
                            case 'P': col->set_value_type( TableColumn::VT_TOTAL_PLANNED ); break;
                            case 'B': col->set_value_type( TableColumn::VT_TOTAL_REMAINING ); break;
                            case 'r': col->set_value_type( TableColumn::VT_AVG_REALIZED); break;
                            case 'p': col->set_value_type( TableColumn::VT_AVG_PLANNED ); break;
                            case 'b': col->set_value_type( TableColumn::VT_AVG_REMAINING ); break;
                        }

                        col->set_conditional( line[ 5 ] == 'C' );
                        break;
                    case 't':   // column tag
                        col->set_tag( m_p2diary->get_entry_by_id(
                                std::stoul( line.substr( 3 ) ) ) );
                        break;
                    case 'c':   // conditions
                        switch( line[ 3 ] )
                        {
                            case 'f':
                                col->set_condition_sub_filter(
                                        m_p2diary->get_filter( line.substr( 4 ) ) );
                                break;
                            case 'l':
                                switch( line[ 4 ] )
                                {
                                    case '<':
                                        col->set_condition_rel_lo( TableColumn::C_LESS );
                                        break;
                                    case '[':
                                        col->set_condition_rel_lo( TableColumn::C_LESS_OR_EQUAL );
                                        break;
                                }
                                if( line[ 4 ] != '_' )
                                    col->set_condition_num_lo( STR::get_d( line.substr( 5 ) ) );
                                break;
                            case 'h':
                                switch( line[ 4 ] )
                                {
                                    case '<':
                                        col->set_condition_rel_hi( TableColumn::C_LESS );
                                        break;
                                    case '[':
                                        col->set_condition_rel_hi( TableColumn::C_LESS_OR_EQUAL );
                                        break;
                                    case '=':
                                        col->set_condition_rel_hi( TableColumn::C_EQUAL );
                                        break;
                                    default:
                                        col->set_condition_rel_hi( TableColumn::C_IGNORE );
                                }
                                if( line[ 4 ] != '_' )
                                    col->set_condition_num_hi( STR::get_d( line.substr( 5 ) ) );
                                break;
                        }
                        break;
                }
                break;
            }
            case 'f':   // filter
                m_filter = m_p2diary->get_filter( line.substr( 2 ) );
                break;
            case 't':
                m_tag_filter = m_p2diary->get_entry_by_id( std::stoul( line.substr( 2 ) ) );
                break;
            case 'o':
                m_flag_group_by_first = ( line[ 2 ] == 'G' );
                m_flag_para_based = ( line[ 3 ] == 'P' );
                break;
            default:
                PRINT_DEBUG( "Unrecognized table string: ", line );
                break;
        }
    }
}

TableColumn*
TableData::add_column()
{
    TableColumn* col{ new TableColumn() };

    if( m_columns.empty() )
        m_i_col_sort = 0;

    m_columns.push_back( col );
    return col;
}

void
TableData::dismiss_column( int i_col )
{
    if( i_col < 0 || i_col >= int( m_columns.size() ) )
        throw LIFEO::Error( "Column index out of bounds!" );

    if( m_i_col_sort >= i_col )
        m_i_col_sort--;

    auto&& iter{ m_columns.begin() };
    std::advance( iter, i_col );

    delete *iter;
    m_columns.erase( iter );
}

void
TableData::move_column( int i_cur, int i_tgt )
{
    if( i_cur == i_tgt )
        return;

    if( i_cur < 0 || i_cur >= int( m_columns.size() ) )
        throw LIFEO::Error( "Column index out of bounds!" );

    if( i_tgt < 0 || i_tgt >= int( m_columns.size() ) )
        throw LIFEO::Error( "Column target index out of bounds!" );

    auto&& iter{ m_columns.begin() };
    std::advance( iter, i_cur );
    auto&& column{ *iter };
    m_columns.erase( iter );

    iter = m_columns.begin();
    std::advance( iter, i_tgt );

    m_columns.insert( iter, column );
}

void
TableData::populate_lines()
{
    if( m_p2diary == nullptr || m_columns.empty() )
        return;

    clear_lines();

    FiltererContainer* fc{ nullptr };
    auto&              entries{ m_p2diary->get_entries() };
    Entry*             entry;

    if( m_filter )
        fc = m_filter->get_filterer_stack();

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

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

        if( m_flag_para_based )
        {
            for( auto& para : *entry->get_paragraphs() )
            {
                if( m_tag_filter && not( para->has_tag_broad( m_tag_filter ) ) )
                    continue;

                add_para( para );
            }
        }
        else
            add_entry( entry );
    }

    // total line & sort column
    m_line_total = new TableLine;
    int i_col{ 0 };
    TableColumn* col_sort{ nullptr };
    for( auto& col : m_columns )
    {
        if( i_col == m_i_col_sort )
            col_sort = col;

        if( col->is_numeric() )
        {
            for( auto& kv_line : m_lines_unsorted )
                m_line_total->add_col_v_num( i_col, kv_line.second->m_values_num[ i_col ],
                                                    kv_line.second->m_weights[ i_col ] );
        } // do nothing for textual columns

        i_col++;
    }

    m_lines_sorted = new SetTableLines( TableLineComparator( m_i_col_sort,
                                                             col_sort,
                                                             m_f_sort_desc ) );

    for( auto& kv_line : m_lines_unsorted )
        m_lines_sorted->emplace( kv_line.second );

    delete fc;
}

Ustring
TableData::get_value_str( int i_line, int j_col ) const
{
    auto&& it_line{ m_lines_sorted->begin() };
    std::advance( it_line, i_line );

    auto&& it_col{ m_columns.begin() };
    std::advance( it_col, j_col );

    if( ( *it_col )->is_percentage() )
        return STR::format_percentage( ( *it_line )->get_col_v_num( j_col ) );
    else
    if( ( *it_col )->is_numeric() )
        return STR::format_number( ( *it_line )->get_col_v_num( j_col ) );
    else
        return ( *it_line )->get_col_v_txt( j_col );
}

Ustring
TableData::get_total_value_str( int j_col ) const
{
    if( m_line_total == nullptr )
        return "";

    auto&& it_col{ m_columns.begin() };
    std::advance( it_col, j_col );

    if( ( *it_col )->is_percentage() )
        return STR::format_percentage( m_line_total->get_col_v_num( j_col ) );
    else
    if( ( *it_col )->is_numeric() )
        return STR::format_number( m_line_total->get_col_v_num( j_col ) );
    else
        return m_line_total->get_col_v_txt( j_col );
}

TableLine*
TableData::add_line( const Ustring& id )
{
    TableLine* line = new TableLine;

    m_lines_unsorted.emplace( id, line );

    return line;
}

void
TableData::add_entry( Entry* entry )
{
    if( m_columns.empty() ) return;

    Ustring&&  id_line{ m_columns.front()->get_entry_v_txt( entry ) };
    auto&&     kv_line{ m_lines_unsorted.find( id_line ) };
    TableLine* line;

    if( kv_line == m_lines_unsorted.end() || m_flag_group_by_first == false )
        line = add_line( id_line );
    else
        line = kv_line->second;

    add_entry_to_line( entry, line );
}

void
TableData::add_para( Paragraph* para )
{
    if( m_columns.empty() ) return;

    const auto&& id_line{ m_columns.front()->get_para_v_txt( para ) };
    auto&&       kv_line{ m_lines_unsorted.find( id_line ) };
    TableLine*   line;

    if( kv_line == m_lines_unsorted.end() || m_flag_group_by_first == false )
        line = add_line( id_line );
    else
        line = kv_line->second;

    add_para_to_line( para, line );
}

void
TableData::add_entry_to_line( Entry* entry, TableLine* line )
{
    int i_col{ 0 };
    for( auto& col : m_columns )
    {
        if( col->is_percentage() )
        {
            auto&& v_for_col{ col->get_entry_v_num( entry ) };
            auto&& w_for_col{ col->get_entry_weight( entry ) };
            line->add_col_v_num( i_col, v_for_col, w_for_col );
        }
        else
        if( col->is_numeric() )
        {
            auto&& v_for_col{ col->get_entry_v_num( entry ) };
            line->add_col_v_num( i_col, v_for_col ); // not-a-fraction so no denominator
        }
        else // textual
        {
            auto&& v_for_col{ col->get_entry_v_txt( entry ) };
            line->add_col_v_txt( i_col, v_for_col );
        }
        i_col++;
    }

    line->m_entries.push_back( entry );
}

void
TableData::add_para_to_line( Paragraph* para, TableLine* line )
{
    int i_col{ 0 };
    for( auto& col : m_columns )
    {
        if( col->is_percentage() )
        {
            auto&& v_for_col{ col->get_para_v_num( para ) };
            auto&& w_for_col{ col->get_para_weight( para ) };
            line->add_col_v_num( i_col, v_for_col, w_for_col );
        }
        else
        if( col->is_numeric() )
        {
            auto&& v_for_col{ col->get_para_v_num( para ) };
            line->add_col_v_num( i_col, v_for_col ); // not-a-fraction so no denominator
        }
        else // textual: name mode shows contents in para mode and concatenating all paras is heavy
        if( not( m_flag_para_based && m_flag_group_by_first && col->is_name() ) )
        {
            auto&& v_for_col{ col->get_para_v_txt( para ) };
            line->add_col_v_txt( i_col, v_for_col );
        }
        i_col++;
    }

    line->m_entries.push_back( const_cast< Entry* >( para->m_host ) );
}

const Ustring TableElem::DEFINITION_DEFAULT{ "T" }; // TODO
