/**
 * Copyright (c) 2021-2022 Huawei Device Co., Ltd.
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

#include "public.h"
#include "public_internal.h"
#include "runtime/include/mem/allocator.h"
#include "verification/config/config_load.h"
#include "verification/config/context/context.h"
#include "verification/cache/results_cache.h"
#include "verification/jobs/service.h"
#include "verification/jobs/job.h"

namespace panda::verifier {

Config *NewConfig()
{
    auto result = new Config;
    result->opts.Initialize();
    return result;
}

bool LoadConfigFile(Config *config, std::string_view configFileName)
{
    return panda::verifier::config::LoadConfig(config, configFileName);
}

void DestroyConfig(Config *config)
{
    config->opts.Destroy();
    delete config;
}

bool IsEnabled(Config const *config)
{
    ASSERT(config != nullptr);
    return config->opts.IsEnabled();
}

bool IsOnlyVerify(Config const *config)
{
    ASSERT(config != nullptr);
    return config->opts.IsOnlyVerify();
}

Service *CreateService(Config const *config, panda::mem::InternalAllocatorPtr allocator, ClassLinker *linker,
                       std::string const &cacheFileName)
{
    if (!cacheFileName.empty()) {
        VerificationResultCache::Initialize(cacheFileName);
    }
    auto res = allocator->New<Service>();
    res->config = config;
    res->classLinker = linker;
    res->allocator = allocator;
    res->config = config;
    res->verifierService = allocator->New<VerifierService>(allocator, linker);
    res->verifierService->Init();
    res->debugCtx.SetConfig(&config->debugCfg);
    return res;
}

void DestroyService(Service *service, bool updateCacheFile)
{
    if (service == nullptr) {
        return;
    }
    VerificationResultCache::Destroy(updateCacheFile);
    auto allocator = service->allocator;
    service->verifierService->Destroy();
    allocator->Delete(service->verifierService);
    allocator->Delete(service);
}

Config const *GetServiceConfig(Service const *service)
{
    return service->config;
}

// Do we really need this public/private distinction here?
inline Status ToPublic(VerificationResultCache::Status status)
{
    switch (status) {
        case VerificationResultCache::Status::OK:
            return Status::OK;
        case VerificationResultCache::Status::FAILED:
            return Status::FAILED;
        case VerificationResultCache::Status::UNKNOWN:
            return Status::UNKNOWN;
        default:
            UNREACHABLE();
    }
}

static void ReportStatus(Service const *service, Method const *method, std::string const &status)
{
    if (service->config->opts.show.status) {
        LOG(DEBUG, VERIFIER) << "Verification result for method " << method->GetFullName(true) << ": " << status;
    }
}

static bool VerifyClass(Class *clazz)
{
    auto *langPlugin = plugin::GetLanguagePlugin(clazz->GetSourceLang());
    auto result = langPlugin->CheckClass(clazz);
    if (!result.IsOk()) {
        LOG(ERROR, VERIFIER) << result.msg << " " << clazz->GetName();
        clazz->SetState(Class::State::ERRONEOUS);
        return false;
    }

    Class *parent = clazz->GetBase();
    if (parent != nullptr && parent->IsFinal()) {
        LOG(ERROR, VERIFIER) << "Cannot inherit from final class: " << clazz->GetName() << "->" << parent->GetName();
        clazz->SetState(Class::State::ERRONEOUS);
        return false;
    }

    clazz->SetState(Class::State::VERIFIED);
    return true;
}

Status Verify(Service *service, panda::Method *method, VerificationMode mode)
{
    using VStage = Method::VerificationStage;
    ASSERT(service != nullptr);

    if (method->IsIntrinsic()) {
        return Status::OK;
    }

    /*
     *  Races are possible where the same method gets simultaneously verified on different threads.
     *  But those are harmless, so we ignore them.
     */
    auto stage = method->GetVerificationStage();
    if (stage == VStage::VERIFIED_OK) {
        return Status::OK;
    }
    if (stage == VStage::VERIFIED_FAIL) {
        return Status::FAILED;
    }

    if (!IsEnabled(mode)) {
        ReportStatus(service, method, "SKIP");
        return Status::OK;
    }
    if (method->GetInstructions() == nullptr) {
        LOG(DEBUG, VERIFIER) << method->GetFullName(true) << " has no code, no meaningful verification";
        return Status::OK;
    }
    service->debugCtx.AddMethod(*method, mode == VerificationMode::DEBUG);
    if (service->debugCtx.SkipVerification(method->GetUniqId())) {
        LOG(DEBUG, VERIFIER) << "Skipping verification of '" << method->GetFullName()
                             << "' according to verifier configuration";
        return Status::OK;
    }

    auto uniqId = method->GetUniqId();
    auto methodName = method->GetFullName();
    if (VerificationResultCache::Enabled()) {
        auto cachedStatus = ToPublic(VerificationResultCache::Check(uniqId));
        if (cachedStatus != Status::UNKNOWN) {
            LOG(DEBUG, VERIFIER) << "Verification result of method '" << methodName
                                 << "' was cached: " << (cachedStatus == Status::OK ? "OK" : "FAIL");
            return cachedStatus;
        }
    }

    auto lang = method->GetClass()->GetSourceLang();
    auto *processor = service->verifierService->GetProcessor(lang);

    if (processor == nullptr) {
        LOG(INFO, VERIFIER) << "Attempt to  verify " << method->GetFullName(true)
                            << "after service started shutdown, ignoring";
        return Status::FAILED;
    }

    auto const &methodOptions = service->config->opts.debug.GetMethodOptions();
    auto const &verifMethodOptions = methodOptions[methodName];
    LOG(DEBUG, VERIFIER) << "Verification config for '" << methodName << "': " << verifMethodOptions.GetName();
    LOG(INFO, VERIFIER) << "Started verification of method '" << method->GetFullName(true) << "'";

    // class verification can be called concurrently
    if ((method->GetClass()->GetState() < Class::State::VERIFIED) && !VerifyClass(method->GetClass())) {
        return Status::FAILED;
    }
    Job job {service, method, verifMethodOptions};
    bool result = job.DoChecks(processor->GetTypeSystem());

    LOG(DEBUG, VERIFIER) << "Verification result for '" << methodName << "': " << result;

    service->verifierService->ReleaseProcessor(processor);

    VerificationResultCache::CacheResult(uniqId, result);

    if (result) {
        method->SetVerificationStage(VStage::VERIFIED_OK);
        ReportStatus(service, method, "OK");
        return Status::OK;
    }
    method->SetVerificationStage(VStage::VERIFIED_FAIL);
    ReportStatus(service, method, "FAIL");
    return Status::FAILED;
}

}  // namespace panda::verifier