#include "nestedcoladapterimp.hpp"

#include <map>
#include <stdint.h>
#include <type_traits>
#include <utility>
#include <variant>

#include <apps/opencs/model/world/cell.hpp>
#include <apps/opencs/model/world/disabletag.hpp>
#include <apps/opencs/model/world/nestedtablewrapper.hpp>
#include <apps/opencs/model/world/record.hpp>

#include <components/esm/attr.hpp>
#include <components/esm3/loadcell.hpp>
#include <components/esm3/loadfact.hpp>
#include <components/esm3/loadinfo.hpp>
#include <components/esm3/loadpgrd.hpp>
#include <components/esm3/loadrace.hpp>
#include <components/esm3/loadregn.hpp>
#include <components/esm3/variant.hpp>

#include "info.hpp"
#include "infoselectwrapper.hpp"
#include "pathgrid.hpp"

namespace CSMWorld
{
    void PathgridPointListAdapter::addRow(Record<Pathgrid>& record, int position) const
    {
        Pathgrid pathgrid = record.get();

        ESM::Pathgrid::PointList& points = pathgrid.mPoints;

        // blank row
        ESM::Pathgrid::Point point;
        point.mX = 0;
        point.mY = 0;
        point.mZ = 0;
        point.mAutogenerated = 0;
        point.mConnectionNum = 0;

        points.insert(points.begin() + position, point);
        pathgrid.mData.mPoints = pathgrid.mPoints.size();

        record.setModified(pathgrid);
    }

    void PathgridPointListAdapter::removeRow(Record<Pathgrid>& record, int rowToRemove) const
    {
        Pathgrid pathgrid = record.get();

        ESM::Pathgrid::PointList& points = pathgrid.mPoints;

        if (rowToRemove < 0 || rowToRemove >= static_cast<int>(points.size()))
            throw std::runtime_error("index out of range");

        // Do not remove dangling edges, does not work with current undo mechanism
        // Do not automatically adjust indices, what would be done with dangling edges?
        points.erase(points.begin() + rowToRemove);
        pathgrid.mData.mPoints = pathgrid.mPoints.size();

        record.setModified(pathgrid);
    }

    void PathgridPointListAdapter::setTable(Record<Pathgrid>& record, const NestedTableWrapperBase& nestedTable) const
    {
        Pathgrid pathgrid = record.get();
        pathgrid.mPoints = static_cast<const NestedTableWrapper<ESM::Pathgrid::PointList>&>(nestedTable).mNestedTable;
        pathgrid.mData.mPoints = pathgrid.mPoints.size();

        record.setModified(pathgrid);
    }

    NestedTableWrapperBase* PathgridPointListAdapter::table(const Record<Pathgrid>& record) const
    {
        // deleted by dtor of NestedTableStoring
        return new NestedTableWrapper<ESM::Pathgrid::PointList>(record.get().mPoints);
    }

    QVariant PathgridPointListAdapter::getData(const Record<Pathgrid>& record, int subRowIndex, int subColIndex) const
    {
        ESM::Pathgrid::Point point = record.get().mPoints[subRowIndex];
        switch (subColIndex)
        {
            case 0:
                return subRowIndex;
            case 1:
                return point.mX;
            case 2:
                return point.mY;
            case 3:
                return point.mZ;
            default:
                throw std::runtime_error("Pathgrid point subcolumn index out of range");
        }
    }

    void PathgridPointListAdapter::setData(
        Record<Pathgrid>& record, const QVariant& value, int subRowIndex, int subColIndex) const
    {
        Pathgrid pathgrid = record.get();
        ESM::Pathgrid::Point point = pathgrid.mPoints[subRowIndex];
        switch (subColIndex)
        {
            case 0:
                return; // return without saving
            case 1:
                point.mX = value.toInt();
                break;
            case 2:
                point.mY = value.toInt();
                break;
            case 3:
                point.mZ = value.toInt();
                break;
            default:
                throw std::runtime_error("Pathgrid point subcolumn index out of range");
        }

        pathgrid.mPoints[subRowIndex] = point;

        record.setModified(pathgrid);
    }

    int PathgridPointListAdapter::getColumnsCount(const Record<Pathgrid>& record) const
    {
        return 4;
    }

    int PathgridPointListAdapter::getRowsCount(const Record<Pathgrid>& record) const
    {
        return static_cast<int>(record.get().mPoints.size());
    }

    void PathgridEdgeListAdapter::addRow(Record<Pathgrid>& record, int position) const
    {
        Pathgrid pathgrid = record.get();

        ESM::Pathgrid::EdgeList& edges = pathgrid.mEdges;

        // blank row
        ESM::Pathgrid::Edge edge;
        edge.mV0 = 0;
        edge.mV1 = 0;

        // NOTE: inserting a blank edge does not really make sense, perhaps this should be a
        // logic_error exception
        //
        // Currently the code assumes that the end user to know what he/she is doing.
        // e.g. Edges come in pairs, from points a->b and b->a
        edges.insert(edges.begin() + position, edge);

        record.setModified(pathgrid);
    }

    void PathgridEdgeListAdapter::removeRow(Record<Pathgrid>& record, int rowToRemove) const
    {
        Pathgrid pathgrid = record.get();

        ESM::Pathgrid::EdgeList& edges = pathgrid.mEdges;

        if (rowToRemove < 0 || rowToRemove >= static_cast<int>(edges.size()))
            throw std::runtime_error("index out of range");

        edges.erase(edges.begin() + rowToRemove);

        record.setModified(pathgrid);
    }

    void PathgridEdgeListAdapter::setTable(Record<Pathgrid>& record, const NestedTableWrapperBase& nestedTable) const
    {
        Pathgrid pathgrid = record.get();

        pathgrid.mEdges = static_cast<const NestedTableWrapper<ESM::Pathgrid::EdgeList>&>(nestedTable).mNestedTable;

        record.setModified(pathgrid);
    }

    NestedTableWrapperBase* PathgridEdgeListAdapter::table(const Record<Pathgrid>& record) const
    {
        // deleted by dtor of NestedTableStoring
        return new NestedTableWrapper<ESM::Pathgrid::EdgeList>(record.get().mEdges);
    }

    QVariant PathgridEdgeListAdapter::getData(const Record<Pathgrid>& record, int subRowIndex, int subColIndex) const
    {
        Pathgrid pathgrid = record.get();

        if (subRowIndex < 0 || subRowIndex >= static_cast<int>(pathgrid.mEdges.size()))
            throw std::runtime_error("index out of range");

        ESM::Pathgrid::Edge edge = pathgrid.mEdges[subRowIndex];
        switch (subColIndex)
        {
            case 0:
                return subRowIndex;
            case 1:
                return static_cast<uint>(edge.mV0);
            case 2:
                return static_cast<uint>(edge.mV1);
            default:
                throw std::runtime_error("Pathgrid edge subcolumn index out of range");
        }
    }

    void PathgridEdgeListAdapter::setData(
        Record<Pathgrid>& record, const QVariant& value, int subRowIndex, int subColIndex) const
    {
        Pathgrid pathgrid = record.get();

        if (subRowIndex < 0 || subRowIndex >= static_cast<int>(pathgrid.mEdges.size()))
            throw std::runtime_error("index out of range");

        ESM::Pathgrid::Edge edge = pathgrid.mEdges[subRowIndex];
        switch (subColIndex)
        {
            case 0:
                return; // return without saving
            case 1:
                edge.mV0 = value.toInt();
                break;
            case 2:
                edge.mV1 = value.toInt();
                break;
            default:
                throw std::runtime_error("Pathgrid edge subcolumn index out of range");
        }

        pathgrid.mEdges[subRowIndex] = edge;

        record.setModified(pathgrid);
    }

    int PathgridEdgeListAdapter::getColumnsCount(const Record<Pathgrid>& record) const
    {
        return 3;
    }

    int PathgridEdgeListAdapter::getRowsCount(const Record<Pathgrid>& record) const
    {
        return static_cast<int>(record.get().mEdges.size());
    }

    void FactionReactionsAdapter::addRow(Record<ESM::Faction>& record, int position) const
    {
        ESM::Faction faction = record.get();

        std::map<ESM::RefId, int>& reactions = faction.mReactions;

        // blank row
        reactions.insert(std::make_pair(ESM::RefId(), 0));

        record.setModified(faction);
    }

    void FactionReactionsAdapter::removeRow(Record<ESM::Faction>& record, int rowToRemove) const
    {
        ESM::Faction faction = record.get();

        std::map<ESM::RefId, int>& reactions = faction.mReactions;

        if (rowToRemove < 0 || rowToRemove >= static_cast<int>(reactions.size()))
            throw std::runtime_error("index out of range");

        // FIXME: how to ensure that the map entries correspond to table indicies?
        // WARNING: Assumed that the table view has the same order as std::map
        auto iter = reactions.begin();
        for (int i = 0; i < rowToRemove; ++i)
            ++iter;
        reactions.erase(iter);

        record.setModified(faction);
    }

    void FactionReactionsAdapter::setTable(
        Record<ESM::Faction>& record, const NestedTableWrapperBase& nestedTable) const
    {
        ESM::Faction faction = record.get();

        faction.mReactions
            = static_cast<const NestedTableWrapper<std::map<ESM::RefId, int>>&>(nestedTable).mNestedTable;

        record.setModified(faction);
    }

    NestedTableWrapperBase* FactionReactionsAdapter::table(const Record<ESM::Faction>& record) const
    {
        // deleted by dtor of NestedTableStoring
        return new NestedTableWrapper<std::map<ESM::RefId, int>>(record.get().mReactions);
    }

    QVariant FactionReactionsAdapter::getData(
        const Record<ESM::Faction>& record, int subRowIndex, int subColIndex) const
    {
        ESM::Faction faction = record.get();

        std::map<ESM::RefId, int>& reactions = faction.mReactions;

        if (subRowIndex < 0 || subRowIndex >= static_cast<int>(reactions.size()))
            throw std::runtime_error("index out of range");

        // FIXME: how to ensure that the map entries correspond to table indicies?
        // WARNING: Assumed that the table view has the same order as std::map
        auto iter = reactions.begin();
        for (int i = 0; i < subRowIndex; ++i)
            ++iter;
        switch (subColIndex)
        {
            case 0:
                return QString((*iter).first.getRefIdString().c_str());
            case 1:
                return (*iter).second;
            default:
                throw std::runtime_error("Faction reactions subcolumn index out of range");
        }
    }

    void FactionReactionsAdapter::setData(
        Record<ESM::Faction>& record, const QVariant& value, int subRowIndex, int subColIndex) const
    {
        ESM::Faction faction = record.get();

        std::map<ESM::RefId, int>& reactions = faction.mReactions;

        if (subRowIndex < 0 || subRowIndex >= static_cast<int>(reactions.size()))
            throw std::runtime_error("index out of range");

        // FIXME: how to ensure that the map entries correspond to table indicies?
        // WARNING: Assumed that the table view has the same order as std::map
        auto iter = reactions.begin();
        for (int i = 0; i < subRowIndex; ++i)
            ++iter;

        ESM::RefId factionId = (*iter).first;
        int reaction = (*iter).second;

        switch (subColIndex)
        {
            case 0:
            {
                reactions.erase(iter);
                reactions.insert(
                    std::make_pair(ESM::RefId::stringRefId(value.toString().toUtf8().constData()), reaction));
                break;
            }
            case 1:
            {
                reactions[factionId] = value.toInt();
                break;
            }
            default:
                throw std::runtime_error("Faction reactions subcolumn index out of range");
        }

        record.setModified(faction);
    }

    int FactionReactionsAdapter::getColumnsCount(const Record<ESM::Faction>& record) const
    {
        return 2;
    }

    int FactionReactionsAdapter::getRowsCount(const Record<ESM::Faction>& record) const
    {
        return static_cast<int>(record.get().mReactions.size());
    }

    void RegionSoundListAdapter::addRow(Record<ESM::Region>& record, int position) const
    {
        ESM::Region region = record.get();

        std::vector<ESM::Region::SoundRef>& soundList = region.mSoundList;

        // blank row
        ESM::Region::SoundRef soundRef;
        soundRef.mSound = ESM::RefId();
        soundRef.mChance = 0;

        soundList.insert(soundList.begin() + position, soundRef);

        record.setModified(region);
    }

    void RegionSoundListAdapter::removeRow(Record<ESM::Region>& record, int rowToRemove) const
    {
        ESM::Region region = record.get();

        std::vector<ESM::Region::SoundRef>& soundList = region.mSoundList;

        if (rowToRemove < 0 || rowToRemove >= static_cast<int>(soundList.size()))
            throw std::runtime_error("index out of range");

        soundList.erase(soundList.begin() + rowToRemove);

        record.setModified(region);
    }

    void RegionSoundListAdapter::setTable(Record<ESM::Region>& record, const NestedTableWrapperBase& nestedTable) const
    {
        ESM::Region region = record.get();

        region.mSoundList
            = static_cast<const NestedTableWrapper<std::vector<ESM::Region::SoundRef>>&>(nestedTable).mNestedTable;

        record.setModified(region);
    }

    NestedTableWrapperBase* RegionSoundListAdapter::table(const Record<ESM::Region>& record) const
    {
        // deleted by dtor of NestedTableStoring
        return new NestedTableWrapper<std::vector<ESM::Region::SoundRef>>(record.get().mSoundList);
    }

    QVariant RegionSoundListAdapter::getData(const Record<ESM::Region>& record, int subRowIndex, int subColIndex) const
    {
        const ESM::Region& region = record.get();

        const std::vector<ESM::Region::SoundRef>& soundList = region.mSoundList;

        const size_t index = static_cast<size_t>(subRowIndex);
        if (subRowIndex < 0 || index >= soundList.size())
            throw std::runtime_error("index out of range");

        const ESM::Region::SoundRef& soundRef = soundList[subRowIndex];
        switch (subColIndex)
        {
            case 0:
                return QString(soundRef.mSound.getRefIdString().c_str());
            case 1:
                return soundRef.mChance;
            case 2:
            {
                float probability = 1.f;
                for (size_t i = 0; i < index; ++i)
                {
                    const float p = std::min(soundList[i].mChance / 100.f, 1.f);
                    probability *= 1.f - p;
                }
                probability *= std::min(soundRef.mChance / 100.f, 1.f) * 100.f;
                return QString("%1%").arg(probability, 0, 'f', 2);
            }
            default:
                throw std::runtime_error("Region sounds subcolumn index out of range");
        }
    }

    void RegionSoundListAdapter::setData(
        Record<ESM::Region>& record, const QVariant& value, int subRowIndex, int subColIndex) const
    {
        ESM::Region region = record.get();

        std::vector<ESM::Region::SoundRef>& soundList = region.mSoundList;

        if (subRowIndex < 0 || subRowIndex >= static_cast<int>(soundList.size()))
            throw std::runtime_error("index out of range");

        ESM::Region::SoundRef soundRef = soundList[subRowIndex];
        switch (subColIndex)
        {
            case 0:
                soundRef.mSound = ESM::RefId::stringRefId(value.toString().toUtf8().constData());
                break;
            case 1:
                soundRef.mChance = static_cast<unsigned char>(value.toInt());
                break;
            default:
                throw std::runtime_error("Region sounds subcolumn index out of range");
        }

        region.mSoundList[subRowIndex] = soundRef;

        record.setModified(region);
    }

    int RegionSoundListAdapter::getColumnsCount(const Record<ESM::Region>& record) const
    {
        return 3;
    }

    int RegionSoundListAdapter::getRowsCount(const Record<ESM::Region>& record) const
    {
        return static_cast<int>(record.get().mSoundList.size());
    }

    void InfoListAdapter::addRow(Record<Info>& record, int position) const
    {
        throw std::logic_error("cannot add a row to a fixed table");
    }

    void InfoListAdapter::removeRow(Record<Info>& record, int rowToRemove) const
    {
        throw std::logic_error("cannot remove a row to a fixed table");
    }

    void InfoListAdapter::setTable(Record<Info>& record, const NestedTableWrapperBase& nestedTable) const
    {
        throw std::logic_error("table operation not supported");
    }

    NestedTableWrapperBase* InfoListAdapter::table(const Record<Info>& record) const
    {
        throw std::logic_error("table operation not supported");
    }

    QVariant InfoListAdapter::getData(const Record<Info>& record, int subRowIndex, int subColIndex) const
    {
        Info info = record.get();

        if (subColIndex == 0)
            return QString(info.mResultScript.c_str());
        else
            throw std::runtime_error("Trying to access non-existing column in the nested table!");
    }

    void InfoListAdapter::setData(Record<Info>& record, const QVariant& value, int subRowIndex, int subColIndex) const
    {
        Info info = record.get();

        if (subColIndex == 0)
            info.mResultScript = value.toString().toStdString();
        else
            throw std::runtime_error("Trying to access non-existing column in the nested table!");

        record.setModified(info);
    }

    int InfoListAdapter::getColumnsCount(const Record<Info>& record) const
    {
        return 1;
    }

    int InfoListAdapter::getRowsCount(const Record<Info>& record) const
    {
        return 1; // fixed at size 1
    }

    void InfoConditionAdapter::addRow(Record<Info>& record, int position) const
    {
        Info info = record.get();

        auto& conditions = info.mSelects;

        // default row
        ESM::DialogueCondition condStruct;
        condStruct.mIndex = conditions.size();

        conditions.insert(conditions.begin() + position, condStruct);

        record.setModified(info);
    }

    void InfoConditionAdapter::removeRow(Record<Info>& record, int rowToRemove) const
    {
        Info info = record.get();

        auto& conditions = info.mSelects;

        if (rowToRemove < 0 || rowToRemove >= static_cast<int>(conditions.size()))
            throw std::runtime_error("index out of range");

        conditions.erase(conditions.begin() + rowToRemove);

        record.setModified(info);
    }

    void InfoConditionAdapter::setTable(Record<Info>& record, const NestedTableWrapperBase& nestedTable) const
    {
        Info info = record.get();

        info.mSelects
            = static_cast<const NestedTableWrapper<std::vector<ESM::DialogueCondition>>&>(nestedTable).mNestedTable;

        record.setModified(info);
    }

    NestedTableWrapperBase* InfoConditionAdapter::table(const Record<Info>& record) const
    {
        // deleted by dtor of NestedTableStoring
        return new NestedTableWrapper<std::vector<ESM::DialogueCondition>>(record.get().mSelects);
    }

    QVariant InfoConditionAdapter::getData(const Record<Info>& record, int subRowIndex, int subColIndex) const
    {
        Info info = record.get();

        auto& conditions = info.mSelects;

        if (subRowIndex < 0 || subRowIndex >= static_cast<int>(conditions.size()))
            throw std::runtime_error("index out of range");

        ConstInfoSelectWrapper infoSelectWrapper(conditions[subRowIndex]);

        switch (subColIndex)
        {
            case 0:
            {
                return infoSelectWrapper.getFunctionName();
            }
            case 1:
            {
                if (infoSelectWrapper.hasVariable())
                    return QString(infoSelectWrapper.getVariableName().c_str());
                else
                    return "";
            }
            case 2:
            {
                return infoSelectWrapper.getRelationType() - ESM::DialogueCondition::Comp_Eq;
            }
            case 3:
            {
                return infoSelectWrapper.getValue();
            }
            default:
                throw std::runtime_error("Info condition subcolumn index out of range");
        }
    }

    void InfoConditionAdapter::setData(
        Record<Info>& record, const QVariant& value, int subRowIndex, int subColIndex) const
    {
        Info info = record.get();

        auto& conditions = info.mSelects;

        if (subRowIndex < 0 || subRowIndex >= static_cast<int>(conditions.size()))
            throw std::runtime_error("index out of range");

        InfoSelectWrapper infoSelectWrapper(conditions[subRowIndex]);
        bool conversionResult = false;

        switch (subColIndex)
        {
            case 0: // Function
            {
                infoSelectWrapper.setFunctionName(static_cast<ESM::DialogueCondition::Function>(value.toInt()));
                break;
            }
            case 1: // Variable
            {
                infoSelectWrapper.setVariableName(value.toString().toUtf8().constData());
                break;
            }
            case 2: // Relation
            {
                infoSelectWrapper.setRelationType(
                    static_cast<ESM::DialogueCondition::Comparison>(value.toInt() + ESM::DialogueCondition::Comp_Eq));
                break;
            }
            case 3: // Value
            {
                switch (infoSelectWrapper.getComparisonType())
                {
                    case ConstInfoSelectWrapper::Comparison_Numeric:
                    {
                        // QVariant seems to have issues converting 0
                        if ((value.toInt(&conversionResult) && conversionResult) || value.toString().compare("0") == 0)
                        {
                            infoSelectWrapper.setValue(value.toInt());
                        }
                        else if (value.toFloat(&conversionResult) && conversionResult)
                        {
                            infoSelectWrapper.setValue(value.toFloat());
                        }
                        break;
                    }
                    case ConstInfoSelectWrapper::Comparison_Boolean:
                    case ConstInfoSelectWrapper::Comparison_Integer:
                    {
                        if ((value.toInt(&conversionResult) && conversionResult) || value.toString().compare("0") == 0)
                        {
                            infoSelectWrapper.setValue(value.toInt());
                        }
                        break;
                    }
                    default:
                        break;
                }
                break;
            }
            default:
                throw std::runtime_error("Info condition subcolumn index out of range");
        }

        record.setModified(info);
    }

    int InfoConditionAdapter::getColumnsCount(const Record<Info>& record) const
    {
        return 4;
    }

    int InfoConditionAdapter::getRowsCount(const Record<Info>& record) const
    {
        return static_cast<int>(record.get().mSelects.size());
    }

    void RaceAttributeAdapter::addRow(Record<ESM::Race>& record, int position) const
    {
        // Do nothing, this table cannot be changed by the user
    }

    void RaceAttributeAdapter::removeRow(Record<ESM::Race>& record, int rowToRemove) const
    {
        // Do nothing, this table cannot be changed by the user
    }

    void RaceAttributeAdapter::setTable(Record<ESM::Race>& record, const NestedTableWrapperBase& nestedTable) const
    {
        ESM::Race race = record.get();

        race.mData = static_cast<const NestedTableWrapper<std::vector<ESM::Race::RADTstruct>>&>(nestedTable)
                         .mNestedTable.at(0);

        record.setModified(race);
    }

    NestedTableWrapperBase* RaceAttributeAdapter::table(const Record<ESM::Race>& record) const
    {
        std::vector<ESM::Race::RADTstruct> wrap;
        wrap.push_back(record.get().mData);
        // deleted by dtor of NestedTableStoring
        return new NestedTableWrapper<std::vector<ESM::Race::RADTstruct>>(wrap);
    }

    QVariant RaceAttributeAdapter::getData(const Record<ESM::Race>& record, int subRowIndex, int subColIndex) const
    {
        ESM::Race race = record.get();
        ESM::RefId attribute = ESM::Attribute::indexToRefId(subRowIndex);
        if (attribute.empty())
            throw std::runtime_error("index out of range");

        switch (subColIndex)
        {
            case 0:
                return subRowIndex;
            case 1:
                return race.mData.getAttribute(attribute, true);
            case 2:
                return race.mData.getAttribute(attribute, false);
            default:
                throw std::runtime_error("Race Attribute subcolumn index out of range");
        }
    }

    void RaceAttributeAdapter::setData(
        Record<ESM::Race>& record, const QVariant& value, int subRowIndex, int subColIndex) const
    {
        ESM::Race race = record.get();
        ESM::RefId attribute = ESM::Attribute::indexToRefId(subRowIndex);
        if (attribute.empty())
            throw std::runtime_error("index out of range");

        switch (subColIndex)
        {
            case 0:
                return; // throw an exception here?
            case 1:
                race.mData.setAttribute(attribute, true, value.toInt());
                break;
            case 2:
                race.mData.setAttribute(attribute, false, value.toInt());
                break;
            default:
                throw std::runtime_error("Race Attribute subcolumn index out of range");
        }

        record.setModified(race);
    }

    int RaceAttributeAdapter::getColumnsCount(const Record<ESM::Race>& record) const
    {
        return 3; // attrib, male, female
    }

    int RaceAttributeAdapter::getRowsCount(const Record<ESM::Race>& record) const
    {
        return ESM::Attribute::Length; // there are 8 attributes
    }

    void RaceSkillsBonusAdapter::addRow(Record<ESM::Race>& record, int position) const
    {
        // Do nothing, this table cannot be changed by the user
    }

    void RaceSkillsBonusAdapter::removeRow(Record<ESM::Race>& record, int rowToRemove) const
    {
        // Do nothing, this table cannot be changed by the user
    }

    void RaceSkillsBonusAdapter::setTable(Record<ESM::Race>& record, const NestedTableWrapperBase& nestedTable) const
    {
        ESM::Race race = record.get();

        race.mData = static_cast<const NestedTableWrapper<std::vector<ESM::Race::RADTstruct>>&>(nestedTable)
                         .mNestedTable.at(0);

        record.setModified(race);
    }

    NestedTableWrapperBase* RaceSkillsBonusAdapter::table(const Record<ESM::Race>& record) const
    {
        std::vector<ESM::Race::RADTstruct> wrap;
        wrap.push_back(record.get().mData);
        // deleted by dtor of NestedTableStoring
        return new NestedTableWrapper<std::vector<ESM::Race::RADTstruct>>(wrap);
    }

    QVariant RaceSkillsBonusAdapter::getData(const Record<ESM::Race>& record, int subRowIndex, int subColIndex) const
    {
        ESM::Race race = record.get();

        if (subRowIndex < 0 || static_cast<size_t>(subRowIndex) >= race.mData.mBonus.size())
            throw std::runtime_error("index out of range");

        switch (subColIndex)
        {
            case 0:
                return race.mData.mBonus[subRowIndex].mSkill; // can be -1
            case 1:
                return race.mData.mBonus[subRowIndex].mBonus;
            default:
                throw std::runtime_error("Race skill bonus subcolumn index out of range");
        }
    }

    void RaceSkillsBonusAdapter::setData(
        Record<ESM::Race>& record, const QVariant& value, int subRowIndex, int subColIndex) const
    {
        ESM::Race race = record.get();

        if (subRowIndex < 0 || static_cast<size_t>(subRowIndex) >= race.mData.mBonus.size())
            throw std::runtime_error("index out of range");

        switch (subColIndex)
        {
            case 0:
                race.mData.mBonus[subRowIndex].mSkill = value.toInt();
                break; // can be -1
            case 1:
                race.mData.mBonus[subRowIndex].mBonus = value.toInt();
                break;
            default:
                throw std::runtime_error("Race skill bonus subcolumn index out of range");
        }

        record.setModified(race);
    }

    int RaceSkillsBonusAdapter::getColumnsCount(const Record<ESM::Race>& record) const
    {
        return 2; // skill, bonus
    }

    int RaceSkillsBonusAdapter::getRowsCount(const Record<ESM::Race>& record) const
    {
        return record.get().mData.mBonus.size();
    }

    void CellListAdapter::addRow(Record<CSMWorld::Cell>& record, int position) const
    {
        throw std::logic_error("cannot add a row to a fixed table");
    }

    void CellListAdapter::removeRow(Record<CSMWorld::Cell>& record, int rowToRemove) const
    {
        throw std::logic_error("cannot remove a row to a fixed table");
    }

    void CellListAdapter::setTable(Record<CSMWorld::Cell>& record, const NestedTableWrapperBase& nestedTable) const
    {
        throw std::logic_error("table operation not supported");
    }

    NestedTableWrapperBase* CellListAdapter::table(const Record<CSMWorld::Cell>& record) const
    {
        throw std::logic_error("table operation not supported");
    }

    QVariant CellListAdapter::getData(const Record<CSMWorld::Cell>& record, int subRowIndex, int subColIndex) const
    {
        CSMWorld::Cell cell = record.get();

        bool isInterior = (cell.mData.mFlags & ESM::Cell::Interior) != 0;
        bool behaveLikeExterior = (cell.mData.mFlags & ESM::Cell::QuasiEx) != 0;
        bool interiorWater = (cell.mData.mFlags & ESM::Cell::HasWater) != 0;

        switch (subColIndex)
        {
            case 0:
                return isInterior;
            // While the ambient information is not necessarily valid if the subrecord wasn't loaded,
            // the user should still be allowed to edit it
            case 1:
                return (isInterior && !behaveLikeExterior) ? cell.mAmbi.mAmbient : DisableTag::getVariant();
            case 2:
                return (isInterior && !behaveLikeExterior) ? cell.mAmbi.mSunlight : DisableTag::getVariant();
            case 3:
                return (isInterior && !behaveLikeExterior) ? cell.mAmbi.mFog : DisableTag::getVariant();
            case 4:
                return (isInterior && !behaveLikeExterior) ? cell.mAmbi.mFogDensity : DisableTag::getVariant();
            case 5:
            {
                if (isInterior && interiorWater)
                    return cell.mWater;
                else
                    return DisableTag::getVariant();
            }
            case 6:
                return isInterior ? DisableTag::getVariant() : cell.mMapColor; // TODO: how to select?
            // case 7: return isInterior ?
            // behaveLikeExterior : DisableTag::getVariant();
            default:
                throw std::runtime_error("Cell subcolumn index out of range");
        }
    }

    void CellListAdapter::setData(
        Record<CSMWorld::Cell>& record, const QVariant& value, int subRowIndex, int subColIndex) const
    {
        CSMWorld::Cell cell = record.get();

        bool isInterior = (cell.mData.mFlags & ESM::Cell::Interior) != 0;
        bool behaveLikeExterior = (cell.mData.mFlags & ESM::Cell::QuasiEx) != 0;
        bool interiorWater = (cell.mData.mFlags & ESM::Cell::HasWater) != 0;

        switch (subColIndex)
        {
            case 0:
            {
                if (value.toBool())
                    cell.mData.mFlags |= ESM::Cell::Interior;
                else
                    cell.mData.mFlags &= ~ESM::Cell::Interior;
                break;
            }
            case 1:
            {
                if (isInterior && !behaveLikeExterior)
                {
                    cell.mAmbi.mAmbient = static_cast<int32_t>(value.toInt());
                    cell.setHasAmbient(true);
                }
                else
                    return; // return without saving
                break;
            }
            case 2:
            {
                if (isInterior && !behaveLikeExterior)
                {
                    cell.mAmbi.mSunlight = static_cast<int32_t>(value.toInt());
                    cell.setHasAmbient(true);
                }
                else
                    return; // return without saving
                break;
            }
            case 3:
            {
                if (isInterior && !behaveLikeExterior)
                {
                    cell.mAmbi.mFog = static_cast<int32_t>(value.toInt());
                    cell.setHasAmbient(true);
                }
                else
                    return; // return without saving
                break;
            }
            case 4:
            {
                if (isInterior && !behaveLikeExterior)
                {
                    cell.mAmbi.mFogDensity = value.toFloat();
                    cell.setHasAmbient(true);
                }
                else
                    return; // return without saving
                break;
            }
            case 5:
            {
                if (isInterior && interiorWater)
                {
                    cell.mWater = value.toFloat();
                    cell.setHasWaterHeightSub(true);
                }
                else
                    return; // return without saving
                break;
            }
            case 6:
            {
                if (!isInterior)
                    cell.mMapColor = value.toInt();
                else
                    return; // return without saving
                break;
            }
#if 0
            // redundant since this flag is shown in the main table as "Interior Sky"
            // keep here for documenting the logic based on vanilla
            case 7:
            {
                if (isInterior)
                {
                    if (value.toBool())
                        cell.mData.mFlags |= ESM::Cell::QuasiEx;
                    else
                        cell.mData.mFlags &= ~ESM::Cell::QuasiEx;
                }
                else
                    return; // return without saving
                break;
            }
#endif
            default:
                throw std::runtime_error("Cell subcolumn index out of range");
        }

        record.setModified(cell);
    }

    int CellListAdapter::getColumnsCount(const Record<CSMWorld::Cell>& record) const
    {
        return 7;
    }

    int CellListAdapter::getRowsCount(const Record<CSMWorld::Cell>& record) const
    {
        return 1; // fixed at size 1
    }

    void RegionWeatherAdapter::addRow(Record<ESM::Region>& record, int position) const
    {
        throw std::logic_error("cannot add a row to a fixed table");
    }

    void RegionWeatherAdapter::removeRow(Record<ESM::Region>& record, int rowToRemove) const
    {
        throw std::logic_error("cannot remove a row from a fixed table");
    }

    void RegionWeatherAdapter::setTable(Record<ESM::Region>& record, const NestedTableWrapperBase& nestedTable) const
    {
        throw std::logic_error("table operation not supported");
    }

    NestedTableWrapperBase* RegionWeatherAdapter::table(const Record<ESM::Region>& record) const
    {
        throw std::logic_error("table operation not supported");
    }

    QVariant RegionWeatherAdapter::getData(const Record<ESM::Region>& record, int subRowIndex, int subColIndex) const
    {
        const char* WeatherNames[]
            = { "Clear", "Cloudy", "Fog", "Overcast", "Rain", "Thunder", "Ash", "Blight", "Snow", "Blizzard" };

        const ESM::Region& region = record.get();

        if (subColIndex == 0 && subRowIndex >= 0 && subRowIndex < 10)
        {
            return WeatherNames[subRowIndex];
        }
        else if (subColIndex == 1)
        {
            if (subRowIndex >= 0 && static_cast<size_t>(subRowIndex) < region.mData.mProbabilities.size())
                return region.mData.mProbabilities[subRowIndex];
        }

        throw std::runtime_error("index out of range");
    }

    void RegionWeatherAdapter::setData(
        Record<ESM::Region>& record, const QVariant& value, int subRowIndex, int subColIndex) const
    {
        ESM::Region region = record.get();
        uint8_t chance = static_cast<uint8_t>(value.toInt());

        if (subColIndex == 1)
        {
            region.mData.mProbabilities.at(subRowIndex) = chance;

            record.setModified(region);
        }
    }

    int RegionWeatherAdapter::getColumnsCount(const Record<ESM::Region>& record) const
    {
        return 2;
    }

    int RegionWeatherAdapter::getRowsCount(const Record<ESM::Region>& record) const
    {
        return 10;
    }

    void FactionRanksAdapter::addRow(Record<ESM::Faction>& record, int position) const
    {
        throw std::logic_error("cannot add a row to a fixed table");
    }

    void FactionRanksAdapter::removeRow(Record<ESM::Faction>& record, int rowToRemove) const
    {
        throw std::logic_error("cannot remove a row from a fixed table");
    }

    void FactionRanksAdapter::setTable(Record<ESM::Faction>& record, const NestedTableWrapperBase& nestedTable) const
    {
        throw std::logic_error("table operation not supported");
    }

    NestedTableWrapperBase* FactionRanksAdapter::table(const Record<ESM::Faction>& record) const
    {
        throw std::logic_error("table operation not supported");
    }

    QVariant FactionRanksAdapter::getData(const Record<ESM::Faction>& record, int subRowIndex, int subColIndex) const
    {
        const ESM::Faction& faction = record.get();

        const auto& rankData = faction.mData.mRankData.at(subRowIndex);

        switch (subColIndex)
        {
            case 0:
                return QString(faction.mRanks[subRowIndex].c_str());
            case 1:
                return rankData.mAttribute1;
            case 2:
                return rankData.mAttribute2;
            case 3:
                return rankData.mPrimarySkill;
            case 4:
                return rankData.mFavouredSkill;
            case 5:
                return rankData.mFactReaction;
            default:
                throw std::runtime_error("Rank subcolumn index out of range");
        }
    }

    void FactionRanksAdapter::setData(
        Record<ESM::Faction>& record, const QVariant& value, int subRowIndex, int subColIndex) const
    {
        ESM::Faction faction = record.get();

        auto& rankData = faction.mData.mRankData.at(subRowIndex);

        switch (subColIndex)
        {
            case 0:
                faction.mRanks[subRowIndex] = value.toString().toUtf8().constData();
                break;
            case 1:
                rankData.mAttribute1 = value.toInt();
                break;
            case 2:
                rankData.mAttribute2 = value.toInt();
                break;
            case 3:
                rankData.mPrimarySkill = value.toInt();
                break;
            case 4:
                rankData.mFavouredSkill = value.toInt();
                break;
            case 5:
                rankData.mFactReaction = value.toInt();
                break;
            default:
                throw std::runtime_error("Rank index out of range");
        }

        record.setModified(faction);
    }

    int FactionRanksAdapter::getColumnsCount(const Record<ESM::Faction>& record) const
    {
        return 6;
    }

    int FactionRanksAdapter::getRowsCount(const Record<ESM::Faction>& record) const
    {
        return 10;
    }
}
