/*
 * 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>
#include "erpt_srv_forced_shutdown.hpp"
#include "erpt_srv_context.hpp"
#include "erpt_srv_context_record.hpp"
#include "erpt_srv_reporter.hpp"
#include "erpt_srv_stream.hpp"

namespace ams::erpt::srv {

    namespace {

        constexpr u32 ForcedShutdownContextBufferSize = 1_KB;

        constexpr u32 ForcedShutdownContextVersion = 1;

        struct ForcedShutdownContextHeader {
            u32 version;
            u32 num_contexts;
        };
        static_assert(sizeof(ForcedShutdownContextHeader) == 8);

        struct ForcedShutdownContextEntry {
            u32 version;
            CategoryId category;
            u32 field_count;
            u32 array_buffer_size;
        };
        static_assert(sizeof(ForcedShutdownContextEntry) == 16);

        os::Event g_forced_shutdown_update_event(os::EventClearMode_ManualClear);

        constinit ContextEntry g_forced_shutdown_contexts[] = {
            { .category = CategoryId_RunningApplicationInfo,   },
            { .category = CategoryId_RunningAppletInfo,        },
            { .category = CategoryId_FocusedAppletHistoryInfo, },
        };

        bool IsForceShutdownDetected() {
            fs::DirectoryEntryType entry_type;
            return R_SUCCEEDED(fs::GetEntryType(std::addressof(entry_type), ForcedShutdownContextFileName));
        }

        Result CreateForcedShutdownContext() {
            /* Create the context. */
            {
                /* Create the stream. */
                Stream stream;
                R_TRY(stream.OpenStream(ForcedShutdownContextFileName, StreamMode_Write, 0));

                /* Write a context header. */
                const ForcedShutdownContextHeader header = { .version = ForcedShutdownContextVersion, .num_contexts = 0, };
                R_TRY(stream.WriteStream(reinterpret_cast<const u8 *>(std::addressof(header)), sizeof(header)));
            }

            /* Commit the context. */
            R_TRY(Stream::CommitStream());

            R_SUCCEED();
        }

        Result CreateReportForForcedShutdown() {
            /* Create a new context record. */
            /* NOTE: Nintendo does not check that this allocation succeeds. */
            auto record = std::make_unique<ContextRecord>(CategoryId_ErrorInfo);

            /* Create error code for the report. */
            char error_code_str[err::ErrorCode::StringLengthMax];
            err::GetErrorCodeString(error_code_str, sizeof(error_code_str), err::ConvertResultToErrorCode(err::ResultForcedShutdownDetected()));

            /* Add error code to the context. */
            R_TRY(record->Add(FieldId_ErrorCode, error_code_str, std::strlen(error_code_str)));

            /* Create report. */
            R_TRY(Reporter::CreateReport(ReportType_Invisible, ResultSuccess(), std::move(record), nullptr, nullptr, 0, erpt::srv::MakeNoCreateReportOptionFlags()));

            R_SUCCEED();
        }

        Result LoadForcedShutdownContext() {
            /* Create the stream to read the context. */
            Stream stream;
            R_TRY(stream.OpenStream(ForcedShutdownContextFileName, StreamMode_Read, ForcedShutdownContextBufferSize));

            /* Read the header. */
            u32 read_size;
            ForcedShutdownContextHeader header;
            R_TRY(stream.ReadStream(std::addressof(read_size), reinterpret_cast<u8 *>(std::addressof(header)), sizeof(header)));

            /* Validate the header. */
            R_SUCCEED_IF(read_size != sizeof(header));
            R_SUCCEED_IF(ForcedShutdownContextVersion);
            R_SUCCEED_IF(header.num_contexts == 0);

            /* Read out the contexts. */
            for (u32 i = 0; i < header.num_contexts; ++i) {
                /* Read the context entry header. */
                ForcedShutdownContextEntry entry_header;
                R_TRY(stream.ReadStream(std::addressof(read_size), reinterpret_cast<u8 *>(std::addressof(entry_header)), sizeof(entry_header)));

                if (read_size != sizeof(entry_header)) {
                    break;
                }

                if (entry_header.field_count == 0) {
                    continue;
                }

                /* Read the saved data into a context entry. */
                ContextEntry ctx = {
                    .version      = entry_header.version,
                    .field_count  = entry_header.field_count,
                    .category     = entry_header.category,
                };

                /* Check that the field count is valid. */
                AMS_ABORT_UNLESS(entry_header.field_count <= util::size(ctx.fields));

                /* Read the fields. */
                R_TRY(stream.ReadStream(std::addressof(read_size), reinterpret_cast<u8 *>(std::addressof(ctx.fields)), entry_header.field_count * sizeof(ctx.fields[0])));
                if (read_size != entry_header.field_count * sizeof(ctx.fields[0])) {
                    break;
                }

                /* Allocate an array buffer. */
                u8 *array_buffer = static_cast<u8 *>(Allocate(entry_header.array_buffer_size));
                if (array_buffer == nullptr) {
                    break;
                }
                ON_SCOPE_EXIT { Deallocate(array_buffer); };

                /* Read the array buffer data. */
                R_TRY(stream.ReadStream(std::addressof(read_size), array_buffer, entry_header.array_buffer_size));
                if (read_size != entry_header.array_buffer_size) {
                    break;
                }

                /* Create a record for the context. */
                auto record = std::make_unique<ContextRecord>();
                if (record == nullptr) {
                    break;
                }

                /* Initialize the record. */
                R_TRY(record->Initialize(std::addressof(ctx), array_buffer, entry_header.array_buffer_size));

                /* Submit the record. */
                R_TRY(Context::SubmitContextRecord(std::move(record)));
            }

            R_SUCCEED();
        }

        u32 GetForcedShutdownContextCount() {
            u32 count = 0;
            for (const auto &ctx : g_forced_shutdown_contexts) {
                if (ctx.field_count != 0) {
                    ++count;
                }
            }
            return count;
        }

        Result SaveForcedShutdownContextImpl() {
            /* Save context to file. */
            {
                /* Create the stream to write the context. */
                Stream stream;
                R_TRY(stream.OpenStream(ForcedShutdownContextFileName, StreamMode_Write, ForcedShutdownContextBufferSize));

                /* Write a context header. */
                const ForcedShutdownContextHeader header = { .version = ForcedShutdownContextVersion, .num_contexts = GetForcedShutdownContextCount(), };
                R_TRY(stream.WriteStream(reinterpret_cast<const u8 *>(std::addressof(header)), sizeof(header)));

                /* Write each context. */
                for (const auto &ctx : g_forced_shutdown_contexts) {
                    /* If the context has no fields, continue. */
                    if (ctx.field_count == 0) {
                        continue;
                    }

                    /* Write a context entry header. */
                    const ForcedShutdownContextEntry entry_header = {
                        .version           = ctx.version,
                        .category          = ctx.category,
                        .field_count       = ctx.field_count,
                        .array_buffer_size = ctx.array_buffer_size,
                    };
                    R_TRY(stream.WriteStream(reinterpret_cast<const u8 *>(std::addressof(entry_header)), sizeof(entry_header)));

                    /* Write all fields. */
                    for (u32 i = 0; i < ctx.field_count; ++i) {
                        R_TRY(stream.WriteStream(reinterpret_cast<const u8 *>(ctx.fields + i), sizeof(ctx.fields[0])));
                    }

                    /* Write the array buffer. */
                    R_TRY(stream.WriteStream(ctx.array_buffer, ctx.array_buffer_size));
                }
            }

            /* Commit the context. */
            R_TRY(Stream::CommitStream());

            R_SUCCEED();
        }

    }

    os::Event *GetForcedShutdownUpdateEvent() {
        return std::addressof(g_forced_shutdown_update_event);
    }

    void InitializeForcedShutdownDetection() {
        /* Check if the forced shutdown context exists; if it doesn't, we should create an empty one. */
        if (!IsForceShutdownDetected()) {
            /* NOTE: Nintendo does not check result here. */
            CreateForcedShutdownContext();
            return;
        }

        /* Load the forced shutdown context. */
        /* NOTE: Nintendo does not check that this succeeds. */
        LoadForcedShutdownContext();

        /* Create report for the forced shutdown. */
        /* NOTE: Nintendo does not check that this succeeds. */
        CreateReportForForcedShutdown();

        /* Clear the forced shutdown categories. */
        /* NOTE: Nintendo does not check that this succeeds. */
        Context::ClearContext(CategoryId_RunningApplicationInfo);
        Context::ClearContext(CategoryId_RunningAppletInfo);
        Context::ClearContext(CategoryId_FocusedAppletHistoryInfo);

        /* Save the forced shutdown context. */
        /* NOTE: Nintendo does not check that this succeeds. */
        SaveForcedShutdownContext();
    }

    void FinalizeForcedShutdownDetection() {
        /* Try to delete the context. */
        const Result result = Stream::DeleteStream(ForcedShutdownContextFileName);
        if (!fs::ResultPathNotFound::Includes(result)) {
            /* We must have succeeded, if the file existed. */
            R_ABORT_UNLESS(result);

            /* Commit the deletion. */
            R_ABORT_UNLESS(Stream::CommitStream());
        }
    }

    void SaveForcedShutdownContext() {
        /* NOTE: Nintendo does not check that saving the report succeeds. */
        SaveForcedShutdownContextImpl();
    }

    void SubmitContextForForcedShutdownDetection(const ContextEntry *entry, const u8 *data, u32 data_size) {
        /* If the context entry matches one of our tracked categories, update our stored category. */
        for (auto &ctx : g_forced_shutdown_contexts) {
            /* Check for a match. */
            if (ctx.category != entry->category) {
                continue;
            }

            /* If we have an existing array buffer, free it. */
            if (ctx.array_buffer != nullptr) {
                Deallocate(ctx.array_buffer);
                ctx.array_buffer      = nullptr;
                ctx.array_buffer_size = 0;
                ctx.array_free_count  = 0;
            }

            /* Copy in the context. */
            ctx = *entry;

            /* Add the submitted data. */
            if (data != nullptr && data_size > 0) {
                /* Allocate new array buffer. */
                ctx.array_buffer = static_cast<u8 *>(Allocate(data_size));
                if (ctx.array_buffer == nullptr) {
                    /* We failed to allocate; this is okay, but clear our field count. */
                    ctx.field_count = 0;
                    break;
                }

                /* Copy in the data. */
                std::memcpy(ctx.array_buffer, data, data_size);

                /* Set buffer extents. */
                ctx.array_buffer_size = data_size;
                ctx.array_free_count  = 0;
            } else {
                ctx.array_buffer      = nullptr;
                ctx.array_buffer_size = 0;
                ctx.array_free_count  = 0;
            }

            /* Signal, to notify that we had an update. */
            g_forced_shutdown_update_event.Signal();

            /* We're done processing, since we found a match. */
            break;
        }
    }

    Result InvalidateForcedShutdownDetection() {
        /* Delete the forced shutdown context. */
        R_TRY(Stream::DeleteStream(ForcedShutdownContextFileName));

        /* Commit the deletion. */
        R_TRY(Stream::CommitStream());

        R_SUCCEED();
    }


}
