/* persdeeppathstore.cc
 * This file belongs to Worker, a file manager for UN*X/X11.
 * Copyright (C) 2012-2019 Ralf Hoffmann.
 * You can contact me at: ralf@boomerangsworld.de
 *   or http://www.boomerangsworld.de/worker
 *
 * This program 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 2 of the License, or
 * (at your option) any later version.
 *
 * This program 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 this program; if not, write to the Free Software
 * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301  USA
 */

#include "persdeeppathstore.hh"
#include <iostream>
#include <fstream>
#include <sstream>
#include "nwc_fsentry.hh"
#include "aguix/util.h"
#include "datei.h"
#include "ajson.hh"
#include "filelock.hh"

PersDeepPathStore::PersDeepPathStore( const std::string &filename )
{
    m_bg_threading.thread_id = std::thread( [this]() { bg_thread(); } );

    m_old_main_db_file = filename;

    m_main_db_file = filename;
    m_main_db_file += "-v2";

    m_program_db_file = filename;
    m_program_db_file += "-prog.json";

    m_lock_file = m_main_db_file;
    m_lock_file += ".lck";
}

PersDeepPathStore::~PersDeepPathStore()
{
    m_bg_threading.order_cv.lock();
    m_bg_threading.stop_thread = true;
    m_bg_threading.order_cv.signal();
    m_bg_threading.order_cv.unlock();

    m_bg_threading.thread_id.join();
}

void PersDeepPathStore::storePath( const std::string &path, time_t ts )
{
    m_bg_threading.order_cv.lock();

    m_bg_threading.push_list.push_back( std::make_pair( path, ts ) );

    m_bg_threading.order_cv.signal();

    m_bg_threading.order_cv.unlock();
}

void PersDeepPathStore::storePathByKeyword( const std::string &keyword,
                                            const std::string &path,
                                            time_t ts )
{
    m_bg_threading.order_cv.lock();

    m_bg_threading.keyword_push_list.push_back( std::make_tuple( keyword, path, ts ) );

    m_bg_threading.order_cv.signal();

    m_bg_threading.order_cv.unlock();
}

void PersDeepPathStore::read()
{
    std::string fn = m_main_db_file;

    NWC::FSEntry tfe( fn );

    if ( ! tfe.entryExists() ) {
        fn = m_old_main_db_file;
    }

    NWC::FSEntry fe( fn );

    if ( ! fe.entryExists() ) return;

    bool changed = true;
    if ( fe.isLink() ) {
        if ( fe.stat_dest_lastmod() == m_lastmod &&
             fe.stat_dest_size() == m_lastsize ) {
            changed = false;
        }
    } else {
        if ( fe.stat_lastmod() == m_lastmod &&
             fe.stat_size() == m_lastsize ) {
            changed = false;
        }
    }

    if ( changed ) {
        std::ifstream ifile( fn.c_str() );
        std::string line;

        if ( fe.isLink() ) {
            m_lastmod = fe.stat_dest_lastmod();
            m_lastsize = fe.stat_dest_size();
        } else {
            m_lastmod = fe.stat_lastmod();
            m_lastsize = fe.stat_size();
        }

        if ( ifile.is_open() ) {

            m_store.clear();

            time_t now = time( NULL );

            while ( std::getline( ifile, line ) ) {
                if ( line.empty() ) {
                    continue;
                }

                if ( line[0] == '/' ) {
                    m_store.storePath( line, now, NULL, DeepPathStore::DPS_TS_UPDATE_ALWAYS );
                } else {
                    std::string::size_type pos;
                    pos = line.find( ':', 0 );
        
                    if ( pos != std::string::npos ) {
                        std::string ts_str( line, 0, pos );
                        std::string path( line, pos + 1 );
                        time_t ts = 0;

                        if ( AGUIXUtils::convertFromString( ts_str, ts ) ) {
                            m_store.storePath( path, ts, NULL, DeepPathStore::DPS_TS_UPDATE_ALWAYS );
                        }
                    }
                }
            }
        }
    }

    // now load program map
    NWC::FSEntry fejson( m_program_db_file );

    if ( fejson.entryExists() ) {
        changed = true;

        if ( fejson.isLink() ) {
            if ( fejson.stat_dest_lastmod() == m_lastmod_prog &&
                 fejson.stat_dest_size() == m_lastsize_prog ) {
                changed = false;
            }
        } else {
            if ( fejson.stat_lastmod() == m_lastmod_prog &&
                 fejson.stat_size() == m_lastsize_prog ) {
                changed = false;
            }
        }

        if ( changed ) {
            if ( fejson.isLink() ) {
                m_lastmod_prog = fejson.stat_dest_lastmod();
                m_lastsize_prog = fejson.stat_dest_size();
            } else {
                m_lastmod_prog = fejson.stat_lastmod();
                m_lastsize_prog = fejson.stat_size();
            }

            m_store_by_keyword.clear();
    
            auto jobj = AJSON::as_object( AJSON::load( m_program_db_file ) );

            if ( jobj ) {
                auto jtype = AJSON::as_string( jobj->get( "type" ) );
                auto jentries = AJSON::as_object( jobj->get( "entries" ) );

                if ( jtype && jtype->get_value() == "program" &&
                     jentries ) {
                    auto keys = jentries->keys();

                    for ( auto &key : keys ) {
                        auto jpathlist = AJSON::as_array( jentries->get( key ) );

                        if ( ! jpathlist ) continue;

                        for ( size_t pos = 0; pos < jpathlist->size(); pos++ ) {
                            auto jentry = AJSON::as_object( jpathlist->get( pos ) );

                            if ( ! jentry ) continue;

                            auto jpath = AJSON::as_string( jentry->get( "path" ) );
                            auto jlastaccess = AJSON::as_number( jentry->get( "last_access" ) );

                            if ( ! jpath || ! jlastaccess ) continue;

                            m_store_by_keyword[ key ].storePath( jpath->get_value(),
                                                                 jlastaccess->get_value(),
                                                                 NULL,
                                                                 DeepPathStore::DPS_TS_UPDATE_ALWAYS );
                        }
                    }
                }
            }
        }
    }
}

void PersDeepPathStore::write()
{
    std::string temp_target_file = m_main_db_file;
    temp_target_file += ".new";

    bool fail = false;

    {
        std::ofstream ofile( temp_target_file.c_str() );
        std::list< std::pair< std::string, time_t > > l = m_store.getPaths();

        for ( auto it1 = l.begin();
              it1 != l.end();
              it1++ ) {
            ofile << it1->second << ":";
            ofile << it1->first << std::endl;
        }

        ofile.flush();

        if ( ofile.fail() ) {
            fail = true;
        } else {
            m_lastmod = time( NULL );
            m_lastsize = ofile.tellp();
        }
    }

    if ( ! fail ) {
        worker_rename( temp_target_file.c_str(), m_main_db_file.c_str() );
    } else {
        worker_unlink( temp_target_file.c_str() );
    }

    if ( m_prog_dirty ) {
        // now write the database per command

        auto jobj = AJSON::make_object();
        jobj->set( "type", AJSON::make_string( "program" ) );

        auto jobj_entries = AJSON::make_object();

        for ( auto &e : m_store_by_keyword ) {
            auto jarr = AJSON::make_array();

            auto l = e.second.getPaths();

            for ( auto &path_element : l ) {
                auto jelem = AJSON::make_object();

                jelem->set( "path", AJSON::make_string( path_element.first ) );
                jelem->set( "last_access", AJSON::make_number( path_element.second ) );

                jarr->append( jelem );
            }

            jobj_entries->set( e.first, jarr );
        }

        jobj->set( "entries" , jobj_entries );

        temp_target_file = m_program_db_file;
        temp_target_file += ".new";

        if ( AJSON::dump( jobj, temp_target_file ) == 0 ) {
            worker_rename( temp_target_file.c_str(), m_program_db_file.c_str() );
        } else {
            worker_unlink( temp_target_file.c_str() );
        }

        m_lastmod_prog = time( NULL );

        NWC::FSEntry t( m_program_db_file );
        m_lastsize_prog = t.stat_size();

        m_prog_dirty = false;
    }
}

void PersDeepPathStore::bg_thread()
{
    m_bg_threading.order_cv.lock();

    while ( ! m_bg_threading.stop_thread ) {
        if ( m_bg_threading.push_list.empty() &&
             m_bg_threading.keyword_push_list.empty() &&
             m_bg_threading.relocate_list.empty() &&
             m_bg_threading.remove_list.empty() &&
             m_bg_threading.remove_list_keyword.empty() &&
             m_bg_threading.results.empty() &&
             m_bg_threading.keyword_results.empty() &&
             m_bg_threading.stop_thread == false ) {
            m_bg_threading.order_cv.wait();
        }

        if ( ! m_bg_threading.push_list.empty() ) {
            std::list< std::pair< std::string, time_t > > push_list_copy = m_bg_threading.push_list;

            m_bg_threading.push_list.clear();

            m_bg_threading.order_cv.unlock();

            addPaths( push_list_copy );

            m_bg_threading.order_cv.lock();
        }

        if ( ! m_bg_threading.keyword_push_list.empty() ) {
            auto keyword_push_list_copy = m_bg_threading.keyword_push_list;

            m_bg_threading.keyword_push_list.clear();

            m_bg_threading.order_cv.unlock();

            addPathsByKeyword( keyword_push_list_copy );

            m_bg_threading.order_cv.lock();
        }

        if ( ! m_bg_threading.relocate_list.empty() ) {
            auto relocate_list_copy = m_bg_threading.relocate_list;

            m_bg_threading.relocate_list.clear();

            m_bg_threading.order_cv.unlock();

            processRelocateOrders( relocate_list_copy );

            m_bg_threading.order_cv.lock();
        }

        if ( ! m_bg_threading.remove_list.empty() ) {
            auto remove_list_copy = m_bg_threading.remove_list;

            m_bg_threading.remove_list.clear();

            m_bg_threading.order_cv.unlock();

            removeEntries( remove_list_copy );

            m_bg_threading.order_cv.lock();
        }

        if ( ! m_bg_threading.remove_list_keyword.empty() ) {
            auto remove_list_keyword_copy = m_bg_threading.remove_list_keyword;

            m_bg_threading.remove_list_keyword.clear();

            m_bg_threading.order_cv.unlock();

            removeKeywords( remove_list_keyword_copy );

            m_bg_threading.order_cv.lock();
        }


        if ( ! m_bg_threading.results.empty() ) {
            {
                FileLock d( m_lock_file );
                if ( d.lock() ) {
                    read();
                }
            }
            for ( auto &e : m_bg_threading.results ) {
                e.set_value( m_store );
            }
            m_bg_threading.results.clear();
        }

        if ( ! m_bg_threading.keyword_results.empty() ) {
            {
                FileLock d( m_lock_file );
                if ( d.lock() ) {
                    read();
                }
            }
            for ( auto &e : m_bg_threading.keyword_results ) {
                e.set_value( m_store_by_keyword );
            }
            m_bg_threading.keyword_results.clear();
        }
    }
    m_bg_threading.order_cv.unlock();
}

void PersDeepPathStore::addPaths( const std::list< std::pair< std::string, time_t > > &list )
{
    FileLock d( m_lock_file );
    if ( !d.lock() ) {
        return;
    }

    read();

    DeepPathNode::node_change_t changed = DeepPathNode::NODE_UNCHANGED;

    for ( auto &p : list ) {
        m_store.storePath( p.first, p.second, &changed, DeepPathStore::DPS_TS_UPDATE_ALWAYS );
    }

    if ( changed != DeepPathNode::NODE_UNCHANGED ) {
        write();
    }
}

void PersDeepPathStore::addPathsByKeyword( const std::list< std::tuple< std::string, std::string, time_t > > &list )
{
    FileLock d( m_lock_file );
    if ( !d.lock() ) {
        return;
    }

    read();

    DeepPathNode::node_change_t changed = DeepPathNode::NODE_UNCHANGED;

    for ( auto &p : list ) {
        m_store_by_keyword[ std::get<0>( p )].storePath( std::get<1>( p ),
                                                         std::get<2>( p ),
                                                         &changed,
                                                         DeepPathStore::DPS_TS_UPDATE_ALWAYS );
    }

    if ( changed != DeepPathNode::NODE_UNCHANGED ) {
        m_prog_dirty = true;
        write();
    }
}

void PersDeepPathStore::relocateEntries( const std::string &dest,
                                         const std::string &source,
                                         time_t ts,
                                         bool move )
{
    m_bg_threading.order_cv.lock();

    m_bg_threading.relocate_list.push_back( std::make_tuple( dest, source, ts, move ) );

    m_bg_threading.order_cv.signal();

    m_bg_threading.order_cv.unlock();
}

void PersDeepPathStore::processRelocateOrders( const std::list< std::tuple< std::string, std::string, time_t, bool > > &relocate_list )
{
    FileLock d( m_lock_file );
    if ( !d.lock() ) {
        return;
    }

    read();

    DeepPathNode::node_change_t changed = DeepPathNode::NODE_UNCHANGED;

    for ( auto &e : relocate_list ) {
        m_store.relocateEntries( std::get<0>( e ),
                                 std::get<1>( e ),
                                 std::get<2>( e ),
                                 std::get<3>( e ),
                                 &changed);
    }

    if ( changed != DeepPathNode::NODE_UNCHANGED ) {
        write();
    }
}

void PersDeepPathStore::removeEntry( const std::string &path )
{
    m_bg_threading.order_cv.lock();

    m_bg_threading.remove_list.push_back( path );

    m_bg_threading.order_cv.signal();

    m_bg_threading.order_cv.unlock();
}

void PersDeepPathStore::removeKeyword( const std::string &keyword )
{
    m_bg_threading.order_cv.lock();

    m_bg_threading.remove_list_keyword.push_back( keyword );

    m_bg_threading.order_cv.signal();

    m_bg_threading.order_cv.unlock();
}

void PersDeepPathStore::removeEntries( const std::list< std::string > &paths )
{
    FileLock d( m_lock_file );
    if ( !d.lock() ) {
        return;
    }

    read();

    bool changed = false;

    for ( auto &e : paths ) {
        if ( m_store.removeEntry( e ) ) {
            changed = true;
        }
    }

    std::vector< std::string > keywords_to_remove;

    for ( auto &t : m_store_by_keyword ) {
        for ( auto &e : paths ) {
            if ( t.second.removeEntry( e ) ) {
                changed = true;
            }
        }

        if ( t.second.empty() ) {
            keywords_to_remove.push_back( t.first );
        }
    }

    for ( auto &key : keywords_to_remove ) {
        m_store_by_keyword.erase( key );
    }

    if ( changed ) {
        m_prog_dirty = true;
        write();
    }
}

void PersDeepPathStore::removeKeywords( const std::list< std::string > &keywords )
{
    FileLock d( m_lock_file );
    if ( !d.lock() ) {
        return;
    }

    read();

    bool changed = false;

    for ( auto &key : keywords ) {
        if ( m_store_by_keyword.count( key ) > 0 ) {
            m_store_by_keyword.erase( key );
            changed = true;
        }
    }

    if ( changed ) {
        m_prog_dirty = true;
        write();
    }
}

DeepPathStore PersDeepPathStore::getCopy()
{
    m_bg_threading.order_cv.lock();

    std::promise< DeepPathStore > p;

    auto f = p.get_future();

    m_bg_threading.results.push_back( std::move( p ) );

    m_bg_threading.order_cv.signal();
    m_bg_threading.order_cv.unlock();

    return f.get();
}

std::map< std::string, DeepPathStore > PersDeepPathStore::getKeywordCopy()
{
    m_bg_threading.order_cv.lock();

    std::promise< std::map< std::string, DeepPathStore > > p;

    auto f = p.get_future();

    m_bg_threading.keyword_results.push_back( std::move( p ) );

    m_bg_threading.order_cv.signal();
    m_bg_threading.order_cv.unlock();

    return f.get();
}

loff_t PersDeepPathStore::getLastSize() const
{
    if ( m_lastsize == 0 ) {
        std::string fn = m_main_db_file;

        NWC::FSEntry tfe( fn );

        if ( ! tfe.entryExists() ) {
            fn = m_old_main_db_file;
        }

        NWC::FSEntry fe( fn );

        if ( ! fe.entryExists() ) return 0;

        if ( fe.isLink() ) {
            return fe.stat_dest_size();
        } else {
            return fe.stat_size();
        }
    }

    return m_lastsize;
}

loff_t PersDeepPathStore::getLastProgSize() const
{
    return m_lastsize_prog;
}
