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

    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/>.

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


#ifdef HAVE_CONFIG_H
#include <config.h>
#endif

#include <cstdio>   // for file operations
#include <string>
#include <sstream>
#include <iostream>
#include <fstream>
#include <iomanip>
#include <gcrypt.h>
#include <cerrno>
#include <cassert>
#include <cfloat>
#include <random>

#include "diary.hpp"
#include "parser_paragraph.hpp"
#include "helpers.hpp"
#include "lifeograph.hpp"
#include "filtering.hpp"

#include "widgets/chart_surface.hpp"


using namespace LIFEO;


// STATIC MEMBERS
Diary*                  Diary::d;
bool                    Diary::s_flag_ignore_locks{ false };

// PARSING HELPERS
date_t
get_db_line_date( const Ustring& line )
{
    date_t date{ 0 };

    for( unsigned int i = 2;
         i < line.size() && i < 12 && int ( line[ i ] ) >= '0' && int ( line[ i ] ) <= '9';
         i++ )
    {
        date = ( date * 10 ) + int ( line[ i ] ) - '0';
    }

    return date;
}

Ustring
get_db_line_name( const Ustring& line )
{
    Ustring::size_type begin( line.find( '\t' ) );
    if( begin == std::string::npos )
        begin = 2;
    else
        begin++;

    return( line.substr( begin ) );
}

// DIARY ===========================================================================================
Diary::Diary()
:   DiaryElement( nullptr, DEID_UNSET )
{
}

Diary::~Diary()
{
    remove_lock_if_necessary();
}

Result
Diary::init_new( const std::string& path, const std::string& pw )
{
    clear();

    set_id( create_new_id( this ) ); // adds itself to the ID pool with a unique ID

    m_read_version = DB_FILE_VERSION_INT;
    Result result{ set_path( path, SPT_NEW ) };

    if( result != LIFEO::SUCCESS )
    {
        clear();
        return result;
    }

    // every diary must at least have one chapter category:
    m_p2chapter_ctg_cur = create_chapter_ctg( _( STRING::DEFAULT ) );

    m_filter_active = create_filter( _( STRING::DEFAULT ), Filter::DEFINITION_DEFAULT );

    m_chart_active = create_chart( _( STRING::DEFAULT ), ChartElem::DEFINITION_DEFAULT );
    m_table_active = create_table( _( STRING::DEFAULT ), TableElem::DEFINITION_DEFAULT );

    m_theme_default = create_theme( ThemeSystem::get()->get_name() );
    ThemeSystem::get()->copy_to( m_theme_default );
    //-------------NAME-------------FONT----BASE-------TEXT-------HEADING----SUBHEAD----HLIGHT----
    create_theme( "Dark",       "Sans 10", "#111111", "#CCCCCC", "#FF6666", "#DD3366", "#661133" );
    create_theme( "Artemisia", "Serif 10", "#FFEEEE", "#000000", "#CC0000", "#A00000", "#F1BBC4" );
    create_theme( "Urgent",     "Sans 10", "#A00000", "#FFAA33", "#FFFFFF", "#FFEE44", "#000000" );
    create_theme( "Well Noted", "Sans 11", "#BBEEEE", "#000000", "#553366", "#224488", "#90E033" );

    add_today(); // must come after m_ptr2chapter_ctg_cur is set
    set_passphrase( pw );

    m_login_status = LOGGED_IN_RO;

    return write();
}

void
Diary::clear()
{
    close_file();

    m_path.clear();

    m_read_version = 0;

    set_id( DEID_UNSET );
    m_force_id = DEID_UNSET;
    m_ids.clear();

    m_entries.clear();
    m_entry_names.clear();

    m_chapter_categories.clear();
    m_p2chapter_ctg_cur = nullptr;

    m_search_text.clear();
    clear_matches();

    m_filters.clear(); // TODO implement custom clear() that deletes the allocated objects
    m_filter_active = nullptr;

    m_charts.clear(); // TODO implement custom clear() that deletes the allocated objects
    m_chart_active = nullptr;

    m_tables.clear(); // TODO implement custom clear() that deletes the allocated objects
    m_table_active = nullptr;

    clear_themes();
    m_theme_default = nullptr;

    m_startup_entry_id = HOME_CURRENT_ENTRY;
    m_last_entry_id = DEID_UNSET;
    m_completion_tag_id = DEID_UNSET;

    m_passphrase.clear();

    // NOTE: only reset body options here:
    m_language.clear();
    m_sorting_criteria = SoCr_DEFAULT;
    m_opt_show_all_entry_locations = false;
    m_opt_ext_panel_cur = 1;

    m_flag_read_only = false;
    m_login_status = LOGGED_OUT;
}

Icon
Diary::get_image( const std::string& uri, int width )
{
    Icon         buf;
    const auto&& rc_name{ STR::compose( uri, "/", width ) };
    auto&&       iter{ m_map_images.find( rc_name ) };

    if( iter == m_map_images.end() )
    {
        if( uri.find( "chart:" ) == 0 )
        {
            auto chart   { Diary::d->get_chart( uri.substr( 6, uri.size() - 6 ) ) };

            if( !chart ) throw LIFEO::Error( "Chart not found" );

            auto surface { new ChartSurface( chart, width ) };
            buf = surface->get_pixbuf();
            delete surface;
        }
        else
        {
#ifndef _WIN32
            buf = Gdk::Pixbuf::create_from_file(
                    Glib::filename_from_uri( convert_rel_uri( uri ) ) );
#else
            const auto&& pth = convert_rel_uri( uri );
            buf = Gdk::Pixbuf::create_from_file( PATH( pth.substr( 7, pth.length() - 7 ) ) );
#endif
            if( buf->get_width() > width )
                buf = buf->scale_simple( width, ( buf->get_height() *  width ) / buf->get_width(),
                                         Gdk::INTERP_BILINEAR );
        }

        m_map_images[ rc_name ] = buf;
    }
    else
        buf = iter->second;

    return buf;
}

LIFEO::Result
Diary::set_path( const std::string& path0, SetPathType type )
{
    std::string path( path0 );

    // RESOLVE SYMBOLIC LINK
    if( g_file_test( PATH( path0 ).c_str(), G_FILE_TEST_IS_SYMLINK ) )
    {
        GError* err = nullptr;
        path = g_file_read_link( PATH( path0 ).c_str(), &err );
        print_info( "Symbolic link resolved to path: ", path );
    }

    // CHECK FILE SYSTEM PERMISSIONS
    if( access( PATH( path ).c_str(), F_OK ) != 0 ) // check existence
    {
        if( errno == ENOENT )
        {
            if( type != SPT_NEW )
            {
                PRINT_DEBUG( "File is not found" );
                return LIFEO::FILE_NOT_FOUND;
            }
        }
        else
            return LIFEO::FAILURE; // should not be the case
    }
    else if( access( PATH( path ).c_str(), R_OK ) != 0 ) // check read access
    {
        PRINT_DEBUG( "File is not readable" );
        return LIFEO::FILE_NOT_READABLE;
    }
    else if( type == SPT_NEW && access( PATH( path ).c_str(), W_OK ) != 0 ) // check write access
    {
        PRINT_DEBUG( "File is not writable" );
        return LIFEO::FILE_NOT_WRITABLE;
    }

    // REMOVE PREVIOUS PATH'S LOCK IF ANY
    remove_lock_if_necessary();

    // ACCEPT PATH
    m_path = path;
    m_name = get_filename_base( path );
    m_flag_read_only = ( type == SPT_READ_ONLY );

    return LIFEO::SUCCESS;
}

const std::string&
Diary::get_path() const
{
    return m_path;
}

std::string
Diary::convert_rel_uri( std::string uri )
{
    if( uri.find( "rel://" ) == 0 )
        uri.replace( 0, 5, "file://" + Glib::path_get_dirname( m_path ) );

    return uri;
}

LIFEO::Result
Diary::enable_editing()
{
    if( m_flag_read_only )
    {
        PRINT_DEBUG( "Diary: editing cannot be enabled. Diary is read-only" );
        return LIFEO::FILE_LOCKED;
    }

    if( access( PATH( m_path ).c_str(), W_OK ) != 0 ) // check write access
    {
        PRINT_DEBUG( "File is not writable" );
        return LIFEO::FILE_NOT_WRITABLE;
        // errno == EACCES or errno == EROFS but no need to bother
    }

    // CHECK AND "TOUCH" THE NEW LOCK
    std::string path_lock = ( m_path + LOCK_SUFFIX);
    if( access( PATH( path_lock ).c_str(), F_OK ) == 0 )
    {
        if( s_flag_ignore_locks )
            print_info( "Ignored file lock" );
        else
            return LIFEO::FILE_LOCKED;
    }

    FILE* fp = fopen( PATH( path_lock ).c_str(), "a+" );
    if( fp )
        fclose( fp );
    else
    {
        print_error( "Could not create lock file" );
        return LIFEO::FILE_LOCKED;
    }

    m_login_status = LOGGED_IN_EDIT;

    return LIFEO::SUCCESS;
}

bool
Diary::set_passphrase( const std::string& passphrase )
{
    if( passphrase.size() >= PASSPHRASE_MIN_SIZE )
    {
        m_passphrase = passphrase;
        return true;
    }
    else
        return false;
}

void
Diary::clear_passphrase()
{
    m_passphrase.clear();
}

const std::string&
Diary::get_passphrase() const
{
    return m_passphrase;
}

bool
Diary::compare_passphrase( const std::string& passphrase ) const
{
    return( m_passphrase == passphrase );
}

bool
Diary::is_passphrase_set() const
{
    return( ( bool ) m_passphrase.size() );
}

// PARSING HELPERS
inline void
parse_todo_status( DiaryElement* elem, char c )
{
    switch( c )
    {
        case 't':
            elem->set_todo_status( ES::TODO );
            break;
        case 'T':
            elem->set_todo_status( ES::NOT_TODO | ES::TODO );
            break;
        case 'p':
            elem->set_todo_status( ES::PROGRESSED );
            break;
        case 'P':
            elem->set_todo_status( ES::NOT_TODO | ES::PROGRESSED );
            break;
        case 'd':
            elem->set_todo_status( ES::DONE );
            break;
        case 'D':
            elem->set_todo_status( ES::NOT_TODO | ES::DONE );
            break;
        case 'c':
            elem->set_todo_status( ES::CANCELED );
            break;
        case 'C':
            elem->set_todo_status( ES::NOT_TODO | ES::CANCELED );
            break;
    }

}

inline void
parse_theme( Theme* ptr2theme, const std::string& line )
{
    switch( line[ 1 ] )
    {
        case 'f':   // font
            ptr2theme->font = Pango::FontDescription( line.substr( 2 ) );
            break;
        case 'b':   // base color
            ptr2theme->color_base.set( line.substr( 2 ) );
            break;
        case 't':   // text color
            ptr2theme->color_text.set( line.substr( 2 ) );
            break;
        case 'h':   // heading color
            ptr2theme->color_heading.set( line.substr( 2 ) );
            break;
        case 's':   // subheading color
            ptr2theme->color_subheading.set( line.substr( 2 ) );
            break;
        case 'l':   // highlight color
            ptr2theme->color_highlight.set( line.substr( 2 ) );
            break;
        case 'i':   // background image
            ptr2theme->image_bg = line.substr( 2 );
            break;
    }
}

void
Diary::tmp_upgrade_ordinal_date_to_2000( date_t& old_date )
{
    if( Date::is_ordinal( old_date ) == false )
        return;

    if( m_read_version < 1020 )
    {
        if( old_date & Date::FLAG_VISIBLE )
            old_date -= Date::FLAG_VISIBLE;
        else
            old_date |= Date::FLAG_VISIBLE;
    }

    old_date = Date::make_ordinal( not( Date::is_hidden( old_date ) ),
                                   Date::get_order_2nd( old_date ),
                                   Date::get_order_3rd( old_date ),
                                   0 );
}

inline void
tmp_add_tags_as_paragraph( Entry* entry_new,
                           std::map< Entry*, Value >& entry_tags,
                           ParserPara& parser_para )
{
    if( entry_new )
    {
        for( auto& kv_tag : entry_tags )
        {
            entry_new->add_tag( kv_tag.first, kv_tag.second );
            parser_para.parse( entry_new->get_paragraphs()->back() );
        }

        entry_tags.clear();
    }
}

inline void
tmp_create_chart_from_tag( Entry* tag, long type, Diary* diary )
{
    auto o2{ ( type & ChartData::UNDERLAY_MASK ) == ChartData::UNDERLAY_PREV_YEAR ? "Y" : "-" };
    auto o3{ ( type & ChartData::PERIOD_MASK ) == ChartData::MONTHLY ?              "M" : "Y" };
    auto o4{ ( type & ChartData::VALUE_TYPE_MASK ) == ChartData::AVERAGE ?          "A" : "P" };
    // create predefined charts for non-boolean tags
    if( tag && ( type & ChartData::VALUE_TYPE_MASK ) != ChartData::BOOLEAN )
    {
        diary->create_chart( tag->get_name(),
                             STR::compose( "Gyt", tag->get_id(), "\nGoT", o2, o3, o4 ) );
    }
}

void
Diary::upgrade_to_1030()
{
    // initialize the status dates:
    for( auto& kv_entry : m_entries )
        kv_entry.second->m_date_status = kv_entry.second->m_date_created;

    // replace old to-do boxes:
    replace_all_matches( "☐", "[ ]" );
    replace_all_matches( "☑", "[+]" );
    replace_all_matches( "☒", "[x]" );
}

void
Diary::upgrade_to_1050()
{
    for( auto& kv_entry : m_entries )
    {
        Entry* entry{ kv_entry.second };

        if( entry->get_todo_status() == ES::NOT_TODO )
            entry->update_todo_status();
    }
}

void
Diary::do_standard_checks_after_parse()
{
    if( m_theme_default == nullptr )
    {
        m_theme_default = create_theme( ThemeSystem::get()->get_name() );
        ThemeSystem::get()->copy_to( m_theme_default );
    }

    // DEFAULT CHART AND TABLE
    if( m_chart_active == nullptr )
        m_chart_active = create_chart( _( STRING::DEFAULT ), ChartElem::DEFINITION_DEFAULT );
    if( m_table_active == nullptr )
        m_table_active = create_table( _( STRING::DEFAULT ), TableElem::DEFINITION_DEFAULT );

    // initialize derived theme colors
    for( auto& kv_theme : m_themes )
        kv_theme.second->calculate_derived_colors();

    // every diary must at least have one chapter category:
    if( m_chapter_categories.empty() )
        m_p2chapter_ctg_cur = create_chapter_ctg( _( STRING::DEFAULT ) );

    if( m_startup_entry_id > DEID_MIN )
    {
        if( get_element( m_startup_entry_id ) == nullptr )
        {
            print_error( "Startup element ", m_startup_entry_id, " cannot be found in db" );
            m_startup_entry_id = HOME_CURRENT_ENTRY;
        }
    }
    else if( m_startup_entry_id == DEID_MIN ) // this is used when upgrading from <2000
        m_startup_entry_id = HOME_CURRENT_ENTRY;

    if( m_entries.empty() )
    {
        print_info( "A dummy entry added to the diary" );
        add_today();
    }
}

void
Diary::add_entries_to_name_map()
{
    for( auto& kv_entry : m_entries )
    {
        Entry* entry{ kv_entry.second };
        // add entries to name map
        if( entry->has_name() )
            m_entry_names.emplace( entry->m_name, entry );
    }
}

// PARSING FUNCTIONS
inline LIFEO::Result
Diary::parse_db_body_text( std::istream& stream )
{
    switch( m_read_version )
    {
        case 2000:
        case 1999:
        case 1997:
            return parse_db_body_text_2000( stream );
        case 1050:
        case 1040:
        case 1030:
        case 1020:
        case 1011:
        case 1010:
            return parse_db_body_text_1050( stream );
    }
    return LIFEO::FAILURE;
}

LIFEO::Result
Diary::parse_db_body_text_2000( std::istream& stream )
{
    std::string         line;
    Theme*              ptr2theme{ nullptr };
    Filter*             ptr2filter{ nullptr };
    ChartElem*          ptr2chart{ nullptr };
    TableElem*          ptr2table{ nullptr };
    CategoryChapters*   ptr2chapter_ctg{ nullptr };
    Chapter*            ptr2chapter{ nullptr };
    Entry*              ptr2entry{ nullptr };
    Paragraph*          ptr2para{ nullptr };
    ParserPara          parser_para;

    // TAG DEFINITIONS & CHAPTERS
    while( getline( stream, line ) )
    {
        if( line.size() < 2 )
            continue;

        switch( line[ 0 ] )
        {
            // DIARY OPTION
            case 'D':
                switch( line[ 1 ] )
                {
                    case 'o':   // options
                        m_opt_show_all_entry_locations = ( line[ 2 ] == 'A' );
                        m_sorting_criteria = std::stol( line.substr( 3, 3 ) );
                        if( line.size() > 6 ) // for v=1999
                            m_opt_ext_panel_cur = std::stol( line.substr( 6, 1 ) );
                        break;
                    case 's':   // spell checking language
                        m_language = line.substr( 2 );
                        break;
                    case 'f':   // first entry to show
                        m_startup_entry_id = std::stol( line.substr( 2 ) );
                        break;
                    case 'l':   // last entry shown in the previous session
                        m_last_entry_id = std::stol( line.substr( 2 ) );
                        break;
                    case 'c':   // completion tag
                        m_completion_tag_id = std::stol( line.substr( 2 ) );
                        break;
                }
                break;
            // ID (START OF A NEW ELEMENT)
            case 'I':   // id
                set_force_id( std::stol( line.substr( 2 ) ) );
                break;
            // THEME
            case 'T':
                if( line[ 1 ] == ' ' ) // declaration
                {
                    ptr2theme = create_theme( line.substr( 3 ) );
                    if( line[ 2 ] == 'D' )
                        m_theme_default = ptr2theme;
                }
                else
                    parse_theme( ptr2theme, line );
                break;
            // FILTER
            case 'F':
                if( line[ 1 ] == ' ' ) // declaration
                {
                    ptr2filter = create_filter( line.substr( 3 ), "" );
                    if( line[ 2 ] == 'A' )
                        m_filter_active = ptr2filter;
                }
                else
                    ptr2filter->add_definition_line( line );
                break;
            // CHART
            case 'G':
                if( line[ 1 ] == ' ' )  // declaration
                {
                    ptr2chart = create_chart( line.substr( 3 ), "" );
                    if( line[ 2 ] == 'A' )
                        m_chart_active = ptr2chart;
                }
                else
                    ptr2chart->add_definition_line( line );
                break;
            // TABLE (MATRIX)
            case 'M':
                if( line[ 1 ] == ' ' )  // declaration
                {
                    ptr2table = create_table( line.substr( 3 ), "" );
                    if( line[ 2 ] == 'A' )
                        m_table_active = ptr2table;
                }
                else
                    ptr2table->add_definition_line( line );
                break;
            // CHAPTER CATEGORY
            case 'C':
                ptr2chapter_ctg = create_chapter_ctg( line.substr( 3 ) );
                if( line[ 2 ] == 'A' )
                    m_p2chapter_ctg_cur = ptr2chapter_ctg;
                break;
            // ENTRY / CHAPTER
            case 'E':
                switch( line[ 1 ] )
                {
                    case ' ':   // declaration
                        ptr2entry = create_entry( std::stoul( line.substr( 6 ) ),
                                                  line[ 2 ] == 'F',
                                                  line[ 3 ] == 'T',
                                                  line[ 5 ] == 'E' );

                        parse_todo_status( ptr2entry, line[ 4 ] );
                        break;
                    case '+':   // chapter declaration
                        ptr2entry = ptr2chapter = ptr2chapter_ctg->create_chapter(
                                std::stoul( line.substr( 6 ) ),
                                line[ 2 ] == 'F',
                                line[ 3 ] == 'T',
                                line[ 5 ] == 'E' );

                        parse_todo_status( ptr2chapter, line[ 4 ] );
                        break;
                    case 'c':
                        ptr2entry->m_date_created = std::stoul( line.substr( 2 ) );
                        break;
                    case 'e':
                        ptr2entry->m_date_edited = std::stoul( line.substr( 2 ) );
                        break;
                    case 't':   // to do status change date
                        ptr2entry->m_date_status = std::stoul( line.substr( 2 ) );
                        break;
                    case 'm':
                        ptr2entry->set_theme( m_themes[ line.substr( 2 ) ] );
                        break;
                    case 's':   // spell checking language
                        ptr2entry->set_lang( line.substr( 2 ) );
                        break;
                    case 'u':   // spell checking language
                        ptr2entry->set_unit( line.substr( 2 ) );
                        break;
                    case 'l':   // location
                        if( line[ 2 ] == 'a' )
                            ptr2entry->m_location.latitude = STR::get_d( line.substr( 3 ) );
                        else if(  line[ 2 ] == 'o' )
                            ptr2entry->m_location.longitude = STR::get_d( line.substr( 3 ) );
                        break;
                    case 'r':   // path (route)
                        if( line[ 2 ] == 'a' )
                            ptr2entry->add_map_path_point( STR::get_d( line.substr( 3 ) ),
                                                           0.0 );
                        else if(  line[ 2 ] == 'o' )
                            ptr2entry->get_map_path_end().longitude =
                                    STR::get_d( line.substr( 3 ) );
                        break;
                    case 'p':   // paragraph
                        ptr2para = ptr2entry->add_paragraph( line.substr( 3 ) );
                        parser_para.parse( ptr2para );
                        ptr2para->m_justification = JustificationType( line[ 2 ] );
                        break;
                    case 'b':   // chapter bg color
                        ptr2chapter->set_color( Color( line.substr( 2 ) ) );
                        break;
                }
                break;
            default:
                print_error( "Unrecognized line: [", line, "]" );
                clear();
                return LIFEO::CORRUPT_FILE;
        }
    }

    do_standard_checks_after_parse();
    add_entries_to_name_map();

    return LIFEO::SUCCESS;
}

LIFEO::Result
Diary::parse_db_body_text_1050( std::istream& stream )
{
    std::string         read_buffer;
    std::string         line;
    std::string::size_type line_offset{ 0 };
    Entry*              entry_new{ nullptr };
    unsigned int        tag_o1{ 0 };
    unsigned int        tag_o2{ 0 };
    unsigned int        tag_o3{ 0 };
    CategoryChapters*   p2chapter_ctg{ nullptr };
    Chapter*            p2chapter{ nullptr };
    Theme*              p2theme{ nullptr };
    bool                flag_in_tag_ctg{ false };
    Ustring             filter_def{ "F&" };
    std::map< Entry*, Value >     entry_tags;
    ParserPara          parser_para;
    Ustring             chart_def_default{ ChartElem::DEFINITION_DEFAULT };

    // PREPROCESSING TO DETERMINE FIRST AVAILABLE ORDER
    while( getline( stream, line ) )
    {
        read_buffer += ( line + '\n' );

        if( line[ 0 ] == 'C' && line[ 1 ] == 'G' )
            tag_o1++;
    }

    // TAGS TOP LEVEL
    entry_new = new Entry( this, Date::make_ordinal( false, ++tag_o1, 0 ) );
    m_entries[ entry_new->m_date.m_date ] = entry_new;
    entry_new->add_paragraph( "[###>" );
    entry_new->set_expanded( true );

    // TAG DEFINITIONS & CHAPTERS
    while( STR::get_line( read_buffer, line_offset, line ) )
    {
        if( line.empty() )    // end of section
            break;
        else if( line.size() >= 3 )
        {
            switch( line[ 0 ] )
            {
                case 'I':   // id
                    set_force_id( std::stol( line.substr( 2 ) ) );
                break;
                // TAGS
                case 'T':   // tag category
                    entry_new = new Entry( this, Date::make_ordinal( false, tag_o1, ++tag_o2 ) );
                    m_entries[ entry_new->m_date.m_date ] = entry_new;
                    entry_new->add_paragraph( line.substr( 2 ) );
                    entry_new->set_expanded( line[ 1 ] == 'e' );
                    m_entry_names.emplace( entry_new->m_name, entry_new );
                    flag_in_tag_ctg = true;
                    tag_o3 = 0;
                    break;
                case 't':   // tag
                    switch( line[ 1 ] )
                    {
                        case ' ':
                            entry_new = new Entry(
                                    this,
                                    flag_in_tag_ctg ?
                                            Date::make_ordinal( false, tag_o1, tag_o2, ++tag_o3 ) :
                                            Date::make_ordinal( false, tag_o1, ++tag_o2 ) );
                            m_entries[ entry_new->m_date.m_date ] = entry_new;
                            entry_new->add_paragraph( line.substr( 2 ) );
                            m_entry_names.emplace( entry_new->m_name, entry_new );
                            p2theme = nullptr;
                            break;
                        case 'c': // not used in 1010
                            tmp_create_chart_from_tag(
                                    entry_new, std::stol( line.substr( 2 ) ), this );
                            break;
                        case 'u': // not used in 1010
                            if( entry_new )
                                entry_new->set_unit( line.substr( 2 ) );
                            break;
                    }
                    break;
                case 'u':
                    if( m_theme_default == nullptr && line[ 1 ] != 'c' ) // chart is ignored
                        m_theme_default = create_theme( ThemeSystem::get()->get_name() );
                    parse_theme( m_theme_default, line );
                    break;
                case 'm':
                    if( p2theme == nullptr )
                        p2theme = create_theme( entry_new->get_name() );
                    parse_theme( p2theme, line );
                    break;
                // DEFAULT FILTER
                case 'f':
                    switch( line[ 1 ] )
                    {
                        case 's':   // status
                            if( m_read_version < 1020 )
                            {
                                filter_def += "\nFsN";
                                filter_def += ( line[ 6 ] == 'T' ) ? 'O' : 'o';
                                // made in-progress entries depend on the preference for open ones:
                                filter_def += ( line[ 6 ] == 'T' ) ? 'P' : 'p';
                                filter_def += ( line[ 7 ] == 'D' ) ? 'D' : 'd';
                                filter_def += ( line[ 8 ] == 'C' ) ? 'C' : 'c';
                            }
                            else
                            {
                                filter_def += "\nFs";
                                filter_def += ( line[ 6 ] == 'N' ) ? 'N' : 'n';
                                filter_def += ( line[ 7 ] == 'T' ) ? 'O' : 'o';
                                filter_def += ( line[ 8 ] == 'P' ) ? 'P' : 'p';
                                filter_def += ( line[ 9 ] == 'D' ) ? 'D' : 'd';
                                filter_def += ( line[ 10 ] == 'C' ) ? 'C' : 'c';
                            }
                            if( line[ 2 ] == 'T' && line[ 3 ] != 't' ) filter_def += "\nFty";
                            else if( line[ 2 ] != 'T' && line[ 3 ] == 't' ) filter_def += "\nFtn";
                            if( line[ 4 ] == 'F' && line[ 5 ] != 'f' ) filter_def += "\nFfy";
                            else if( line[ 4 ] != 'F' && line[ 5 ] == 'f' ) filter_def += "\nFfn";
                            break;
                        case 't':   // tag
                        {
                            auto kv_tag( m_entry_names.find( line.substr( 2 ) ) );
                            if( kv_tag != m_entry_names.end() )
                                filter_def += STR::compose( "\nFt", kv_tag->second->get_id() );
                            else
                                print_error( "Reference to undefined tag: ", line.substr( 2 ) );
                            break;
                        }
                        case 'b':   // begin date: in the new system this is an after filter
                            filter_def += STR::compose( "\nFa", line.substr( 2 ) );
                            break;
                        case 'e':   // end date: in the new system this is a before filter
                            filter_def += STR::compose( "\nFb", line.substr( 2 ) );
                            break;
                    }
                    break;
                // CHAPTERS
                case 'o':   // ordinal chapter (topic) (<1020)
                    entry_new = new Entry( this, get_db_line_date( line ) );
                    tmp_upgrade_ordinal_date_to_2000( entry_new->m_date.m_date );
                    m_entries[ entry_new->m_date.m_date ] = entry_new;
                    entry_new->set_text( get_db_line_name( line ) );
                    entry_new->set_expanded( line[ 1 ] == 'e' );
                    break;
                case 'd':   // to-do group (<1020)
                    if( line[ 1 ] == ':' ) // declaration
                    {
                        entry_new = new Entry( this, get_db_line_date( line ) );
                        tmp_upgrade_ordinal_date_to_2000( entry_new->m_date.m_date );
                        m_entries[ entry_new->m_date.m_date ] = entry_new;
                        entry_new->set_text( get_db_line_name( line ) );
                    }
                    else // options
                    {
                        entry_new->set_expanded( line[ 2 ] == 'e' );
                        if( line[ 3 ] == 'd' )
                            entry_new->set_todo_status( ES::DONE );
                        else if( line[ 3 ] == 'c' )
                            entry_new->set_todo_status( ES::CANCELED );
                        else
                            entry_new->set_todo_status( ES::TODO );
                    }
                    break;
                case 'c':   // temporal chapter (<1020)
                    if( p2chapter_ctg )
                    {
                        auto d_c{ get_db_line_date( line ) };

                        entry_new = p2chapter =
                                p2chapter_ctg->create_chapter( d_c, false, false, true );
                        p2chapter->set_text( get_db_line_name( line ) );
                    }
                    else
                        print_error( "No chapter category defined" );
                    break;
                case 'C':
                    if( m_read_version < 1020 ) // chapter category
                    {
                        p2chapter_ctg = create_chapter_ctg( line.substr( 2 ) );
                        if( line[ 1 ] == 'c' )
                            m_p2chapter_ctg_cur = p2chapter_ctg;
                        break;
                    }
                    switch( line[ 1 ] ) // any chapter item based on line[1] (>=1020)
                    {
                        case 'C':   // chapter category
                            p2chapter_ctg = create_chapter_ctg( line.substr( 3 ) );
                            if( line[ 2 ] == 'c' )
                                m_p2chapter_ctg_cur = p2chapter_ctg;
                            break;
                        case 'c':   // chapter color
                            p2chapter->set_color( Color( line.substr( 2 ) ) );
                            break;
                        case 'T':   // temporal chapter
                            entry_new = p2chapter =
                                    p2chapter_ctg->create_chapter( get_db_line_date( line ),
                                                                     false, false, true );
                            p2chapter->set_text( get_db_line_name( line ) );
                            break;
                        case 'O':   // ordinal chapter (used to be called topic)
                        case 'G':   // free chapter (replaced todo_group in v1020)
                            entry_new = new Entry( this, get_db_line_date( line ) );
                            tmp_upgrade_ordinal_date_to_2000( entry_new->m_date.m_date );
                            m_entries[ entry_new->m_date.m_date ] = entry_new;
                            entry_new->set_text( get_db_line_name( line ) );
                            break;
                        case 'p':   // chapter preferences
                            entry_new->set_expanded( line[ 2 ] == 'e' );
                            parse_todo_status( entry_new, line[ 3 ] );
                            //line[ 4 ] (Y) is ignored as we no longer create charts for chapters
                            break;
                    }
                    break;
                case 'O':   // options
                    switch( line[ 2 ] )
                    {
                        case 'd': m_sorting_criteria = SoCr_DATE; break;
                        case 's': m_sorting_criteria = SoCr_SIZE_C; break;
                        case 'c': m_sorting_criteria = SoCr_CHANGE; break;
                    }
                    if( m_read_version == 1050 )
                    {
                        m_sorting_criteria |= ( line[ 3 ] == 'd' ? SoCr_DESCENDING : SoCr_ASCENDING );

                        if( line.size() > 4 && line[ 4 ] == 'Y' )
                            chart_def_default = ChartElem::DEFINITION_DEFAULT_Y;
                    }
                    else if( m_read_version == 1040 )
                    {
                        if( line.size() > 3 && line[ 3 ] == 'Y' )
                            chart_def_default = ChartElem::DEFINITION_DEFAULT_Y;
                    }
                    break;
                case 'l':   // language
                    m_language = line.substr( 2 );
                    break;
                case 'S':   // startup action
                    m_startup_entry_id = std::stol( line.substr( 2 ) );
                    break;
                case 'L':
                    m_last_entry_id = std::stol( line.substr( 2 ) );
                    break;
                default:
                    print_error( "Unrecognized line:\n", line );
                    clear();
                    return LIFEO::CORRUPT_FILE;
            }
        }
    }

    // ENTRIES
    entry_new = nullptr;
    while( STR::get_line( read_buffer, line_offset, line ) )
    {
        if( line.size() < 2 )
            continue;
        else if( line[ 0 ] != 'I' && line[ 0 ] != 'E' && line[ 0 ] != 'e' && entry_new == nullptr )
        {
            print_error( "No entry declared for the attribute" );
            continue;
        }

        switch( line[ 0 ] )
        {
            case 'I':
                set_force_id( std::stol( line.substr( 2 ) ) );
                break;
            case 'E':   // new entry
            case 'e':   // trashed
                if( line.size() < 5 )
                    continue;

                // add tags as inline tags
                tmp_add_tags_as_paragraph( entry_new, entry_tags, parser_para );

                entry_new = new Entry( this, std::stoul( line.substr( 4 ) ),
                                       line[ 1 ] == 'f' ?
                                               ES::ENTRY_DEFAULT_FAV : ES::ENTRY_DEFAULT );
                tmp_upgrade_ordinal_date_to_2000( entry_new->m_date.m_date );
                m_entries[ entry_new->m_date.m_date ] = entry_new;

                if( line[ 0 ] == 'e' )
                    entry_new->set_trashed( true );
                if( line[ 2 ] == 'h' )
                    filter_def += STR::compose( "\nFn", entry_new->get_id() );

                parse_todo_status( entry_new, line[ 3 ] );

                // all hidden entries were to-do items once:
                if( m_read_version < 1020 && entry_new->get_date().is_hidden() )
                    entry_new->set_todo_status( ES::TODO );

                break;
            case 'D':   // creation & change dates (optional)
                switch( line[ 1 ] )
                {
                    case 'r':
                        entry_new->m_date_created = std::stoul( line.substr( 2 ) );
                        break;
                    case 'h':
                        entry_new->m_date_edited = std::stoul( line.substr( 2 ) );
                        break;
                    case 's':
                        entry_new->m_date_status = std::stoul( line.substr( 2 ) );
                        break;
                }
                break;
            case 'T':   // tag
            {
                NameAndValue&& nav{ NameAndValue::parse( line.substr( 2 ) ) };
                auto&& kv_tag{ m_entry_names.find( nav.name ) };
                if( kv_tag != m_entry_names.end() )
                {
                    entry_tags.emplace( kv_tag->second, nav.value );
                    if( line[ 1 ] == 'T' )
                        entry_new->set_theme( m_themes[ nav.name ] );
                }
                else
                    print_error( "Reference to undefined tag: ", nav.name );
                break;
            }
            case 'l':   // language
                entry_new->set_lang( line.substr( 2 ) );
                break;
            case 'P':    // paragraph
                parser_para.parse( entry_new->add_paragraph( line.substr( 2 ) ) );
                break;
            default:
                print_error( "Unrecognized line:\n", line );
                clear();
                return LIFEO::CORRUPT_FILE;
        }
    }

    // add tags to the last entry as inline tags
    tmp_add_tags_as_paragraph( entry_new, entry_tags, parser_para );

    update_entries_in_chapters();

    if( m_read_version < 1030 )
        upgrade_to_1030();

    if( m_read_version < 1050 )
        upgrade_to_1050();

    // DEFAULT FILTER AND CHART
    m_filter_active = create_filter( _( STRING::DEFAULT ), filter_def );
    m_chart_active = create_chart( _( STRING::DEFAULT ), chart_def_default );

    do_standard_checks_after_parse();
    add_entries_to_name_map();

    return LIFEO::SUCCESS;
}

// READING
LIFEO::Result
Diary::read_header()
{
    m_ifstream = new std::ifstream( PATH( m_path ).c_str() );

    if( ! m_ifstream->is_open() )
    {
        print_error( "Failed to open diary file: ", m_path );
        clear();
        return LIFEO::COULD_NOT_START;
    }
    std::string line;

    getline( *m_ifstream, line );

    if( line != LIFEO::DB_FILE_HEADER )
    {
        clear();
        return LIFEO::CORRUPT_FILE;
    }

    while( getline( *m_ifstream, line ) )
    {
        switch( line[ 0 ] )
        {
            case 'V':
                m_read_version = std::stol( line.substr( 2 ) );
                if( m_read_version < LIFEO::DB_FILE_VERSION_INT_MIN )
                {
                    clear();
                    return LIFEO::INCOMPATIBLE_FILE_OLD;
                }
                else if( m_read_version > LIFEO::DB_FILE_VERSION_INT )
                {
                    clear();
                    return LIFEO::INCOMPATIBLE_FILE_NEW;
                }
                break;
            case 'E':
                // passphrase is set to a dummy value to indicate that diary
                // is an encrypted one until user enters the real passphrase
                m_passphrase = ( line[ 2 ] == 'y' ? " " : "" );
                break;
            case 'I':
                set_force_id( std::stol( line.substr( 2 ) ) );
                break;
            case 0:
                set_id( create_new_id( this ) );
                return( m_read_version ? LIFEO::SUCCESS : LIFEO::CORRUPT_FILE );
            default:
                print_error( "Unrecognized header line: ", line );
                break;
        }
    }

    clear();
    return LIFEO::CORRUPT_FILE;
}

LIFEO::Result
Diary::read_body()
{
    Result res( m_passphrase.empty() ? read_plain() : read_encrypted() );

    close_file();

    if( res == SUCCESS )
        m_login_status = LOGGED_IN_RO;

    return res;
}

LIFEO::Result
Diary::read_plain()
{
    if( ! m_ifstream->is_open() )
    {
        print_error( "Internal error while reading the diary file: ", m_path );
        clear();
        return LIFEO::COULD_NOT_START;
    }

    return parse_db_body_text( *m_ifstream );
}

LIFEO::Result
Diary::read_encrypted()
{
    if( ! m_ifstream->is_open() )
    {
        print_error( "Failed to open diary file: ", m_path );
        clear();
        return LIFEO::COULD_NOT_START;
    }

    CipherBuffers buf;

#ifdef _WIN32
    m_ifstream->close();
    m_ifstream->open( PATH( m_path ).c_str(),
                      std::ifstream::in | std::ifstream::binary | std::ifstream::ate );

    size_t fsize = m_ifstream->tellg();
    m_ifstream->seekg( 0, std::ios_base::beg );

    // seek... this is preposterous!
    char ch;
    int ch_count = 0;
    while( m_ifstream->get( ch ) )
    {
        if( ch == '\n' )
        {
            if( ch_count )
                break;
            else
                ch_count = 1;
        }
        else
            ch_count = 0;
    }
#else
    size_t fsize = LIFEO::get_file_size( *m_ifstream );
#endif

    try
    {
        // allocate memory for salt
        buf.salt = new unsigned char[ LIFEO::Cipher::cSALT_SIZE ];
        // read salt value
        m_ifstream->read( ( char* ) buf.salt, LIFEO::Cipher::cSALT_SIZE );

        buf.iv = new unsigned char[ LIFEO::Cipher::cIV_SIZE ];
        // read IV
        m_ifstream->read( ( char* ) buf.iv, LIFEO::Cipher::cIV_SIZE );

        LIFEO::Cipher::expand_key( m_passphrase.c_str(), buf.salt, &buf.key );

        // calculate bytes of data in file
        size_t size = fsize - m_ifstream->tellg();
        if( size <= 3 )
        {
            buf.clear();
            clear();
            return LIFEO::CORRUPT_FILE;
        }
        buf.buffer = new unsigned char[ size + 1 ];
        if( ! buf.buffer )
            throw LIFEO::Error( "Unable to allocate memory for buffer" );

        m_ifstream->read( ( char* ) buf.buffer, size );
        LIFEO::Cipher::decrypt_buffer( buf.buffer, size, buf.key, buf.iv );

        // passphrase check
        if( buf.buffer[ 0 ] != m_passphrase[ 0 ] && buf.buffer[ 1 ] != '\n' )
        {
            buf.clear();
            clear();
            return LIFEO::WRONG_PASSWORD;
        }

        buf.buffer[ size ] = 0;   // terminating zero
    }
    catch( ... )
    {
        buf.clear();
        clear();
        return LIFEO::COULD_NOT_START;
    }

    std::stringstream stream;
    buf.buffer += 2;    // ignore first two chars which are for passphrase checking
    stream << buf.buffer;
    buf.buffer -= 2;    // restore pointer to the start of the buffer before deletion

    buf.clear();
    return parse_db_body_text( stream );
}

// WRITING
inline char
get_elem_todo_status_char( const DiaryElement* elem )
{
    switch( elem->get_todo_status() )
    {
        case ES::TODO:                          return 't';
        case ( ES::NOT_TODO | ES::TODO ):       return 'T';
        case ES::PROGRESSED:                    return 'p';
        case ( ES::NOT_TODO | ES::PROGRESSED ): return 'P';
        case ES::DONE:                          return 'd';
        case ( ES::NOT_TODO | ES::DONE ):       return 'D';
        case ES::CANCELED:                      return 'c';
        case ( ES::NOT_TODO | ES::CANCELED ):   return 'C';
        case ES::NOT_TODO:
        default:    /* should never occur */    return 'n';
    }
}

inline void
Diary::create_db_entry_text( const Entry* entry, std::stringstream& output )
{
    // ENTRY DATE
    output << "\n\nID" << entry->get_id()
           << "\nE"  << ( entry->get_type() == ET_CHAPTER ? '+' : ' ' )
                     << ( entry->is_favored() ? 'F' : '_' )
                     << ( entry->is_trashed() ? 'T' : '_' )
                     << get_elem_todo_status_char( entry )
                     << ( entry->get_expanded() ? 'E' : '_' )
                     << entry->m_date.m_date;

    output << "\nEc" << entry->m_date_created;
    output << "\nEe" << entry->m_date_edited;
    output << "\nEt" << entry->m_date_status;

    // THEME
    if( entry->is_theme_set() )
        output << "\nEm" << entry->get_theme()->get_name_std();

    // SPELLCHECKING LANGUAGE
    if( entry->get_lang() != LANG_INHERIT_DIARY )
        output << "\nEs" << entry->get_lang().c_str();

    // UNIT
    if( entry->m_unit.empty() == false )
        output << "\nEu" << entry->m_unit;

    // LOCATION
    if( entry->is_location_set() )
        output << "\nEla" << entry->m_location.latitude
               << "\nElo" << entry->m_location.longitude;
    // PATH (ROUTE)
    if( entry->is_map_path_set() )
        for( auto& point : entry->m_map_path )
        output << "\nEra" << point.latitude
               << "\nEro" << point.longitude;

    // PARAGRAPHS
    for( auto& para : entry->m_paragraphs )
        output << "\nEp" << char( para->m_justification ) << para->get_text_std();
}

void
Diary::create_db_header_text( std::stringstream& output, bool encrypted )
{
    output << LIFEO::DB_FILE_HEADER;
    output << "\nV " << LIFEO::DB_FILE_VERSION_INT;
    output << "\nE " << ( encrypted ? 'y' : 'n' );
    output << "\nId" << get_id(); // diary id
    output << "\n\n"; // end of header
}

bool
Diary::create_db_body_text( std::stringstream& output )
{
    // DIARY OPTIONS
    output << "Do" << ( m_opt_show_all_entry_locations ? 'A' : '_' ) <<
            std::setfill( '0' ) << std::setw( 3 ) << m_sorting_criteria <<
            std::setw( 1 ) << m_opt_ext_panel_cur;

    // DEFAULT SPELLCHECKING LANGUAGE
    if( !m_language.empty() )
        output << "\nDs" << m_language;

    // FIRST ENTRY TO SHOW AT STARTUP (HOME ITEM) & LAST ENTRY SHOWN IN PREVIOUS SESSION
    output << "\nDf" << m_startup_entry_id;
    output << "\nDl" << m_last_entry_id;

    // COMPLETION TAG
    if( m_completion_tag_id != DEID_UNSET )
        output << "\nDc" << m_completion_tag_id;

    // THEMES
    for( auto& kv_theme : m_themes )
    {
        Theme* theme{ kv_theme.second };
        output << "\n\nID" << theme->get_id();
        output << "\nT " << ( theme == m_theme_default ? 'D' : '_' ) << theme->get_name_std();
        output << "\nTf" << theme->font.to_string().c_str();
        output << "\nTb" << convert_gdkrgba_to_string( theme->color_base );
        output << "\nTt" << convert_gdkrgba_to_string( theme->color_text );
        output << "\nTh" << convert_gdkrgba_to_string( theme->color_heading );
        output << "\nTs" << convert_gdkrgba_to_string( theme->color_subheading );
        output << "\nTl" << convert_gdkrgba_to_string( theme->color_highlight );
        if( theme->image_bg.empty() == false )
            output << "\nTi" << theme->image_bg;
    }

    // FILTERS
    for( auto& kv_filter : m_filters )
    {
        output << "\n\nID" << kv_filter.second->get_id();
        output << "\nF " << ( kv_filter.second == m_filter_active ? 'A' : '_' )
                         << kv_filter.first.c_str();
        output << '\n' << kv_filter.second->get_definition().c_str();
    }

    // CHARTS
    for( auto& kv_chart : m_charts )
    {
        output << "\n\nID" << kv_chart.second->get_id();
        output << "\nG " << ( kv_chart.second == m_chart_active ? 'A' : '_' )
                         << kv_chart.first.c_str();
        output << '\n' << kv_chart.second->get_definition().c_str();
    }

    // TABLES
    for( auto& kv_table : m_tables )
    {
        output << "\n\nID" << kv_table.second->get_id();
        output << "\nM " << ( kv_table.second == m_table_active ? 'A' : '_' )
                         << kv_table.first.c_str();
        output << '\n' << kv_table.second->get_definition().c_str();
    }

    // CHAPTERS
    for( auto& kv_cc : m_chapter_categories )
    {
        // chapter category:
        CategoryChapters* ctg{ kv_cc.second };
        Chapter* chapter;
        output << "\n\nID" << ctg->get_id()
               << "\nC " << ( ctg == m_p2chapter_ctg_cur ? 'A' : '_' ) << ctg->get_name_std();
        // chapters in it:
        for( auto& kv_chapter : *ctg )
        {
            chapter = kv_chapter.second;
            create_db_entry_text( chapter, output );
            if( chapter->get_color() != Color( "White" ) )
                output << "\nEb" << convert_gdkrgba_to_string( chapter->get_color() );
        }
    }

    // ENTRIES
    for( auto& kv_entry : m_entries )
    {
        Entry* entry{ kv_entry.second };

        // purge empty entries:
        if( entry->is_empty() ) continue;
        // optionally only save filtered entries:
        else if( entry->get_filtered_out() && m_flag_only_save_filtered ) continue;

        create_db_entry_text( entry, output );
    }

    return true;
}

LIFEO::Result
Diary::write()
{
    if( m_flag_read_only ) throw LIFEO::Error( "Trying to save read-only diary!" );

    // BACKUP FOR THE LAST VERSION BEFORE UPGRADE
    if( m_read_version != DB_FILE_VERSION_INT )
        copy_file_suffix( m_path, ".", m_read_version, true );

    // BACKUP THE PREVIOUS VERSION
    if( access( PATH( m_path ).c_str(), F_OK ) == 0 )
    {
        auto&& path_prev{ PATH( m_path + ".~previousversion~" ) };
        if( access( path_prev.c_str(), F_OK ) == 0 )
            remove( path_prev.c_str() );
        rename( PATH( m_path ).c_str(), path_prev.c_str() );
    }

    // WRITE THE FILE
    const Result result{ write( m_path ) };

    // DAILY BACKUP SAVES
    if( Lifeograph::settings.save_backups && !Lifeograph::settings.backup_folder_path.empty() )
    {
        const auto&& name{ m_name.rfind( ".diary" ) == m_name.size() - 6 ?
                           m_name.substr( 0, m_name.size() - 6 ) : m_name };

        if( copy_file( m_path,
                       Glib::build_filename( Lifeograph::settings.backup_folder_path,
                                             name + "_(" +std::to_string( get_id() ) + ")_"
                                                  + Date::format_string( Date::get_today(),
                                                                         "YMD", '-' )
                                                  + ".diary" ),
                       false ) )
            print_info( "Daily backup saved" );
    }

    return result;
}

LIFEO::Result
Diary::write( const std::string& path )
{
    m_flag_only_save_filtered = false;

    if( m_passphrase.empty() )
        return write_plain( path );
    else
        return write_encrypted( path );
}

LIFEO::Result
Diary::write_copy( const std::string& path, const std::string& passphrase, bool flag_filtered )
{
    m_flag_only_save_filtered = flag_filtered;

    std::string passphrase_actual = m_passphrase;
    m_passphrase = passphrase;
    Result result{ write( path ) };
    m_passphrase = passphrase_actual;

    return result;
}

LIFEO::Result
Diary::write_txt( const std::string& path, bool flag_filtered_only )
{
    std::ofstream file( PATH( path ).c_str(), std::ios::out | std::ios::trunc );
    if( ! file.is_open() )
    {
        print_error( "I/O error: ", path );
        return LIFEO::FILE_NOT_WRITABLE;
    }

    // HELPERS
    const std::string separator         = "---------------------------------------------\n";
    const std::string separator_favored = "+++++++++++++++++++++++++++++++++++++++++++++\n";
    const std::string separator_thick   = "=============================================\n";
    const std::string separator_chapter = ":::::::::::::::::::::::::::::::::::::::::::::\n";

    CategoryChapters::reverse_iterator iter_chapter{ m_p2chapter_ctg_cur->rbegin() };
    Chapter* chapter{ iter_chapter == m_p2chapter_ctg_cur->rend() ?
                        nullptr : iter_chapter->second };

    // DIARY TITLE
    file << separator_thick << get_filename_base( path ).c_str() << '\n' << separator_thick;

    // ENTRIES
    for( EntryIterReverse iter_entry = m_entries.rbegin();
         iter_entry != m_entries.rend();
         iter_entry++ )
    {
        Entry* entry{ iter_entry->second };

        // CHAPTER
        while( chapter && entry->get_date_t() < chapter->get_date_t() )
        {
            file << "\n\n" << separator_chapter
                 << chapter->get_date().format_string()
                 << " - " << chapter->get_name_std() << '\n'
                 << separator_chapter << "\n\n";

            chapter = ( ++iter_chapter == m_p2chapter_ctg_cur->rend() ?
                            nullptr : iter_chapter->second );
        }

        // PURGE EMPTY OR FILTERED OUT ENTRIES
        if( entry->is_empty() || ( entry->get_filtered_out() && flag_filtered_only ) )
            continue;

        // FAVOREDNESS AND DATE
        file << ( entry->is_favored() ? separator_favored : separator );
        if( not( entry->get_date().is_hidden() ) )
            file << entry->get_date().format_string() << '\n';

        // TO-DO STATUS
        switch( entry->get_status() & ES::FILTER_TODO)
        {
            case ES::TODO:
                file << "[ ] ";
                break;
            case ES::PROGRESSED:
                file << "[~] ";
                break;
            case ES::DONE:
                file << "[+] ";
                break;
            case ES::CANCELED:
                file << "[X] ";
                break;
        }

        bool flag_first{ true };

        // CONTENT
        for( auto& para : *entry->get_paragraphs() )
        {
            file << para->get_text().c_str() << '\n';
            if( flag_first )
            {
                file << ( entry->is_favored() ? separator_favored : separator );
                flag_first = false;
            }
        }

        file << "\n\n";
    }

    // EMPTY CHAPTERS
    while( chapter )
    {
        file << "\n\n" << separator_chapter
             << chapter->get_date().format_string()
             << " - " << chapter->get_name_std() << '\n'
             << separator_chapter << "\n\n";

        chapter = ( ++iter_chapter == m_p2chapter_ctg_cur->rend() ?
                        nullptr : iter_chapter->second );
    }

    file << '\n';

    file.close();

    return SUCCESS;
}

LIFEO::Result
Diary::write_plain( const std::string& path, bool flag_header_only )
{
    // NOTE: ios::binary prevents windows to use \r\n for line ends
    std::ofstream file( PATH( path ).c_str(), std::ios::binary | std::ios::out | std::ios::trunc );
    if( ! file.is_open() )
    {
        print_error( "I/O error: ", path );
        return LIFEO::COULD_NOT_START;
    }

    std::stringstream output;

    create_db_header_text( output, flag_header_only ); // header only mode = encrypted diary

    if( ! flag_header_only )
        create_db_body_text( output );

    file << output.str();
    file.close();

    return LIFEO::SUCCESS;
}

LIFEO::Result
Diary::write_encrypted( const std::string& path )
{
    // writing header:
    write_plain( path, true );
    std::ofstream file( PATH( path ).c_str(), std::ios::out | std::ios::app | std::ios::binary );
    if( ! file.is_open() )
    {
        print_error( "I/O error: ", path );
        return LIFEO::COULD_NOT_START;
    }
    std::stringstream output;
    CipherBuffers buf;

    // first char of passphrase for validity checking
    output << m_passphrase[ 0 ] << '\n';
    create_db_body_text( output );

    // encryption
    try {
        size_t size =  output.str().size() + 1;

        LIFEO::Cipher::create_new_key( m_passphrase.c_str(), &buf.salt, &buf.key );

        LIFEO::Cipher::create_iv( &buf.iv );

        buf.buffer = new unsigned char[ size ];
        memcpy( buf.buffer, output.str().c_str(), size );

        LIFEO::Cipher::encrypt_buffer( buf.buffer, size, buf.key, buf.iv );

        file.write( ( char* ) buf.salt, LIFEO::Cipher::cSALT_SIZE );
        file.write( ( char* ) buf.iv, LIFEO::Cipher::cIV_SIZE );
        file.write( ( char* ) buf.buffer, size );
    }
    catch( ... )
    {
        buf.clear();
        return LIFEO::FAILURE;
    }

    file.close();
    buf.clear();
    return LIFEO::SUCCESS;
}

void
Diary::close_file()
{
    if( m_ifstream )
    {
        m_ifstream->close();
        delete m_ifstream;
        m_ifstream = nullptr;
    }
}

bool
Diary::remove_lock_if_necessary()
{
    if( m_login_status != LOGGED_IN_EDIT || m_path.empty() )
        return false;

    std::string path_lock( m_path + LOCK_SUFFIX );
    if( access( PATH( path_lock ).c_str(), F_OK ) == 0 )
        remove( PATH( path_lock ).c_str() );
    return true;
}

// ELEMENTS
DEID
Diary::create_new_id( DiaryElement* element )
{
    DEID retval;
    if( m_force_id == DEID_UNSET )
    {
        do
        {
#ifndef _WIN32
            std::random_device rd;  //seed
            std::mt19937 gen( rd() ); // generator
#else
            std::mt19937 gen( time( NULL ) ); // generator
#endif
            std::uniform_int_distribution<> dis0(0, 9);
            std::uniform_int_distribution<> dis1(1, 9);

            retval = ( dis1( gen ) * pow( 10, 6 ) );

            for( int i = 0; i < 6; i++ )
                retval += ( dis0( gen ) * pow( 10, i ) );
        }
        while( m_ids.find( retval ) != m_ids.end() );
    }
    else
    {
        retval = m_force_id;
        m_force_id = DEID_UNSET;
    }
    m_ids[ retval ] = element;

    return retval;
}

DiaryElement*
Diary::get_element( DEID id ) const
{
    PoolDEIDs::const_iterator iter( m_ids.find( id ) );
    return( iter == m_ids.end() ? nullptr : iter->second );
}

Entry*
Diary::get_startup_entry() const
{
    Entry* entry{ nullptr };

    switch( m_startup_entry_id )
    {
        case HOME_CURRENT_ENTRY:
            entry = get_most_current_entry();
            break;
        case HOME_LAST_ENTRY:
            entry = get_entry_by_id( m_last_entry_id );
            break;
        case DEID_UNSET:
            break;
        default:
            entry = get_entry_by_id( m_startup_entry_id );
            break;
    }

    if( entry == nullptr )
    {
        entry = m_entries.begin()->second;
        print_info( "Failed to detect a startup entry. Will show the first entry.");
    }

    return entry;
}

Entry*
Diary::get_most_current_entry() const
{
    date_t date( Date::get_today() );
    date_t diff1( Date::FLAG_ORDINAL );
    long diff2;
    Entry* most_curr{ nullptr };
    bool descending( false );

    for( auto& kv_entry : m_entries )
    {
        Entry* entry( kv_entry.second );

        if( ! entry->get_filtered_out() && ! entry->get_date().is_ordinal() )
        {
            diff2 = entry->get_date_t() - date;
            if( diff2 < 0 ) diff2 *= -1;
            if( static_cast< unsigned long >( diff2 ) < diff1 )
            {
                diff1 = diff2;
                most_curr = entry;
                descending = true;
            }
            else
            if( descending )
                break;
        }
    }

    return most_curr;
}

// ENTRIES
Entry*
Diary::get_entry_today() const
{
    return get_entry_by_date( Date::get_today( 1 ) );  // 1 is the order
}

Entry*
Diary::get_entry_by_id( const DEID id ) const
{
    DiaryElement* elem{ get_element( id ) };

    if( elem && elem->get_type() == ET_ENTRY )
        return dynamic_cast< Entry* >( elem );
    else
        return nullptr;
}

Entry*
Diary::get_entry_by_date( const date_t date, bool filtered_too ) const
{
    EntryIterConst iter( m_entries.find( date ) );
    if( iter != m_entries.end() )
        if( filtered_too || iter->second->get_filtered_out() == false )
            return iter->second;

    return nullptr;
}

VecEntries
Diary::get_entries_by_date( date_t date, bool filtered_too ) const // takes pure date
{
    VecEntries entries;

    EntryIterConst&& iter{ m_entries.find( date + 1 ) };
    if( iter == m_entries.end() )
        return entries; // return empty vector

    for( ; ; --iter )
    {
        if( iter->second->get_date().get_pure() == date )
        {
            if( filtered_too || iter->second->get_filtered_out() == false )
                entries.push_back( iter->second );
        }
        else
            break;
        if( iter == m_entries.begin() )
            break;
    }
    return entries;
}

Entry*
Diary::get_entry_by_name( const Ustring& name ) const
{
    auto iter( m_entry_names.find( name ) );
    if( iter != m_entry_names.end() )
        return iter->second;

    return nullptr;
}

VecEntries
Diary::get_entries_by_name( const Ustring& name ) const
{
    VecEntries ev;

    auto range{ m_entry_names.equal_range( name ) };

    for( auto& iter = range.first; iter!=range.second; ++iter )
        ev.push_back( iter->second );

    return ev;
}

VecEntries
Diary::get_entries_by_filter( const Ustring& filter_name )
{
    VecEntries ev;
    auto&& it_filter{ m_filters.find( filter_name ) };

    if( it_filter != m_filters.end() )
    {
        FiltererContainer* fc{ it_filter->second->get_filterer_stack() };

        for( auto& kv_entry : m_entries )
            if( fc->filter( kv_entry.second ) )
                ev.push_back( kv_entry.second );

        delete fc;
    }

    return ev;
}

unsigned int
Diary::get_entry_count_on_day( const Date& date_impure ) const
{
    unsigned int count{ 0 };
    date_t date = date_impure.get_pure();
    while( m_entries.find( date + count + 1 ) != m_entries.end() )
        count++;

    return count;
}

Entry*
Diary::get_entry_next_in_day( const Date& date ) const
{
    if( date.is_ordinal() )
        return nullptr;

    for( auto iter = m_entries.rbegin(); iter != m_entries.rend(); iter++ )
    {
        if( date.get_day() == Date::get_day( iter->first ) &&
            date.get_order_3rd() < Date::get_order_3rd( iter->first ) )
            return( iter->second );
    }

    return nullptr;
}

Entry*
Diary::get_entry_first_untrashed() const
{
    for( auto& kv_entry : m_entries )
    {
        if( ! kv_entry.second->is_trashed() )
            return kv_entry.second;
    }
    return nullptr;
}

Entry*
Diary::get_entry_latest() const
{
    for( auto& kv_entry : m_entries )
    {
        if( ! Date::is_ordinal( kv_entry.first ) )
            return kv_entry.second;
    }
    return nullptr;
}

Entry*
Diary::get_entry_closest_to( const date_t source, bool only_unfiltered ) const
{
    Entry* entry_after{ nullptr };
    Entry* entry_before{ nullptr };
    auto iter = m_entries.begin();

    for( ; iter != m_entries.end(); ++iter )
    {
        if( source < iter->first )
        {
            entry_before = iter->second;
            break;
        }
        else if( source > iter->first )
            entry_after = iter->second;
    }

    if( only_unfiltered && entry_after && entry_after->get_filtered_out() )
    {
        PoolEntries::const_reverse_iterator riter{ iter };
        riter--;

        for( ; riter != m_entries.rend(); ++riter )
        {
            if( not( riter->second->get_filtered_out() ) )
            {
                entry_before = riter->second;
                break;
            }
        }
    }

    if( only_unfiltered && entry_before && entry_before->get_filtered_out() )
    {
        for( ; iter != m_entries.end(); ++iter )
        {
            if( not( iter->second->get_filtered_out() ) )
            {
                entry_before = iter->second;
                break;
            }
        }
    }

    if( !entry_before && !entry_after )
        return nullptr;
    if( entry_before && !entry_after )
        return entry_before;
    if( !entry_before && entry_after )
        return entry_after;

    return( ( source - entry_before->get_date_t() ) < ( entry_after->get_date_t() - source ) ?
            entry_before : entry_after );
}

Entry*
Diary::get_first_descendant( const date_t date ) const
{
    if( ! Date::is_ordinal( date ) )
        return nullptr;

    auto&& iter{ m_entries.find( date ) };

    if( iter == m_entries.end() || iter == m_entries.begin() )
        return nullptr;
    else
        iter--;

    if( Date::is_descendant_of( iter->first, date ) )
        return iter->second;
    else
        return nullptr;
}

void
Diary::set_entry_date( Entry* entry, date_t date )
{
    VecEntries ev;
    bool       flag_move_children{ false };
    bool       flag_has_children{ get_first_descendant( entry->get_date_t() ) != nullptr };
    int        level_diff{ entry->m_date.get_level() - Date::get_level( date ) };

    m_entries.erase( entry->get_date_t() );

    if( entry->is_ordinal() )
    {
        // remove and store sub-entries first
        if( Date::is_ordinal( date ) &&
            Date::get_level( date ) + entry->get_descendant_depth() <= 3 )
        {
            flag_move_children = true;

            for( auto iter = m_entries.begin(); iter != m_entries.end(); )
            {
                if( !Date::is_ordinal( iter->first ) )
                    break;

                if( Date::is_descendant_of( iter->first, entry->get_date_t() ) )
                {
                    ev.push_back( iter->second );
                    iter = m_entries.erase( iter );
                }
                else
                    ++iter;
            }

            // then shift the following siblings and their children backwards
            shift_entry_orders_after( entry->get_date_t(), -1 );

            // update target date per the shift above
            if( date > entry->get_date_t() &&
                // below two together mean "is_nephew?"
                not( Date::is_sibling( date, entry->get_date_t() ) ) &&
                Date::is_descendant_of( date, entry->get_date().get_parent() ) )
                Date::backward_order( date, entry->get_date().get_level() );
        }
        else if( flag_has_children ) // no depth under the target to hold the descendants
            throw LIFEO::Error( "Tried to set an unfit date for the entry" );
    }
    else
    {
        shift_dated_entry_orders_after( entry->get_date_t(), -1 );
    }

    if( Date::is_ordinal( date ) )
    {
        // then shift the entries after the target forwards to make room (if date is taken)
        if( m_entries.find( date ) != m_entries.end() )
            shift_entry_orders_after( date, 1 );

        // add back removed sub entries
        if( flag_move_children )
        {
            for( auto& e : ev )
            {
                Date date_new { Date::make_ordinal( !Date::is_hidden( date ), 0, 0, 0 ) };
                int  level    { 1 };
                // copy all orders from the new parent
                for( ; level <= Date::get_level( date ); level++ )
                    date_new.set_order( level, Date::get_order( date, level ) );
                // copy all sub-orders from the previous
                for( ; level <= 3; level++ )
                    date_new.set_order( level, e->m_date.get_order( level + level_diff ) );

                e->m_date.m_date = date_new.m_date;
                m_entries[ e->get_date_t() ] = e;
            }
        }
    }
    else
    {
        shift_dated_entry_orders_after( date, 1 );
    }

    entry->set_date( date );
    m_entries[ date ] = entry;

    update_entries_in_chapters(); // date changes require full update
}

void
Diary::update_entry_name( Entry* entry )
{
    Ustring old_name;
    bool    flag_name_changed{ true };

    for( auto&& iter = m_entry_names.begin(); iter != m_entry_names.end(); ++iter )
    {
        if( iter->second == entry )
        {
            old_name = iter->first;

            if( old_name == entry->get_name() )
                flag_name_changed = false;
            else
                m_entry_names.erase( iter );

            break;
        }
    }

    if( flag_name_changed && entry->has_name() )
    {
        m_entry_names.emplace( entry->get_name(), entry );

        // update references in other entries
        if( old_name.empty() == false )
            replace_all_matches( STR::compose( ":", old_name, ":" ),
                                 STR::compose( ":", entry->get_name(), ":" ) );
    }
}

Entry*
Diary::create_entry( date_t date, const Ustring& content, bool flag_favorite )
{
    // make it the last entry of its day:
    while( m_entries.find( date ) != m_entries.end() && Date::get_order_3rd( date ) )
    {
        if( Date::get_order_3rd( date ) == Date::ORDER_3RD_MAX )
        {
            print_error( "Day is full!" );
            return nullptr;
        }
        ++date;
    }

    Entry* entry = new Entry( this, date,
                              flag_favorite ? ES::ENTRY_DEFAULT_FAV : ES::ENTRY_DEFAULT );

    entry->set_text( content );

    m_entries[ date ] = entry;
    add_entry_to_related_chapter( entry );
    if( entry->has_name() )
        m_entry_names.emplace( entry->get_name(), entry );

    return( entry );
}
// USED DURING DIARY FILE READ:
Entry*
Diary::create_entry( date_t date, bool flag_favorite, bool flag_trashed, bool flag_expanded )
{
    ElemStatus status{ ES::NOT_TODO |
                       ( flag_favorite ? ES::FAVORED : ES::NOT_FAVORED ) |
                       ( flag_trashed ? ES::TRASHED : ES::NOT_TRASHED ) };
    if( flag_expanded ) status |= ES::EXPANDED;

    Entry* entry = new Entry( this, date, status );

    m_entries[ date ] = entry;
    add_entry_to_related_chapter( entry );

    return( entry );
}

Entry*
Diary::add_today()
{
    return create_entry( Date::get_today( 1 ) );
}

bool
Diary::dismiss_entry( Entry* entry, bool flag_update_chapters )
{
    // fix startup element:
    if( m_startup_entry_id == entry->get_id() )
        m_startup_entry_id = HOME_CURRENT_ENTRY;

    // remove from filters:
    remove_entry_from_filters( entry );

    // remove from map:
    m_entries.erase( entry->get_date_t() );

    // remove from name map:
    auto range{ m_entry_names.equal_range( entry->get_name() ) };
    for( auto& iter = range.first; iter!=range.second; ++iter )
        if( iter->second->is_equal_to( entry ) )
        {
            m_entry_names.erase( iter );
            break;
        }

    // fix entry order:
    VecEntries ev;
    if( entry->is_ordinal() )
    {
        // remove and store sub-entries first
        for( auto iter = m_entries.begin(); iter != m_entries.end(); ++iter )
        {
            if( !Date::is_ordinal( iter->first ) )
                break;

            if( Date::is_descendant_of( iter->first, entry->get_date_t() ) )
            {
                ev.push_back( iter->second );
                iter = m_entries.erase( iter );
            }
        }

        // then, shift the following top items and their sub items backwards
        shift_entry_orders_after( entry->get_date_t(), -1 );

        // add back removed sub entries as top level entries
        date_t lowest_top{ get_available_order_1st( entry->is_ordinal_hidden() ) };
        for( auto iter = ev.rbegin(); iter != ev.rend(); ++iter )
        {
            Entry* e{ *iter };
            e->m_date.m_date = lowest_top;
            m_entries[ lowest_top ] = e;
            Date::forward_order_1st( lowest_top );
        }
    }
    else
        shift_dated_entry_orders_after( entry->get_date_t(), -1 );

    delete entry;

    if( m_entries.empty() ) // an open diary must always contain at least one entry
        add_today();

    if( flag_update_chapters )
        update_entries_in_chapters(); // date changes require full update

    return true;
}

void
Diary::shift_entry_orders_after( date_t begin, int shift )
{
    if( shift == 0 || Date::is_ordinal( begin ) == false )
        return;

    Entry* entry;

    if( shift < 0 )
    {
        for( auto iter = m_entries.rbegin(); iter != m_entries.rend(); )
        {
            if( ! Date::is_same_kind( begin, iter->first ) )
            {
                ++iter;
                continue;
            }

            auto&& fiter{ iter.base() };
            fiter--;

            entry = iter->second;
            if( Date::is_descendant_of( entry->get_date_t(), Date::get_parent( begin ) ) &&
                entry->get_date_t() > begin )
            {
                m_entries.erase( entry->get_date_t() );
                entry->m_date.backward_order( Date::get_level( begin ) );
                // TODO check if the following is absolutely necessary:
                entry->m_date.m_date = get_available_order_next( entry->get_date_t() );
                fiter = m_entries.emplace( entry->get_date_t(), entry ).first;
            }
            iter = PoolEntries::reverse_iterator{ fiter };
        }
    }
    else
    {
        for( auto iter = m_entries.begin(); iter != m_entries.end(); ++iter )
        {
            if( ! Date::is_same_kind( begin, iter->first ) )
                continue;

            entry = iter->second;
            if( Date::is_descendant_of( entry->get_date_t(), Date::get_parent( begin ) ) &&
                entry->get_date_t() >= begin )
            {
                m_entries.erase( iter );
                entry->m_date.forward_order( Date::get_level( begin ) );
                iter = m_entries.emplace( entry->get_date_t(), entry ).first;
            }
        }
    }
}

void
Diary::shift_dated_entry_orders_after( date_t begin, int shift )
{
    if( shift == 0 )
        return;

    Entry* entry;

    if( shift < 0 )
    {
        for( auto iter = m_entries.rbegin(); iter != m_entries.rend(); )
        {
            if( Date::is_ordinal( iter->first ) )
                break;

            entry = iter->second;
            auto&& fiter{ iter.base() };
            fiter--;
            if( entry->get_date().get_pure() == Date::get_pure( begin ) &&
                entry->get_date().get_order_3rd() > Date::get_order_3rd( begin ) )
            {
                m_entries.erase( fiter );
                entry->m_date.m_date--;
                fiter = m_entries.emplace( entry->get_date_t(), entry ).first;
            }
            iter = PoolEntries::reverse_iterator{ fiter };
        }
    }
    else
    {
        for( auto iter = m_entries.begin(); iter != m_entries.end(); ++iter )
        {
            if( Date::is_ordinal( iter->first ) )
                continue;

            entry = iter->second;
            if( entry->get_date().get_pure() == Date::get_pure( begin ) &&
                entry->get_date().get_order_3rd() >= Date::get_order_3rd( begin ) )
            {
                m_entries.erase( iter );
                entry->m_date.m_date++;
                iter = m_entries.emplace( entry->get_date_t(), entry ).first;
            }
        }
    }
}

date_t
Diary::get_available_order_1st( bool flag_hidden ) const
{
    date_t result{ 0 };

    for( auto& kv_entry : m_entries )
    {
        if( kv_entry.second->is_ordinal() &&
            kv_entry.second->is_ordinal_hidden() == flag_hidden )
        {
            result = kv_entry.second->get_date_t();
            break;
        }
    }

    if( result != 0 )
        result = Date::make_ordinal( !flag_hidden, Date::get_order_1st( result ) + 1, 0, 0 );
    else
        result = Date::get_lowest_ordinal( flag_hidden );

    return result;
}

date_t
Diary::get_available_order_sub( date_t date ) const
{
    const int level{ Date::get_level( date ) + 1 };
    if( level > 3 )
        return Date::NOT_SET;

    Date::set_order( date, level, 1 );
    while( m_entries.find( date ) != m_entries.end() )
    {
        if( Date::get_order( date, level ) >= Date::ORDER_MAX )
            return Date::NOT_SET;

        Date::forward_order( date, level );
    }

    return date;
}

date_t
Diary::get_available_order_next( date_t date ) const
{
    while( m_entries.find( date ) != m_entries.end() )
    {
        if( Date::get_order( date, Date::get_level( date ) ) >= Date::ORDER_MAX )
            return Date::NOT_SET;

        Date::forward_order( date, Date::get_level( date ) );
    }

    return date;
}

bool
Diary::is_trash_empty() const
{
    for( auto& kv_entry : m_entries )
        if( kv_entry.second->is_trashed() )
            return false;

    for( auto& kv_ctg : m_chapter_categories )
        for( auto& kv_chapter : *kv_ctg.second )
            if( kv_chapter.second->is_trashed() )
                return false;

    return true;
}

int
Diary::get_time_span() const
{
    int timespan{ 0 };
    Entry* entry_latest{ get_entry_latest() };
    if( entry_latest )
        timespan = m_entries.rbegin()->second->get_date().
                calculate_days_between( entry_latest->get_date() );
    return timespan;
}

// SEARCHING
int
Diary::set_search_text( const Ustring& text, bool flag_only_unfiltered )
{
    m_search_text = text;
    clear_matches();

    if( text.empty() )
        return 0;

    UstringSize pos_para;
    UstringSize pos_entry;
    auto        length { m_search_text.size() };
    Entry*      entry;
    int         count  { 0 };

    for( auto& kv_entry : m_entries )
    {
        entry = kv_entry.second;
        pos_entry = 0;
        if( flag_only_unfiltered && entry->get_filtered_out() )
            continue;

        for( auto& para : entry->m_paragraphs )
        {
            const Ustring&& entrytext { STR::lowercase( para->get_text() ) };
            pos_para = 0;

            while( ( pos_para = entrytext.find( m_search_text, pos_para ) ) != Ustring::npos )
            {
                m_matches.push_back( new Match( para,
                                                pos_entry + pos_para,
                                                pos_para ) );
                count++;
                pos_para += length;
            }

            pos_entry += ( entrytext.length() + 1 );
        }
    }

    return count;
}

void
Diary::remove_entry_from_search( const Entry* entry )
{
    if( m_search_text.empty() || !entry ) return;

    auto&& iter_insert{ m_matches.begin() };

    for( ; iter_insert != m_matches.end(); )
    {
        if( ( *iter_insert )->para == nullptr ||
            ( *iter_insert )->para->m_host->get_id() == entry->get_id() )
        {
            delete *iter_insert;
            iter_insert = m_matches.erase( iter_insert );
        }
        else
            iter_insert++;
    }
}

void
Diary::update_search_for_entry( const Entry* entry, bool flag_only_unfiltered )
{
    if( m_search_text.empty() || !entry ) return;
    if( flag_only_unfiltered && entry->get_filtered_out() ) return;

    auto&& iter_insert{ m_matches.begin() };

    for( ; iter_insert != m_matches.end(); )
    {
        if( ( *iter_insert )->para == nullptr ||
            ( *iter_insert )->para->m_host->get_id() == entry->get_id() )
        {
            delete *iter_insert;
            iter_insert = m_matches.erase( iter_insert );
        }
        else
            iter_insert++;
    }

    UstringSize pos_para;
    UstringSize pos_entry { 0 };
    auto        length    { m_search_text.size() };
    int         count     { 0 };

    for( auto& para : entry->m_paragraphs )
    {
        const Ustring&& entrytext { STR::lowercase( para->get_text() ) };
        pos_para = 0;

        while( ( pos_para = entrytext.find( m_search_text, pos_para ) ) != Ustring::npos )
        {
            m_matches.insert( iter_insert, new Match( para,
                                                      pos_entry + pos_para,
                                                      pos_para ) );
            count++;
            pos_para += length;
        }

        pos_entry += ( entrytext.length() + 1 );
    }

    return;
}

void
Diary::replace_match( Match& match, const Ustring& newtext )
{
    if( not( match.valid ) ) return;

    const int delta{ ( int ) newtext.length() - ( int ) m_search_text.length() };

    match.para->erase_text( match.pos_para, m_search_text.length() );
    match.para->insert_text( match.pos_para, newtext );

    for( auto& m : m_matches )
    {
        if( m->para->m_host == match.para->m_host && m->pos_entry > match.pos_entry )
        {
            m->pos_entry += delta;

            if( m->para == match.para && m->pos_para > match.pos_para )
                m->pos_para += delta;
        }
    }

    match.valid = false;
}

void
Diary::replace_all_matches( const Ustring& newtext )
{
    const auto  length_s  { m_search_text.length() };
    const int   delta     { ( int ) newtext.length() - ( int ) length_s };
    int         delta_cum { 0 };
    Paragraph*  para_cur  { nullptr };

    for( auto& m : m_matches )
    {
        if( not( m->valid ) ) continue;

        if( m->para != para_cur )
        {
            delta_cum = 0;
            para_cur = m->para;
        }
        else
            m->pos_para += delta_cum;

        m->para->erase_text( m->pos_para, length_s );
        m->para->insert_text( m->pos_para, newtext );

        delta_cum += delta;

        delete m;
    }

    m_matches.clear();
}

// this version does not disturb the active search and is case-sensitive
void
Diary::replace_all_matches( const Ustring& oldtext, const Ustring& newtext )
{
    if( oldtext.empty() )
        return;

    UstringSize pos_para;
    UstringSize pos_entry;
    const auto  length_old { oldtext.length() };
    const int   delta      { ( int ) newtext.length() - ( int ) length_old };
    int         delta_cum  { 0 };
    Paragraph*  para_cur   { nullptr };
    VecMatches  matches;

    // FIND
    for( auto& kv_entry : m_entries )
    {
        pos_entry = 0;

        for( auto& para : kv_entry.second->m_paragraphs )
        {
            const auto& entrytext{ para->get_text() };
            pos_para = 0;

            while( ( pos_para = entrytext.find( oldtext, pos_para ) ) != Ustring::npos )
            {
                matches.push_back( Match{ para,
                                          pos_entry + pos_para,
                                          pos_para } );
                pos_para += length_old;
            }

            pos_entry += ( entrytext.length() + 1 );
        }
    }

    // REPLACE
    for( auto&& m : matches )
    {
        if( m.para != para_cur )
        {
            delta_cum = 0;
            para_cur = m.para;
        }
        else
            m.pos_para += delta_cum;

        m.para->erase_text( m.pos_para, length_old );
        m.para->insert_text( m.pos_para, newtext );

        delta_cum += delta;
    }
}

Match*
Diary::get_match_at( int i )
{
    if( m_matches.empty() )
        return nullptr;

    auto&& iter{ m_matches.begin() };
    std::advance( iter, i );

    return( *iter );
}

void
Diary::clear_matches()
{
    for( auto& m : m_matches )
        delete m;

    m_matches.clear();
}

// FILTERS
Filter*
Diary::create_filter( const Ustring& name0, const Ustring& definition )
{
    Ustring name{ create_unique_name_for_map( m_filters, name0 ) };
    Filter* filter{ new Filter( this, name, definition ) };
    m_filters.emplace( name, filter );

    return filter;
}

Filter*
Diary::get_filter( const Ustring& name ) const
{
    auto&& it_filter{ m_filters.find( name ) };
    return( it_filter ==  m_filters.end() ? nullptr : it_filter->second );
}

bool
Diary::set_filter_active( const Ustring& name )
{
    auto&& it_filter{ m_filters.find( name ) };
    if( it_filter == m_filters.end() )
        return false;

    m_filter_active = it_filter->second;
    return true;
}

bool
Diary::rename_filter( const Ustring& old_name, const Ustring& new_name )
{
    if( new_name.empty() )
        return false;
    if( m_filters.find( new_name ) != m_filters.end() )
        return false;

    auto&& it_filter{ m_filters.find( old_name ) };
    if( it_filter == m_filters.end() )
        return false;

    auto filter{ it_filter->second };

    filter->set_name( new_name );

    m_filters.erase( old_name );
    m_filters.emplace( new_name, filter );

    return true;
}

bool
Diary::dismiss_filter( const Ustring& name )
{
    if( m_filters.size() < 2 )
        return false;

    auto&& it_filter{ m_filters.find( name ) };

    if( it_filter == m_filters.end() )
        return false;

    Filter* filter_to_delete{ it_filter->second };

    m_filters.erase( name );

    if( name == m_filter_active->get_name() )
        m_filter_active = m_filters.begin()->second;

    delete filter_to_delete;

    return true;
}

bool
Diary::dismiss_filter_active()
{
    return dismiss_filter( m_filter_active->get_name() );
}

void
Diary::remove_entry_from_filters( Entry* entry )
{
    const Ustring     rep_str    { STR::compose( "Fr", DEID_UNSET ) };

    for( auto& kv_filter : m_filters )
    {
        Filter*       filter     { kv_filter.second };
        Ustring       definition { filter->get_definition() };
        const Ustring ref_str    { STR::compose( "Fr", entry->get_id() ) };

        while( true )
        {
            auto pos{ definition.find( ref_str ) };
            if( pos != Ustring::npos )
                definition.replace( pos, ref_str.size(), rep_str );
            else
                break;
        }

        filter->set_definition( definition );
    }
}

// CHARTS
ChartElem*
Diary::create_chart( const Ustring& name0, const Ustring& definition )
{
    Ustring name{ create_unique_name_for_map( m_charts, name0 ) };
    ChartElem* chart{ new ChartElem( this, name, definition ) };
    m_charts.emplace( name, chart );

    return chart;
}

ChartElem*
Diary::get_chart( const Ustring& name ) const
{
    auto&& it_chart{ m_charts.find( name ) };
    return( it_chart ==  m_charts.end() ? nullptr : it_chart->second );
}

bool
Diary::set_chart_active( const Ustring& name )
{
    auto&& it_chart{ m_charts.find( name ) };
    if( it_chart == m_charts.end() )
        return false;

    m_chart_active = it_chart->second;
    return true;
}

bool
Diary::rename_chart( const Ustring& old_name, const Ustring& new_name )
{
    if( new_name.empty() )
        return false;
    if( m_charts.find( new_name ) != m_charts.end() )
        return false;

    auto&& it_chart{ m_charts.find( old_name ) };
    if( it_chart == m_charts.end() )
        return false;

    auto chart{ it_chart->second };

    chart->set_name( new_name );

    m_charts.erase( old_name );
    m_charts.emplace( new_name, chart );

    return true;
}

bool
Diary::dismiss_chart( const Ustring& name )
{
    auto&& it_chart{ m_charts.find( name ) };

    if( it_chart == m_charts.end() )
        return false;

    ChartElem* chart_to_delete{ it_chart->second };

    m_charts.erase( name );

    if( name == m_chart_active->get_name() )
        m_chart_active = m_charts.begin()->second;

    delete chart_to_delete;

    return true;
}

void
Diary::fill_up_chart_data( ChartData* cd ) const
{
    if( !cd )
        return;

    if( cd->get_span() < 1 )
        return;

    for( auto kv_chapter = m_p2chapter_ctg_cur->rbegin();
         kv_chapter != m_p2chapter_ctg_cur->rend(); ++kv_chapter )
    {
        const auto&& d_chapter{ kv_chapter->second->get_date() };
        double pos{ double( cd->calculate_distance( cd->get_start_date(), d_chapter.m_date ) ) };
        if( cd->get_start_date() > d_chapter.m_date )
            pos = -pos;
        switch( cd->get_period() )
        {
            case ChartData::YEARLY:
                pos += ( double( d_chapter.get_yearday() - 1.0 ) / d_chapter.get_days_in_year() );
                break;
            case ChartData::MONTHLY:
                pos += ( double( d_chapter.get_day() - 1.0 ) / d_chapter.get_days_in_month() );
                break;
            case ChartData::WEEKLY:
                pos += ( double( d_chapter.get_weekday() ) / 7.0 );
                break;
        }

        cd->chapters.push_back(
                ChartData::PairChapter( pos,
                        midtone( kv_chapter->second->get_color(), Color( "#FFFFFF" ), 0.7 ) ) );
    }

    // dummy entry just to have the last chapter drawn:
    cd->chapters.push_back( ChartData::PairChapter( FLT_MAX, Color( "#000000" ) ) );
}

// TABLES
TableElem*
Diary::create_table( const Ustring& name0, const Ustring& definition )
{
    Ustring name{ create_unique_name_for_map( m_tables, name0 ) };
    TableElem* table{ new TableElem( this, name, definition ) };
    m_tables.emplace( name, table );

    return table;
}

TableElem*
Diary::get_table( const Ustring& name ) const
{
    auto&& it_table{ m_tables.find( name ) };
    return( it_table ==  m_tables.end() ? nullptr : it_table->second );
}

bool
Diary::set_table_active( const Ustring& name )
{
    auto&& it_table{ m_tables.find( name ) };
    if( it_table == m_tables.end() )
        return false;

    m_table_active = it_table->second;
    return true;
}

bool
Diary::rename_table( const Ustring& old_name, const Ustring& new_name )
{
    if( new_name.empty() )
        return false;
    if( m_tables.find( new_name ) != m_tables.end() )
        return false;

    auto&& it_table{ m_tables.find( old_name ) };
    if( it_table == m_tables.end() )
        return false;

    auto table{ it_table->second };

    table->set_name( new_name );

    m_tables.erase( old_name );
    m_tables.emplace( new_name, table );

    return true;
}

bool
Diary::dismiss_table( const Ustring& name )
{
    auto&& it_table{ m_tables.find( name ) };

    if( it_table == m_tables.end() )
        return false;

    TableElem* table_to_delete{ it_table->second };

    m_tables.erase( name );

    if( name == m_table_active->get_name() )
        m_table_active = m_tables.begin()->second;

    delete table_to_delete;

    return true;
}

// THEMES
Theme*
Diary::create_theme( const Ustring& name0 )
{
    Ustring name = create_unique_name_for_map( m_themes, name0 );
    Theme* theme = new Theme( this, name );
    m_themes.emplace( name, theme );

    return theme;
}

Theme*
Diary::create_theme( const Ustring& name0,
                     const Ustring& font,
                     const std::string& base, const std::string& text,
                     const std::string& head, const std::string& subh,
                     const std::string& hlgt )
{
    Ustring name = create_unique_name_for_map( m_themes, name0 );
    Theme* theme = new Theme( this, name, font, base, text, head, subh, hlgt );
    m_themes.emplace( name, theme );

    return theme;
}

void
Diary::clear_themes()
{
    for( auto iter = m_themes.begin(); iter != m_themes.end(); ++iter )
        delete iter->second;

    m_themes.clear();

    m_theme_default = nullptr;
}

void
Diary::dismiss_theme( Theme* theme )
{
    if( theme->is_default() ) throw LIFEO::Error( "Trying to delete the default theme" );

    for( auto& kv_entry : m_entries )
    {
        if( kv_entry.second->get_theme() == theme )
            kv_entry.second->set_theme( nullptr );
    }

    // remove from associated filters
    for( auto& kv_filter : m_filters )
    {
        Ustring def{ kv_filter.second->get_definition() };
        auto    pos{ def.find( "\nFh" + theme->get_name() ) };
        if( pos != Ustring::npos )
        {
            def.erase( pos, theme->get_name().length() + 3 );
            kv_filter.second->set_definition( def );
        }
    }

    m_themes.erase( theme->get_name() );
    delete theme;
}

void
Diary::rename_theme( Theme* theme, const Ustring& new_name )
{
    m_themes.erase( theme->get_name() );
    theme->set_name( create_unique_name_for_map( m_themes, new_name ) );
    m_themes[ theme->get_name() ] = theme;
}

// CHAPTERS
CategoryChapters*
Diary::create_chapter_ctg( const Ustring& name0 )
{
    Ustring name = create_unique_name_for_map( m_chapter_categories, name0 );
    CategoryChapters* ctg = new CategoryChapters( this, name );
    m_chapter_categories.insert( PoolCategoriesChapters::value_type( name, ctg ) );
    return ctg;
}

bool
Diary::dismiss_chapter_ctg( CategoryChapters* ctg )
{
    if( m_chapter_categories.size() < 2 )
        return false;

    if( m_chapter_categories.erase( ctg->get_name() ) > 0 )
    {
        if( m_p2chapter_ctg_cur->is_equal_to( ctg ) )
            m_p2chapter_ctg_cur = m_chapter_categories.begin()->second;

        delete ctg;

        return true;
    }
    else
        return false;
}

void
Diary::rename_chapter_ctg( CategoryChapters* ctg, const Ustring& new_name )
{
    m_chapter_categories.erase( ctg->get_name() );
    ctg->set_name( create_unique_name_for_map( m_chapter_categories, new_name ) );
    m_chapter_categories[ ctg->get_name() ] = ctg;
}

void
Diary::dismiss_chapter( Chapter* chapter, bool flag_dismiss_contained )
{
    auto&& iter{ m_p2chapter_ctg_cur->find( chapter->get_date_t() ) };
    if( iter == m_p2chapter_ctg_cur->end() )
    {
        print_error( "Chapter could not be found in assumed category" );
        return;
    }
    else if( ( ++iter ) != m_p2chapter_ctg_cur->end() )  // fix time span
    {
        Chapter* chapter_earlier( iter->second );
        if( chapter->get_time_span() > 0 )
            chapter_earlier->set_time_span(
                    chapter_earlier->get_time_span() + chapter->get_time_span() );
        else
            chapter_earlier->set_time_span( 0 );
    }

    if( flag_dismiss_contained )
    {
        for( Entry* entry : *chapter )
            dismiss_entry( entry, false );
    }

    m_p2chapter_ctg_cur->erase( chapter->get_date_t() );

    delete chapter;
    update_entries_in_chapters();
}

int
Diary::get_chapter_count() const
{
    int count{ 0 };

    for( auto& kv_ctg : m_chapter_categories )
        count += kv_ctg.second->size();

    return count;
}

void
Diary::update_entries_in_chapters()
{
    PRINT_DEBUG( "Diary::update_entries_in_chapters()" );

    Chapter*   chapter;
    auto&&     itr_entry{ m_entries.begin() };
    Entry*     entry{ itr_entry != m_entries.end() ? itr_entry->second : nullptr };

    while( entry && entry->is_ordinal() )
    {
        entry = ( ++itr_entry != m_entries.end() ? itr_entry->second : nullptr );
    }

    for( auto& kv_chapter : *m_p2chapter_ctg_cur )
    {
        chapter = kv_chapter.second;
        chapter->clear();

        while( entry && entry->get_date() > chapter->get_date() )
        {
            chapter->insert( entry );
            entry = ( ++itr_entry != m_entries.end() ? itr_entry->second : nullptr );
        }
    }
}

void
Diary::add_entry_to_related_chapter( Entry* entry )
{
    // NOTE: works as per the current listing options needs to be updated when something
    // changes the arrangement such as a change in the current chapter category

    if( not( entry->is_ordinal() ) )
    {
        for( auto& kv_chapter : *m_p2chapter_ctg_cur )
        {
            Chapter* chapter{ kv_chapter.second };

            if( entry->get_date() > chapter->get_date() )
            {
                chapter->insert( entry );
                return;
            }
        }
    }
}

void
Diary::remove_entry_from_chapters( Entry* entry )
{
    for( auto& kv_chapter : *m_p2chapter_ctg_cur )
    {
        Chapter* chapter( kv_chapter.second );

        if( chapter->find( entry ) != chapter->end() )
        {
            chapter->erase( entry );
            return;
        }
    }
}

// IMPORTING
bool
Diary::import_entry( const Entry* entry_r, Entry* tag_all, bool flag_add )
{
    if( flag_add && !get_element( entry_r->get_id() ) ) // if id is available
        m_force_id = entry_r->get_id();

    // TODO: optimize passing text using paragraphs
    Entry* entry_l{ flag_add ?
            create_entry( entry_r->m_date.m_date, entry_r->get_text(), false ) :
            dynamic_cast< Entry* >( get_element( entry_r->get_id() ) ) };

    if( entry_l == nullptr )
        return false;

    if( !flag_add )
    {
        entry_l->set_text( entry_r->get_text() ); // TODO: optimize passing text using paragraphs
        set_entry_date( entry_l, entry_r->get_date_t() );
    }

    // preserve the theme:
    if( entry_r->is_theme_set() )
    {
        DiaryElement* elem_theme{ get_element( entry_r->get_theme()->get_id() ) };
        if( elem_theme && elem_theme->get_type() == ET_THEME )
            entry_l->set_theme( dynamic_cast< Theme* >( elem_theme ) );
    }

    if( tag_all )
        entry_l->add_tag( tag_all );

    // copy other data:
    entry_l->m_date_created = entry_r->m_date_created;
    entry_l->m_date_edited = entry_r->m_date_edited;
    entry_l->m_date_status = entry_r->m_date_status;
    entry_l->m_option_lang = entry_r->m_option_lang;
    entry_l->m_status = entry_r->m_status;
    entry_l->m_unit = entry_r->m_unit;
    entry_l->m_location = entry_r->m_location;
    entry_l->m_map_path = entry_r->m_map_path;

    return true;
}

void
Diary::import_chapter_ctg( const CategoryChapters* cc_r, bool flag_add )
{
    if( flag_add && !get_element( cc_r->get_id() ) )
        m_force_id = cc_r->get_id();

    CategoryChapters* cc_l{ flag_add ?
            create_chapter_ctg(
                    create_unique_name_for_map( m_chapter_categories, cc_r->get_name() ) ) :
            dynamic_cast< CategoryChapters* >( get_element( cc_r->get_id() ) ) };

    if( cc_l == nullptr )
        return;

    if( !flag_add )
        rename_chapter_ctg( cc_l, cc_r->get_name() );
}

bool
Diary::import_chapter( const Chapter* chapter_r, bool flag_add )
{
    DiaryElement* elem_ctg{ nullptr };
    CategoryChapters* ctg{ nullptr };
    date_t date{ chapter_r->get_date().m_date };

    if( chapter_r->get_ctg() )
        elem_ctg = get_element( chapter_r->get_ctg()->get_id() );

    if( elem_ctg && elem_ctg->get_type() == ET_CHAPTER_CTG )
        ctg = dynamic_cast< CategoryChapters* >( elem_ctg );
    else
        ctg = m_p2chapter_ctg_cur;

    if( flag_add && ctg->get_chapter_at( date ) != nullptr )
        return false;

    if( flag_add && !get_element( chapter_r->get_id() ) )
        m_force_id = chapter_r->get_id();

    Chapter* chapter_l{ flag_add ?
            ctg->create_chapter( date, chapter_r->is_favored(), chapter_r->is_trashed(), true ) :
            dynamic_cast< Chapter* >( get_element( chapter_r->get_id() ) ) };

    if( chapter_l == nullptr )
        return false;

    if( !flag_add )
    {
        chapter_l->set_name( chapter_r->get_name() );
        ctg->set_chapter_date( chapter_l, date );
    }
    chapter_l->set_color( chapter_r->get_color() );
    chapter_l->set_status( chapter_r->get_status() );

    return true;
}

bool
Diary::import_theme( const Theme* theme_r, bool flag_add )
{
    if( flag_add && !get_element( theme_r->get_id() ) )
        m_force_id = theme_r->get_id();

    Theme* theme_l{ flag_add ?
            create_theme( create_unique_name_for_map( m_themes, theme_r->get_name() ) ) :
            dynamic_cast< Theme* >( get_element( theme_r->get_id() ) ) };

    if( theme_l == nullptr )
        return false;

    if( !flag_add )
        rename_theme( theme_l, theme_r->get_name() );

    theme_r->copy_to( theme_l );

    return true;
}

bool
Diary::import_filter( const Filter* filter_r, bool flag_add )
{
    if( flag_add && !get_element( filter_r->get_id() ) )
        m_force_id = filter_r->get_id();

    if( flag_add )
    {
        create_filter( create_unique_name_for_map( m_filters, filter_r->get_name() ),
                       filter_r->get_definition() );
    }
    else
    {
        auto filter_l{ dynamic_cast< Filter* >( get_element( filter_r->get_id() ) ) };

        if( filter_l == nullptr ) return false; // should never occur

        rename_filter( filter_l->get_name(), filter_r->get_name() );
        filter_l->set_definition( filter_r->get_definition() );
    }

    return true;
}

bool
Diary::import_chart( const ChartElem* chart_r, bool flag_add )
{
    if( flag_add && !get_element( chart_r->get_id() ) )
        m_force_id = chart_r->get_id();

    if( flag_add )
    {
        create_chart( create_unique_name_for_map( m_charts, chart_r->get_name() ),
                      chart_r->get_definition() );
    }
    else
    {
        auto chart_l{ dynamic_cast< ChartElem* >( get_element( chart_r->get_id() ) ) };

        if( chart_l == nullptr ) return false; // should never occur

        rename_chart( chart_l->get_name(), chart_r->get_name() );
        chart_l->set_definition( chart_r->get_definition() );
    }

    return true;
}

bool
Diary::import_table( const TableElem* table_r, bool flag_add )
{
    if( flag_add && !get_element( table_r->get_id() ) )
        m_force_id = table_r->get_id();

    if( flag_add )
    {
        create_table( create_unique_name_for_map( m_tables, table_r->get_name() ),
                      table_r->get_definition() );
    }
    else
    {
        auto table_l{ dynamic_cast< ChartElem* >( get_element( table_r->get_id() ) ) };

        if( table_l == nullptr ) return false; // should never occur

        rename_table( table_l->get_name(), table_r->get_name() );
        table_l->set_definition( table_r->get_definition() );
    }

    return true;
}

DiaryElement*
Diary::get_corresponding_elem( const DiaryElement* elem_r ) const
{
    DiaryElement* elem_l{ get_element( elem_r->get_id() ) };
    if( elem_l && elem_r->get_type() == elem_l->get_type() )
        return elem_l;
    else
        return nullptr;
}

SI
Diary::compare_foreign_elem( const DiaryElement* elem_r,
                             const DiaryElement*& elem_l ) const
{
    elem_l = get_corresponding_elem( elem_r );

    if( elem_l == nullptr )
        return SI::NEW;
    else if( elem_r->get_as_skvvec() == elem_l->get_as_skvvec() )
        return SI::INTACT;

    //else
    return SI::CHANGED;
}
