// Copyright (c) 2023 Google LLC.
//
// 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 "extract_source.h"

#include <cassert>
#include <string>
#include <unordered_map>
#include <vector>

#include "source/latest_version_spirv_header.h"
#include "source/opt/log.h"
#include "spirv-tools/libspirv.hpp"
#include "tools/util/cli_consumer.h"

namespace {

constexpr auto kDefaultEnvironment = SPV_ENV_UNIVERSAL_1_6;

// Extract a string literal from a given range.
// Copies all the characters from `begin` to the first '\0' it encounters, while
// removing escape patterns.
// Not finding a '\0' before reaching `end` fails the extraction.
//
// Returns `true` if the extraction succeeded.
// `output` value is undefined if false is returned.
spv_result_t ExtractStringLiteral(const spv_position_t& loc, const char* begin,
                                  const char* end, std::string* output) {
  size_t sourceLength = std::distance(begin, end);
  std::string escapedString;
  escapedString.resize(sourceLength);

  size_t writeIndex = 0;
  size_t readIndex = 0;
  for (; readIndex < sourceLength; writeIndex++, readIndex++) {
    const char read = begin[readIndex];
    if (read == '\0') {
      escapedString.resize(writeIndex);
      output->append(escapedString);
      return SPV_SUCCESS;
    }

    if (read == '\\') {
      ++readIndex;
    }
    escapedString[writeIndex] = begin[readIndex];
  }

  spvtools::Error(spvtools::utils::CLIMessageConsumer, "", loc,
                  "Missing NULL terminator for literal string.");
  return SPV_ERROR_INVALID_BINARY;
}

spv_result_t extractOpString(const spv_position_t& loc,
                             const spv_parsed_instruction_t& instruction,
                             std::string* output) {
  assert(output != nullptr);
  assert(instruction.opcode == static_cast<unsigned>(spv::Op::OpString));
  if (instruction.num_operands != 2) {
    spvtools::Error(spvtools::utils::CLIMessageConsumer, "", loc,
                    "Missing operands for OpString.");
    return SPV_ERROR_INVALID_BINARY;
  }

  const auto& operand = instruction.operands[1];
  const char* stringBegin =
      reinterpret_cast<const char*>(instruction.words + operand.offset);
  const char* stringEnd = reinterpret_cast<const char*>(
      instruction.words + operand.offset + operand.num_words);
  return ExtractStringLiteral(loc, stringBegin, stringEnd, output);
}

spv_result_t extractOpSourceContinued(
    const spv_position_t& loc, const spv_parsed_instruction_t& instruction,
    std::string* output) {
  assert(output != nullptr);
  assert(instruction.opcode ==
         static_cast<unsigned>(spv::Op::OpSourceContinued));
  if (instruction.num_operands != 1) {
    spvtools::Error(spvtools::utils::CLIMessageConsumer, "", loc,
                    "Missing operands for OpSourceContinued.");
    return SPV_ERROR_INVALID_BINARY;
  }

  const auto& operand = instruction.operands[0];
  const char* stringBegin =
      reinterpret_cast<const char*>(instruction.words + operand.offset);
  const char* stringEnd = reinterpret_cast<const char*>(
      instruction.words + operand.offset + operand.num_words);
  return ExtractStringLiteral(loc, stringBegin, stringEnd, output);
}

spv_result_t extractOpSource(const spv_position_t& loc,
                             const spv_parsed_instruction_t& instruction,
                             spv::Id* filename, std::string* code) {
  assert(filename != nullptr && code != nullptr);
  assert(instruction.opcode == static_cast<unsigned>(spv::Op::OpSource));
  // OpCode [ Source Language | Version | File (optional) | Source (optional) ]
  if (instruction.num_words < 3) {
    spvtools::Error(spvtools::utils::CLIMessageConsumer, "", loc,
                    "Missing operands for OpSource.");
    return SPV_ERROR_INVALID_BINARY;
  }

  *filename = 0;
  *code = "";
  if (instruction.num_words < 4) {
    return SPV_SUCCESS;
  }
  *filename = instruction.words[3];

  if (instruction.num_words < 5) {
    return SPV_SUCCESS;
  }

  const char* stringBegin =
      reinterpret_cast<const char*>(instruction.words + 4);
  const char* stringEnd =
      reinterpret_cast<const char*>(instruction.words + instruction.num_words);
  return ExtractStringLiteral(loc, stringBegin, stringEnd, code);
}

}  // namespace

bool ExtractSourceFromModule(
    const std::vector<uint32_t>& binary,
    std::unordered_map<std::string, std::string>* output) {
  auto context = spvtools::SpirvTools(kDefaultEnvironment);
  context.SetMessageConsumer(spvtools::utils::CLIMessageConsumer);

  // There is nothing valuable in the header.
  spvtools::HeaderParser headerParser = [](const spv_endianness_t,
                                           const spv_parsed_header_t&) {
    return SPV_SUCCESS;
  };

  std::unordered_map<uint32_t, std::string> stringMap;
  std::vector<std::pair<spv::Id, std::string>> sources;
  spv::Op lastOpcode = spv::Op::Max;
  size_t instructionIndex = 0;

  spvtools::InstructionParser instructionParser =
      [&stringMap, &sources, &lastOpcode,
       &instructionIndex](const spv_parsed_instruction_t& instruction) {
        const spv_position_t loc = {0, 0, instructionIndex + 1};
        spv_result_t result = SPV_SUCCESS;

        if (instruction.opcode == static_cast<unsigned>(spv::Op::OpString)) {
          std::string content;
          result = extractOpString(loc, instruction, &content);
          if (result == SPV_SUCCESS) {
            stringMap.emplace(instruction.result_id, std::move(content));
          }
        } else if (instruction.opcode ==
                   static_cast<unsigned>(spv::Op::OpSource)) {
          spv::Id filenameId;
          std::string code;
          result = extractOpSource(loc, instruction, &filenameId, &code);
          if (result == SPV_SUCCESS) {
            sources.emplace_back(std::make_pair(filenameId, std::move(code)));
          }
        } else if (instruction.opcode ==
                   static_cast<unsigned>(spv::Op::OpSourceContinued)) {
          if (lastOpcode != spv::Op::OpSource) {
            spvtools::Error(spvtools::utils::CLIMessageConsumer, "", loc,
                            "OpSourceContinued MUST follow an OpSource.");
            return SPV_ERROR_INVALID_BINARY;
          }

          assert(sources.size() > 0);
          result = extractOpSourceContinued(loc, instruction,
                                            &sources.back().second);
        }

        ++instructionIndex;
        lastOpcode = static_cast<spv::Op>(instruction.opcode);
        return result;
      };

  if (!context.Parse(binary, headerParser, instructionParser)) {
    return false;
  }

  std::string defaultName = "unnamed-";
  size_t unnamedCount = 0;
  for (auto & [ id, code ] : sources) {
    std::string filename;
    const auto it = stringMap.find(id);
    if (it == stringMap.cend() || it->second.empty()) {
      filename = "unnamed-" + std::to_string(unnamedCount) + ".hlsl";
      ++unnamedCount;
    } else {
      filename = it->second;
    }

    if (output->count(filename) != 0) {
      spvtools::Error(spvtools::utils::CLIMessageConsumer, "", {},
                      "Source file name conflict.");
      return false;
    }
    output->insert({filename, code});
  }

  return true;
}
