/* dmcacheentrynwc.cc
 * This file belongs to Worker, a file manager for UN*X/X11.
 * Copyright (C) 2012-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 "dmcacheentrynwc.hh"
#include "dirfiltersettings.hh"
#include "dirsortsettings.hh"
#include "dirbookmarkssettings.hh"
#include "nwcentryselectionstate.hh"
#include "verzeichnis.hh"
#include "stringcomparator.hh"
#include "simplelist.hh"
#include "wconfig.h"
#include "wcfiletype.hh"
#include "aguix/util.h"
#include "commonprefix.hh"
#include "stringmatcher_fnmatch.hh"

DMCacheEntryNWC::DMCacheEntryNWC( std::unique_ptr< NWC::Dir > dir,
                                  DirFilterSettings *dir_filter_sets,
                                  DirSortSettings *dir_sort_sets,
                                  DirBookmarksSettings *dir_bookmarks_sets,
                                  std::function< void () > begin_check_cb,
                                  std::function< void () > end_check_cb )
    : m_dir( dir.release() ),
      m_dir_filter_sets( dir_filter_sets ),
      m_dir_sort_sets( dir_sort_sets ),
      m_dir_bookmarks_sets( dir_bookmarks_sets ),
      m_filter_serial_nr( 0 ),
      m_sort_serial_nr( 0 ),
      m_bookmarks_serial_nr( 0 ),
      m_x_offset( -1 ),
      m_y_offset( -1 ),
      m_unchecked_entries( 0 ),
      m_dont_check_content( false ),
      m_modification_mode( MOD_UNSET ),
      m_begin_check_cb( begin_check_cb ),
      m_end_check_cb( end_check_cb )
{
    setupEntries();

    updateView( true );
}

DMCacheEntryNWC::~DMCacheEntryNWC()
{
}

void DMCacheEntryNWC::setDir( std::unique_ptr< NWC::Dir > dir )
{
    m_dir.reset( dir.release() );

    setupEntries();

    updateView( true );
}

void DMCacheEntryNWC::reload( int flags )
{
    if ( ! m_dir.get() ) return;

    // make copy of old elements

    std::vector< NWCEntrySelectionState > old_entries;

    for ( auto &e : m_entries ) {
        old_entries.push_back( NWCEntrySelectionState( e, true ) );
    }

    active_entry old_active_entry = m_active_entry;

    m_dir->readDir( false );

    setupEntries();

    // now apply old values

    DirSortSettings sorter;
    sorter.setSortMode( SORT_STRICT_NAME | SORT_DIRMIXED );

    sorter.check( m_entries );
    sorter.check( old_entries );

    applyOldSettings( m_entries, old_entries, m_active_entry, old_active_entry, flags );

    updateView( true );

    if ( m_active_entry.view_element == -1 &&
         old_active_entry.view_element != -1 ) {
        // lost active element
        // find next from old pos

        int pos = old_active_entry.view_element;

        while ( pos < (int)m_entries.size() &&
                m_entries[pos].getUse() == false ) {
            pos++;
        }

        if ( pos >= (int)m_entries.size() ) {
            pos = (int)m_entries.size() - 1;

            while ( pos >= 0 &&
                    m_entries[pos].getUse() == false ) {
                pos--;
            }
        }

        setActiveEntryPos( pos );
    }
}

void DMCacheEntryNWC::applyOldSettings( std::vector< NWCEntrySelectionState > &new_entries,
                                        std::vector< NWCEntrySelectionState > &old_entries,
                                        active_entry &new_active_entry,
                                        active_entry &old_active_entry,
                                        int flags )
{
    auto old_es_it = old_entries.begin();

    for ( auto &new_es : new_entries ) {
        auto new_fse = new_es.getNWCEntry();

        if ( ! new_fse ) continue;

        while ( old_es_it != old_entries.end() ) {
            auto old_fse = old_es_it->getNWCEntry();

            if ( old_fse != NULL ) {
                int comp = NWCEntrySelectionState::sortfunction( *old_es_it,
                                                                 new_es,
                                                                 SORT_STRICT_NAME | SORT_DIRMIXED );

                if ( comp > 0 ) {
                    // old larger than new so skip to next new element
                    break;
                } else if ( comp == 0 ) {
                    // same name -> apply values

                    new_es.setSelected( old_es_it->getSelected() );

                    bool apply_old_filetype = true;

                    if ( old_fse->isBrokenLink() ) {
                        if ( ! new_fse->isBrokenLink() ) {
                            // old was a broken symlink, but new entry
                            // is not so do not use the file type
                            apply_old_filetype = false;
                        }
                    } else if ( old_fse->isLink() ) {
                        if ( new_fse->isBrokenLink() ) {
                            // old entry was a valid symlink and new
                            // entry is broken, so do not use the file
                            // type
                            apply_old_filetype = false;
                        }
                    }

                    if ( old_es_it->getFiletype() ) {
                        if ( flags & RESET_FILETYPE_FOR_CHANGED_SIZE_OF_UNKNOWN ) {
                            // if size has changed for an unknown
                            // type, do not apply the unknown type and
                            // instead let it recheck the actual type
                            if ( new_fse->stat_size() != old_fse->stat_size() &&
                                 old_es_it->getFiletype()->getinternID() == UNKNOWNTYPE ) {
                                apply_old_filetype = false;
                            }
                        }

                        if ( apply_old_filetype ) {
                            new_es.setFiletype( old_es_it->getFiletype() );
                        }
                    }
                    new_es.setColor( old_es_it->getColor() );
                    new_es.setBookmarkInfo( old_es_it->getBookmarkInfo() );
                    new_es.setFiletypeFileOutput( old_es_it->getFiletypeFileOutput() );
                    new_es.setMimeType( old_es_it->getMimeType() );
                    new_es.setCustomAttribute( old_es_it->getCustomAttribute() );

                    NWC::Dir *old_dir = dynamic_cast< NWC::Dir * >( old_fse );
                    NWC::Dir *new_dir = dynamic_cast< NWC::Dir * >( new_fse );

                    if ( old_dir && new_dir ) {
                        new_dir->setBytesInDir( old_dir->getBytesInDir() );
                    }

                    // reapply active entry state
                    if ( old_es_it->getEntryID() == old_active_entry.dir_entry_id ) {
                        new_active_entry.dir_entry_id = new_es.getEntryID();
                        new_active_entry.view_element = -1;
                    }

                    old_es_it++;
                    break;
                }

                // old smaller than new so go to next old entry
            }

            old_es_it++;
        }

        if ( old_es_it == old_entries.end() ) break;
    }
}

const NWCEntrySelectionState *DMCacheEntryNWC::getEntry( int pos )
{
    if ( pos < 0 || pos >= (int)m_entries.size() ) return NULL;

    return &m_entries[pos];
}

std::pair< const NWCEntrySelectionState *, int > DMCacheEntryNWC::getEntry( const std::string &fullname, int pos, bool also_hidden )
{
    if ( pos >= 0 ) {
        if ( also_hidden || getUse( pos ) ) {
            auto es = getEntry( pos );

            if ( es ) {
                auto fse = es->getNWCEntry();

                if ( fse &&
                     fse->getFullname() == fullname ) {
                    return std::make_pair( es, pos );
                }
            }
        }
    }

    for ( pos = 0;
          pos < (int)getSize();
          pos++ ) {
        if ( also_hidden || getUse( pos ) ) {
            auto es = getEntry( pos );

            if ( ! es ) continue;

            auto fse = es->getNWCEntry();

            if ( ! fse ) continue;

            if ( fse->getFullname() == fullname ) {
                return std::make_pair( es, pos );
            }
        }
    }

    return { NULL, -1 };
}

void DMCacheEntryNWC::modifyEntry( int pos,
                                   std::function< void( NWCEntrySelectionState & ) > mod_function )
{
    if ( pos < 0 || pos >= (int)m_entries.size() ) return;

    changeEntryInStats( m_entries[pos], true );

    mod_function( m_entries[pos] );

    changeEntryInStats( m_entries[pos], false );
}

bool DMCacheEntryNWC::getUse( int pos )
{
    if ( pos < 0 || pos >= (int)m_entries.size() ) return false;

    return m_entries[pos].getUse();
}

size_t DMCacheEntryNWC::getSize()
{
    return m_entries.size();
}

void DMCacheEntryNWC::updateView( bool force )
{
    if ( m_dir.get() == NULL )
        return;

    bool dirty = false;

    if ( m_dir_bookmarks_sets->getSerialNr() != m_bookmarks_serial_nr || force == true ) {
        m_dir_bookmarks_sets->check( m_entries );

        m_bookmarks_serial_nr = m_dir_bookmarks_sets->getSerialNr();
        dirty = true;
    }

    std::string dirname;

    // keep it empty in case of real dir for speedup
    if ( ! isRealDir() ) {
        dirname = getCommonPrefix();
    }

    if ( m_dir_filter_sets->getSerialNr() != m_filter_serial_nr || force == true ) {

        if ( m_begin_check_cb ) m_begin_check_cb();

        for ( auto &se : m_entries ) {
            if ( se.getNWCEntry()->getBasename() == ".." ) continue;

            m_dir_filter_sets->check( dirname, se );
        }

        if ( m_end_check_cb ) m_end_check_cb();
        
        m_filter_serial_nr = m_dir_filter_sets->getSerialNr();
        dirty = true;
    }

    if ( m_dir_sort_sets->getSerialNr() != m_sort_serial_nr || force == true ) {
        m_dir_sort_sets->check( m_entries );

        m_sort_serial_nr = m_dir_sort_sets->getSerialNr();
        dirty = true;
    }

    if ( m_active_entry.dir_entry_id != -1 ) {

        m_active_entry.view_element = -1;

        for ( int p = 0; p < (int)m_entries.size(); p++ ) {
            if ( m_entries[p].getEntryID() == m_active_entry.dir_entry_id &&
                 m_entries[p].getUse() == true ) {
                m_active_entry.view_element = p;
                break;
            }
        }
    }

    if ( dirty == true ) {
        recalcStats();
        resetFiletypeCheckState();
    }
}

void DMCacheEntryNWC::setupEntries()
{
    m_entries.clear();
    m_active_entry = active_entry();

    m_common_path_prefix.calculated = false;

    if ( m_dir.get() == NULL ) return;

    m_entries.resize( m_dir->size(),
                      NWCEntrySelectionState() );

    for ( int pos = 0; pos < (int)m_dir->size(); pos++ ) {
        m_entries[pos].setEntryID( pos );
        m_entries[pos].setDir( m_dir );
        m_entries[pos].setUse( true );
    }

    m_common_path_prefix = common_prefix();
}

void DMCacheEntryNWC::recalcStats()
{
    m_files_dirs = entry_counter();

    if ( m_dir.get() == NULL ) return;

    m_unchecked_entries = 0;

    for ( const auto &es : m_entries ) {
        changeEntryInStats( es, false );
    }
}

long DMCacheEntryNWC::getNrOfFiles( bool selected ) const
{
    if ( selected ) return m_files_dirs.files_selected;

    return m_files_dirs.files;
}

loff_t DMCacheEntryNWC::getSizeOfFiles( bool selected ) const
{
    if ( selected ) return m_files_dirs.files_bytes_selected;

    return m_files_dirs.files_bytes;
}

long DMCacheEntryNWC::getNrOfDirs( bool selected ) const
{
    if ( selected ) return m_files_dirs.dirs_selected;

    return m_files_dirs.dirs;
}

loff_t DMCacheEntryNWC::getSizeOfDirs( bool selected ) const
{
    if ( selected ) return m_files_dirs.dirs_bytes_selected;

    return m_files_dirs.dirs_bytes;
}

long DMCacheEntryNWC::getNrOfHiddenFiles() const
{
    return m_files_dirs.hidden_files;
}

long DMCacheEntryNWC::getNrOfHiddenDirs() const
{
    return m_files_dirs.hidden_dirs;
}

const std::string &DMCacheEntryNWC::getCommonPrefix()
{
    if ( ! m_common_path_prefix.calculated ) {
        recalcCommonPrefix();
    }

    return m_common_path_prefix.prefix_string;
}

void DMCacheEntryNWC::recalcCommonPrefix()
{
    CommonPrefix cp;

    for ( const auto &es : m_entries ) {
        const NWC::FSEntry *fse = es.getNWCEntry();

        if ( !fse ) continue;

        cp.updateCommonPrefix( fse->getFullname() );
    }

    m_common_path_prefix.prefix_string = cp.getCommonPrefix();

    m_common_path_prefix.calculated = true;
}

int DMCacheEntryNWC::getActiveEntryPos() const
{
    return m_active_entry.view_element;
}

void DMCacheEntryNWC::setActiveEntryPos( int pos )
{
    if ( pos < 0 || pos >= (int)m_entries.size() ) {
        m_active_entry = active_entry();
        return;
    }

    m_active_entry.view_element = pos;
    m_active_entry.dir_entry_id = m_entries[pos].getEntryID();
}

std::string DMCacheEntryNWC::getNameOfDir() const
{
    if ( ! m_dir.get() ) return "";

    return m_dir->getFullname();
}

bool DMCacheEntryNWC::isRealDir() const
{
    if ( ! m_dir.get() ) return false;

    return m_dir->isRealDir();
}

void DMCacheEntryNWC::changeEntryInStats( const NWCEntrySelectionState &es,
                                          bool remove )
{
    const NWC::FSEntry *fse = es.getNWCEntry();
    int modifier = ( remove ) ? -1 : 1;

    if ( ! fse ) return;

    if ( es.getUse() &&
         fse->getBasename() != ".." ) {
        if ( fse->isDir( true ) ) {
            m_files_dirs.dirs += 1 * modifier;

            loff_t ds = -1;

            const NWC::Dir *d = dynamic_cast< const NWC::Dir * >( fse );
            if ( d ) {
                ds = d->getBytesInDir();
            }

            if ( ds < 0 ) ds = fse->stat_size();

            m_files_dirs.dirs_bytes += ds * modifier;

            if ( es.getSelected() ) {
                m_files_dirs.dirs_selected += 1 * modifier;
                m_files_dirs.dirs_bytes_selected += ds * modifier;
            }
        } else {
            m_files_dirs.files += 1 * modifier;
            m_files_dirs.files_bytes += fse->stat_size() * modifier;

            if ( es.getSelected() ) {
                m_files_dirs.files_selected += 1 * modifier;
                m_files_dirs.files_bytes_selected += fse->stat_size() * modifier;
            }

            if ( es.getFiletype() == NULL ) {
                m_unchecked_entries += modifier;
            }
        }
    } else if ( fse->getBasename() != ".." ) {
        if ( fse->isDir( true ) ) {
            m_files_dirs.hidden_dirs += 1 * modifier;
        } else {
            m_files_dirs.hidden_files += 1 * modifier;
        }
    }
}

void DMCacheEntryNWC::setXOffset( int xoffset ){
    m_x_offset = xoffset;
}

int DMCacheEntryNWC::getXOffset()
{
    return m_x_offset;
}

void DMCacheEntryNWC::setYOffset( int yoffset )
{
    m_y_offset = yoffset;
}

int DMCacheEntryNWC::getYOffset()
{
    return m_y_offset;
}

void DMCacheEntryNWC::resetFiletypeCheckState()
{
    m_filetype_check_state = filetype_check();
}

int DMCacheEntryNWC::updateNextEntryForFiletypeCheck()
{
    int pos = m_filetype_check_state.next_entry_for_filetype_check;

    if ( pos == filetype_check::NO_ENTRY ) {
        return filetype_check::NO_ENTRY;
    }

    if ( pos == filetype_check::UNKNOWN ) {
        pos = 0;
    } else pos++;

    // search next entry from pos with NULL filetype

    for (; pos < (int)m_entries.size(); pos++ ) {
        if ( m_entries[pos].getFiletype() == NULL &&
             m_entries[pos].getUse() &&
             m_entries[pos].getNWCEntry() != NULL &&
             m_entries[pos].getNWCEntry()->isDir( true ) == false ) {
            break;
        }
    }

    if ( pos >= (int)m_entries.size() ) {
        pos = filetype_check::NO_ENTRY;
    }

    m_filetype_check_state.next_entry_for_filetype_check = pos;

    return pos;
}

int DMCacheEntryNWC::getEntryForFiletypeCheck()
{
    int pos = m_filetype_check_state.next_entry_for_filetype_check;

    if ( pos == filetype_check::UNKNOWN ) {
        return updateNextEntryForFiletypeCheck();
    }

    return pos;
}

int DMCacheEntryNWC::getNumberOfUncheckedEntries() const
{
    return m_unchecked_entries;
}

void DMCacheEntryNWC::resetFiletypesAndColors()
{
    for ( auto &es : m_entries ) {
        es.setFiletype( NULL );
        es.clearCustomColor();
    }

    // do this for recalculating the m_unchecked_entries count
    recalcStats();

    resetFiletypeCheckState();
}

void DMCacheEntryNWC::checkForDCD()
{
    const std::list< WConfigTypes::dont_check_dir > &dcd = wconfig->getDontCheckDirs();
    std::string dirstr;

    m_dont_check_content = false;

    if ( isRealDir() ) {
        dirstr = getNameOfDir();
    } else {
        dirstr = getCommonPrefix();
    }

    if ( ! dirstr.empty() ) {
        for ( const auto &d : dcd ) {
            if ( d.is_pattern ) {
                StringMatcherFNMatch matcher;
                matcher.setMatchString( d.dir );

                if ( matcher.match( dirstr ) ) {
                    m_dont_check_content = true;
                    break;
                }
            } else {
                if ( strncmp( d.dir.c_str(), dirstr.c_str(), strlen( d.dir.c_str() ) ) == 0 ) {
                    m_dont_check_content = true;
                    break;
                }
            }
        }
    } else {
        m_dont_check_content = true;
    }
}

bool DMCacheEntryNWC::getDontCheckContent() const
{
    return m_dont_check_content;
}

std::string DMCacheEntryNWC::getVisibleName()
{
    if ( isRealDir() ) {
        return getNameOfDir();
    } else {
        std::string n = getNameOfDir();
        n += ":";
        n += getCommonPrefix();
        return n;
    }
}

void DMCacheEntryNWC::resetDirSizes()
{
    for ( auto &es : m_entries ) {
        if ( auto d = dynamic_cast< NWC::Dir *>( es.getNWCEntry() ) ) {
            d->setBytesInDir( -1 );
        }
    }
    updateView( true );
}

const NWC::Dir *DMCacheEntryNWC::getNWCDir() const
{
    return m_dir.get();
}

int DMCacheEntryNWC::removeEntry( int pos )
{
    if ( m_modification_mode != MOD_REMOVAL ) return 1;

    if ( m_dir->isRealDir() ) return 2;

    resetFiletypeCheckState();

    if ( pos < 0 || pos >= (int)m_entries.size() ) return 3;

    auto es = getEntry( pos );

    if ( ! es ) return 4;

    auto fse = es->getNWCEntry();

    if ( ! fse ) return 5;

    m_dir->removeFromList( fse->getFullname(),
                           m_entries[pos].getEntryID() );

    for ( auto &it : m_entries ) {
        if ( it.getEntryID() > m_entries[pos].getEntryID() ) {
            it.setEntryID( it.getEntryID() - 1 );
        }
    }

    if ( m_active_entry.dir_entry_id == m_entries[pos].getEntryID() ) {
        int tpos = pos + 1;

        for ( ; tpos < (int)m_entries.size(); tpos++ ) {
            if ( m_entries[tpos].getEntryID() != -1 ) {
                m_active_entry.dir_entry_id = m_entries[tpos].getEntryID();
                break;
            }
        }

        if ( tpos >= (int)m_entries.size() ) {
            tpos = pos - 1;

            for ( ; tpos >= 0; tpos-- ) {
                if ( m_entries[tpos].getEntryID() != -1 ) {
                    m_active_entry.dir_entry_id = m_entries[tpos].getEntryID();
                    break;
                }
            }

            if ( tpos < 0 ) {
                m_active_entry.dir_entry_id = -1;
            }
        }
        m_active_entry.view_element = -1;
    } else if ( m_active_entry.dir_entry_id > m_entries[pos].getEntryID() ) {
        m_active_entry.dir_entry_id--;
        m_active_entry.view_element = -1;
    }

    m_entries[pos].setEntryID( -1 );

    return 0;
}

int DMCacheEntryNWC::updateActiveForRemoval( int pos )
{
    if ( pos < 0 || pos >= (int)m_entries.size() ) return 3;

    if ( m_active_entry.dir_entry_id == m_entries.at( pos ).getEntryID() ) {
        bool found = false;
        for ( ; pos + 1 < (int)m_entries.size(); pos++ ) {
            if ( m_entries.at( pos + 1 ).getUse() == true ) {
                setActiveEntryPos( pos + 1 );
                found = true;
                break;
            }
        }

        if ( ! found ) {
            while ( true ) {
                if ( pos > 0 &&
                     m_entries.at( pos - 1 ).getUse() == true ) {
                    setActiveEntryPos( pos - 1 );
                    break;
                } else if ( pos == 0 ) {
                    setActiveEntryPos( -1 );
                    break;
                }

                pos--;
            }
        }
    }

    return 0;
}

int DMCacheEntryNWC::temporarilyHideEntry( int pos )
{
    if ( pos < 0 || pos >= (int)m_entries.size() ) return 1;

    changeEntryInStats( m_entries[pos], true );

    m_entries[pos].setUse( false );

    // the stats are not updated since the counters must be updated
    // afterwards anyway

    return 0;
}

int DMCacheEntryNWC::beginModification( modification_mode_t mode )
{
    m_modification_mode = mode;
    return 0;
}

int DMCacheEntryNWC::endModification()
{
    if ( m_modification_mode == MOD_UNSET ) return 1;

    reload();

    checkForDCD();

    m_modification_mode = MOD_UNSET;

    return 0;
}

int DMCacheEntryNWC::insertEntry( const std::string &fullname )
{
    if ( m_modification_mode != MOD_INSERTION ) return 1;

    if ( m_dir->isRealDir() ) return 2;

    resetFiletypeCheckState();

    m_dir->add( fullname );

    return 0;
}

int DMCacheEntryNWC::changeBasenameOfEntry( const std::string &basename,
                                            int pos )
{
    if ( m_modification_mode != MOD_RENAMING ) return 1;

    if ( pos < 0 || pos >= (int)m_entries.size() ) return 2;

    if ( m_dir.get() == NULL ) return 3;

    return m_dir->changeBasenameOfEntry( basename,
                                         m_entries[pos].getEntryID(),
                                         true );
}

const std::string &DMCacheEntryNWC::getCustomDirectoryInfo()
{
    if ( m_custom_directory_info_future.valid() ) {
        if ( ! customDirectoryInfoPending() ) {
            m_custom_directory_info = m_custom_directory_info_future.get();
            m_custom_directory_info_state = CDI_DONE;
        }
    }

    return m_custom_directory_info;
}

void DMCacheEntryNWC::setCustomDirectoryInfoFuture( std::future< std::string > f )
{
    m_custom_directory_info_future = std::move( f );
    m_custom_directory_info_state = CDI_CALCULATING;
}

bool DMCacheEntryNWC::customDirectoryInfoPending()
{
    if ( ! m_custom_directory_info_future.valid() ) {
        return false;
    }

    std::future_status status = m_custom_directory_info_future.wait_for( std::chrono::duration<int>::zero() );

    if ( status == std::future_status::ready ) {
        m_custom_directory_info_state = CDI_RESULT_AVAILABLE;
        return false;
    } else {
        return true;
    }
}

DMCacheEntryNWC::custom_directory_info_state DMCacheEntryNWC::customDirectoryInfoState() const
{
    return m_custom_directory_info_state;
}

std::string DMCacheEntryNWC::getBasicDirname()
{
    if ( isRealDir() ) {
        return getNameOfDir();
    } else {
        return getCommonPrefix();
    }
}
