// This file Copyright © Mnemosyne LLC.
// It may be used under GPLv2 (SPDX: GPL-2.0-only), GPLv3 (SPDX: GPL-3.0-only),
// or any future license endorsed by Mnemosyne LLC.
// License text can be found in the licenses/ folder.

#include <algorithm>
#include <cassert>
#include <utility>
#include <ranges>

#include <small/set.hpp>

#include <libtransmission/transmission.h> // priorities

#include "FileTreeItem.h"
#include "FileTreeModel.h"
#include "Formatter.h"
#include "IconCache.h"

std::unordered_map<QString, int> const& FileTreeItem::getMyChildRows() const
{
    // ensure that all the rows are hashed
    for (int const n = childCount(); first_unhashed_row_ < n; ++first_unhashed_row_)
    {
        child_rows_.emplace(children_[first_unhashed_row_]->name(), first_unhashed_row_);
    }

    return child_rows_;
}

FileTreeItem::~FileTreeItem()
{
    assert(std::empty(children_));

    if (parent_ == nullptr)
    {
        return;
    }

    // find the parent's reference to this child
    auto& siblings = parent_->children_;
    auto it = std::ranges::find(siblings, this);
    if (it == std::ranges::end(siblings))
    {
        return;
    }

    // remove this child from the parent
    parent_->child_rows_.erase(name());
    it = siblings.erase(it);

    // invalidate the row numbers of the siblings that came after this child
    parent_->first_unhashed_row_ = std::distance(std::begin(siblings), it);
}

void FileTreeItem::appendChild(FileTreeItem* child)
{
    int const n = childCount();
    child->parent_ = this;
    children_.push_back(child);
    first_unhashed_row_ = n;
}

FileTreeItem* FileTreeItem::child(QString const& filename)
{
    auto const& child_rows = getMyChildRows();

    if (auto const iter = child_rows.find(filename); iter != std::end(child_rows))
    {
        auto* const item = child(iter->second);
        assert(filename == item->name());
        return item;
    }

    return {};
}

int FileTreeItem::row() const
{
    auto const& child_rows = parent_->getMyChildRows();

    if (auto const iter = child_rows.find(name()); iter != std::end(child_rows))
    {
        auto const idx = iter->second;
        assert(this == parent_->children_[idx]);
        return idx;
    }

    return -1;
}

QVariant FileTreeItem::data(int column, int role) const
{
    QVariant value;

    switch (role)
    {
    case FileTreeModel::FileIndexRole:
        value.setValue(file_index_);
        break;

    case FileTreeModel::WantedRole:
        value.setValue(isSubtreeWanted());
        break;

    case FileTreeModel::CompleteRole:
        value.setValue(isComplete());
        break;

    case Qt::ToolTipRole:
    case Qt::EditRole:
        if (column == FileTreeModel::COL_NAME)
        {
            value.setValue(name());
        }

        break;

    case Qt::TextAlignmentRole:
        if (column == FileTreeModel::COL_SIZE)
        {
            value = static_cast<int>(Qt::AlignRight | Qt::AlignVCenter);
        }

        break;

    case Qt::DisplayRole:
    case FileTreeModel::SortRole:
        switch (column)
        {
        case FileTreeModel::COL_NAME:
            value.setValue(name());
            break;

        case FileTreeModel::COL_SIZE:
            if (role == Qt::DisplayRole)
            {
                value.setValue(sizeString());
            }
            else
            {
                value.setValue<quint64>(size());
            }

            break;

        case FileTreeModel::COL_PROGRESS:
            value.setValue(progress());
            break;

        case FileTreeModel::COL_WANTED:
            value.setValue(isSubtreeWanted());
            break;

        case FileTreeModel::COL_PRIORITY:
            if (role == Qt::DisplayRole)
            {
                value.setValue(priorityString());
            }
            else
            {
                value.setValue(priority());
            }

            break;

        default:
            break;
        }

        break;

    case Qt::DecorationRole:
        if (column == FileTreeModel::COL_NAME)
        {
            auto const& icon_cache = IconCache::get();
            value = (file_index_ < 0 || childCount() > 0) ? icon_cache.folderIcon() :
                                                            icon_cache.guessMimeIcon(name(), icon_cache.fileIcon());
        }

        break;

    default:
        break;
    }

    return value;
}

std::pair<uint64_t, uint64_t> FileTreeItem::get_subtree_wanted_size() const
{
    auto have = is_wanted_ ? have_size_ : 0U;
    auto total = is_wanted_ ? total_size_ : 0U;

    for (auto const* const child : children_)
    {
        auto const [child_have, child_total] = child->get_subtree_wanted_size();
        have += child_have;
        total += child_total;
    }

    return { have, total };
}

double FileTreeItem::progress() const
{
    auto const [have, total] = get_subtree_wanted_size();

    return have >= total ? 1.0 : static_cast<double>(have) / static_cast<double>(total);
}

QString FileTreeItem::sizeString() const
{
    return Formatter::storage_to_string(size());
}

uint64_t FileTreeItem::size() const
{
    auto const [have, total] = get_subtree_wanted_size();
    return total;
}

std::pair<int, int> FileTreeItem::update(QString const& name, bool wanted, int priority, uint64_t have_size, bool update_fields)
{
    auto changed_columns = small::max_size_set<int, FileTreeModel::NUM_COLUMNS>{};

    if (name_ != name)
    {
        if (parent_ != nullptr)
        {
            parent_->first_unhashed_row_ = row();
        }

        name_ = name;
        changed_columns.insert(FileTreeModel::COL_NAME);
    }

    if (fileIndex() != -1)
    {
        if (have_size_ != have_size)
        {
            have_size_ = have_size;
            changed_columns.insert(FileTreeModel::COL_PROGRESS);
        }

        if (update_fields)
        {
            if (is_wanted_ != wanted)
            {
                is_wanted_ = wanted;
                changed_columns.insert(FileTreeModel::COL_WANTED);
            }

            if (priority_ != priority)
            {
                priority_ = priority;
                changed_columns.insert(FileTreeModel::COL_PRIORITY);
            }
        }
    }

    std::pair<int, int> changed(-1, -1);

    if (!changed_columns.empty())
    {
        changed.first = *std::cbegin(changed_columns);
        changed.second = *std::crbegin(changed_columns);
    }

    return changed;
}

QString FileTreeItem::priorityString() const
{
    int const i = priority();

    switch (i)
    {
    case Low:
        return tr("Low");

    case High:
        return tr("High");

    case Normal:
        return tr("Normal");

    default:
        return tr("Mixed");
    }
}

int FileTreeItem::priority() const
{
    int i(0);

    if (std::empty(children_))
    {
        switch (priority_)
        {
        case TR_PRI_LOW:
            i |= Low;
            break;

        case TR_PRI_HIGH:
            i |= High;
            break;

        default:
            i |= Normal;
            break;
        }
    }

    for (FileTreeItem const* const child : children_)
    {
        i |= child->priority();
    }

    return i;
}

void FileTreeItem::setSubtreePriority(int priority, file_indices_t& setme_changed_ids)
{
    if (priority_ != priority)
    {
        priority_ = priority;

        if (file_index_ >= 0)
        {
            setme_changed_ids.insert(file_index_);
        }
    }

    for (FileTreeItem* const child : children_)
    {
        child->setSubtreePriority(priority, setme_changed_ids);
    }
}

int FileTreeItem::isSubtreeWanted() const
{
    if (std::empty(children_))
    {
        return is_wanted_ ? Qt::Checked : Qt::Unchecked;
    }

    int wanted(-1);

    for (FileTreeItem const* const child : children_)
    {
        int const child_wanted = child->isSubtreeWanted();

        if (wanted == -1)
        {
            wanted = child_wanted;
        }

        if (wanted != child_wanted)
        {
            wanted = Qt::PartiallyChecked;
        }

        if (wanted == Qt::PartiallyChecked)
        {
            return wanted;
        }
    }

    return wanted;
}

void FileTreeItem::setSubtreeWanted(bool wanted, file_indices_t& setme_changed_ids)
{
    if (is_wanted_ != wanted)
    {
        is_wanted_ = wanted;

        if (file_index_ >= 0)
        {
            setme_changed_ids.insert(file_index_);
        }
    }

    for (FileTreeItem* const child : children_)
    {
        child->setSubtreeWanted(wanted, setme_changed_ids);
    }
}

QString FileTreeItem::path() const
{
    QString item_path;
    FileTreeItem const* item = this;

    while (item != nullptr && !item->name().isEmpty())
    {
        if (item_path.isEmpty())
        {
            item_path = item->name();
        }
        else
        {
            item_path = item->name() + QLatin1Char('/') + item_path;
        }

        item = item->parent();
    }

    return item_path;
}
