#include "PrinterFileSystem.h"
#include "libslic3r/Utils.hpp"
#include "libslic3r/Format/bbs_3mf.hpp"
#include "libslic3r/Model.hpp"
#include "slic3r/GUI/I18N.hpp"

#include "../../Utils/NetworkAgent.hpp"
#include "../BitmapCache.hpp"

#include <boost/algorithm/hex.hpp>
#include <boost/endian/conversion.hpp>
#include <boost/log/trivial.hpp>
#include <boost/uuid/detail/md5.hpp>
#include <boost/regex.hpp>

#include <wx/mstream.h>

#include "nlohmann/json.hpp"

#include <cstring>

#ifndef NDEBUG
//#define PRINTER_FILE_SYSTEM_TEST
#endif

std::string last_system_error() {
    return Slic3r::decode_path(std::error_code(
#ifdef _WIN32
        GetLastError(),
#else
        errno,
#endif
        std::system_category()).message().c_str());
}

wxDEFINE_EVENT(EVT_STATUS_CHANGED, wxCommandEvent);
wxDEFINE_EVENT(EVT_MODE_CHANGED, wxCommandEvent);
wxDEFINE_EVENT(EVT_FILE_CHANGED, wxCommandEvent);
wxDEFINE_EVENT(EVT_SELECT_CHANGED, wxCommandEvent);
wxDEFINE_EVENT(EVT_THUMBNAIL, wxCommandEvent);
wxDEFINE_EVENT(EVT_DOWNLOAD, wxCommandEvent);
wxDEFINE_EVENT(EVT_RAMDOWNLOAD, wxCommandEvent);
wxDEFINE_EVENT(EVT_MEDIA_ABILITY_CHANGED, wxCommandEvent);
wxDEFINE_EVENT(EVT_UPLOADING, wxCommandEvent);
wxDEFINE_EVENT(EVT_UPLOAD_CHANGED, wxCommandEvent);

wxDEFINE_EVENT(EVT_FILE_CALLBACK, wxCommandEvent);

static wxBitmap default_thumbnail;

static std::map<int, std::string> error_messages = {
     {PrinterFileSystem::ERROR_PIPE, L("Reconnecting the printer, the operation cannot be completed immediately, please try again later.")},
     {PrinterFileSystem::ERROR_RES_BUSY, L("The device cannot handle more conversations. Please retry later.")},
     {PrinterFileSystem::ERROR_TIME_OUT, L("Timeout, please try again.")},
     {PrinterFileSystem::FILE_NO_EXIST, L("File does not exist.")},
     {PrinterFileSystem::FILE_CHECK_ERR, L("File checksum error. Please retry.")},
     {PrinterFileSystem::FILE_TYPE_ERR, L("Not supported on the current printer version.")},
     {PrinterFileSystem::STORAGE_UNAVAILABLE, L("Please check if the storage is inserted into the printer.\nIf it still cannot be read, you can try formatting the storage.")},
     {PrinterFileSystem::API_VERSION_UNSUPPORT, L("The firmware version of the printer is too low. Please update the firmware and try again.")},
     {PrinterFileSystem::FILE_EXIST, L("The file already exists, do you want to replace it?")},
     {PrinterFileSystem::STORAGE_SPACE_NOT_ENOUGH, L("Insufficient storage space, please clear the space and try again.")},
     {PrinterFileSystem::FILE_CREATE_ERR, L("File creation failed, please try again.")},
     {PrinterFileSystem::FILE_WRITE_ERR, L("File write failed, please try again.")},
     {PrinterFileSystem::MD5_COMPARE_ERR, L("MD5 verification failed, please try again.")},
     {PrinterFileSystem::FILE_RENAME_ERR, L("File renaming failed, please try again.")},
     {PrinterFileSystem::SEND_ERR, L("File upload failed, please try again.")}
};

struct StaticBambuLib : BambuLib {
    static StaticBambuLib &get(BambuLib * copy = nullptr);
    static int Fake_Bambu_Create(Bambu_Tunnel*, char const*) { return -2; }
    static void reset();
    static void release();
private:
    std::vector<BambuLib *> copies_;
};

PrinterFileSystem::PrinterFileSystem()
    : BambuLib(StaticBambuLib::get(this))
{
    if (!default_thumbnail.IsOk()) {
        Slic3r::GUI::BitmapCache c;
        auto thumbnail = c.load_svg("printer_file", 0, 0);
        if (thumbnail && thumbnail->IsOk()) {
            default_thumbnail = *thumbnail;
#ifdef __APPLE__
            default_thumbnail = wxBitmap(default_thumbnail.ConvertToImage(), -1, 1);
#endif
        }
    }
    m_session.owner = this;
#ifdef PRINTER_FILE_SYSTEM_TEST
    auto time = wxDateTime::Now();
    wxString path = "D:\\work\\pic\\";
    for (int i = 0; i < 10; ++i) {
        auto name = wxString::Format(L"gcode-%02d.3mf", i + 1);
        m_file_list.push_back({name.ToUTF8().data(), "", time.GetTicks(), 26937, i < 5 ? FF_DOWNLOAD : 0, default_thumbnail});
        std::ifstream ifs((path + name).ToUTF8().data(), std::ios::binary);
        if (ifs)
            ParseThumbnail(m_file_list.back(), ifs);
        time.Add(wxDateSpan::Days(-1));
    }
    m_file_list.swap(m_file_list_cache[{F_MODEL, ""}]);
    time = wxDateTime::Now();
    for (int i = 0; i < 100; ++i) {
        auto name = wxString::Format(L"img-%03d.jpg", i + 1);
        wxImage im(path + name);
        m_file_list.push_back({name.ToUTF8().data(), "", time.GetTicks(), 26937, i < 20 ? FF_DOWNLOAD : 0, i > 3 ? im : default_thumbnail});
        time.Add(wxDateSpan::Days(-1));
    }
    m_file_list[0].thumbnail = default_thumbnail;
    m_file_list.swap(m_file_list_cache[{F_TIMELAPSE, ""}]);
#endif
}

PrinterFileSystem::~PrinterFileSystem()
{
    m_recv_thread.detach();
}

void PrinterFileSystem::SetFileType(FileType type, std::string const &storage)
{
    if (m_file_type == type && m_file_storage == storage)
        return;
    SelectAll(false);
    assert(m_file_list_cache[std::make_pair(m_file_type, m_file_storage)].empty());
    m_file_list.swap(m_file_list_cache[{m_file_type, m_file_storage}]);
    std::swap(m_file_type, type);
    m_file_storage = storage;
    m_file_list.swap(m_file_list_cache[{m_file_type, m_file_storage}]);
    m_lock_start = m_lock_end = 0;
    BuildGroups();
    UpdateGroupSelect();
    SendChangedEvent(EVT_FILE_CHANGED);
    if (type == F_INVALID_TYPE)
        return;
    if (m_session.tunnel == nullptr)
        return;
    m_status = Status::ListSyncing;
    SendChangedEvent(EVT_STATUS_CHANGED, m_status);
}

void PrinterFileSystem::SetGroupMode(GroupMode mode)
{
    if (this->m_group_mode == mode)
        return;
    this->m_group_mode = mode;
    m_lock_start = m_lock_end = 0;
    UpdateGroupSelect();
    SendChangedEvent(EVT_MODE_CHANGED);
}

size_t PrinterFileSystem::EnterSubGroup(size_t index)
{
    if (m_group_mode == G_NONE)
        return index;
    index = m_group_mode == G_YEAR ? m_group_year[index] : m_group_month[index];
    SetGroupMode((GroupMode)(m_group_mode - 1));
    return index;
}

void PrinterFileSystem::ListAllFiles()
{
    json req;
    char const * types[] {"timelapse","video", "model" };
    req["type"] = types[m_file_type];
    if (!m_file_storage.empty())
        req["storage"] = m_file_storage;
    req["api_version"] = 2;
    req["notify"] = "DETAIL";
    SendRequest<FileList>(LIST_INFO, req, [this, type = m_file_type](json const& resp, FileList & list, auto) -> int {
        json files = resp["file_lists"];
        for (auto& f : files) {
            std::string     name = f["name"];
            std::string     path = f.value("path", "");
            time_t          time = f.value("time", 0);
            boost::uint64_t size = f["size"];
            if (type > F_TIMELAPSE && path.empty()) // Fix old printer that always return timelapses
                return FILE_TYPE_ERR;
            File            ff   = {name, path, time, size, 0};
            list.push_back(ff);
        }
        return 0;
    }, [this, type = m_file_type](int result, FileList list) {
        if (result != 0) {
            m_last_error = result;
            m_status = Status::Failed;
            m_file_list.clear();
            BuildGroups();
            UpdateGroupSelect();
            SendChangedEvent(EVT_STATUS_CHANGED, m_status, "", result);
            SendChangedEvent(EVT_FILE_CHANGED);
            return 0;
        }
        if (type != m_file_type)
            return 0;
        m_file_list.swap(list);
        for (auto & file : m_file_list)
            file.thumbnail = default_thumbnail;
        std::sort(m_file_list.begin(), m_file_list.end());
        auto iter1 = m_file_list.begin();
        auto end1  = m_file_list.end();
        auto iter2 = list.begin();
        auto end2  = list.end();
        while (iter1 != end1 && iter2 != end2) {
            if (*iter1 < *iter2) {
                ++iter1;
            } else if (*iter2 < *iter1) {
                ++iter2;
            } else {
                if (iter1->path == iter2->path && iter1->name == iter2->name) {
                    iter1->thumbnail = iter2->thumbnail;
                    iter1->flags     = iter2->flags;
                    if (!iter1->thumbnail.IsOk())
                        iter1->flags &= ~FF_THUMNAIL;
                    iter1->download   = iter2->download;
                    iter1->local_path = iter2->local_path;
                    iter1->metadata   = iter2->metadata;
                }
                ++iter1;
                ++iter2;
            }
        }
        BuildGroups();
        UpdateGroupSelect();
        m_last_error = 0;
        m_status = Status::ListReady;
        SendChangedEvent(EVT_STATUS_CHANGED, m_status);
        SendChangedEvent(EVT_FILE_CHANGED);
        if ((m_task_flags & FF_DOWNLOAD) == 0)
            DownloadNextFile();
        return 0;
    });
}

void PrinterFileSystem::DeleteFiles(size_t index)
{
    if (index == size_t(-1)) {
        size_t n = 0;
        for (size_t i = 0; i < m_file_list.size(); ++i) {
            auto &file = m_file_list[i];
            if ((file.flags & FF_SELECT) != 0 && (file.flags & FF_DELETED) == 0) {
                file.flags |= FF_DELETED;
                ++n;
            }
        }
        if (n == 0) return;
    } else {
        if (index >= m_file_list.size())
            return;
        auto &file = m_file_list[index];
        if ((file.flags & FF_DELETED) != 0)
            return;
        file.flags |= FF_DELETED;
    }
    if ((m_task_flags & FF_DELETED) == 0)
        DeleteFilesContinue();
}

struct PrinterFileSystem::Download : Progress
{
    size_t                      index;
    std::string                 name;
    std::string                 path;
    std::string                 local_path;
    std::string                 error;
    boost::filesystem::ofstream ofs;
    boost::uuids::detail::md5   boost_md5;
};

struct PrinterFileSystem::Upload : Progress
{
    std::string                 error;
    boost::uint32_t             frag_id{0};
    MD5_CTX                     ctx;
    boost::filesystem::ifstream ifs;
};


void PrinterFileSystem::GetPickImages(const std::vector<std::string> &local_paths, const std::vector<std::string> &targetpaths)
{
    m_download_states.clear();

    GetPickImage(1, local_paths[0], targetpaths[0]);
    GetPickImage(2, local_paths[1], targetpaths[1]);
    GetPickImage(3, local_paths[2], targetpaths[2]);

}

void PrinterFileSystem::GetPickImage(int id, const std::string &local_path, const std::string &targetpath)
{
    json j;

    j["sequence_id"]   = id;
    j["version"]       = 1;
    j["peer_host"]     = "studio";
    j["command"]       = "get_project_file";
    j["file_rel_path"] = targetpath;

    std::string param = j.dump();

    DownloadRamFile(16, local_path, param);
}


void PrinterFileSystem::DownloadRamFile(int index, const std::string &local_path, const std::string & param)
{
    std::shared_ptr<Download> download(new Download);
    download->local_path = local_path;

    json req;
    req["path"]              = "mem:/" + std::to_string(index);
    req["offset"] = 0;
    req["mem_dl_param_size"] = param.size();

    m_download_seq = SendRequest<Progress>(
        FILE_DOWNLOAD, req,
        [download](json const &resp, Progress &prog, unsigned char const *data) -> int {
            size_t size = resp.value("size", 0);
            prog.size   = resp["offset"];
            prog.total  = resp["total"];

            if (resp.contains("mem_dl_param_size")) {
                size_t s = resp["mem_dl_param_size"].get<size_t>();
                std::string json_str(reinterpret_cast<const char *>(data), s);
                // OutputDebugStringA(json_str.c_str());
                // OutputDebugStringA("\n");
                json        mem_dl_json = json::parse(json_str);
                //  download->mem_dl_param_size = size;
                if (!mem_dl_json.contains("result") || mem_dl_json["result"] == 1 ) {
                        wxLogWarning("Download failed: result = 1");
                    return ERROR_JSON;
                    }
                if(mem_dl_json.contains("size") && mem_dl_json["size"] == 0 )
                    return FILE_SIZE_ERR;

                return CONTINUE;
            }

            if (prog.size == 0 ) {
                download->ofs.open(download->local_path, std::ios::binary);
                if (!download->ofs) {
                    download->error = last_system_error();
                    wxLogWarning("DownloadImageFromRam open error: %s\n", wxString::FromUTF8(download->error));
                    return FILE_OPEN_ERR;
                }
            }

            download->ofs.write(reinterpret_cast<const char *>(data), size);
            if (!download->ofs) {
                download->error = last_system_error();
                wxLogWarning("DownloadImageFromRam write error: %s\n", wxString::FromUTF8(download->error));
                return FILE_READ_WRITE_ERR;
            }

            download->boost_md5.process_bytes(data, size);

            prog.size += size;
            download->total = prog.total;
            download->size  = prog.size;

            if (prog.size < prog.total) {
                return 0;
            }
            download->ofs.close();

            std::string                     md5 = resp["file_md5"];
            boost::uuids::detail::md5::digest_type digest;
            download->boost_md5.get_digest(digest);
            for (int i = 0; i < 4; ++i) digest[i] = boost::endian::endian_reverse(digest[i]);
            std::string str_md5;
            const auto  char_digest = reinterpret_cast<const char *>(&digest[0]);
            boost::algorithm::hex(char_digest, char_digest + sizeof(digest), std::back_inserter(str_md5));
            if (!boost::iequals(str_md5, md5)) {
                wxLogWarning("DownloadImageFromRam checksum error: %s != %s\n", str_md5, md5);
                boost::system::error_code ec;
                boost::filesystem::rename(download->local_path, download->local_path + ".tmp", ec);
                return FILE_CHECK_ERR;
            }
            return SUCCESS;
        },

        [this, download](int result, Progress const &data) {
            //OutputDebugStringA(std::to_string(result).c_str());
            //OutputDebugStringA("\n");
            if (result == CONTINUE) { return; }
            std::string msg;
            if (result == SUCCESS) {
                if (std::filesystem::exists(download->local_path)) {
                    m_download_states.emplace_back(true);
                    BOOST_LOG_TRIVIAL(info) <<"DownloadImageFromRam finished: " << download->local_path << "result = " << result;
                }else{
                    m_download_states.emplace_back(false);
                    BOOST_LOG_TRIVIAL(warning) <<"DownloadImageFromRam finished, but file not exist: " << download->local_path << "result = " << result;
                }
            } else if (result != CONTINUE) {
                m_download_states.emplace_back(false);
                BOOST_LOG_TRIVIAL(warning) << "DownloadImageFromRam failed: " << download->error << "result = " << result;
            }

            if(m_download_states.size() == 3){
                if(m_download_states[0] && m_download_states[1] && m_download_states[2]){
                    SendChangedEvent(EVT_RAMDOWNLOAD, SUCCESS);
                }else{
                    // FILE_NO_EXIST is not really error_code
                    SendChangedEvent(EVT_RAMDOWNLOAD, FILE_NO_EXIST);
                }
            }else{
                 BOOST_LOG_TRIVIAL(warning) << "m_download_states current size is : " << m_download_states.size();
            }
        },param);
}

void PrinterFileSystem::SendExistedFile(){
    SendChangedEvent(EVT_RAMDOWNLOAD, SUCCESS);
}
void PrinterFileSystem::SendConnectFail(){
    SendChangedEvent(EVT_RAMDOWNLOAD, ERROR_PIPE);
}


void PrinterFileSystem::DownloadFiles(size_t index, std::string const &path)
{
    if (index == (size_t) -1) {
        size_t n = 0;
        for (size_t i = 0; i < m_file_list.size(); ++i) {
            auto &file = m_file_list[i];
            if ((file.flags & FF_SELECT) == 0) continue;
            if ((file.flags & FF_DOWNLOAD) != 0 && file.DownloadProgress() >= -1) continue;
            file.flags |= FF_DOWNLOAD;
            std::shared_ptr<Download> download(new Download);
            download->progress = -1;
            download->local_path = (boost::filesystem::path(path) / file.name).string();
            file.download = download;
            ++n;
        }
        if (n == 0) return;
    } else {
        if (index >= m_file_list.size())
            return;
        auto &file = m_file_list[index];
        if ((file.flags & FF_DOWNLOAD) != 0 && file.DownloadProgress() >= -1)
            return;
        file.flags |= FF_DOWNLOAD;
        std::shared_ptr<Download> download(new Download);
        download->progress   = -1;
        download->local_path = (boost::filesystem::path(path) / file.name).string();
        file.download        = download;
    }
    boost::filesystem::create_directories(path);
    if ((m_task_flags & FF_DOWNLOAD) == 0)
        DownloadNextFile();
}





void PrinterFileSystem::DownloadCheckFiles(std::string const &path)
{
    for (size_t i = 0; i < m_file_list.size(); ++i) {
        auto &file = m_file_list[i];
        if ((file.flags & FF_DOWNLOAD) != 0 && file.download) continue;
        auto path2 = boost::filesystem::path(path) / file.name;
        boost::system::error_code ec;
        if (boost::filesystem::file_size(path2, ec) == file.size) {
            file.flags |= FF_DOWNLOAD;
            file.local_path = path2.string();
        }
    }
}

bool PrinterFileSystem::DownloadCheckFile(size_t index)
{
    if (index >= m_file_list.size()) return false;
    auto &file = m_file_list[index];
    if ((file.flags & FF_DOWNLOAD) == 0 || file.local_path.empty())
        return false;
    if (!boost::filesystem::exists(file.local_path)) {
        file.flags &= ~FF_DOWNLOAD;
        file.local_path.clear();
        SendChangedEvent(EVT_DOWNLOAD, index, file.local_path);
        return false;
    }
    return true;
}

void PrinterFileSystem::DownloadCancel(size_t index)
{
    if (index == (size_t) -1) return;
    if (index >= m_file_list.size()) return;
    auto &file = m_file_list[index];
    if ((file.flags & FF_DOWNLOAD) == 0 || !file.download) return;
    if (file.DownloadProgress() >= 0)
        CancelRequest(m_download_seq);
    else
        file.flags &= ~FF_DOWNLOAD, file.download.reset();
}

void PrinterFileSystem::FetchModel(size_t index, std::function<void(int, std::string const &)> callback)
{
    if (m_task_flags & FF_FETCH_MODEL)
        return;
    json req;
    json arr;
    if (index == (size_t) -1) return;
    if (index >= m_file_list.size()) return;
    auto &file = m_file_list[index];
    arr.push_back(file.path + "#_rels/.rels");
    arr.push_back(file.path + "#3D/3dmodel.model");
    arr.push_back(file.path + "#Metadata/model_settings.config");
    arr.push_back(file.path + "#Metadata/slice_info.config");
    arr.push_back(file.path + "#Metadata/project_settings.config");
    for (auto & meta : file.metadata) {
        if (boost::algorithm::starts_with(meta.first, "plate_thumbnail_"))
            arr.push_back(file.path + "#" + meta.second);
    }
    req["paths"] = arr;
    req["zip"] = true;
    m_task_flags |= FF_FETCH_MODEL;
    std::shared_ptr<std::string> file_data(new std::string());
    m_fetch_model_seq = SendRequest<Void>(
        SUB_FILE, req,
        [file_data](json const &resp, Void &, unsigned char const *data) -> int {
            // in work thread, continue recv
            // receive data
            boost::uint32_t size      = resp["size"];
            if (size > 0) {
                *file_data += std::string((char *) data, size);
            }
            return 0;
        },
        [this, file_data, callback](int result, Void const &) {
            if (result == CONTINUE) return;
            m_task_flags &= ~FF_FETCH_MODEL;
            if (result != 0) {
                auto iter = error_messages.find(result);
                if (iter != error_messages.end())
                    *file_data = _u8L(iter->second.c_str());
                else
                    file_data->clear();
            }
            callback(result, *file_data);
        });
}

void PrinterFileSystem::FetchModelCancel()
{
    if ((m_task_flags & FF_FETCH_MODEL) == 0) return;
    CancelRequests2({m_fetch_model_seq});
}

size_t PrinterFileSystem::GetCount() const
{
    if (m_group_mode == G_NONE)
        return m_file_list.size();
    return m_group_mode == G_YEAR ? m_group_year.size() : m_group_month.size();
}

int PrinterFileSystem::File::DownloadProgress() const { return download ? download->progress : !local_path.empty() ? 100 : -2; }

std::string PrinterFileSystem::File::Title() const { return Metadata("Title", ""); }

std::string PrinterFileSystem::File::Metadata(std::string const &key, std::string const &dflt) const
{
    auto iter = metadata.find(key);
    return iter == metadata.end() || iter->second.empty() ? dflt : iter->second;
}

PrinterFileSystem::UploadFile::~UploadFile()
{
    if (upload && upload->ifs.is_open()) {
        upload->ifs.close();
    }
}

size_t PrinterFileSystem::GetIndexAtTime(boost::uint32_t time)
{
    auto   iter = std::upper_bound(m_file_list.begin(), m_file_list.end(), File{"", "", time});
    size_t n = std::distance(m_file_list.begin(), iter) - 1;
    if (m_group_mode == G_NONE) {
        return n;
    }
    auto & group = m_group_mode == G_YEAR ? m_group_year : m_group_month;
    auto iter2 = std::upper_bound(group.begin(), group.end(), n);
    return std::distance(group.begin(), iter2) - 1;
}

void PrinterFileSystem::ToggleSelect(size_t index)
{
    if (m_group_mode != G_NONE) {
        size_t beg = m_group_mode == G_YEAR ? m_group_month[m_group_year[index]] : m_group_month[index];
        size_t end_month = m_group_mode == G_YEAR ? ((index + 1) < m_group_year.size() ? m_group_year[index + 1] : m_group_month.size()) : index + 1;
        size_t end       = end_month < m_group_month.size() ? m_group_month[end_month] : m_file_list.size();
        if ((m_group_flags[index] & FF_SELECT) == 0) {
            for (int i = beg; i < end; ++i) {
                if ((m_file_list[i].flags & FF_SELECT) == 0) {
                    m_file_list[i].flags |= FF_SELECT;
                    ++m_select_count;
                }
            }
            m_group_flags[index] |= FF_SELECT;
        } else {
            for (int i = beg; i < end; ++i) {
                if (m_file_list[i].flags & FF_SELECT) {
                    m_file_list[i].flags &= ~FF_SELECT;
                    --m_select_count;
                }
            }
            m_group_flags[index] &= ~FF_SELECT;
        }
    } else if (index < m_file_list.size()) {
        m_file_list[index].flags ^= FF_SELECT;
        if (m_file_list[index].flags & FF_SELECT)
            ++m_select_count;
        else
            --m_select_count;
    }
    SendChangedEvent(EVT_SELECT_CHANGED, m_select_count);
}

void PrinterFileSystem::SelectAll(bool select)
{
    if (select) {
        for (auto &f : m_file_list) f.flags |= FF_SELECT;
        m_select_count = m_file_list.size();
        for (auto &s : m_group_flags) s |= FF_SELECT;
    } else {
        for (auto &f : m_file_list) f.flags &= ~FF_SELECT;
        m_select_count = 0;
        for (auto &s : m_group_flags) s &= ~FF_SELECT;
    }
    SendChangedEvent(EVT_SELECT_CHANGED, m_select_count);
}

size_t PrinterFileSystem::GetSelectCount() const { return m_select_count; }

void PrinterFileSystem::SetFocusRange(size_t start, size_t count)
{
    m_lock_start = start;
    m_lock_end = start + count;
    if (!m_stopped && (m_task_flags & FF_THUMNAIL) == 0)
        UpdateFocusThumbnail();
}

PrinterFileSystem::File const &PrinterFileSystem::GetFile(size_t index)
{
    if (m_group_mode == G_NONE)
        return m_file_list[index];
    if (m_group_mode == G_YEAR) index = m_group_year[index];
    return m_file_list[m_group_month[index]];
}

PrinterFileSystem::File const &PrinterFileSystem::GetFile(size_t index, bool &select)
{
    if (m_group_mode == G_NONE) {
        select = m_file_list[index].IsSelect();
        return m_file_list[index];
    }
    select = m_group_flags[index] & FF_SELECT;
    if (m_group_mode == G_YEAR)
        index = m_group_year[index];
    return m_file_list[m_group_month[index]];
}

void PrinterFileSystem::Attached()
{
    boost::unique_lock lock(m_mutex);
    m_recv_thread = std::move(boost::thread([w = weak_from_this()] {
        boost::shared_ptr<PrinterFileSystem> s = w.lock();
        if (s) s->RecvMessageThread();
    }));
}

void PrinterFileSystem::Start()
{
    boost::unique_lock l(m_mutex);
    if (!m_stopped) return;
    m_stopped = false;
    m_cond.notify_all();
}

void PrinterFileSystem::Retry()
{
    boost::unique_lock l(m_mutex);
    m_stopped = false;
    m_cond.notify_all();
}

void PrinterFileSystem::SetUrl(std::string const &url)
{
    boost::unique_lock l(m_mutex);
    m_messages.push_back(url);
    m_cond.notify_all();
}

void PrinterFileSystem::Stop(bool quit)
{
    boost::unique_lock l(m_mutex);
    if (quit) {
        m_session.owner = nullptr;
    } else if (m_stopped) {
        return;
    }
    m_stopped = true;
    m_cond.notify_all();
}

void PrinterFileSystem::SetUploadFile(const std::string &path, const std::string &name, const std::string &select_storage)
{
    boost::unique_lock l(m_mutex);
    if (!m_upload_file) {
        m_upload_file = std::make_unique<UploadFile>();
    }
    m_upload_file->path           = path;
    m_upload_file->name           = name;
    m_upload_file->select_storage = select_storage;
}

void PrinterFileSystem::BuildGroups()
{
    m_group_year.clear();
    m_group_month.clear();
    if (m_file_list.empty())
        return;
    wxDateTime t = wxDateTime((time_t) m_file_list.front().time);
    m_group_year.push_back(0);
    m_group_month.push_back(0);
    for (size_t i = 0; i < m_file_list.size(); ++i) {
        wxDateTime s = wxDateTime((time_t) m_file_list[i].time);
        if (s.GetYear() != t.GetYear()) {
            m_group_year.push_back(m_group_month.size());
            m_group_month.push_back(i);
        } else if (s.GetMonth() != t.GetMonth()) {
            m_group_month.push_back(i);
        }
        t = s;
    }
}

void PrinterFileSystem::UpdateGroupSelect()
{
    m_group_flags.clear();
    int beg = 0;
    if (m_group_mode != G_NONE) {
        auto group = m_group_mode == G_YEAR ? m_group_year : m_group_month;
        if (m_group_mode == G_YEAR)
            for (auto &g : group) g = m_group_month[g];
        m_group_flags.resize(group.size(), FF_SELECT);
        for (int i = 0; i < m_file_list.size(); ++i) {
            if ((m_file_list[i].flags & FF_SELECT) == 0) {
                auto iter = std::upper_bound(group.begin(), group.end(), i);
                m_group_flags[iter - group.begin() - 1] &= ~FF_SELECT;
                if (iter == group.end()) break;
                i = *iter - 1; // start from next group
            }
        }
    }
}

void PrinterFileSystem::DeleteFilesContinue()
{
    std::vector<size_t> indexes;
    std::vector<std::string> names;
    std::vector<std::string> paths;
    for (size_t i = 0; i < m_file_list.size(); ++i)
        if ((m_file_list[i].flags & FF_DELETED) && !m_file_list[i].name.empty()) {
            indexes.push_back(i);
            auto &file = m_file_list[i];
            if (file.path.empty())
                names.push_back(file.name);
            else
                paths.push_back(file.path);
            if (names.size() >= 64 || paths.size() >= 64)
                break;
        }
    m_task_flags &= ~FF_DELETED;
    if (names.empty() && paths.empty())
        return;
    json req;
    json arr;
    if (paths.empty()) {
        for (auto &name : names) arr.push_back(name);
        req["delete"] = arr;
    } else {
        for (auto &path : paths) arr.push_back(path);
        req["paths"] = arr;
    }
    m_task_flags |= FF_DELETED;
    auto type = std::make_pair(m_file_type, m_file_storage);
    SendRequest<Void>(
        FILE_DEL, req, nullptr,
        [indexes, type, names = paths.empty() ? names : paths, bypath = !paths.empty(), this](int, Void const &) {
            // TODO:
            for (size_t i = indexes.size() - 1; i != size_t(-1); --i)
                FileRemoved(type, indexes[i], names[i], bypath);
            SendChangedEvent(EVT_FILE_CHANGED, indexes.size());
            DeleteFilesContinue();
        });
}

void PrinterFileSystem::DownloadNextFile()
{
    size_t index = size_t(-1);
    for (size_t i = 0; i < m_file_list.size(); ++i) {
        if (m_file_list[i].IsDownload() && m_file_list[i].DownloadProgress() == -1) {
            index = i;
            break;
        }
    }
    m_task_flags &= ~FF_DOWNLOAD;
    if (index >= m_file_list.size())
        return;
    auto &file = m_file_list[index];
    json req;
    if (file.path.empty())
        req["file"] = file.name;
    else
        req["path"] = file.path;
    SendChangedEvent(EVT_DOWNLOAD, index, m_file_list[index].name);
    std::shared_ptr<Download> download(m_file_list[index].download);
    download->index = index;
    download->name = file.name;
    download->path = file.path;
    m_task_flags |= FF_DOWNLOAD;
    m_download_seq = SendRequest<Progress>(
        FILE_DOWNLOAD, req,
        [download](json const &resp, Progress &prog, unsigned char const *data) -> int {
            // in work thread, continue recv
            size_t size = resp.value("size", 0);
            prog.size   = resp["offset"];
            prog.total  = resp["total"];
            if (prog.size == 0) {
                download->ofs.open(download->local_path, std::ios::binary);
                if (!download->ofs) {
                    download->error = last_system_error();
                    wxLogWarning("PrinterFileSystem::DownloadNextFile open error: %s\n", wxString::FromUTF8(download->error));
                    return FILE_OPEN_ERR;
                }
            }
            if (download->total && (download->size != prog.size || download->total != prog.total)) {
                wxLogWarning("PrinterFileSystem::DownloadNextFile data error: %d != %d\n", download->size, prog.size);
            }
            // receive data
            download->ofs.write((char const *) data, size);
            if (!download->ofs) {
                download->error = last_system_error();
                wxLogWarning("PrinterFileSystem::DownloadNextFile write error: %s\n", wxString::FromUTF8(download->error));
                return FILE_READ_WRITE_ERR;
            }
            download->boost_md5.process_bytes(data, size);
            prog.size += size;
            download->total = prog.total;
            download->size = prog.size;
            if (prog.size < prog.total) { return 0; }
            download->ofs.close();
            int         result = 0;
            std::string md5    = resp["file_md5"];
            // check size and md5
            if (prog.size == prog.total) {
                boost::uuids::detail::md5::digest_type digest;
                download->boost_md5.get_digest(digest);
                for (int i = 0; i < 4; ++i) digest[i] = boost::endian::endian_reverse(digest[i]);
                std::string str_md5;
                const auto  char_digest = reinterpret_cast<const char *>(&digest[0]);
                boost::algorithm::hex(char_digest, char_digest + sizeof(digest), std::back_inserter(str_md5));
                if (!boost::iequals(str_md5, md5)) {
                    wxLogWarning("PrinterFileSystem::DownloadNextFile checksum error: %s != %s\n", str_md5, md5);
                    boost::system::error_code ec;
                    boost::filesystem::rename(download->local_path, download->local_path + ".tmp", ec);
                    result = FILE_CHECK_ERR;
                }
            } else {
                result = FILE_SIZE_ERR;
            }
            if (result != 0) {
                boost::system::error_code ec;
                boost::filesystem::remove(download->local_path, ec);
            }
            return result;
        },
        [this, download, type = std::make_pair(m_file_type, m_file_storage)](int result, Progress const &data) {
            int progress = data.total ? data.size * 100 / data.total : 0;
            if (result == CONTINUE) {
                if (download->progress == progress)
                    return;
            }
            download->progress = progress;
            if (download->index != size_t(-1)) {
                auto file_index = FindFile(type, download->index, download->path.empty() ? download->name : download->path, !download->path.empty());
                download->index = file_index.second;
                if (download->index != size_t(-1)) {
                    auto &file = file_index.first[download->index];
                    if (result == CONTINUE)
                        ;
                    else if (result == SUCCESS)
                        file.download.reset(), file.local_path = download->local_path;
                    else if (result == ERROR_CANCEL)
                        file.download.reset(), file.flags &= ~FF_DOWNLOAD;
                    else // FAILED
                        file.download.reset();
                    if (&file_index.first == &m_file_list)
                        SendChangedEvent(EVT_DOWNLOAD, download->index, result ? download->error : file.local_path, result);
                }
            }
            if (result != CONTINUE) DownloadNextFile();
        });
}

enum ThumbnailType
{
    OldThumbnail = 0,
    VideoThumbnail = 1,
    ModelMetadata = 2,
    ModelThumbnail = 3,
    FinishThumbnail
};

void PrinterFileSystem::UpdateFocusThumbnail()
{
    m_task_flags &= ~FF_THUMNAIL;
    if (m_lock_start >= m_file_list.size() || m_lock_start >= m_lock_end)
        return;
    size_t start = m_lock_start;
    size_t end   = std::min(m_lock_end, GetCount());
    std::vector<File> names;
    std::vector<File> paths;
    for (; start < end; ++start) {
        auto &file = GetFile(start);
        if ((file.flags & FF_THUMNAIL) == 0) {
            if (m_file_type == F_MODEL) {
                const_cast<File &>(file).metadata.emplace("Time", "...");
                const_cast<File &>(file).metadata.emplace("Weight", "...");
            }
            if (file.path.empty())
                names.push_back({file.name, ""});
            else
                paths.push_back({file.name, file.path});
            if (names.size() >= 2 || paths.size() >= 2)
                break;
            if ((file.flags & FF_THUMNAIL_RETRY) != 0) {
                const_cast<File&>(file).flags &= ~FF_THUMNAIL_RETRY;
                break;
            }
        }
    }
    if (names.empty() && paths.empty())
        return;
    m_task_flags |= FF_THUMNAIL;
    UpdateFocusThumbnail2(std::make_shared<std::vector<File>>(paths.empty() ? names : paths),
        paths.empty() ? OldThumbnail : m_file_type == F_MODEL ? ModelMetadata : VideoThumbnail);
}

bool PrinterFileSystem::ParseThumbnail(File &file)
{
    std::istringstream iss(file.local_path, std::ios::binary);
    return ParseThumbnail(file, iss);
}

static std::string durationString(long duration)
{
    static boost::regex rx("^0d(0h)?");
    auto time = boost::format("%1%d%2%h%3%m") % (duration / 86400) % ((duration % 86400) / 3600) % ((duration % 3600) / 60);
    return boost::regex_replace(time.str(), rx, "");
}

bool PrinterFileSystem::ParseThumbnail(File &file, std::istream &is)
{
    Slic3r::DynamicPrintConfig config;
    Slic3r::Model              model;
    Slic3r::PlateDataPtrs      plate_data_list;
    Slic3r::Semver file_version;
    if (!Slic3r::load_gcode_3mf_from_stream(is, &config, &model, &plate_data_list, &file_version))
        return false;
    float time      = 0.f;
    float weight    = 0.f;
    for (auto &plate : plate_data_list) {
        time += atof(plate->gcode_prediction.c_str());
        weight += atof(plate->gcode_weight.c_str());
        if (!plate->gcode_file.empty() && !plate->thumbnail_file.empty())
            file.metadata.emplace("plate_thumbnail_" + std::to_string(plate->plate_index), plate->thumbnail_file);
    }
    file.metadata.emplace("Title", model.model_info->model_name);
    file.metadata.emplace("Time", durationString(round(time)));
    file.metadata.emplace("Weight", std::to_string(int(round(weight))) + 'g');
    auto thumbnail = model.model_info->metadata_items["Thumbnail"];
    if (thumbnail.empty() && !plate_data_list.empty()) {
        thumbnail = plate_data_list.front()->thumbnail_file;
    }
    file.metadata.emplace("Thumbnail", thumbnail);
    return true;
}

void PrinterFileSystem::UpdateFocusThumbnail2(std::shared_ptr<std::vector<File>> files, int type)
{
    json req;
    json arr;
    if (type == OldThumbnail) {
        for (auto &file : *files) arr.push_back(file.name);
        req["files"] = arr;
    } else {
        if (type == VideoThumbnail) {
            for (auto &file : *files) arr.push_back(file.path + "#thumbnail");
        } else if (type == ModelMetadata) {
            for (auto &file : *files) {
                arr.push_back(file.path + "#_rels/.rels");
                arr.push_back(file.path + "#3D/3dmodel.model");
                arr.push_back(file.path + "#Metadata/model_settings.config");
                arr.push_back(file.path + "#Metadata/slice_info.config");
                arr.push_back(file.path + "#Metadata/project_settings.config");
            }
            req["zip"] = true;
        } else { // ModelThumbnail, FinishThumbnail
            std::vector<std::string> fails;
            for (auto &file : *files) {
                if ((file.flags & FF_THUMNAIL) == 0) {
                    fails.push_back(file.path);
                    file.flags |= FF_THUMNAIL;
                }
            }
            for (auto &path : fails) {
                auto iter = std::find_if(m_file_list.begin(), m_file_list.end(), [&path](auto &f) { return f.path == path; });
                if (iter != m_file_list.end()) {
                    if (type == ModelThumbnail) iter->metadata.clear();
                    iter->flags |= fails.size() == 1 ? FF_THUMNAIL : FF_THUMNAIL_RETRY;
                }
            }
            if (type == ModelThumbnail) {
                for (auto &file : *files) {
                    auto thumbnail = file.metadata["Thumbnail"];
                    if (!thumbnail.empty()) {
                        arr.push_back(file.path + "#" + thumbnail);
                        file.flags &= ~FF_THUMNAIL;
                        file.local_path.clear();
                    }
                }
            }
            if (arr.empty()) {
                UpdateFocusThumbnail();
                return;
            }
        }
        req["paths"] = arr;
    }
    SendRequest<File>(
        SUB_FILE, req, [type, files](json const &resp, File &file, unsigned char const *data) -> int {
            // in work thread, continue recv
            // receive data
            wxString        mimetype  = resp.value("mimetype", "");
            std::string     thumbnail = resp.value("thumbnail", "");
            std::string     path      = resp.value("path", "");
            boost::uint32_t size      = resp.value("size", 0);
            bool            cont      = resp.value("continue", false);
            if (size == 0) {
                file.name = thumbnail;
                file.path = path;
                return FILE_SIZE_ERR;
            }
            auto n = type == ModelMetadata ? std::string::npos : path.find_last_of('#'); // ModelMetadata is zipped without subpath
            auto path2 = n == std::string::npos ? path : path.substr(0, n);
            auto subpath = n == std::string::npos ? path : path.substr(n + 1);
            auto iter = std::find_if(files->begin(), files->end(), [&path2](auto &f) { return f.path == path2; });
            if (cont) {
                if (iter != files->end())
                    iter->local_path += std::string((char *) data, size);
                return 0;
            }
            if (type == ModelMetadata) {
                if (iter != files->end())
                    file.local_path = iter->local_path + std::string((char *) data, size);
                else
                    file.local_path = std::string((char *) data, size);
                ParseThumbnail(file);
            } else {
                if (mimetype.empty()) {
                    if (subpath.empty()) subpath = thumbnail;
                    auto n = subpath.find_last_of('.');
                    if (n != std::string::npos)
                        mimetype = "image/" + subpath.substr(n + 1);
                    else if (subpath == "thumbnail")
                        mimetype = "image/jpeg"; // default jpg
                }
                if (iter != files->end() && !iter->local_path.empty()) {
                    iter->local_path += std::string((char *) data, size);
                    data = reinterpret_cast<unsigned char const *>(iter->local_path.c_str());
                    size = iter->local_path.size();
                }
                wxMemoryInputStream mis(data, size);
                mimetype.Replace("jpg", "jpeg");
                file.thumbnail = wxImage(mis, mimetype);
                if (!file.thumbnail.IsOk()) {
                    BOOST_LOG_TRIVIAL(info) << "PrinterFileSystem: parse thumbnail failed" << size << mimetype;
                }
            }
            file.name = thumbnail;
            file.path = path;
            return 0;
        },
        [this, files, type](int result, File const &file) {
            auto n    = file.name.find_last_of('.');
            auto name  = n == std::string::npos ? file.name : file.name.substr(0, n) + ".mp4";
            n          = (type == ModelMetadata) ? std::string::npos : file.path.find_last_of('#');
            auto path = n == std::string::npos ? file.path : file.path.substr(0, n);
            auto iter = path.empty() ? std::find_if(m_file_list.begin(), m_file_list.end(), [&name](auto &f) { return f.name == name; }) :
                                       std::find_if(m_file_list.begin(), m_file_list.end(), [&path](auto &f) { return f.path == path; });
            auto iter2 = path.empty() ? std::find_if(files->begin(), files->end(), [&name](auto &f) { return f.name == name; }) :
                                        std::find_if(files->begin(), files->end(), [&path](auto &f) { return f.path == path; });
            if (iter != m_file_list.end()) {
                if (type == ModelMetadata) {
                    iter->metadata = file.metadata;
                    auto thumbnail = iter->metadata["Thumbnail"];
                    if (thumbnail.empty())
                        iter->flags |= FF_THUMNAIL; // DOTO: retry on fail
                    int index       = iter - m_file_list.begin();
                    SendChangedEvent(EVT_THUMBNAIL, index, file.name);
                    if (iter2 != files->end())
                        iter2->metadata = file.metadata;
                } else {
                    iter->flags |= FF_THUMNAIL; // DOTO: retry on fail
                    if (file.thumbnail.IsOk()) {
                        iter->thumbnail = file.thumbnail;
                        int index       = iter - m_file_list.begin();
                        SendChangedEvent(EVT_THUMBNAIL, index, file.name);
                    }
                }
            }
            if (iter2 != files->end())
                iter2->flags |= FF_THUMNAIL; // have received response
            if (result != CONTINUE)
                UpdateFocusThumbnail2(files, type == ModelMetadata ? ModelThumbnail : FinishThumbnail);
        });
}

std::pair<PrinterFileSystem::FileList &, size_t> PrinterFileSystem::FindFile(std::pair<FileType, std::string> type, size_t index, std::string const &name, bool by_path)
{
    FileList & file_list = type == std::make_pair(m_file_type, m_file_storage) ?
                               m_file_list :
                               m_file_list_cache[type];
    if (index >= file_list.size() || (by_path ? file_list[index].path : file_list[index].name) != name) {
        auto iter = std::find_if(m_file_list.begin(), file_list.end(),
                [name, by_path](File &f) { return (by_path ? f.path : f.name) == name; });
        if (iter == m_file_list.end()) return {file_list, -1};
        index = std::distance(m_file_list.begin(), iter);
    }
    return {file_list, index};
}

void PrinterFileSystem::FileRemoved(std::pair<FileType, std::string> type, size_t index, std::string const &name, bool by_path)
{
    auto file_index = FindFile(type, index, name, by_path);
    if (file_index.second == size_t(-1))
        return;
    if (&file_index.first == &m_file_list) {
        auto removeFromGroup = [](std::vector<size_t> &group, size_t index, size_t total) {
            for (auto iter = group.begin(); iter != group.end(); ++iter) {
                size_t index2 = -1;
                if (*iter < index) continue;
                if (*iter == index) {
                    auto iter2 = iter + 1;
                    if (index + 1 == (iter2 == group.end() ? total : *iter2)) {
                        index2 = std::distance(group.begin(), iter);
                    }
                    ++iter;
                }
                for (; iter != group.end(); ++iter) {
                    --*iter;
                }
                return index2;
            }
            return size_t(-1);
        };
        size_t index2 = removeFromGroup(m_group_month, index, m_file_list.size());
        if (index2 < m_group_month.size()) {
            int index3 = removeFromGroup(m_group_year, index2, m_group_month.size());
            if (index3 < m_group_year.size()) {
                m_group_year.erase(m_group_year.begin() + index3);
                if (m_group_mode == G_YEAR)
                    m_group_flags.erase(m_group_flags.begin() + index3);
            }
            m_group_month.erase(m_group_month.begin() + index2);
            if (m_group_mode == G_MONTH)
                m_group_flags.erase(m_group_flags.begin() + index2);
        }
    }
    file_index.first.erase(file_index.first.begin() + index);
}

struct CallbackEvent : wxCommandEvent
{
    CallbackEvent(std::function<void(void)> const &callback, boost::weak_ptr<PrinterFileSystem> owner) : wxCommandEvent(EVT_FILE_CALLBACK), callback(callback), owner(owner) {}
    ~CallbackEvent(){ if (!owner.expired()) callback(); }
    std::function<void(void)> const callback;
    boost::weak_ptr<PrinterFileSystem> owner;
};

void PrinterFileSystem::PostCallback(std::function<void(void)> const& callback)
{
    wxCommandEvent *e = new CallbackEvent(callback, boost::weak_ptr(shared_from_this()));
    wxQueueEvent(this, e);
}

void PrinterFileSystem::SendChangedEvent(wxEventType type, size_t index, std::string const &str, long extra)
{
    wxCommandEvent event(type);
    event.SetEventObject(this);
    event.SetInt(index);
    if (!str.empty())
        event.SetString(wxString::FromUTF8(str.c_str()));
    else if (auto iter = error_messages.find(extra); iter != error_messages.end())
        event.SetString(_L(iter->second.c_str()));
    else if (extra > CONTINUE && extra != ERROR_CANCEL)
        event.SetString(wxString::Format(_L("Error code: %d"), int(extra)));
    event.SetExtraLong(extra);
    if (wxThread::IsMain())
        ProcessEventLocally(event);
    else
        wxPostEvent(this, event);
}

void PrinterFileSystem::DumpLog(void * thiz, int, tchar const *msg)
{

#if !BBL_RELEASE_TO_PUBLIC
    BOOST_LOG_TRIVIAL(info) << "PrinterFileSystem: " << wxString(msg).ToUTF8().data();
#endif

    static_cast<PrinterFileSystem*>(thiz)->Bambu_FreeLogMsg(msg);
}

boost::uint32_t PrinterFileSystem::RequestMediaAbility(int api_version)
{
    json req;
    req["peer"] = "studio";
    req["api_version"] = api_version;

    return SendRequest<MediaAbilityList>(
        REQUEST_MEDIA_ABILITY, req, [this](const json &resp, MediaAbilityList &list, auto) -> int {
            json abliity_list = resp["storage"];
            list              = abliity_list.get<MediaAbilityList>();
            return 0;
        },
        [this](int result, MediaAbilityList list){
            if (result != 0) {
                m_last_error = result;
                m_media_ability_list.clear();
                SendChangedEvent(EVT_MEDIA_ABILITY_CHANGED, RequestMediaAbilityStatus::S_FAILED, "", m_last_error);
                return result;
            }

            m_media_ability_list.swap(list);
            SendChangedEvent(EVT_MEDIA_ABILITY_CHANGED, RequestMediaAbilityStatus::S_SUCCESS);
            return 0;
        });
}

void PrinterFileSystem::RequestUploadFile()
{
    if (!m_upload_file) {
        return;
    }

    json req;
    req["type"]    = "model";
    req["storage"] = m_upload_file->select_storage;
    req["path"]    = m_upload_file->name;

    m_upload_file->upload = std::make_unique<Upload>();
    boost::filesystem::path   path = boost::filesystem::path(m_upload_file->path);
    boost::system::error_code ec;
    boost::uint32_t           file_size = boost::filesystem::file_size(path, ec);

    req["total"] = file_size;
    m_upload_file->size          = file_size;
    m_upload_file->upload->total = file_size;

    m_upload_seq = SendRequest(
        FILE_UPLOAD, req,
        [this](int result, const json& resp, auto) -> int{
            if (result != SUCCESS && result != CONTINUE && result != FILE_EXIST) {
                std::string error_msg = "";
                if (result == ERROR_CANCEL) {
                    error_msg = _L("User cancels task.").ToStdString();
                } else if (result == FILE_READ_WRITE_ERR || result == FILE_OPEN_ERR) {
                    error_msg = _L("Failed to read file, please try again.").ToStdString();
                }
                wxLogWarning("PrinterFileSystem::UploadFile error: %d\n", result);
                SendChangedEvent(EVT_UPLOAD_CHANGED, FF_UPLOADCANCEL, error_msg, result);
            } else if (result == SUCCESS) {
                SendChangedEvent(EVT_UPLOADING, 100);
                SendChangedEvent(EVT_UPLOAD_CHANGED, FF_UPLOADDONE);
            } else if (result == CONTINUE || result == FILE_EXIST) {
                if (m_upload_file) {
                    m_upload_file->chunk_size   = resp["chunk_size"];
                    m_upload_file->upload->size = resp["offset"];
                    m_upload_file->flags |= FF_UPLOADING;
                }

                {
                    boost::unique_lock l(m_mutex);
                    auto cb = [this, upload_file = m_upload_file, seq = m_upload_seq](std::string &msg) -> int {
                        return UploadFileTask(upload_file, seq, msg);
                    };
                    m_produce_message_cb_map[m_upload_seq] = cb;
                }

                return CONTINUE;
            }

            // reset m_upload_file
            if (m_upload_file) {
                if (m_upload_file->upload->ifs.is_open()) {
                    m_upload_file->upload->ifs.close();
                }
                m_upload_file.reset();
            }
            return result;
        });
}

int PrinterFileSystem::UploadFileTask(std::shared_ptr<UploadFile> upload_file, boost::uint64_t seq, std::string &msg)
{
    if (!upload_file)
        return FILE_OPEN_ERR;

    if (!(upload_file->flags & FF_UPLOADING))
        return FILE_OPEN_ERR;

    auto &upload = upload_file->upload;
    if (!upload->ifs.is_open()) {
        upload->ifs.open(upload_file->path, std::ios::binary);
        if (!upload_file->upload->ifs) {
            wxLogWarning("PrinterFileSystem::UploadFile open error: %s\n", wxString::FromUTF8(upload_file->path));
            return FILE_OPEN_ERR;
        }
        MD5_Init(&upload->ctx);
    }

    const boost::uint32_t buffer_size = upload_file->chunk_size * 1024;
    char *buffer = new char[buffer_size];

    upload->ifs.seekg(upload->size, std::ios::beg);
    upload->ifs.read(buffer, buffer_size);
    boost::int32_t read_size = upload->ifs.gcount();

    if (read_size <= 0) {
        wxLogWarning("PrinterFileSystem::Upload read error.\n");
        upload->ifs.close();

        if (buffer) {
            delete[] buffer;
            buffer = nullptr;
        }
        return FILE_READ_WRITE_ERR;
    }

    json req;
    req["frag_id"] = upload->frag_id;
    req["offset"]  = upload->size;
    req["size"]    = read_size;

    MD5_Update(&upload->ctx, buffer, read_size);
    upload->size += read_size;
    if (upload->size == upload->total) {
        unsigned char digest[16];
        MD5_Final(digest, &upload->ctx);
        char md5_str[33];
        for (int j = 0; j < 16; j++) { sprintf(&md5_str[j * 2], "%02X", (unsigned int) digest[j]); }
        std::string md5_out = std::string(md5_str);
        std::transform(md5_out.begin(), md5_out.end(), md5_out.begin(), ::tolower);

        req["file_md5"]     = md5_out;
        // OutputDebugStringA(md5_out.c_str());
        // OutputDebugStringA("\n");
    }

    if (m_upload_file && m_upload_file->flags & FF_UPLOADING) {
        upload->frag_id++;
        upload->progress = upload->size * 100 / upload->total;
        int progress     = upload->progress == 100 ? 99 : upload->progress;
        SendChangedEvent(EVT_UPLOADING, progress);
    }

    json root;

    root["cmdtype"] = FILE_UPLOAD;
    root["sequence"] = seq;
    root["req"]      = req;

    std::ostringstream oss;
    oss << root;
    oss << "\n\n";
    oss << std::string(buffer, read_size);
    msg = oss.str();

    if (buffer) {
        delete[] buffer;
        buffer = nullptr;
    }

    if (upload->size == upload->total) {
        upload->ifs.close();
        return SUCCESS;
    }

    return CONTINUE;
}

PrinterFileSystem::MediaAbilityList PrinterFileSystem::GetMediaAbilityList() const
{
    return m_media_ability_list;
}

void PrinterFileSystem::CancelUploadTask(bool send_cancel_req)
{
    if (!m_upload_file)
        return;

    {
        boost::unique_lock l(m_mutex);
        if (m_produce_message_cb_map.find(m_upload_seq) != m_produce_message_cb_map.end())
            m_produce_message_cb_map.erase(m_upload_seq);
        if (m_upload_file->upload->ifs.is_open()) {
            m_upload_file->upload->ifs.close();
        }
        m_upload_file.reset();
    }

    if (send_cancel_req) {
        CancelRequest(m_upload_seq);
    } else {
        CancelRequests2({m_upload_seq});
    }
}

boost::uint32_t PrinterFileSystem::SendRequest(int type, json const &req, callback_t2 const &callback,const std::string& param)
{
    if (m_session.tunnel == nullptr) {
        Retry();
        callback(ERROR_PIPE, json(), nullptr);
        return 0;
    }
    boost::uint32_t seq  = m_sequence + m_callbacks.size();
    json root;
    root["cmdtype"] = type;
    root["sequence"] = seq;
    root["req"] = req;
    std::ostringstream oss;
    oss << root;

    if (!param.empty()) {
        oss << "\n\n";
        oss << param;
    }
    // OutputDebugStringA(oss.str().c_str());
    // OutputDebugStringA("\n");
    auto               msg = oss.str();
    boost::unique_lock l(m_mutex);
    m_messages.push_back(msg);
    m_callbacks.push_back(callback);
    m_cond.notify_all();
    return seq;
}

void PrinterFileSystem::InstallNotify(int type, callback_t2 const &callback)
{
    type -= NOTIFY_FIRST;
    if (m_notifies.size() <= size_t(type)) m_notifies.resize(type + 1);
    m_notifies[type] = callback;
}

void PrinterFileSystem::CancelRequest(boost::uint32_t seq) { CancelRequests({seq}); }

void PrinterFileSystem::CancelRequests(std::vector<boost::uint32_t> const &seqs)
{
    json req;
    json arr;
    for (auto seq : seqs)
        arr.push_back(seq);
    req["tasks"] = arr;
    SendRequest(TASK_CANCEL, req, [this](int result, json const &resp, unsigned char const *) -> int {
        if (result != 0) return result;
        json tasks = resp["tasks"];
        std::vector<boost::uint32_t> seqs;
        for (auto &f : tasks) seqs.push_back(f);
        CancelRequests2(seqs);
        return 0;
    });
}

void PrinterFileSystem::CancelRequests2(std::vector<boost::uint32_t> const &seqs)
{
    std::vector<std::pair<boost::uint32_t, callback_t2>> callbacks;
    boost::unique_lock      l(m_mutex);
    for (auto &f : seqs) {
        boost::uint32_t seq = f;
        seq -= m_sequence;
        if (size_t(seq) >= m_callbacks.size()) continue;
        auto &c = m_callbacks[seq];
        if (c == nullptr) continue;
        callbacks.emplace_back(f, c);
        c = nullptr;

        // erase m_produce_message_cb
        if (m_produce_message_cb_map.find(seq) != m_produce_message_cb_map.end())
            m_produce_message_cb_map.erase(seq);
    }
    while (!m_callbacks.empty() && m_callbacks.front() == nullptr) {
        m_callbacks.pop_front();
        ++m_sequence;
    }
    l.unlock();
    for (auto &c : callbacks) {
        wxLogInfo("PrinterFileSystem::CancelRequests2: %u\n", c.first);
        c.second(ERROR_CANCEL, json(), nullptr);
    }
}

void PrinterFileSystem::RecvMessageThread()
{
    Bambu_Sample sample;
    boost::unique_lock l(m_mutex);
    Reconnect(l, 0);
    while (true) {
        if (m_stopped && (m_session.owner == nullptr || (m_messages.empty() && m_callbacks.empty()))) {
            Reconnect(l, 0); // Close and wait start again
            if (m_session.owner == nullptr) {
                // clear callbacks first
                auto callbacks(std::move(m_callbacks));
                break;
            }
        }
        if (m_messages.empty() && !m_produce_message_cb_map.empty()) {
            auto it = m_produce_message_cb_map.begin();
            while(it != m_produce_message_cb_map.end()) {
                std::string     msg;
                auto            prodeuce_message_cb = it->second;
                l.unlock();
                int res = prodeuce_message_cb(msg);
                l.lock();
                if (res == CONTINUE || res == SUCCESS) {
                    m_messages.emplace_back(msg);
                    if (res == SUCCESS) {
                        it = m_produce_message_cb_map.erase(it);
                        continue;
                    }
                    it++;
                } else {
                    int seq2 = it->first - m_sequence;
                    // erase it
                    it = m_produce_message_cb_map.erase(it);
                    if (size_t(seq2) >= m_callbacks.size())
                        continue;
                    auto c = m_callbacks[seq2];
                    if (c == nullptr)
                        continue;;
                    m_callbacks[seq2] = nullptr;
                    if (seq2 == 0) {
                        // if produce message return error, erase callback and sequence should plus
                        while (!m_callbacks.empty() && m_callbacks.front() == nullptr) {
                            m_callbacks.pop_front();
                            ++m_sequence;
                        }
                    }

                    l.unlock();
                    c(res, json(), nullptr);
                    l.lock();
                }
            }
        }
        if (!m_messages.empty()) {
            auto & msg = m_messages.front();
            // OutputDebugStringA(msg.c_str());
            // OutputDebugStringA("\n");
            wxLogInfo("PrinterFileSystem::SendRequest >>>: \n%s\n", wxString::FromUTF8(msg));
            l.unlock();
            int n = Bambu_SendMessage(m_session.tunnel, CTRL_TYPE, msg.c_str(), msg.length());
            l.lock();
            if (n == 0)
                m_messages.pop_front();
            else if (n != Bambu_would_block) {
                Reconnect(l, n);
                continue;
            }
        }
        l.unlock();
        int n = Bambu_ReadSample(m_session.tunnel, &sample);
        l.lock();
        if (n == 0) {
            HandleResponse(l, sample);
        } else if (n == Bambu_stream_end) {
            m_stopped = true;
            Reconnect(l, m_status == ListSyncing ? ERROR_RES_BUSY : ERROR_PIPE);
        } else if (n == Bambu_would_block) {
            m_cond.timed_wait(l, boost::posix_time::milliseconds(m_messages.empty() && m_callbacks.empty() ? 1000 : 20));
        } else {
            Reconnect(l, n);
        }
    } // while
}

void PrinterFileSystem::HandleResponse(boost::unique_lock<boost::mutex> &l, Bambu_Sample const &sample)
{
    unsigned char const *end      = sample.buffer + sample.size;
    unsigned char const *json_end = (unsigned char const *) memchr(sample.buffer, '\n', sample.size);
    while (json_end && json_end + 3 < end && json_end[1] != '\n') json_end = (unsigned char const *) memchr(json_end + 2, '\n', end - json_end - 2);
    if (json_end)
        json_end += 2;
    else
        json_end = end;
    std::string msg((char const *) sample.buffer, json_end - sample.buffer);
    json        root;
    // OutputDebugStringA(msg.c_str());
    // OutputDebugStringA("\n");
    wxLogInfo("PrinterFileSystem::HandleResponse <<<: \n%s\n", wxString::FromUTF8(msg));
    std::istringstream iss(msg);
    int                cmd    = 0;
    int                seq    = -1;
    int                result = 0;
    json               resp;
    try {
        iss >> root;
        if (!root["result"].is_null()) {
            result = root["result"];
            seq    = root["sequence"];
            resp   = root["reply"];
        } else {
            // maybe notify
            cmd  = root["cmdtype"];
            seq  = root["sequence"];
            resp = root["notify"];
        }
    } catch (...) {
        result = ERROR_JSON;
        return;
    }
    if (cmd > 0) {
        if (cmd < NOTIFY_FIRST) return;
        cmd -= NOTIFY_FIRST;
        if (size_t(cmd) >= m_notifies.size()) return;
        auto n = m_notifies[cmd];
        l.unlock();
        n(result, resp, json_end);
        l.lock();
    } else {
        int seq2 = seq - m_sequence;
        if (size_t(seq2) >= m_callbacks.size()) return;
        auto c = m_callbacks[seq2];
        if (c == nullptr) return;
        l.unlock();
        int result2 = c(result, resp, json_end);
        l.lock();
        if (result2 != CONTINUE) {
            int seq2          = seq - m_sequence;
            m_callbacks[seq2] = callback_t2();
            if (seq2 == 0) {
                while (!m_callbacks.empty() && m_callbacks.front() == nullptr) {
                    m_callbacks.pop_front();
                    ++m_sequence;
                }
            }
            if (result == CONTINUE) {
                l.unlock();
                CancelRequest(seq);
                l.lock();
            }

            // error should erase m_produce_message_cb
            if (m_produce_message_cb_map.find(seq2) != m_produce_message_cb_map.end()) {
                m_produce_message_cb_map.erase(seq2);
            }
        }
    }
}

void PrinterFileSystem::Reconnect(boost::unique_lock<boost::mutex> &l, int result)
{
    if (m_session.tunnel) {
        auto tunnel = m_session.tunnel;
        m_session.tunnel = nullptr;
        wxLogMessage("PrinterFileSystem::Reconnect close %d", result);
        l.unlock();
        Bambu_Close(tunnel);
        Bambu_Destroy(tunnel);
        l.lock();
    }
    if (m_session.owner == nullptr)
        return;
    json r;
    while(!m_callbacks.empty()) {
        auto c = m_callbacks.front();
        m_callbacks.pop_front();
        ++m_sequence;
        if (c) c(result, r, nullptr);
    }
    m_messages.clear();
    if (result)
        m_cond.timed_wait(l, boost::posix_time::seconds(10));


    while (true) {
        while (m_stopped) {
            if (m_session.owner == nullptr)
                return;
           m_status = Status::Reconnecting;
           SendChangedEvent(EVT_STATUS_CHANGED, m_status);
           m_cond.wait(l);
        }
        wxLogMessage("PrinterFileSystem::Reconnect Initializing");
        m_status = Status::Initializing;
        m_last_error = 0;
        SendChangedEvent(EVT_STATUS_CHANGED, m_status);
        // wait for url
        while (!m_stopped && m_messages.empty())
            m_cond.wait(l);
        if (m_stopped || m_messages.empty()) continue;
        std::string url = m_messages.front();
        m_messages.clear();
        if (url.size() < 2) {
            wxLogMessage("PrinterFileSystem::Reconnect Initialize failed: %s", wxString::FromUTF8(url));
            m_last_error = atoi(url.c_str());
            if (m_last_error == 0)
                m_stopped = true;
        } else {
            wxLogInfo("PrinterFileSystem::Reconnect Initialized: %s", wxString::FromUTF8(url));
            l.unlock();
            m_status = Status::Connecting;
            wxLogMessage("PrinterFileSystem::Reconnect Connecting");
            SendChangedEvent(EVT_STATUS_CHANGED, m_status);
            Bambu_Tunnel tunnel = nullptr;
            int ret = Bambu_Create(&tunnel, url.c_str());
            if (ret == 0) {

                Bambu_SetLogger(tunnel, DumpLog, this);
                ret = Bambu_Open(tunnel);
            }

            if (ret == 0)
            {
                auto                             start_time = boost::posix_time::microsec_clock::universal_time();
                boost::posix_time::time_duration timeout    = boost::posix_time::seconds(3);
                do{
                    ret = Bambu_StartStreamEx ? Bambu_StartStreamEx(tunnel, CTRL_TYPE) : Bambu_StartStream(tunnel, false);
                    if (ret == Bambu_would_block)
                        boost::this_thread::sleep(boost::posix_time::milliseconds(100));

                     auto now = boost::posix_time::microsec_clock::universal_time();
                    if (now - start_time > timeout) {
                        BOOST_LOG_TRIVIAL(warning) << "StartStream timeout after 5 seconds.";
                        break;
                    }

                } while (ret == Bambu_would_block && !m_stopped);
            }
            l.lock();
            if (ret == 0) {
                m_session.tunnel = tunnel;
                wxLogMessage("PrinterFileSystem::Reconnect Connected");
                break;
            } else if (ret == 1) {
                m_stopped = true;
                ret = ERROR_RES_BUSY;
            }
            if (tunnel) {
                Bambu_Close(tunnel);
                Bambu_Destroy(tunnel);
            }
            m_last_error = ret;
        }
        wxLogMessage("PrinterFileSystem::Reconnect Failed");
        m_status = Status::Failed;

        SendChangedEvent(EVT_STATUS_CHANGED, m_status, "", url.size() < 2 ? 1 : m_last_error);
        m_cond.timed_wait(l, boost::posix_time::seconds(10));
    }

#ifdef PRINTER_FILE_SYSTEM_TEST
    PostCallback([this] { SendChangedEvent(EVT_FILE_CHANGED); });
#else
    PostCallback([this] {
        m_task_flags = 0;
        m_status     = Status::ListSyncing;
        SendChangedEvent(EVT_STATUS_CHANGED, m_status);
        });
#endif
}


#include <stdlib.h>
#if defined(_MSC_VER) || defined(_WIN32)
#include <Windows.h>
#else
#include <dlfcn.h>
#endif

#if defined(_MSC_VER) || defined(_WIN32)
static HMODULE module = NULL;
#else
static void* module = NULL;
#endif

static void* get_function(const char* name)
{
    void* function = nullptr;

    if (!module)
        return function;

#if defined(_MSC_VER) || defined(_WIN32)
    function = GetProcAddress(module, name);
#else
    function = dlsym(module, name);
#endif

    if (!function) {
        BOOST_LOG_TRIVIAL(warning) << __FUNCTION__ << boost::format(", can not find function %1%") % name;
    }
    return function;
}

#define GET_FUNC(x) lib.x = reinterpret_cast<decltype(lib.x)>(get_function(#x))

StaticBambuLib &StaticBambuLib::get(BambuLib *copy)
{
    static StaticBambuLib lib;
    // first load the library

    if (lib.Bambu_Create)
        return lib;

    if (!module) {
        module = Slic3r::NetworkAgent::get_bambu_source_entry();
    }

    if (!module) {
        BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << ", can not Load Library";
    }

    GET_FUNC(Bambu_Create);
    GET_FUNC(Bambu_Open);
    GET_FUNC(Bambu_StartStream);
    GET_FUNC(Bambu_StartStreamEx);
    GET_FUNC(Bambu_GetStreamCount);
    GET_FUNC(Bambu_GetStreamInfo);
    GET_FUNC(Bambu_SendMessage);
    GET_FUNC(Bambu_ReadSample);
    GET_FUNC(Bambu_Close);
    GET_FUNC(Bambu_Destroy);
    GET_FUNC(Bambu_SetLogger);
    GET_FUNC(Bambu_FreeLogMsg);
    GET_FUNC(Bambu_Deinit);

    if (!lib.Bambu_Create) {
        lib.Bambu_Create = Fake_Bambu_Create;
        if (copy)
            lib.copies_.push_back(copy);
    }
    return lib;
}

void StaticBambuLib::reset()
{
    get().Bambu_Create = nullptr;
    auto &lib = get();
    for (auto c : lib.copies_)
        *c = lib;
}

void StaticBambuLib::release()
{
    if (auto f = get().Bambu_Deinit)
        f();
}

extern "C" BambuLib *bambulib_get() {
    return &StaticBambuLib::get(); }
