/* searchthread.cc
 * This file belongs to Worker, a file manager for UN*X/X11.
 * Copyright (C) 2006-2024 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 "searchthread.hh"
#include "aguix/lowlevelfunc.h"
#include "aguix/util.h"
#include "nwc_file.hh"
#include "pdatei.h"
#include "worker_locale.h"
#include "nwc_virtualdir.hh"
#include <typeinfo>
#include <set>
#include "nwcentryselectionstate.hh"

#include <iostream>

SearchThread::SearchThread( std::unique_ptr<NWC::Dir> searchdir,
                            const std::string &skip_initial_dir ) :
    _current_mode( SEARCHING ),
    _mode_order( SEARCH ),
    m_pt( "/" ),
    m_skip_initial_dir( skip_initial_dir )
{
  _searchdir = searchdir.release();
}

SearchThread::~SearchThread()
{
  delete _searchdir;
}

int SearchThread::run()
{
  debugmsg( "Search thread started" );

  doSearch();
  
  debugmsg( "Search thread finished" );
  _current_mode = FINISHED;

  return 0;
}

void SearchThread::setOrder( search_order_t neworder )
{
  if ( amIThisThread() == true ) return;

  _order_cv.lock();
  if ( _mode_order != neworder ) {
    _mode_order = neworder;
    _order_cv.signal();
  }
  _order_cv.unlock();
}

int SearchThread::doSearch()
{
  _searchdir->walk( *this, NWC::Dir::RECURSIVE_DEMAND );
  return 0;
}

void SearchThread::lockResults()
{
  _result_lock.lock();
}

void SearchThread::unlockResults()
{
  _result_lock.unlock();
}

int SearchThread::getNrOfResults()
{
  _result_lock.lock();
  int erg = _results.size();
  _result_lock.unlock();
  return erg;
}

size_t SearchThread::getExcludedEntries() const
{
    return m_excluded_entries;
}

bool SearchThread::resultsAvailable()
{
  _result_lock.lock();
  bool erg = ! _results.empty();
  _result_lock.unlock();
  return erg;
}

SearchThread::SearchResult SearchThread::getResultLocked()
{
  SearchResult res = _results.front();

  _results.pop_front();

  return res;
}

SearchThread::search_mode_t SearchThread::getCurrentMode()
{
  return _current_mode;
}

int SearchThread::visit( NWC::File &file, NWC::Dir::WalkControlObj &cobj )
{
    bool candidate = true;

    if ( m_sets.getCheckYounger() == true && file.stat_lastmod() < m_sets.getYoungerThanDate() ) {
        candidate = false;
    } else if ( m_sets.getCheckOlder() == true && file.stat_lastmod() > m_sets.getOlderThanDate() ) {
        candidate = false;
    } else if ( m_sets.getCheckSizeGEQ() == true ||
                m_sets.getCheckSizeLEQ() == true ) {
        if ( file.isReg( m_sets.getFollowSymlinks() ) == true ) {
            loff_t filesize = 0;
            if ( file.isLink() == true && m_sets.getFollowSymlinks() == true ) {
                filesize = file.stat_dest_size();
            } else {
                filesize = file.stat_size();
            }

            if ( m_sets.getCheckSizeGEQ() == true && filesize < m_sets.getSizeGEQAsInt() ) {
                candidate = false;
            } else if ( m_sets.getCheckSizeLEQ() == true && filesize > m_sets.getSizeLEQAsInt() ) {
                candidate = false;
            }
        } else {
            candidate = false;
        }
    }

    if ( candidate == true ) {
        m_temp_expr_filter_results.clear();

        if ( m_expr_filter ) {
            NWCEntrySelectionState nwc_entry( file );
            if ( m_expr_filter->check( nwc_entry ) ) {
                auto results = m_expr_filter->get_results();

                if ( results.attributes.empty() ) {
                    m_temp_expr_filter_results.push_back( SearchResult( file.getFullname(), -1 ) );
                } else {
                    for ( auto &r : results.attributes ) {
                        m_temp_expr_filter_results.push_back( SearchResult( file.getFullname(), r ) );
                    }
                }
            } else {
                candidate = false;
            }
        } else {
            // for an unset expression, assume match with any line so
            // merge code can just work
            m_temp_expr_filter_results.push_back( SearchResult( file.getFullname(), -1 ) );
        }

        if ( candidate && checkName( m_sets.getMatchNameFullPath() == true ? file.getFullname() : file.getBasename() ) == true ) {
            std::list<SearchResult> temp_results;
            m_content_checker.checkContent( file, [this]() { return checkInterrupt(); },
                                            [this]( const std::string &feedback ) {
                                                _current_action_lock.lock();
                                                _current_action = feedback;
                                                _current_action_lock.unlock();
                                            },
                                            [&temp_results]( const NWC::File &file,
                                                             int line_number ) {
                                                temp_results.push_back( SearchResult( file.getFullname(), line_number ) );
                                            } );

            mergeResults( m_temp_expr_filter_results, temp_results );
        }
    }
    
    if ( checkInterrupt() == true ) {
        cobj.setMode( NWC::Dir::WalkControlObj::ABORT );
    }
    return 0;
}

int SearchThread::visitEnterDir( NWC::Dir &dir, NWC::Dir::WalkControlObj &cobj )
{
    if ( dir.getFullname() != m_skip_initial_dir ) {
        const auto node = m_pt.findNode( dir.getFullname() );
        if ( node && node->getIsExactHit() ) {
            cobj.setMode( NWC::Dir::WalkControlObj::SKIP );
            m_excluded_entries++;
            return 0;
        }
    }

    bool candidate = true;
    
    _current_action_lock.lock();
    _current_action = catalog.getLocale( 747 );
    _current_action += dir.getFullname();
    _current_action_lock.unlock();

    if ( m_sets.getMatchAlsoDirectories() ) {
        if ( m_sets.getCheckYounger() == true && dir.stat_lastmod() < m_sets.getYoungerThanDate() ) {
            candidate = false;
        } else if ( m_sets.getCheckOlder() == true && dir.stat_lastmod() > m_sets.getOlderThanDate() ) {
            candidate = false;
        } else if ( m_sets.getCheckSizeGEQ() == true ||
                    m_sets.getCheckSizeLEQ() == true ) {
            candidate = false;
        }

        if ( candidate == true ) {
            if ( m_expr_filter ) {
                NWCEntrySelectionState nwc_entry( dir );
                if ( ! m_expr_filter->check( nwc_entry ) ) {
                    candidate = false;
                }
            }
            if ( candidate && checkName( m_sets.getMatchNameFullPath() == true ? dir.getFullname() : dir.getBasename() ) == true ) {
                if ( m_sets.getMatchContent().length() < 1 ) {
                    _result_lock.lock();
                    _results.push_back( SearchResult( dir.getFullname(), -1, true ) );
                    _result_lock.unlock();
                }
            }
        }
    }
    
    return 0;
}

int SearchThread::visitLeaveDir( NWC::Dir &dir, NWC::Dir::WalkControlObj &cobj )
{
  return 0;
}

int SearchThread::prepareDirWalk( const NWC::Dir &dir, NWC::Dir::WalkControlObj &cobj,
                                  std::list< RefCount< NWC::FSEntry > > &return_add_entries,
                                  std::list< RefCount< NWC::FSEntry > > &return_skip_entries )
{
    if ( m_sets.getSearchVFS() == true && m_sets.getMaxVFSDepth() > 0 ) {
        try {
            const NWC::VirtualDir &vdir = dynamic_cast<const NWC::VirtualDir&>( dir );

            if ( vdir.getVirtualDepthCount() < m_sets.getMaxVFSDepth() ) {

                std::list< std::string > entries = dir.getFullnamesOfSubentries();
                std::set< std::string > entries_set;

                for ( std::list< std::string >::const_iterator it1 = entries.begin();
                      it1 != entries.end();
                      it1++ ) {
                    entries_set.insert( *it1 );
                }

                for ( std::list< std::string >::const_iterator it1 = entries.begin();
                      it1 != entries.end();
                      it1++ ) {

                    std::string vname = *it1 + '#';

                    if ( entries_set.find( vname ) == entries_set.end() ) {
                        NWC::VirtualDir *vd =  new NWC::VirtualDir( vname,
                                                                    vdir.getFollowSymlinks(),
                                                                    false,
                                                                    vdir.getSearchSameFS() );
                        if ( vd->entryExists() == true && vd->isDir() ) {
                            vd->setVirtualDepthCount( vdir.getVirtualDepthCount() + 1 );
                            return_add_entries.push_back( RefCount< NWC::FSEntry >( vd ) );
                        }
                    }
                }
            }
        } catch ( std::bad_cast & ) {
        }
    }

    return 0;
}

bool SearchThread::checkName( const std::string &str )
{
    bool matched = false;

    for ( const auto &match_name : m_sets.getSeparatedMatchNames() ) {
        if ( m_sets.getMatchNameRE() == true ) {
            if ( re.match( match_name.c_str(), str.c_str(), ( m_sets.getMatchNameCase() == false ) ? true : false ) ) {
                matched = true;
                break;
            }
        } else {
            if ( m_sets.getMatchNameCase() == false ) {
#ifdef HAVE_GNU_FNMATCH
                if ( fnmatch( match_name.c_str(), str.c_str(), FNM_CASEFOLD ) == 0 ) {
                    matched = true;
                    break;
                }
#else
                auto lstr = AGUIXUtils::tolower( str );
                auto pstr = AGUIXUtils::tolower( match_name );
      
                if ( fnmatch( pstr.c_str(), lstr.c_str(), 0 ) == 0 ) {
                    matched = true;
                    break;
                }
#endif
            } else {
                if ( fnmatch( match_name.c_str(), str.c_str(), 0 ) == 0 ) {
                    matched = true;
                    break;
                }
            }
        }
    }

    return matched;
}

bool SearchThread::checkInterrupt()
{
  bool abort = false;

  _order_cv.lock();
  if ( _mode_order == SUSPEND ) {
    _current_mode = SUSPENDED;
    while ( _mode_order != STOP && _mode_order != SEARCH ) {
      _order_cv.wait();
    }
    _current_mode = SEARCHING;
  }
  if ( _mode_order == STOP ) {
    abort = true;
  }
  _order_cv.unlock();
  return abort;
}

SearchThread::SearchResult::SearchResult( const std::string &fullname, int linenr, bool is_dir ) : _fullname( fullname ),
                                                                                                   _linenr( linenr ),
                                                                                                   _is_dir( is_dir )
{
}

SearchThread::SearchResult::~SearchResult()
{
}

std::string SearchThread::SearchResult::getFullname() const
{
  return _fullname;
}

int SearchThread::SearchResult::getLineNr() const
{
  return _linenr;
}

bool SearchThread::SearchResult::getIsDir() const
{
  return _is_dir;
}

std::string SearchThread::getCurrentActionInfo()
{
  _current_action_lock.lock();
  std::string erg = _current_action;
  _current_action_lock.unlock();

  return erg;
}

void SearchThread::setSearchSettings( const SearchSettings &sets )
{
    m_sets = sets;
    if ( m_sets.getMatchExpression().empty() ) {
        m_expr_filter.reset();
    } else {
        ExprFilterOptions options;
        options.disabled_tokens = {
            "type",
            "state"
        };
        options.follow = sets.getFollowSymlinks();

        m_expr_filter = std::make_unique< ExprFilter >( m_sets.getMatchExpression(),
                                                        options );
        m_expr_filter->setCheckInterrupt( [this]() { return checkInterrupt(); } );
        m_expr_filter->setFeedbackCallback( [this]( const std::string &feedback ) {
            _current_action_lock.lock();
            _current_action = feedback;
            _current_action_lock.unlock();
        } );
    }
    m_content_checker.setRegex( m_sets.getMatchContent(),
                                m_sets.getMatchContentCase(),
                                0 );
}

void SearchThread::setExcludePaths( const std::list< std::string > &paths )
{
    m_pt = PathTree< bool >( "/" );
    for ( const auto &p : paths ) {
        m_pt.addPath( p, true );
    }
}

void SearchThread::mergeResults( const std::list<SearchResult> &l1,
                                 const std::list<SearchResult> &l2 )
{
    if ( l1.empty() || l2.empty() ) return;

    // easy case 1: first list matches all lines, just take second list
    if ( l1.front().getLineNr() < 0 ) {
        _result_lock.lock();
        for ( auto &e : l2 ) {
            _results.push_back( e );
        }
        _result_lock.unlock();
        return;
    }

    // easy case 2: second list matches all lines, just take first list
    if ( l2.front().getLineNr() < 0 ) {
        _result_lock.lock();
        for ( auto &e : l1 ) {
            _results.push_back( e );
        }
        _result_lock.unlock();
        return;
    }

    // now we actually have to merge. But we can assume the line
    // numbers are in increasing order

    auto it1 = l1.begin();
    auto it2 = l2.begin();

    _result_lock.lock();

    for (; it1 != l1.end() && it2 != l2.end();) {
        if ( it1->getLineNr() == it2->getLineNr() ) {
            _results.push_back( *it1 );
            it1++;
            it2++;
            continue;
        }

        if ( it1->getLineNr() < it2->getLineNr() ) {
            it1++;
        } else {
            it2++;
        }
    }

    _result_lock.unlock();
}
