/////////////////////////////////////////////////////////////////////////////
// Name:        src/common/fs_arc.cpp
// Purpose:     wxArchive file system
// Author:      Vaclav Slavik, Mike Wetherell
// Copyright:   (c) 1999 Vaclav Slavik, (c) 2006 Mike Wetherell
// Licence:     wxWindows licence
/////////////////////////////////////////////////////////////////////////////

#include "wx/wxprec.h"

#include <memory>

#if wxUSE_FS_ARCHIVE

#include "wx/fs_arc.h"

#ifndef WX_PRECOMP
    #include "wx/intl.h"
    #include "wx/log.h"
#endif

#include "wx/archive.h"
#include "wx/private/fileback.h"

//---------------------------------------------------------------------------
// wxArchiveFSCacheDataImpl
//
// Holds the catalog of an archive file, and if it is being read from a
// non-seekable stream, a copy of its backing file.
//
// This class is actually the reference counted implementation for the
// wxArchiveFSCacheData class below. It was done that way to allow sharing
// between instances of wxFileSystem, though that's a feature not used in this
// version.
//---------------------------------------------------------------------------

using wxArchiveFSEntryHash =
    std::unordered_map<wxString, std::unique_ptr<wxArchiveEntry>>;

struct wxArchiveFSEntry
{
    wxArchiveEntry *entry;
    wxArchiveFSEntry *next;
};

class wxArchiveFSCacheDataImpl
{
public:
    wxArchiveFSCacheDataImpl(const wxArchiveClassFactory& factory,
                             const wxBackingFile& backer);
    wxArchiveFSCacheDataImpl(const wxArchiveClassFactory& factory,
                             wxInputStream *stream);

    ~wxArchiveFSCacheDataImpl();

    void Release() { if (--m_refcount == 0) delete this; }
    wxArchiveFSCacheDataImpl *AddRef() { m_refcount++; return this; }

    wxArchiveEntry *Get(const wxString& name);
    wxInputStream *NewStream() const;

    wxArchiveFSEntry *GetNext(wxArchiveFSEntry *fse);

private:
    // Takes ownership of "entry".
    wxArchiveFSEntry *AddToCache(wxArchiveEntry *entry);
    void CloseStreams();

    int m_refcount;

    wxArchiveFSEntryHash m_hash;
    wxArchiveFSEntry *m_begin;
    wxArchiveFSEntry **m_endptr;

    wxBackingFile m_backer;
    wxInputStream *m_stream;
    wxArchiveInputStream *m_archive;
};

wxArchiveFSCacheDataImpl::wxArchiveFSCacheDataImpl(
        const wxArchiveClassFactory& factory,
        const wxBackingFile& backer)
 :  m_refcount(1),
    m_begin(nullptr),
    m_endptr(&m_begin),
    m_backer(backer),
    m_stream(new wxBackedInputStream(backer)),
    m_archive(factory.NewStream(*m_stream))
{
}

wxArchiveFSCacheDataImpl::wxArchiveFSCacheDataImpl(
        const wxArchiveClassFactory& factory,
        wxInputStream *stream)
 :  m_refcount(1),
    m_begin(nullptr),
    m_endptr(&m_begin),
    m_stream(stream),
    m_archive(factory.NewStream(*m_stream))
{
}

wxArchiveFSCacheDataImpl::~wxArchiveFSCacheDataImpl()
{
    wxArchiveFSEntry *entry = m_begin;

    while (entry)
    {
        wxArchiveFSEntry *next = entry->next;
        delete entry;
        entry = next;
    }

    CloseStreams();
}

wxArchiveFSEntry *wxArchiveFSCacheDataImpl::AddToCache(wxArchiveEntry *entry)
{
    m_hash[entry->GetName(wxPATH_UNIX)] = std::unique_ptr<wxArchiveEntry>(entry);
    wxArchiveFSEntry *fse = new wxArchiveFSEntry;
    *m_endptr = fse;
    (*m_endptr)->entry = entry;
    (*m_endptr)->next = nullptr;
    m_endptr = &(*m_endptr)->next;
    return fse;
}

void wxArchiveFSCacheDataImpl::CloseStreams()
{
    wxDELETE(m_archive);
    wxDELETE(m_stream);
}

wxArchiveEntry *wxArchiveFSCacheDataImpl::Get(const wxString& name)
{
    const auto it = m_hash.find(name);

    if (it != m_hash.end())
        return it->second.get();

    if (!m_archive)
        return nullptr;

    wxArchiveEntry *entry;

    while ((entry = m_archive->GetNextEntry()) != nullptr)
    {
        AddToCache(entry);

        if (entry->GetName(wxPATH_UNIX) == name)
            return entry;
    }

    CloseStreams();

    return nullptr;
}

wxInputStream* wxArchiveFSCacheDataImpl::NewStream() const
{
    if (m_backer)
        return new wxBackedInputStream(m_backer);
    else
        return nullptr;
}

wxArchiveFSEntry *wxArchiveFSCacheDataImpl::GetNext(wxArchiveFSEntry *fse)
{
    wxArchiveFSEntry *next = fse ? fse->next : m_begin;

    if (!next && m_archive)
    {
        wxArchiveEntry *entry = m_archive->GetNextEntry();

        if (entry)
            next = AddToCache(entry);
        else
            CloseStreams();
    }

    return next;
}

//---------------------------------------------------------------------------
// wxArchiveFSCacheData
//
// This is the interface for wxArchiveFSCacheDataImpl above. Holds the catalog
// of an archive file, and if it is being read from a non-seekable stream, a
// copy of its backing file.
//---------------------------------------------------------------------------

class wxArchiveFSCacheData
{
public:
    wxArchiveFSCacheData() : m_impl(nullptr) { }
    wxArchiveFSCacheData(const wxArchiveClassFactory& factory,
                         const wxBackingFile& backer);
    wxArchiveFSCacheData(const wxArchiveClassFactory& factory,
                         wxInputStream *stream);

    wxArchiveFSCacheData(const wxArchiveFSCacheData& data);
    wxArchiveFSCacheData& operator=(const wxArchiveFSCacheData& data);

    ~wxArchiveFSCacheData() { if (m_impl) m_impl->Release(); }

    wxArchiveEntry *Get(const wxString& name) { return m_impl->Get(name); }
    wxInputStream *NewStream() const { return m_impl->NewStream(); }
    wxArchiveFSEntry *GetNext(wxArchiveFSEntry *fse)
        { return m_impl->GetNext(fse); }

private:
    wxArchiveFSCacheDataImpl *m_impl;
};

wxArchiveFSCacheData::wxArchiveFSCacheData(
        const wxArchiveClassFactory& factory,
        const wxBackingFile& backer)
  : m_impl(new wxArchiveFSCacheDataImpl(factory, backer))
{
}

wxArchiveFSCacheData::wxArchiveFSCacheData(
        const wxArchiveClassFactory& factory,
        wxInputStream *stream)
  : m_impl(new wxArchiveFSCacheDataImpl(factory, stream))
{
}

wxArchiveFSCacheData::wxArchiveFSCacheData(const wxArchiveFSCacheData& data)
  : m_impl(data.m_impl ? data.m_impl->AddRef() : nullptr)
{
}

wxArchiveFSCacheData& wxArchiveFSCacheData::operator=(
        const wxArchiveFSCacheData& data)
{
    if (data.m_impl != m_impl)
    {
        if (m_impl)
            m_impl->Release();

        m_impl = data.m_impl;

        if (m_impl)
            m_impl->AddRef();
    }

    return *this;
}

//---------------------------------------------------------------------------
// wxArchiveFSCache
//
// wxArchiveFSCacheData caches a single archive, and this class holds a
// collection of them to cache all the archives accessed by this instance
// of wxFileSystem.
//---------------------------------------------------------------------------

using wxArchiveFSCacheDataHash =
    std::unordered_map<wxString, wxArchiveFSCacheData>;

class wxArchiveFSCache
{
public:
    wxArchiveFSCache() { }
    ~wxArchiveFSCache() { }

    wxArchiveFSCacheData* Add(const wxString& name,
                              const wxArchiveClassFactory& factory,
                              wxInputStream *stream);

    wxArchiveFSCacheData *Get(const wxString& name);

private:
    wxArchiveFSCacheDataHash m_hash;
};

wxArchiveFSCacheData* wxArchiveFSCache::Add(
        const wxString& name,
        const wxArchiveClassFactory& factory,
        wxInputStream *stream)
{
    wxArchiveFSCacheData& data = m_hash[name];

    if (stream->IsSeekable())
        data = wxArchiveFSCacheData(factory, stream);
    else
        data = wxArchiveFSCacheData(factory, wxBackingFile(stream));

    return &data;
}

wxArchiveFSCacheData *wxArchiveFSCache::Get(const wxString& name)
{
    const auto it = m_hash.find(name);

    if (it != m_hash.end())
        return &it->second;

    return nullptr;
}

//----------------------------------------------------------------------------
// wxArchiveFSHandler
//----------------------------------------------------------------------------

wxIMPLEMENT_DYNAMIC_CLASS(wxArchiveFSHandler, wxFileSystemHandler);

wxArchiveFSHandler::wxArchiveFSHandler()
 :  wxFileSystemHandler()
{
    m_Archive = nullptr;
    m_FindEntry = nullptr;
    m_AllowDirs = m_AllowFiles = true;
    m_DirsFound = nullptr;
    m_cache = nullptr;
}

wxArchiveFSHandler::~wxArchiveFSHandler()
{
    Cleanup();
    delete m_cache;
}

void wxArchiveFSHandler::Cleanup()
{
    wxDELETE(m_DirsFound);
}

bool wxArchiveFSHandler::CanOpen(const wxString& location)
{
    wxString p = GetProtocol(location);
    return wxArchiveClassFactory::Find(p) != nullptr;
}

wxFSFile* wxArchiveFSHandler::OpenFile(
        wxFileSystem& WXUNUSED(fs),
        const wxString& location)
{
    wxString right = GetRightLocation(location);
    wxString left = GetLeftLocation(location);
    wxString protocol = GetProtocol(location);
    wxString key = left + wxT("#") + protocol + wxT(":");

    if (right.Contains(wxT("./")))
    {
        if (right.GetChar(0) != wxT('/')) right = wxT('/') + right;
        wxFileName rightPart(right, wxPATH_UNIX);
        rightPart.Normalize(wxPATH_NORM_DOTS, wxT("/"), wxPATH_UNIX);
        right = rightPart.GetFullPath(wxPATH_UNIX);
    }

    if (!right.empty() && right.GetChar(0) == wxT('/')) right = right.Mid(1);

    if (!m_cache)
        m_cache = new wxArchiveFSCache;

    const wxArchiveClassFactory *factory;
    factory = wxArchiveClassFactory::Find(protocol);
    if (!factory)
        return nullptr;

    wxArchiveFSCacheData *cached = m_cache->Get(key);
    if (!cached)
    {
        wxFSFile *leftFile = m_fs.OpenFile(left);
        if (!leftFile)
            return nullptr;
        cached = m_cache->Add(key, *factory, leftFile->DetachStream());
        delete leftFile;
    }

    wxArchiveEntry *entry = cached->Get(right);
    if (!entry)
        return nullptr;

    wxInputStream *leftStream = cached->NewStream();
    if (!leftStream)
    {
        wxFSFile *leftFile = m_fs.OpenFile(left);
        if (!leftFile)
            return nullptr;
        leftStream = leftFile->DetachStream();
        delete leftFile;
    }

    wxArchiveInputStream *s = factory->NewStream(leftStream);
    if ( !s )
        return nullptr;

    s->OpenEntry(*entry);

    if (!s->IsOk())
    {
        delete s;
        return nullptr;
    }

    return new wxFSFile(s,
                        key + right,
                        wxEmptyString,
                        GetAnchor(location)
#if wxUSE_DATETIME
                        , entry->GetDateTime()
#endif // wxUSE_DATETIME
                        );
}

wxString wxArchiveFSHandler::FindFirst(const wxString& spec, int flags)
{
    wxString right = GetRightLocation(spec);
    wxString left = GetLeftLocation(spec);
    wxString protocol = GetProtocol(spec);
    wxString key = left + wxT("#") + protocol + wxT(":");

    if (!right.empty() && right.Last() == wxT('/')) right.RemoveLast();

    if (!m_cache)
        m_cache = new wxArchiveFSCache;

    const wxArchiveClassFactory *factory;
    factory = wxArchiveClassFactory::Find(protocol);
    if (!factory)
        return wxEmptyString;

    m_Archive = m_cache->Get(key);
    if (!m_Archive)
    {
        wxFSFile *leftFile = m_fs.OpenFile(left);
        if (!leftFile)
            return wxEmptyString;
        m_Archive = m_cache->Add(key, *factory, leftFile->DetachStream());
        delete leftFile;
    }

    m_FindEntry = nullptr;

    switch (flags)
    {
        case wxFILE:
            m_AllowDirs = false; m_AllowFiles = true; break;
        case wxDIR:
            m_AllowDirs = true; m_AllowFiles = false; break;
        default:
            m_AllowDirs = m_AllowFiles = true; break;
    }

    m_ZipFile = key;

    m_Pattern = right.AfterLast(wxT('/'));
    m_BaseDir = right.BeforeLast(wxT('/'));
    if (m_BaseDir.StartsWith(wxT("/")))
        m_BaseDir = m_BaseDir.Mid(1);

    if (m_Archive)
    {
        if (m_AllowDirs)
        {
            delete m_DirsFound;
            m_DirsFound = new wxArchiveFilenameHashMap();
            if (right.empty())  // allow "/" to match the archive root
                return spec;
        }
        return DoFind();
    }
    return wxEmptyString;
}

wxString wxArchiveFSHandler::FindNext()
{
    if (!m_Archive) return wxEmptyString;
    return DoFind();
}

wxString wxArchiveFSHandler::DoFind()
{
    wxString namestr, dir, filename;
    wxString match;

    while (match.empty())
    {
        m_FindEntry = m_Archive->GetNext(m_FindEntry);

        if (!m_FindEntry)
        {
            m_Archive = nullptr;
            m_FindEntry = nullptr;
            break;
        }
        namestr = m_FindEntry->entry->GetName(wxPATH_UNIX);

        if (m_AllowDirs)
        {
            dir = namestr.BeforeLast(wxT('/'));
            while (!dir.empty())
            {
                if( m_DirsFound->find(dir) == m_DirsFound->end() )
                {
                    (*m_DirsFound)[dir] = 1;
                    filename = dir.AfterLast(wxT('/'));
                    dir = dir.BeforeLast(wxT('/'));
                    if (!filename.empty() && m_BaseDir == dir &&
                                wxMatchWild(m_Pattern, filename, false))
                        match = m_ZipFile + dir + wxT("/") + filename;
                }
                else
                    break; // already transversed
            }
        }

        filename = namestr.AfterLast(wxT('/'));
        dir = namestr.BeforeLast(wxT('/'));
        if (m_AllowFiles && !filename.empty() && m_BaseDir == dir &&
                            wxMatchWild(m_Pattern, filename, false))
            match = m_ZipFile + namestr;
    }

    return match;
}

#endif // wxUSE_FS_ARCHIVE
