// *****************************************************************************
// * This file is part of the FreeFileSync project. It is distributed under    *
// * GNU General Public License: https://www.gnu.org/licenses/gpl-3.0          *
// * Copyright (C) Zenju (zenju AT freefilesync DOT org) - All Rights Reserved *
// *****************************************************************************

#include "icon_buffer.h"
#include <map>
#include <variant>
#include <zen/thread.h> //includes <std/thread.hpp>
#include <wx+/dc.h>
#include <wx+/image_resources.h>
#include <wx+/std_button_layout.h>
#include "base/icon_loader.h"


using namespace zen;
using namespace fff;
using AFS = AbstractFileSystem;


namespace
{
const size_t BUFFER_SIZE_MAX = 1000; //maximum number of icons to hold in buffer: must be big enough to hold visible icons + preload buffer!


}

//################################################################################################################################################

std::variant<ImageHolder, FileIconHolder> getDisplayIcon(const AbstractPath& itemPath, IconBuffer::IconSize sz)
{
    //1. try to load thumbnails
    switch (sz)
    {
        case IconBuffer::IconSize::small:
            break;
        case IconBuffer::IconSize::medium:
        case IconBuffer::IconSize::large:
            try
            {
                if (ImageHolder ih = AFS::getThumbnailImage(itemPath, IconBuffer::getPixSize(sz))) //throw FileError; optional return value
                    return ih;
            }
            catch (FileError&) {}

            //else: fallback to non-thumbnail icon
            break;
    }

    //2. retrieve file icons
        try
        {
            if (FileIconHolder fih = AFS::getFileIcon(itemPath, IconBuffer::getPixSize(sz))) //throw FileError; optional return value
                return fih;
        }
        catch (FileError&) {}

    //run getIconByTemplatePath()/genericFileIcon() fallbacks on main thread:
    //extractWxImage() might fail if icon theme is missing a MIME type!
    return ImageHolder();
}

//################################################################################################################################################

//---------------------- Shared Data -------------------------
class WorkLoad
{
public:
    //context of main thread
    void set(const std::vector<AbstractPath>& newLoad)
    {
        assert(runningOnMainThread());
        {
            std::lock_guard dummy(lockFiles_);
            workLoad_ = newLoad;
        }
        conditionNewWork_.notify_all(); //instead of notify_one(); work around bug: https://svn.boost.org/trac/boost/ticket/7796
        //condition handling, see: https://www.boost.org/doc/libs/1_43_0/doc/html/thread/synchronization.html#thread.synchronization.condvar_ref
    }

    void add(const AbstractPath& filePath) //context of main thread
    {
        assert(runningOnMainThread());
        {
            std::lock_guard dummy(lockFiles_);
            workLoad_.emplace_back(filePath); //set as next item to retrieve
        }
        conditionNewWork_.notify_all();
    }

    //context of worker thread, blocking:
    AbstractPath extractNext() //throw ThreadStopRequest
    {
        assert(!runningOnMainThread());
        std::unique_lock dummy(lockFiles_);

        interruptibleWait(conditionNewWork_, dummy, [this] { return !workLoad_.empty(); }); //throw ThreadStopRequest

        AbstractPath filePath = workLoad_.    back(); //yes, no strong exception guarantee (std::bad_alloc)
        /**/                    workLoad_.pop_back(); //
        return filePath;
    }

private:
    //AbstractPath is thread-safe like an int!
    std::mutex                lockFiles_;
    std::condition_variable   conditionNewWork_; //signal event: data for processing available
    std::vector<AbstractPath> workLoad_; //processes last elements of vector first!
};


class Buffer
{
public:
    //called by main and worker thread:
    bool hasIcon(const AbstractPath& filePath) const
    {
        std::lock_guard dummy(lockIconList_);
        return iconList.contains(filePath);
    }

    //- must be called by main thread only! => wxImage is NOT thread-safe like an int (non-atomic ref-count!!!)
    //- check wxImage::IsOk() + implement fallback if needed
    std::optional<wxImage> retrieve(const AbstractPath& filePath)
    {
        assert(runningOnMainThread());
        std::lock_guard dummy(lockIconList_);

        auto it = iconList.find(filePath);
        if (it == iconList.end())
            return {};

        markAsHot(it);

        IconData& idata = refData(it);

        if (ImageHolder* ih = std::get_if<ImageHolder>(&idata.iconHolder))
        {
            if (*ih) //if not yet converted...
            {
                idata.iconImg = std::make_unique<wxImage>(extractWxImage(std::move(*ih))); //convert in main thread!
                assert(!*ih);
            }
        }
        else
        {
            if (FileIconHolder& fih = std::get<FileIconHolder>(idata.iconHolder)) //if not yet converted...
            {
                idata.iconImg = std::make_unique<wxImage>(extractWxImage(std::move(fih))); //convert in main thread!
                assert(!fih);
                //!idata.iconImg->IsOk(): extractWxImage() might fail if icon theme is missing a MIME type!
            }
        }

        return idata.iconImg ? *idata.iconImg : wxNullImage; //idata.iconHolder may be inserted as empty from worker thread!
    }

    //called by main and worker thread:
    void insert(const AbstractPath& filePath, std::variant<ImageHolder, FileIconHolder>&& ih)
    {
        std::lock_guard dummy(lockIconList_);

        //thread safety: moving ImageHolder is free from side effects, but ~wxImage() is NOT! => do NOT delete items from iconList here!
        const auto [it, inserted] = iconList.try_emplace(filePath);
        assert(inserted);
        if (inserted)
        {
            refData(it).iconHolder = std::move(ih);
            priorityListPushBack(it);
        }
    }

    //must be called by main thread only! => ~wxImage() is NOT thread-safe!
    //call at an appropriate time, e.g. after Workload::set()
    void limitSize()
    {
        assert(runningOnMainThread());
        std::lock_guard dummy(lockIconList_);

        while (iconList.size() > BUFFER_SIZE_MAX)
        {
            auto itDelPos = firstInsertPos_;
            priorityListPopFront();
            iconList.erase(itDelPos); //remove oldest element
        }
    }

private:
    struct IconData;
    using FileIconMap = std::map<AbstractPath, IconData>;
    IconData& refData(FileIconMap::iterator it) { return it->second; }

    //call while holding lock:
    void priorityListPopFront()
    {
        assert(firstInsertPos_!= iconList.end());
        firstInsertPos_ = refData(firstInsertPos_).next;

        if (firstInsertPos_ != iconList.end())
            refData(firstInsertPos_).prev = iconList.end();
        else //priority list size > BUFFER_SIZE_MAX in this context, but still for completeness:
            lastInsertPos_ = iconList.end();
    }

    //call while holding lock:
    void priorityListPushBack(FileIconMap::iterator it)
    {
        if (lastInsertPos_ == iconList.end())
        {
            assert(firstInsertPos_ == iconList.end());
            firstInsertPos_ = lastInsertPos_ = it;
            refData(it).prev = refData(it).next = iconList.end();
        }
        else
        {
            refData(it).next = iconList.end();
            refData(it).prev = lastInsertPos_;
            refData(lastInsertPos_).next = it;
            lastInsertPos_ = it;
        }
    }

    //call while holding lock:
    void markAsHot(FileIconMap::iterator it) //mark existing buffer entry as if newly inserted
    {
        assert(it != iconList.end());
        if (refData(it).next != iconList.end())
        {
            if (refData(it).prev != iconList.end())
            {
                refData(refData(it).prev).next = refData(it).next; //remove somewhere from the middle
                refData(refData(it).next).prev = refData(it).prev; //
            }
            else
            {
                assert(it == firstInsertPos_);
                priorityListPopFront();
            }
            priorityListPushBack(it);
        }
        else
        {
            if (refData(it).prev != iconList.end())
                assert(it == lastInsertPos_); //nothing to do
            else
                assert(iconList.size() == 1 && it == firstInsertPos_ && it == lastInsertPos_); //nothing to do
        }
    }

    struct IconData
    {
        IconData() {}
        IconData(IconData&& tmp) noexcept : iconHolder(std::move(tmp.iconHolder)), iconImg(std::move(tmp.iconImg)), prev(tmp.prev), next(tmp.next) {}

        std::variant<ImageHolder, FileIconHolder> iconHolder; //native icon representation: may be used by any thread

        std::unique_ptr<wxImage> iconImg; //use ONLY from main thread!
        //wxImage is NOT thread-safe: non-atomic ref-count just to begin with...
        //- prohibit implicit calls to wxImage()
        //- prohibit calls to ~wxImage() and transitively ~IconData()
        //- prohibit even wxImage() default constructor - better be safe than sorry!

        FileIconMap::iterator prev; //store list sorted by time of insertion into buffer
        FileIconMap::iterator next; //
    };

    mutable std::mutex lockIconList_;
    FileIconMap iconList; //shared resource; Zstring is thread-safe like an int
    FileIconMap::iterator firstInsertPos_ = iconList.end();
    FileIconMap::iterator lastInsertPos_  = iconList.end();
};

//################################################################################################################################################


//#########################  redirect to impl  #####################################################

struct IconBuffer::Impl
{
    //communication channel used by threads:
    WorkLoad workload; //manage life time: enclose InterruptibleThread's (until joined)!!!
    Buffer   buffer;   //

    InterruptibleThread worker;
    //-------------------------
    //-------------------------
    std::unordered_map<Zstring, wxImage, StringHashAsciiNoCase, StringEqualAsciiNoCase> extensionIcons; //no item count limit!? Test case C:\ ~ 3800 unique file extensions
};


IconBuffer::IconBuffer(IconSize sz) : pimpl_(std::make_unique<Impl>()), iconSizeType_(sz)
{
    pimpl_->worker = InterruptibleThread([&workload = pimpl_->workload, &buffer = pimpl_->buffer, sz]
    {
        setCurrentThreadName(Zstr("Icon Buffer"));

        for (;;)
        {
            //start work: blocks until next icon to load is retrieved:
            const AbstractPath itemPath = workload.extractNext(); //throw ThreadStopRequest

            if (!buffer.hasIcon(itemPath)) //perf: workload may contain duplicate entries?
                buffer.insert(itemPath, getDisplayIcon(itemPath, sz));
        }
    });
}


IconBuffer::~IconBuffer()
{
    setWorkload({}); //make sure interruption point is always reached! needed???
    pimpl_->worker.requestStop(); //end thread life time *before*
    pimpl_->worker.join();        //IconBuffer::Impl member clean up!
}


int IconBuffer::getPixSize(IconSize sz)
{
    //coordinate with getIconByIndexImpl() and linkOverlayIcon()!
    switch (sz)
    {
        case IconSize::small:
            return dipToScreen(getMenuIconDipSize());
        case IconSize::medium:
            return dipToScreen(48);
        case IconSize::large:
            return dipToScreen(128);
    }
    assert(false);
    return 0;
}


bool IconBuffer::readyForRetrieval(const AbstractPath& filePath)
{
    return pimpl_->buffer.hasIcon(filePath);
}


std::optional<wxImage> IconBuffer::retrieveFileIcon(const AbstractPath& filePath)
{
    const Zstring fileName = AFS::getItemName(filePath);
    if (std::optional<wxImage> ico = pimpl_->buffer.retrieve(filePath))
    {
        if (ico->IsOk())
            return ico;
        else //fallback
            return this->getIconByExtension(fileName); //buffered!
    }

    //since this icon seems important right now, we don't want to wait until next setWorkload() to start retrieving
    pimpl_->workload.add(filePath);
    pimpl_->buffer.limitSize();
    return {};
}


void IconBuffer::setWorkload(const std::vector<AbstractPath>& load)
{
    assert(load.size() < BUFFER_SIZE_MAX / 2);

    pimpl_->workload.set(load); //since buffer can only increase due to new workload,
    pimpl_->buffer.limitSize(); //this is the place to impose the limit from main thread!
}


wxImage IconBuffer::getIconByExtension(const Zstring& filePath)
{
    const Zstring& ext = getFileExtension(filePath);

    assert(runningOnMainThread());

    auto it = pimpl_->extensionIcons.find(ext);
    if (it == pimpl_->extensionIcons.end())
    {
        const Zstring& templateName(ext.empty() ? Zstr("file") : Zstr("file.") + ext);
        //don't pass actual file name to getIconByTemplatePath(), e.g. "AUTHORS" has own mime type on Linux!!!
        //=> buffer by extension to minimize buffer-misses!

        wxImage img;
        try
        {
            img = extractWxImage(getIconByTemplatePath(templateName, getPixSize(iconSizeType_))); //throw SysError
        }
        catch (SysError&) {}
        if (!img.IsOk()) //Linux: not all MIME types have icons!
            img = IconBuffer::genericFileIcon(iconSizeType_);

        it = pimpl_->extensionIcons.emplace(ext, img).first;
    }
    //need buffer size limit???
    return it->second;
}


wxImage IconBuffer::genericFileIcon(IconSize sz)
{
    try
    {
        return extractWxImage(fff::genericFileIcon(IconBuffer::getPixSize(sz))); //throw SysError
    }
    catch (SysError&) { assert(false); return wxNullImage; }
}


wxImage IconBuffer::genericDirIcon(IconSize sz)
{
    try
    {
        return extractWxImage(fff::genericDirIcon(IconBuffer::getPixSize(sz))); //throw SysError
    }
    catch (SysError&) { assert(false); return wxNullImage; }
}


wxImage IconBuffer::linkOverlayIcon(IconSize sz)
{
    //coordinate with IconBuffer::getPixSize()!
    return loadImage([sz]
    {
        const int iconSize = IconBuffer::getPixSize(sz);

        if (iconSize >= dipToScreen(128)) return "file_link_128";
        if (iconSize >= dipToScreen( 48)) return "file_link_48";
        if (iconSize >= dipToScreen( 20)) return "file_link_20";
        return "file_link_16";
    }());
}


wxImage IconBuffer::plusOverlayIcon(IconSize sz)
{
    //coordinate with IconBuffer::getPixSize()!
    return loadImage([sz]
    {
        const int iconSize = IconBuffer::getPixSize(sz);

        if (iconSize >= dipToScreen(128)) return "file_plus_128";
        if (iconSize >= dipToScreen( 48)) return "file_plus_48";
        if (iconSize >= dipToScreen( 20)) return "file_plus_20";
        return "file_plus_16";
    }());
}


wxImage IconBuffer::minusOverlayIcon(IconSize sz)
{
    //coordinate with IconBuffer::getPixSize()!
    return loadImage([sz]
    {
        const int iconSize = IconBuffer::getPixSize(sz);

        if (iconSize >= dipToScreen(128)) return "file_minus_128";
        if (iconSize >= dipToScreen( 48)) return "file_minus_48";
        if (iconSize >= dipToScreen( 20)) return "file_minus_20";
        return "file_minus_16";
    }());
}


bool fff::hasLinkExtension(const Zstring& filepath)
{
    const Zstring& ext = getFileExtension(filepath);
    return ext == "desktop";

}
