/*========================== begin_copyright_notice ============================

Copyright (C) 2017-2021 Intel Corporation

SPDX-License-Identifier: MIT

============================= end_copyright_notice ===========================*/

#include "ocl_igc_interface/impl/fcl_ocl_translation_ctx_impl.h"
#include "AdaptorCommon/customApi.hpp"
#include "common/StringMacros.hpp"
#include "common/VCPlatformSelector.hpp"
#include "common/debug/Dump.hpp"
#include "ocl_igc_interface/fcl_ocl_translation_ctx.h"
#include "ocl_igc_interface/impl/platform_impl.h"
#include "3d/common/iStdLib/utility.h"
#include "3d/common/iStdLib/File.h"
#include "OCLFE/igd_fcl_mcl/headers/clang_tb.h"

#include <optional>

#pragma warning(disable : 4141)
#pragma warning(disable : 4146)
#pragma warning(disable : 4242)
#include <llvm/ADT/Triple.h>
#include <llvm/ADT/STLExtras.h>
#include <llvm/Support/CommandLine.h>
#include <llvm/Support/FileSystem.h>
#include <llvm/Support/Path.h>
#include <llvm/Support/Process.h>
#include <llvm/Support/Program.h>
#include <llvm/Support/DynamicLibrary.h>
#include <llvm/Support/StringSaver.h>
#include <llvm/Support/Host.h>
#include <llvmWrapper/ADT/Optional.h>
#pragma warning(default : 4242)
#pragma warning(default : 4146)
#pragma warning(default : 4141)

#include <algorithm>
#include <numeric>
#include <fstream>
#include <iomanip>
#include <iostream>
#include <optional>
#include <string>
#include <vector>
#if defined(__ANDROID__)
#include <sstream>
#endif

#if defined(_DEBUG) || defined(_INTERNAL)
#include <numeric>
#include <array>
#endif // defined( _DEBUG ) || defined( _INTERNAL )
#if defined(IGC_VC_ENABLED)
#include "Frontend.h"
#if defined(_WIN32)
#include <Windows.h>
#include "inc/common/DriverStore.h"
#endif
#endif // defined(IGC_VC_ENABLED)

#include "cif/macros/enable.h"

#include <memory>

namespace {

using HashType = unsigned long long;

struct FeShaderDumpData {
  struct DumpDescriptor {
    llvm::StringRef OutputName;
    llvm::StringRef Contents;
  };
  FeShaderDumpData(PLATFORM *Platform, const char *Selected, int Stepping, CIF::Builtins::BufferSimple *src,
                   CIF::Builtins::BufferSimple *options, CIF::Builtins::BufferSimple *internalOptions,
                   CIF::Builtins::BufferSimple *tracingOptions);
  std::vector<DumpDescriptor> getDumps() const;

private:
  std::string PlatformInformation;
  CIF::Builtins::BufferSimple *InputSource = nullptr;
  CIF::Builtins::BufferSimple *Options = nullptr;
  CIF::Builtins::BufferSimple *InternalOptions = nullptr;
  CIF::Builtins::BufferSimple *TracingOptions = nullptr;
};

FeShaderDumpData::FeShaderDumpData(PLATFORM *Platform, const char *Selected, int Stepping,
                                   CIF::Builtins::BufferSimple *Src, CIF::Builtins::BufferSimple *Opts,
                                   CIF::Builtins::BufferSimple *InternalOpts,
                                   CIF::Builtins::BufferSimple *TracingOpts) {
  std::ostringstream Os;
  if (Platform) {
    auto Core = Platform->eDisplayCoreFamily;
    auto RenderCore = Platform->eRenderCoreFamily;
    auto Product = Platform->eProductFamily;
    auto RevId = Platform->usRevId;

    Os << "NEO passed: DisplayCore = " << Core << ", RenderCore = " << RenderCore << ", Product = " << Product
       << ", Revision = " << RevId << "\n";
    Os << "IGC translated into: " << Selected << ", " << Stepping << "\n";
  } else {
    Os << "Nothing came from NEO\n";
  }
  PlatformInformation = Os.str();

  InputSource = Src;
  Options = Opts;
  InternalOptions = InternalOpts;
  TracingOptions = TracingOpts;
}

std::vector<FeShaderDumpData::DumpDescriptor> FeShaderDumpData::getDumps() const {
  auto AsStringRef = [](CIF::Builtins::BufferSimple *Src) {
    if (!Src)
      return llvm::StringRef();
    size_t Size = Src->GetSize<char>();
    const char *Buff = Src->GetMemory<char>();
    return llvm::StringRef(Buff, Size);
  };
  return {{"platform", PlatformInformation.c_str()},
          {"source_code", AsStringRef(InputSource)},
          {"options", AsStringRef(Options)},
          {"internal_options", AsStringRef(InternalOptions)},
          {"tracing_options", AsStringRef(TracingOptions)}};
}

std::optional<FeShaderDumpData> getShaderDumpData(PLATFORM *Platform, const char *Selected, int Stepping,
                                                  CIF::Builtins::BufferSimple *Src,
                                                  CIF::Builtins::BufferSimple *Options,
                                                  CIF::Builtins::BufferSimple *InternalOptions,
                                                  CIF::Builtins::BufferSimple *TracingOptions) {
#if defined(_DEBUG) || defined(_INTERNAL)
  if (!FCL_IGC_IS_FLAG_ENABLED(ShaderDumpEnable))
    return {};

  return FeShaderDumpData(Platform, Selected, Stepping, Src, Options, InternalOptions, TracingOptions);
#else
  return {};
#endif
}

void emitFCLDump(llvm::StringRef FilenamePrefix, FeShaderDumpData::DumpDescriptor DataToDump) {
#if defined(_DEBUG) || defined(_INTERNAL)
  // do factual dump, this part can be replaced to DumpShaderFile
  const char *DstDir = FCL::GetShaderOutputFolder();
  std::ostringstream FullPath(DstDir, std::ostringstream::ate);
  FullPath << FilenamePrefix.str() << "_" << DataToDump.OutputName.str() << ".txt";
  // NOTE: for now, we expect only text data here
  std::ofstream OutF(FullPath.str(), std::ofstream::out);
  // if we can create dump file at all...
  if (OutF)
    OutF.write(DataToDump.Contents.data(), DataToDump.Contents.size());
#endif
}

std::string makeShaderDumpPrefixWithHash(llvm::StringRef Prefix, HashType Hash) {
  std::ostringstream OS;
  OS << Prefix.str() << std::hex << std::setfill('0') << std::setw(sizeof(Hash) * CHAR_BIT / 4) << Hash;
  return OS.str();
}

void DoShaderDumpPreFE(const FeShaderDumpData &DumpInfo) {
  auto Dumps = DumpInfo.getDumps();
  auto Hash = std::accumulate(Dumps.begin(), Dumps.end(), 0ull, [](const auto &HashVal, const auto &DI) {
    const auto *BuffStart = DI.Contents.data();
    auto BuffSize = DI.Contents.size();
    return HashVal ^ iSTD::HashFromBuffer(BuffStart, BuffSize);
  });
  auto DumpNamePrefix = makeShaderDumpPrefixWithHash("VC_fe_", Hash);
  for (const auto &DumpDescriptor : Dumps)
    emitFCLDump(DumpNamePrefix, DumpDescriptor);
};

void DoShaderDumpPostFE(const FeShaderDumpData &DumpInfo, HashType Hash) {
  auto DumpNamePrefix = makeShaderDumpPrefixWithHash("VC_PostFe_", Hash);
  for (const auto &DumpDescriptor : DumpInfo.getDumps())
    emitFCLDump(DumpNamePrefix, DumpDescriptor);
};

} // namespace

namespace IGC {

OclTranslationOutputBase *CIF_GET_INTERFACE_CLASS(FclOclTranslationCtx, 1)::TranslateImpl(
    CIF::Version_t outVersion, CIF::Builtins::BufferSimple *src, CIF::Builtins::BufferSimple *options,
    CIF::Builtins::BufferSimple *internalOptions, CIF::Builtins::BufferSimple *tracingOptions,
    uint32_t tracingOptionsCount) {

  return CIF_GET_PIMPL()->Translate(outVersion, src, options, internalOptions, tracingOptions, tracingOptionsCount);
}

void CIF_GET_INTERFACE_CLASS(FclOclTranslationCtx, 2)::GetFclOptions(CIF::Builtins::BufferSimple *opts) {
  CIF_GET_PIMPL()->GetFclOptions(opts);
}

void CIF_GET_INTERFACE_CLASS(FclOclTranslationCtx, 2)::GetFclInternalOptions(CIF::Builtins::BufferSimple *opts) {
  CIF_GET_PIMPL()->GetFclInternalOptions(opts);
}

std::optional<std::vector<char>> readBinaryFile(const std::string &fileName) {
  std::ifstream file(fileName, std::ios_base::binary);
  if (!file.good()) {
    return std::nullopt;
  }
  size_t length;
  file.seekg(0, file.end);
  length = static_cast<size_t>(file.tellg());
  file.seekg(0, file.beg);
  std::vector<char> binary(length);
  file.read(binary.data(), length);
  return binary;
}

#if defined(IGC_VC_ENABLED)

using InvocationInfo = IGC::AdaptorCM::Frontend::IDriverInvocation;
using PathT = llvm::SmallVector<char, 1024>;

using CMStringVect = IGC::AdaptorCM::Frontend::StringVect_t;
static std::vector<char> makeVcOptPayload(uint64_t IR_size, const std::string &VCApiOpts,
                                          const CMStringVect &VCLLVMOptions, const std::string &TargetFeaturesStr) {
  std::string InternalVcOptions;
  if (!VCLLVMOptions.empty()) {
    InternalVcOptions += " -llvm-options='" + llvm::join(VCLLVMOptions, " ") + "'";
  }
  if (!TargetFeaturesStr.empty()) {
    InternalVcOptions.append(" -target-features=").append(TargetFeaturesStr);
  }
  // Payload format:
  // |-vc-payload|api opts|internal opts|i64(IR size)|i64(Payload size)|-vc-payload|
  // NOTE: <api/internal opts> are c-strings.
  //
  // Should be in sync with:
  //      Source/IGC/AdaptorOCL/dllInterfaceCompute.cpp
  const std::string PayloadMarker = "-vc-payload";
  std::vector<char> VcPayload;
  VcPayload.insert(VcPayload.end(), PayloadMarker.begin(), PayloadMarker.end());
  VcPayload.insert(VcPayload.end(), VCApiOpts.begin(), VCApiOpts.end());
  VcPayload.push_back(0); // zero-terminate api options
  VcPayload.insert(VcPayload.end(), InternalVcOptions.begin(), InternalVcOptions.end());
  VcPayload.push_back(0); // zero-terminate internal options
  uint64_t payloadSize = VcPayload.size() +
                         /* as suffix */ PayloadMarker.size() +
                         /* for sanity-checks */ +sizeof(IR_size) +
                         /* include the size itself */ +sizeof(uint64_t);
  VcPayload.insert(VcPayload.end(), reinterpret_cast<const char *>(&IR_size),
                   reinterpret_cast<const char *>(&IR_size + 1));
  VcPayload.insert(VcPayload.end(), reinterpret_cast<const char *>(&payloadSize),
                   reinterpret_cast<const char *>(&payloadSize + 1));
  VcPayload.insert(VcPayload.end(), PayloadMarker.begin(), PayloadMarker.end());
  return VcPayload;
}

static void finalizeFEOutput(const IGC::AdaptorCM::Frontend::IOutputArgs &FEOutput, const std::string &VCApiOptions,
                             const CMStringVect &VCLLVMOptions, const std::string &TargetFeatures,
                             OclTranslationOutputBase &Output, const std::optional<FeShaderDumpData> &ShaderDumpInfo) {
  auto &OutputInterface = *Output.GetImpl();
  const auto &ErrLog = FEOutput.getLog();
  if (FEOutput.getStatus() == Intel::CM::ClangFE::IOutputArgs::ErrT::COMPILE_PROGRAM_FAILURE) {
    if (ErrLog.empty())
      OutputInterface.SetError(TranslationErrorType::Internal, "unknown error during cm source compilation");
    else
      OutputInterface.SetError(TranslationErrorType::UnhandledInput, ErrLog.c_str());
    return;
  }

  if (!ErrLog.empty())
    OutputInterface.AddWarning(ErrLog);

  const auto &IR = FEOutput.getIR();
  // This is where the tricky part starts
  // Right now we have no way to pass auxiliary options to vc-codegen backend
  // So we introduce a temporary hack to incorporate the options
  // into the underlying buffer which contains SPIRV IR
  std::vector<char> Payload = makeVcOptPayload(IR.size(), VCApiOptions, VCLLVMOptions, TargetFeatures);

  std::vector<char> FinalOutput;
  FinalOutput.reserve(IR.size() + Payload.size());
  FinalOutput.insert(FinalOutput.end(), IR.begin(), IR.end());
  FinalOutput.insert(FinalOutput.end(), Payload.begin(), Payload.end());

  if (!OutputInterface.SetSuccessfulAndCloneOutput(FinalOutput.data(), FinalOutput.size())) {
    OutputInterface.SetError(TranslationErrorType::Internal, "OOM (cm FE)");
    return;
  }

  if (ShaderDumpInfo) {
    auto HashValue = iSTD::HashFromBuffer(FinalOutput.data(), FinalOutput.size());
    DoShaderDumpPostFE(*ShaderDumpInfo, HashValue);
  }
}

static std::optional<std::string> MakeTemporaryCMSource(CIF::Builtins::BufferSimple *Src, std::string tmpFilename,
                                                        OclTranslationOutputBase &outI);

static bool processCmSrcOptions(llvm::SmallVectorImpl<const char *> &userArgs, std::string optname,
                                std::string &inputFile) {

  auto toErase = std::find_if(userArgs.begin(), userArgs.end(),
                              [&optname](const auto &Item) { return std::strcmp(Item, optname.c_str()) == 0; });
  if (toErase != userArgs.end()) {
    auto itFilename = toErase + 1;
    if (itFilename != userArgs.end() && strcmp(*itFilename, "--") != 0) {
      inputFile = *itFilename;
      userArgs.erase(toErase, toErase + 2);
      return true;
    }
  }

  optname += "=";
  toErase = std::find_if(userArgs.begin(), userArgs.end(), [&optname](const auto &Item) {
    llvm::StringRef S = Item;
    return S.startswith(optname);
  });
  if (toErase != userArgs.end()) {
    inputFile = *toErase;
    inputFile = inputFile.substr(optname.length());
    userArgs.erase(toErase);
    return true;
  }

  return false;
}

static std::vector<const char *> processFeOptions(llvm::sys::DynamicLibrary &LibInfo, CIF::Builtins::BufferSimple *Src,
                                                  OclTranslationOutputBase &outI, CIF::Builtins::BufferSimple *options,
                                                  llvm::StringSaver &stringSaver, const char *platform,
                                                  unsigned stepping, bool &isMemFile) {

  llvm::SmallVector<const char *, 20> userArgs;

  std::string userArgsStr{options->GetMemory<char>(), options->GetSize<char>()};
  if (llvm::Triple(llvm::sys::getProcessTriple()).isOSWindows())
    llvm::cl::TokenizeWindowsCommandLine(userArgsStr, stringSaver, userArgs);
  else
    llvm::cl::TokenizeGNUCommandLine(userArgsStr, stringSaver, userArgs);

  auto toErase =
      std::remove_if(userArgs.begin(), userArgs.end(), [](const auto &Item) { return std::strcmp(Item, "-cmc") == 0; });
  userArgs.erase(toErase, userArgs.end());

  // this was old hack before FE can pass platform, now we prefer to use argument
  // but if it is null, it may be still useful, so let it be for a while
  auto cmfeDefaultArchOpt = IGCLLVM::makeOptional(llvm::sys::Process::GetEnv("IGC_CMFE_DEFAULT_ARCH"));
  const std::string &cmfeDefaultArch = cmfeDefaultArchOpt ? cmfeDefaultArchOpt.value() : "";

  std::string inputFile = "src.cm";
  isMemFile = processCmSrcOptions(userArgs, "-cm-src", inputFile) || processCmSrcOptions(userArgs, "-s", inputFile);
  if (!isMemFile) {
    auto OptSrc = MakeTemporaryCMSource(Src, inputFile, outI);
    if (!OptSrc)
      return {};
    inputFile = OptSrc.value();
  }

  std::vector<const char *> result = {
      "-emit-spirv",
  };

  // Pass the runtime-specified architecture.
  // User still can override it if -march is passed in user arguments
  if (platform)
    result.push_back(stringSaver.save("-march=" + std::string(platform)).data());
  else
    result.push_back(stringSaver.save("-march=" + cmfeDefaultArch).data());

  result.push_back(stringSaver.save(inputFile).data());
  result.insert(result.end(), userArgs.begin(), userArgs.end());

  auto ExtraCMOpts = IGCLLVM::makeOptional(llvm::sys::Process::GetEnv("IGC_ExtraCMOptions"));
  if (ExtraCMOpts) {
    llvm::SmallVector<const char *, 8> Argv;
    llvm::cl::TokenizeGNUCommandLine(ExtraCMOpts.value(), stringSaver, Argv);
    result.insert(result.end(), Argv.begin(), Argv.end());
  }

  return result;
}

// TODO: most probably we should remove this function once FrontendWrapper
// is capable to handle in-memory objects properly
static std::optional<std::string> MakeTemporaryCMSource(CIF::Builtins::BufferSimple *Src, std::string tmpFilename,
                                                        OclTranslationOutputBase &outI) {

  auto &outputInterface = *outI.GetImpl();

  assert(Src);

  PathT workDir;

  // TODO: resulting temporary file can collide with the existing one
  auto Err = llvm::sys::fs::getPotentiallyUniqueTempFileName("tmp_cm_to_spv", "", workDir);
  if (Err) {
    outputInterface.SetError(TranslationErrorType::Internal, "could not determine temporary directory name");
    return {};
  }
  if (llvm::sys::fs::create_directories(workDir)) {
    outputInterface.SetError(TranslationErrorType::Internal, "could not create temporary directory");
    return {};
  }

  size_t sizeToDump = Src->GetSize<char>();
  // TODO: consider removing this legacy code.
  // It looks like that we are trying to handle some kind of weird user input,
  // instead of reporting an error.
  while ((sizeToDump > 0) && (Src->GetMemory<char>()[sizeToDump - 1] == 0)) {
    --sizeToDump;
  }
  if (sizeToDump == 0) {
    outputInterface.SetError(TranslationErrorType::Internal, "effective input is zero");
    return {};
  }

  // Dump Src
  PathT srcPath{workDir.begin(), workDir.end()};
  llvm::sys::path::append(srcPath, tmpFilename);

  std::string strPath(srcPath.begin(), srcPath.end());
  std::ofstream TmpSrc(strPath, std::ios::out);
  TmpSrc.write(Src->GetMemory<char>(), sizeToDump);
  TmpSrc.close();
  if (!TmpSrc) {
    outputInterface.SetError(TranslationErrorType::Internal, "could not create temporary temporary file");
    return {};
  }
  return strPath;
}

// Retrieve directory where FEWrapper is located.
// Return driver store path on windows and empty path on linux.
static std::string getCMFEWrapperDir() {
#if defined(_WIN32)
  // Expand libname to full driver path on windows.
  char DriverPath[MAX_PATH] = {};
  GetDependencyPath(DriverPath, "");
  return DriverPath;
#else
  return "";
#endif
}

#endif // defined(IGC_VC_ENABLED)

OclTranslationOutputBase *CIF_PIMPL(FclOclTranslationCtx)::TranslateCM(CIF::Version_t outVersion,
                                                                       CIF::Builtins::BufferSimple *src,
                                                                       CIF::Builtins::BufferSimple *options,
                                                                       CIF::Builtins::BufferSimple *internalOptions,
                                                                       CIF::Builtins::BufferSimple *tracingOptions,
                                                                       uint32_t tracingOptionsCount) {

  // Output
  auto *outputInterface = CIF::InterfaceCreator<OclTranslationOutput>::CreateInterfaceVer(outVersion, outType);
  if (outputInterface == nullptr)
    return nullptr;

  PLATFORM *platformDescr = nullptr;
  const char *platformStr = nullptr;
  unsigned stepping = 0U;

  if (globalState.GetPlatformImpl()) {
    // NEO supports platform interface
    auto *PlatformImpl = globalState.GetPlatformImpl();
    platformDescr = &PlatformImpl->p;
    stepping = PlatformImpl->p.usRevId;
    platformStr = cmc::getPlatformStr(PlatformImpl->p, /* inout */ stepping);
  }

  // we need output shader dumps before reporting fatal error for
  // unsupported/empty platform
  auto ShaderDumpData = getShaderDumpData(platformDescr, platformStr ? platformStr : "(empty)", stepping, src, options,
                                          internalOptions, tracingOptions);
  if (ShaderDumpData)
    DoShaderDumpPreFE(*ShaderDumpData);

  if (platformStr == nullptr) {
    // llvm::report_fatal_error("TranslateCM: unsupported platform");
    // this is temporary to unblock customers
    platformStr = "SKL";
  }

  OclTranslationOutputBase &Out = *outputInterface;

#if defined(IGC_VC_ENABLED)
  auto ErrFn = [&Out](const std::string &Err) { Out.GetImpl()->SetError(TranslationErrorType::Internal, Err.c_str()); };

  auto MaybeFE = IGC::AdaptorCM::Frontend::makeFEWrapper(ErrFn, getCMFEWrapperDir());

  if (!MaybeFE)
    return outputInterface;

  llvm::BumpPtrAllocator A;
  llvm::StringSaver Saver(A);
  auto &FE = MaybeFE.value();
  bool isMemFile = false;
  auto FeArgs = processFeOptions(FE.LibInfo(), src, Out, options, Saver, platformStr, stepping, isMemFile);
  if (FeArgs.empty())
    return outputInterface; // proper error message is already set

  auto Drv = FE.buildDriverInvocation(FeArgs.size(), FeArgs.data());
  if (!Drv) {
    ErrFn("Null driver invocation in CMFE");
    return outputInterface;
  }
  if (Drv->getOutputType() != InvocationInfo::OutputTypeT::SPIRV) {
    ErrFn("CM frontend: unsupported output request");
    return outputInterface;
  }

  IGC::AdaptorCM::Frontend::InputArgs InputArgs;
  InputArgs.CompilationOpts = Drv->getFEArgs();
  // if real file is used, then don't fill InputArgs.InputText
  // with content of source to avoid compilation from
  // both memory and real temporary files
  if (isMemFile)
    InputArgs.InputText = src->GetMemory<char>();

  // CM frontend option parser is not thread-safe, so guard the frontend call
  // with a mutex.
  auto FEOutput = [&]() {
    static std::mutex M;
    const std::lock_guard<std::mutex> Lock(M);
    return FE.translate(InputArgs);
  }();

  if (!FEOutput) {
    ErrFn("Null output in CMFE");
    return outputInterface;
  }

  const auto &TargetFeatures = Drv->getTargetFeaturesStr();
  const auto &[VCLLVMOpts, VCFinalizerOpts] =
      IGC::AdaptorCM::Frontend::convertBackendArgsToVcAndFinalizerOpts(Drv->getBEArgs());
  auto VCApiOpts = FE.getVCApiOptions(&*Drv);
  if (!VCFinalizerOpts.empty())
    VCApiOpts += " -Xfinalizer '" + llvm::join(VCFinalizerOpts, " ") + "'";

  finalizeFEOutput(*FEOutput, VCApiOpts, VCLLVMOpts, TargetFeatures, Out, ShaderDumpData);

  FclInternalOpts = std::string(" -llvm-options='" + llvm::join(VCLLVMOpts, " ") + "'");
  FclOpts = VCApiOpts;

#else
  Out.GetImpl()->SetError(TranslationErrorType::Internal, "CM compilation is not supported in this configuration");
#endif // defined(IGC_VC_ENABLED)

  return outputInterface;
}

} // namespace IGC

#include "cif/macros/disable.h"
