/* prefixdb.cc
 * This file belongs to Worker, a file manager for UN*X/X11.
 * Copyright (C) 2010-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 "prefixdb.hh"
#include <algorithm>
#include <iostream>
#include <fstream>
#include "nwc_fsentry.hh"
#include "datei.h"
#include "configtokens.h"
#include "configheader.h"
#include "configparser.hh"
#include "aguix/util.h"
#include "filelock.hh"

PrefixDB::PrefixDB( const std::string &filename ) : m_next_aging( 0 ),
						    m_filename( filename ),
						    m_lastmod( 0 ),
						    m_lastsize( 0 )

{
    time_t now = time( NULL );

    m_next_aging = now + 24 * 60 * 60;
}

PrefixDB::~PrefixDB()
{
}

std::string PrefixDB::getBestHit( const std::string &prefix, time_t &return_last_use )
{
    read();

    std::vector< PrefixDBEntry >::const_iterator it1;

    PrefixDBEntry pdb_test( prefix );

    // m_db needs to be sorted

    it1 = std::lower_bound( m_db.begin(),
                            m_db.end(),
                            pdb_test );

    // now it1 is the first entry which is not lower than pdb_test

    return_last_use = 0;
    if ( it1 == m_db.end() ) return "";

    std::string best_hit;
    float best_hit_count = -1.0;
    time_t best_last_use = 0;

    // for exact hit consider only this entry
    if ( it1->getPrefix() == prefix ) {
        return it1->getBestHit( best_hit_count,
                                return_last_use );
    }

    // otherwise search all entries for which prefix is a prefix

    for ( ;
          it1 != m_db.end();
          it1++ ) {
        if ( it1->getPrefix().compare( 0, prefix.length(), prefix ) == 0 ) {
            if ( best_hit_count < 0 ) {
                best_hit = it1->getBestHit( best_hit_count, best_last_use );
            } else {
                float t1;
                std::string s1;
                time_t t2;

                s1 = it1->getBestHit( t1, t2 );
		
                if ( t1 > best_hit_count ) {
                    best_hit = s1;
                    best_hit_count = t1;
                    best_last_use = t2;
                }
            }
        } else {
            // since m_db is sorted we can stop here
            break;
        }
    }

    return_last_use = best_last_use;

    return best_hit;
}

void PrefixDB::pushAccess( const std::string &prefix,
                           const std::string &value,
                           time_t last_use )
{
    read();

    std::vector< PrefixDBEntry >::iterator it1;

    PrefixDBEntry pdb_test( prefix );

    // m_db needs to be sorted

    it1 = std::lower_bound( m_db.begin(),
                            m_db.end(),
                            pdb_test );

    // now it1 is the first entry which is not lower than pdb_test

    if ( it1 == m_db.end() ||
         it1->getPrefix() != prefix ) {
        PrefixDBEntry pe( prefix );
        pe.storeAccess( value, last_use );
        m_db.push_back( pe );
        std::sort( m_db.begin(), m_db.end() );
    } else {
        it1->storeAccess( value, last_use );
    }

    write();
}

void PrefixDB::read()
{
    NWC::FSEntry fe( m_filename );

    if ( ! fe.entryExists() ) return;

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

    m_db.clear();
    
    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();
    }

    std::string lockname = m_filename;
    lockname += ".lock";

    FileLock fl( lockname );

    if ( fl.lock_retry() ) {
#if 0
        // this is a working version with plain key value pairs
        // it uses no scanner but is much stricter
        std::string myfilename = m_filename;
        myfilename += "-plain_kv";
        std::ifstream ifile( myfilename.c_str() );
        std::string line;

        if ( ifile.is_open() ) {
            bool failed = true;
            if ( std::getline( ifile, line ) ) {
                if ( line.compare( 0, std::string( "next_aging=" ).length(), "next_aging=" ) == 0 ) {
                    m_next_aging = atoi( line.c_str() + std::string( "next_aging=" ).length() );
                    failed = false;
                }
            }

            if ( ! failed ) {
                while ( ! failed && std::getline( ifile, line ) ) {
                    if ( line == "begin_entry" ) {
                        PrefixDBEntry pe( "" );
                        failed = pe.read( ifile );
                        if ( ! failed ) {
                            m_db.push_back( pe );
                        }
                    }
                }
            }

            if ( failed ) {
                std::cout << "read failed" << std::endl;
            }
        }
#else
        FILE *fp;
        int found_error = 0;

        fp = worker_fopen( m_filename.c_str(), "r" );
        if ( fp != NULL ) {
            yyrestart( fp );
            readtoken();
            for (;;) {
                if ( worker_token == NEXTAGING_WCP ) {
                    readtoken();

                    if ( worker_token != '=' ) {
                        found_error = 1;
                        break;
                    }
                    readtoken();

                    if ( worker_token == STRING_WCP ) {
                        if ( ! AGUIXUtils::convertFromString( yylval.strptr,
                                                              m_next_aging ) ) {
                            fprintf( stderr, "Worker:unable to parse nextaging\n" );
                            m_next_aging = 0;
                            //TODO abort parsing?
                        }
                    } else {
                        found_error = 1;
                        break;
                    }
                    readtoken();

                    if ( worker_token != ';' ) {
                        found_error = 1;
                        break;
                    }
                    readtoken();
                } else if ( worker_token == ENTRY_WCP ) {
                    readtoken();

                    if ( worker_token != LEFTBRACE_WCP ) {
                        found_error = 1;
                        break;
                    }
                    readtoken();
        
                    PrefixDBEntry pe( "" );
                    bool failed = pe.read( fp );
                    if ( ! failed ) {
                        m_db.push_back( pe );
                    } else {
                        found_error = 1;
                        break;
                    }
        
                    if ( worker_token != RIGHTBRACE_WCP ) {
                        found_error = 1;
                        break;
                    }
                    readtoken();
                } else if ( worker_token != 0 ) {
                    // parse error
                    found_error = 1;
                    break;
                } else break;  // end
            }
            if ( found_error != 0 ) {
                //TODO: Requester zeigen?
                //      Eigentlich kann der User eh nicht viel machen
                //      denn beim Beenden wird neue Datei geschrieben
                fprintf( stderr, "Worker:error in prefixdb\n" );
            }
            worker_fclose( fp );
        }
#endif

        fl.unlock();
    } else {
        fprintf( stderr, "Worker:locking file %s failed\n", lockname.c_str() );
    }

    std::sort( m_db.begin(), m_db.end() );
}

void PrefixDB::write()
{
    time_t now = time( NULL );

    if ( now >= m_next_aging ) {
        age();
        m_next_aging = now + 24 * 60 * 60;
    }

    std::string lockname = m_filename;
    lockname += ".lock";

    FileLock fl( lockname );

    if ( fl.lock_retry() ) {
#if 0
        std::string myfilename = m_filename;
        myfilename += "-plain_kv";
        std::ofstream ofile( myfilename.c_str() );
        
        ofile << "next_aging=" << m_next_aging << std::endl;
        
        std::vector< PrefixDBEntry >::iterator it1;
        
        for ( it1 = m_db.begin();
              it1 != m_db.end();
              it1++ ) {
            ofile << "begin_entry" << std::endl;
            
            it1->write( ofile );
            
            ofile << "end_entry" << std::endl;
        }
#else
        Datei fh;

        std::string temp_target_file = m_filename;
        temp_target_file += ".new";

        if ( fh.open( temp_target_file.c_str(), "w" ) == 0 ) {
            fh.configPutPairString( "nextaging", AGUIXUtils::convertToString( m_next_aging ).c_str() );
        
            std::vector< PrefixDBEntry >::iterator it1;
        
            for ( it1 = m_db.begin();
                  it1 != m_db.end();
                  it1++ ) {
                fh.configOpenSection( "entry" );
            
                it1->write( fh );
            
                fh.configCloseSection();
            }
            fh.close();

            if ( fh.errors() ) {
                worker_unlink( temp_target_file.c_str() );
            } else {
                worker_rename( temp_target_file.c_str(), m_filename.c_str() );
            }
        }
#endif

        fl.unlock();
    } else {
        fprintf( stderr, "Worker:locking file %s failed\n", lockname.c_str() );
    }
    //TODO set lastsize and lastmod to avoid next read?
}

void PrefixDB::age( float factor )
{
    std::vector< PrefixDBEntry >::iterator it1;

    for ( it1 = m_db.begin();
	  it1 != m_db.end();
	  it1++ ) {
	it1->age( factor );
    }
}

std::set< std::string > PrefixDB::getAllStrings()
{
    std::set< std::string > strings;

    read();

    for ( auto &dbe : m_db ) {
        for ( auto &s1 : dbe.getStrings() ) {
            strings.insert( std::get<0>( s1 ) );
        }
    }

    return strings;
}

std::map< std::string, std::tuple< std::string, float, time_t > > PrefixDB::getAllBestHits()
{
    std::map< std::string, std::tuple< std::string, float, time_t > > res;

    read();

    for ( auto &dbe : m_db ) {
        float f = 1.234;
        time_t last_use = 0;
        std::string p = dbe.getBestHit( f, last_use );

        if ( ! p.empty() ) {
            res[ dbe.getPrefix() ] = std::make_tuple( p, f, last_use );
        }
    }

    return res;
}

void PrefixDB::removeEntry( const std::string &prefix )
{
    read();

    auto it = std::find_if( m_db.begin(),
                            m_db.end(),
                            [prefix] ( const PrefixDBEntry &e ) {
                                return e.getPrefix() == prefix;
                            } );

    if ( it == m_db.end() ) {
        return;
    }

    m_db.erase( it );

    write();
}

loff_t PrefixDB::getLastSize() const
{
    return m_lastsize;
}
