/*
 * Copyright (c) Atmosphère-NX
 *
 * This program is free software; you can redistribute it and/or modify it
 * under the terms and conditions of the GNU General Public License,
 * version 2, as published by the Free Software Foundation.
 *
 * This program is distributed in the hope it will be useful, but WITHOUT
 * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
 * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License for
 * more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
 */
#include <stratosphere.hpp>

namespace ams::fs {

    s64 HierarchicalRomFileTable::QueryDirectoryEntryBucketStorageSize(StorageSizeType count) {
        return DirectoryEntryMapTable::QueryBucketStorageSize(count);
    }

    s64 HierarchicalRomFileTable::QueryDirectoryEntrySize(StorageSizeType aux_size) {
        return DirectoryEntryMapTable::QueryEntrySize(aux_size);
    }

    s64 HierarchicalRomFileTable::QueryFileEntryBucketStorageSize(StorageSizeType count) {
        return FileEntryMapTable::QueryBucketStorageSize(count);
    }

    s64 HierarchicalRomFileTable::QueryFileEntrySize(StorageSizeType aux_size) {
        return FileEntryMapTable::QueryEntrySize(aux_size);
    }

    Result HierarchicalRomFileTable::Format(SubStorage dir_bucket, SubStorage file_bucket) {
        s64 dir_bucket_size;
        R_TRY(dir_bucket.GetSize(std::addressof(dir_bucket_size)));
        R_TRY(DirectoryEntryMapTable::Format(dir_bucket, DirectoryEntryMapTable::QueryBucketCount(dir_bucket_size)));

        s64 file_bucket_size;
        R_TRY(file_bucket.GetSize(std::addressof(file_bucket_size)));
        R_TRY(FileEntryMapTable::Format(file_bucket, FileEntryMapTable::QueryBucketCount(file_bucket_size)));

        R_SUCCEED();
    }

    HierarchicalRomFileTable::HierarchicalRomFileTable() { /* ... */ }

    Result HierarchicalRomFileTable::Initialize(SubStorage dir_bucket, SubStorage dir_entry, SubStorage file_bucket, SubStorage file_entry) {
        s64 dir_bucket_size;
        R_TRY(dir_bucket.GetSize(std::addressof(dir_bucket_size)));
        R_TRY(m_dir_table.Initialize(dir_bucket, DirectoryEntryMapTable::QueryBucketCount(dir_bucket_size), dir_entry));

        s64 file_bucket_size;
        R_TRY(file_bucket.GetSize(std::addressof(file_bucket_size)));
        R_TRY(m_file_table.Initialize(file_bucket, FileEntryMapTable::QueryBucketCount(file_bucket_size), file_entry));

        R_SUCCEED();
    }

    void HierarchicalRomFileTable::Finalize() {
        m_dir_table.Finalize();
        m_file_table.Finalize();
    }

    Result HierarchicalRomFileTable::CreateRootDirectory() {
        Position root_pos = RootPosition;
        EntryKey root_key = {};
        root_key.key.parent = root_pos;
        RomDirectoryEntry root_entry = {
            .next = InvalidPosition,
            .dir  = InvalidPosition,
            .file = InvalidPosition,
        };
        R_RETURN(m_dir_table.Add(std::addressof(root_pos), root_key, root_entry));
    }

    Result HierarchicalRomFileTable::CreateDirectory(RomDirectoryId *out, const RomPathChar *path) {
        AMS_ASSERT(out != nullptr);
        AMS_ASSERT(path != nullptr);

        RomDirectoryEntry parent_entry = {};
        EntryKey new_key = {};
        R_TRY(this->FindDirectoryRecursive(std::addressof(new_key), std::addressof(parent_entry), path));

        R_TRY(this->CheckSameEntryExists(new_key, fs::ResultDbmAlreadyExists()));

        RomDirectoryEntry new_entry = {
            .next = InvalidPosition,
            .dir  = InvalidPosition,
            .file = InvalidPosition,
        };

        Position new_pos = 0;
        R_TRY_CATCH(m_dir_table.Add(std::addressof(new_pos), new_key, new_entry)) {
            R_CONVERT(fs::ResultDbmKeyFull, fs::ResultDbmDirectoryEntryFull())
        } R_END_TRY_CATCH;

        *out = PositionToDirectoryId(new_pos);

        if (parent_entry.dir == InvalidPosition) {
            parent_entry.dir = new_pos;

            R_TRY(m_dir_table.SetByPosition(new_key.key.parent, parent_entry));
        } else {
            Position cur_pos = parent_entry.dir;
            while (true) {
                RomEntryKey cur_key = {};
                RomDirectoryEntry cur_entry = {};
                R_TRY(m_dir_table.GetByPosition(std::addressof(cur_key), std::addressof(cur_entry), cur_pos));

                if (cur_entry.next == InvalidPosition) {
                    cur_entry.next = new_pos;

                    R_TRY(m_dir_table.SetByPosition(cur_pos, cur_entry));
                    break;
                }

                cur_pos = cur_entry.next;
            }
        }

        R_SUCCEED();
    }

    Result HierarchicalRomFileTable::CreateFile(RomFileId *out, const RomPathChar *path, const FileInfo &info) {
        AMS_ASSERT(out != nullptr);
        AMS_ASSERT(path != nullptr);

        RomDirectoryEntry parent_entry = {};
        EntryKey new_key = {};
        R_TRY(this->FindFileRecursive(std::addressof(new_key), std::addressof(parent_entry), path));

        R_TRY(this->CheckSameEntryExists(new_key, fs::ResultDbmAlreadyExists()));

        RomFileEntry new_entry = {
            .next = InvalidPosition,
            .info = info,
        };

        Position new_pos = 0;
        R_TRY_CATCH(m_file_table.Add(std::addressof(new_pos), new_key, new_entry)) {
            R_CONVERT(fs::ResultDbmKeyFull, fs::ResultDbmFileEntryFull())
        } R_END_TRY_CATCH;

        *out = PositionToFileId(new_pos);

        if (parent_entry.file == InvalidPosition) {
            parent_entry.file = new_pos;

            R_TRY(m_dir_table.SetByPosition(new_key.key.parent, parent_entry));
        } else {
            Position cur_pos = parent_entry.file;
            while (true) {
                RomEntryKey cur_key = {};
                RomFileEntry cur_entry = {};
                R_TRY(m_file_table.GetByPosition(std::addressof(cur_key), std::addressof(cur_entry), cur_pos));

                if (cur_entry.next == InvalidPosition) {
                    cur_entry.next = new_pos;

                    R_TRY(m_file_table.SetByPosition(cur_pos, cur_entry));
                    break;
                }

                cur_pos = cur_entry.next;
            }
        }

        R_SUCCEED();
    }

    Result HierarchicalRomFileTable::ConvertPathToDirectoryId(RomDirectoryId *out, const RomPathChar *path) {
        AMS_ASSERT(out != nullptr);
        AMS_ASSERT(path != nullptr);

        RomDirectoryEntry parent_entry = {};
        EntryKey key = {};
        R_TRY(this->FindDirectoryRecursive(std::addressof(key), std::addressof(parent_entry), path));

        Position pos = 0;
        RomDirectoryEntry entry = {};
        R_TRY(this->GetDirectoryEntry(std::addressof(pos), std::addressof(entry), key));

        *out = PositionToDirectoryId(pos);
        R_SUCCEED();
    }

    Result HierarchicalRomFileTable::ConvertPathToFileId(RomFileId *out, const RomPathChar *path) {
        AMS_ASSERT(out != nullptr);
        AMS_ASSERT(path != nullptr);

        RomDirectoryEntry parent_entry = {};
        EntryKey key = {};
        R_TRY(this->FindDirectoryRecursive(std::addressof(key), std::addressof(parent_entry), path));

        Position pos = 0;
        RomFileEntry entry = {};
        R_TRY(this->GetFileEntry(std::addressof(pos), std::addressof(entry), key));

        *out = PositionToFileId(pos);
        R_SUCCEED();
    }

    Result HierarchicalRomFileTable::OpenFile(FileInfo *out, const RomPathChar *path) {
        AMS_ASSERT(out != nullptr);
        AMS_ASSERT(path != nullptr);

        RomDirectoryEntry parent_entry = {};
        EntryKey key = {};
        R_TRY(this->FindFileRecursive(std::addressof(key), std::addressof(parent_entry), path));

        R_RETURN(this->OpenFile(out, key));
    }

    Result HierarchicalRomFileTable::OpenFile(FileInfo *out, RomFileId id) {
        AMS_ASSERT(out != nullptr);

        RomFileEntry entry = {};
        R_TRY(this->GetFileEntry(std::addressof(entry), id));

        *out = entry.info;
        R_SUCCEED();
    }

    Result HierarchicalRomFileTable::FindOpen(FindPosition *out, const RomPathChar *path) {
        AMS_ASSERT(out != nullptr);
        AMS_ASSERT(path != nullptr);

        RomDirectoryEntry parent_entry = {};
        EntryKey key = {};
        R_TRY(this->FindDirectoryRecursive(std::addressof(key), std::addressof(parent_entry), path));

        R_RETURN(this->FindOpen(out, key));
    }

    Result HierarchicalRomFileTable::FindOpen(FindPosition *out, RomDirectoryId id) {
        AMS_ASSERT(out != nullptr);

        out->next_dir  = InvalidPosition;
        out->next_file = InvalidPosition;

        RomDirectoryEntry entry = {};
        R_TRY(this->GetDirectoryEntry(std::addressof(entry), id));

        out->next_dir  = entry.dir;
        out->next_file = entry.file;

        R_SUCCEED();
    }

    Result HierarchicalRomFileTable::FindNextDirectory(RomPathChar *out, FindPosition *find, size_t length) {
        AMS_ASSERT(out != nullptr);
        AMS_ASSERT(find != nullptr);
        AMS_ASSERT(length > RomPathTool::MaxPathLength);
        AMS_UNUSED(length);

        R_UNLESS(find->next_dir != InvalidPosition, fs::ResultDbmFindFinished());

        RomEntryKey key = {};
        RomDirectoryEntry entry = {};
        size_t aux_size = 0;
        R_TRY(m_dir_table.GetByPosition(std::addressof(key), std::addressof(entry), out, std::addressof(aux_size), find->next_dir));
        AMS_ASSERT(aux_size / sizeof(RomPathChar) <= RomPathTool::MaxPathLength);

        out[aux_size / sizeof(RomPathChar)] = RomStringTraits::NullTerminator;

        find->next_dir = entry.next;
        R_SUCCEED();
    }

    Result HierarchicalRomFileTable::FindNextFile(RomPathChar *out, FindPosition *find, size_t length) {
        AMS_ASSERT(out != nullptr);
        AMS_ASSERT(find != nullptr);
        AMS_ASSERT(length > RomPathTool::MaxPathLength);
        AMS_UNUSED(length);

        R_UNLESS(find->next_file != InvalidPosition, fs::ResultDbmFindFinished());

        RomEntryKey key = {};
        RomFileEntry entry = {};
        size_t aux_size = 0;
        R_TRY(m_file_table.GetByPosition(std::addressof(key), std::addressof(entry), out, std::addressof(aux_size), find->next_file));
        AMS_ASSERT(aux_size / sizeof(RomPathChar) <= RomPathTool::MaxPathLength);

        out[aux_size / sizeof(RomPathChar)] = RomStringTraits::NullTerminator;

        find->next_file = entry.next;
        R_SUCCEED();
    }

    Result HierarchicalRomFileTable::QueryRomFileSystemSize(s64 *out_dir_entry_size, s64 *out_file_entry_size) {
        AMS_ASSERT(out_dir_entry_size != nullptr);
        AMS_ASSERT(out_file_entry_size != nullptr);

        *out_dir_entry_size  = m_dir_table.GetTotalEntrySize();
        *out_file_entry_size = m_file_table.GetTotalEntrySize();
        R_SUCCEED();
    }

    Result HierarchicalRomFileTable::GetParent(Position *out_pos, EntryKey *out_dir_key, RomDirectoryEntry *out_dir_entry, Position pos, RomPathTool::RomEntryName name, const RomPathChar *path) {
        AMS_ASSERT(out_pos != nullptr);
        AMS_ASSERT(out_dir_key != nullptr);
        AMS_ASSERT(out_dir_entry != nullptr);
        AMS_ASSERT(path != nullptr);

        RomEntryKey p_key = {};
        RomDirectoryEntry p_entry = {};
        R_TRY(m_dir_table.GetByPosition(std::addressof(p_key), std::addressof(p_entry), pos));

        out_dir_key->key = p_key;
        R_TRY(RomPathTool::GetParentDirectoryName(std::addressof(out_dir_key->name), name, path));

        R_TRY(this->GetDirectoryEntry(out_pos, out_dir_entry, *out_dir_key));

        R_SUCCEED();
    }

    Result HierarchicalRomFileTable::FindParentDirectoryRecursive(Position *out_pos, EntryKey *out_dir_key, RomDirectoryEntry *out_dir_entry, RomPathTool::PathParser *parser, const RomPathChar *path) {
        AMS_ASSERT(out_pos != nullptr);
        AMS_ASSERT(out_dir_key != nullptr);
        AMS_ASSERT(out_dir_entry != nullptr);
        AMS_ASSERT(parser != nullptr);
        AMS_ASSERT(path != nullptr);

        Position dir_pos = RootPosition;
        EntryKey dir_key = {};
        RomDirectoryEntry dir_entry = {};
        dir_key.key.parent = RootPosition;

        R_TRY(parser->GetNextDirectoryName(std::addressof(dir_key.name)));
        R_TRY(this->GetDirectoryEntry(std::addressof(dir_pos), std::addressof(dir_entry), dir_key));

        Position parent_pos = dir_pos;
        while (!parser->IsParseFinished()) {
            EntryKey old_key = dir_key;

            R_TRY(parser->GetNextDirectoryName(std::addressof(dir_key.name)));

            if (dir_key.name.IsCurrentDirectory()) {
                dir_key = old_key;
                continue;
            } else if (dir_key.name.IsParentDirectory()) {
                R_UNLESS(parent_pos != RootPosition, fs::ResultDirectoryUnobtainable());

                R_TRY(this->GetParent(std::addressof(parent_pos), std::addressof(dir_key), std::addressof(dir_entry), dir_key.key.parent, dir_key.name, path));
            } else {
                dir_key.key.parent = parent_pos;
                R_TRY_CATCH(this->GetDirectoryEntry(std::addressof(dir_pos), std::addressof(dir_entry), dir_key)) {
                    R_CONVERT(fs::ResultDbmInvalidOperation, fs::ResultDbmNotFound())
                } R_END_TRY_CATCH;

                parent_pos = dir_pos;
            }
        }

        *out_pos = parent_pos;
        *out_dir_key = dir_key;
        *out_dir_entry = dir_entry;
        R_SUCCEED();
    }

    Result HierarchicalRomFileTable::FindPathRecursive(EntryKey *out_key, RomDirectoryEntry *out_dir_entry, bool is_dir, const RomPathChar *path) {
        AMS_ASSERT(out_key != nullptr);
        AMS_ASSERT(out_dir_entry != nullptr);
        AMS_ASSERT(path != nullptr);

        RomPathTool::PathParser parser;
        R_TRY(parser.Initialize(path));

        EntryKey parent_key = {};
        Position parent_pos = 0;
        R_TRY(this->FindParentDirectoryRecursive(std::addressof(parent_pos), std::addressof(parent_key), out_dir_entry, std::addressof(parser), path));

        if (is_dir) {
            RomPathTool::RomEntryName name = {};
            R_TRY(parser.GetAsDirectoryName(std::addressof(name)));

            if (name.IsCurrentDirectory()) {
                *out_key = parent_key;
                if (out_key->key.parent != RootPosition) {
                    Position pos = 0;
                    R_TRY(this->GetParent(std::addressof(pos), std::addressof(parent_key), out_dir_entry, out_key->key.parent, out_key->name, path));
                }
            } else if (name.IsParentDirectory()) {
                R_UNLESS(parent_pos != RootPosition, fs::ResultDirectoryUnobtainable());

                Position pos = 0;
                RomDirectoryEntry cur_entry = {};
                R_TRY(this->GetParent(std::addressof(pos), out_key, std::addressof(cur_entry), parent_key.key.parent, parent_key.name, path));

                if (out_key->key.parent != RootPosition) {
                    R_TRY(this->GetParent(std::addressof(pos), std::addressof(parent_key), out_dir_entry, out_key->key.parent, out_key->name, path));
                }
            } else {
                out_key->name = name;
                out_key->key.parent = out_key->name.IsRootDirectory() ? RootPosition : parent_pos;
            }
        } else {
            {
                RomPathTool::RomEntryName name = {};
                R_TRY(parser.GetAsDirectoryName(std::addressof(name)));
                R_UNLESS(!name.IsParentDirectory() || parent_pos != RootPosition, fs::ResultDirectoryUnobtainable());
            }

            R_UNLESS(!parser.IsDirectoryPath(), fs::ResultDbmInvalidOperation());

            out_key->key.parent = parent_pos;
            R_TRY(parser.GetAsFileName(std::addressof(out_key->name)));
        }

        R_SUCCEED();
    }

    Result HierarchicalRomFileTable::FindDirectoryRecursive(EntryKey *out_key, RomDirectoryEntry *out_dir_entry, const RomPathChar *path) {
        AMS_ASSERT(out_key != nullptr);
        AMS_ASSERT(out_dir_entry != nullptr);
        AMS_ASSERT(path != nullptr);

        R_RETURN(this->FindPathRecursive(out_key, out_dir_entry, true, path));
    }

    Result HierarchicalRomFileTable::FindFileRecursive(EntryKey *out_key, RomDirectoryEntry *out_dir_entry, const RomPathChar *path) {
        AMS_ASSERT(out_key != nullptr);
        AMS_ASSERT(out_dir_entry != nullptr);
        AMS_ASSERT(path != nullptr);

        R_RETURN(this->FindPathRecursive(out_key, out_dir_entry, false, path));
    }

    Result HierarchicalRomFileTable::CheckSameEntryExists(const EntryKey &key, Result if_exists) {
        /* Check dir */
        {
            Position pos = InvalidPosition;
            RomDirectoryEntry entry = {};
            const Result get_res = m_dir_table.Get(std::addressof(pos), std::addressof(entry), key);
            if (!fs::ResultDbmKeyNotFound::Includes(get_res)) {
                R_TRY(get_res);
                R_THROW(if_exists);
            }
        }

        /* Check file */
        {
            Position pos = InvalidPosition;
            RomFileEntry entry = {};
            const Result get_res = m_file_table.Get(std::addressof(pos), std::addressof(entry), key);
            if (!fs::ResultDbmKeyNotFound::Includes(get_res)) {
                R_TRY(get_res);
                R_THROW(if_exists);
            }
        }

        R_SUCCEED();
    }

    Result HierarchicalRomFileTable::GetDirectoryEntry(Position *out_pos, RomDirectoryEntry *out_entry, const EntryKey &key) {
        AMS_ASSERT(out_pos != nullptr);
        AMS_ASSERT(out_entry != nullptr);

        const Result dir_res = m_dir_table.Get(out_pos, out_entry, key);
        R_UNLESS(R_FAILED(dir_res),                           dir_res);
        R_UNLESS(fs::ResultDbmKeyNotFound::Includes(dir_res), dir_res);

        Position pos = 0;
        RomFileEntry entry = {};
        const Result file_res = m_file_table.Get(std::addressof(pos), std::addressof(entry), key);
        R_UNLESS(R_FAILED(file_res),                            fs::ResultDbmInvalidOperation());
        R_UNLESS(!fs::ResultDbmKeyNotFound::Includes(file_res), fs::ResultDbmDirectoryNotFound());
        R_RETURN(file_res);
    }

    Result HierarchicalRomFileTable::GetDirectoryEntry(RomDirectoryEntry *out_entry, RomDirectoryId id) {
        AMS_ASSERT(out_entry != nullptr);
        Position pos = DirectoryIdToPosition(id);

        RomEntryKey key = {};
        const Result dir_res = m_dir_table.GetByPosition(std::addressof(key), out_entry, pos);
        R_UNLESS(R_FAILED(dir_res),                           dir_res);
        R_UNLESS(fs::ResultDbmKeyNotFound::Includes(dir_res), dir_res);

        RomFileEntry entry = {};
        const Result file_res = m_file_table.GetByPosition(std::addressof(key), std::addressof(entry), pos);
        R_UNLESS(R_FAILED(file_res),                            fs::ResultDbmInvalidOperation());
        R_UNLESS(!fs::ResultDbmKeyNotFound::Includes(file_res), fs::ResultDbmDirectoryNotFound());
        R_RETURN(file_res);
    }

    Result HierarchicalRomFileTable::GetFileEntry(Position *out_pos, RomFileEntry *out_entry, const EntryKey &key) {
        AMS_ASSERT(out_pos != nullptr);
        AMS_ASSERT(out_entry != nullptr);

        const Result file_res = m_file_table.Get(out_pos, out_entry, key);
        R_UNLESS(R_FAILED(file_res),                           file_res);
        R_UNLESS(fs::ResultDbmKeyNotFound::Includes(file_res), file_res);

        Position pos = 0;
        RomDirectoryEntry entry = {};
        const Result dir_res = m_dir_table.Get(std::addressof(pos), std::addressof(entry), key);
        R_UNLESS(R_FAILED(dir_res),                            fs::ResultDbmInvalidOperation());
        R_UNLESS(!fs::ResultDbmKeyNotFound::Includes(dir_res), fs::ResultDbmFileNotFound());
        R_RETURN(dir_res);
    }

    Result HierarchicalRomFileTable::GetFileEntry(RomFileEntry *out_entry, RomFileId id) {
        AMS_ASSERT(out_entry != nullptr);
        Position pos = FileIdToPosition(id);

        RomEntryKey key = {};
        const Result file_res = m_file_table.GetByPosition(std::addressof(key), out_entry, pos);
        R_UNLESS(R_FAILED(file_res),                           file_res);
        R_UNLESS(fs::ResultDbmKeyNotFound::Includes(file_res), file_res);

        RomDirectoryEntry entry = {};
        const Result dir_res = m_dir_table.GetByPosition(std::addressof(key), std::addressof(entry), pos);
        R_UNLESS(R_FAILED(dir_res),                            fs::ResultDbmInvalidOperation());
        R_UNLESS(!fs::ResultDbmKeyNotFound::Includes(dir_res), fs::ResultDbmFileNotFound());
        R_RETURN(dir_res);
    }

    Result HierarchicalRomFileTable::OpenFile(FileInfo *out, const EntryKey &key) {
        AMS_ASSERT(out != nullptr);

        Position pos = 0;
        RomFileEntry entry = {};
        R_TRY(this->GetFileEntry(std::addressof(pos), std::addressof(entry), key));

        *out = entry.info;
        R_SUCCEED();
    }

    Result HierarchicalRomFileTable::FindOpen(FindPosition *out, const EntryKey &key) {
        AMS_ASSERT(out != nullptr);

        out->next_dir  = InvalidPosition;
        out->next_file = InvalidPosition;

        Position pos = 0;
        RomDirectoryEntry entry = {};
        R_TRY(this->GetDirectoryEntry(std::addressof(pos), std::addressof(entry), key));

        out->next_dir  = entry.dir;
        out->next_file = entry.file;

        R_SUCCEED();
    }

}