//===----------------------------------------------------------------------===//
//
// Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions.
// See https://llvm.org/LICENSE.txt for license information.
// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
//
//===----------------------------------------------------------------------===//

#include "llvm/DWARFCFIChecker/DWARFCFIState.h"
#include "llvm/BinaryFormat/Dwarf.h"
#include "llvm/DebugInfo/DWARF/LowLevel/DWARFUnwindTable.h"
#include "llvm/MC/MCDwarf.h"
#include "llvm/Support/Error.h"
#include "llvm/Support/ErrorHandling.h"
#include "llvm/Support/FormatVariadic.h"
#include <cassert>
#include <optional>

using namespace llvm;

std::optional<dwarf::UnwindRow> DWARFCFIState::getCurrentUnwindRow() const {
  if (!IsInitiated)
    return std::nullopt;
  return Row;
}

void DWARFCFIState::update(const MCCFIInstruction &Directive) {
  auto CFIP = convert(Directive);

  // This is a copy of the current row, its value will be updated by
  // `parseRows`.
  dwarf::UnwindRow NewRow = Row;

  // `parseRows` updates the current row by applying the `CFIProgram` to it.
  // During this process, it may create multiple rows preceding the newly
  // updated row and following the previous rows. These middle rows are stored
  // in `PrecedingRows`. For now, there is no need to store these rows in the
  // state, so they are ignored in the end.
  dwarf::UnwindTable::RowContainer PrecedingRows;

  // TODO: `.cfi_remember_state` and `.cfi_restore_state` directives are not
  // supported yet. The reason is that `parseRows` expects the stack of states
  // to be produced and used in a single `CFIProgram`. However, in this use
  // case, each instruction creates its own `CFIProgram`, which means the stack
  // of states is forgotten between instructions. To fix it, `parseRows` should
  // be refactored to read the current stack of states from the argument and
  // update it based on the `CFIProgram.`
  if (Error Err = parseRows(CFIP, NewRow, nullptr).takeError()) {
    Context->reportError(
        Directive.getLoc(),
        formatv("could not parse this CFI directive due to: {0}",
                toString(std::move(Err))));

    // Proceed the analysis by ignoring this CFI directive.
    return;
  }

  Row = NewRow;
  IsInitiated = true;
}

dwarf::CFIProgram DWARFCFIState::convert(MCCFIInstruction Directive) {
  auto CFIP = dwarf::CFIProgram(
      /* CodeAlignmentFactor */ 1, /* DataAlignmentFactor */ 1,
      Context->getTargetTriple().getArch());

  auto MaybeCurrentRow = getCurrentUnwindRow();
  switch (Directive.getOperation()) {
  case MCCFIInstruction::OpSameValue:
    CFIP.addInstruction(dwarf::DW_CFA_same_value, Directive.getRegister());
    break;
  case MCCFIInstruction::OpRememberState:
    // TODO: remember state is not supported yet, the following line does not
    // work:
    // CFIP.addInstruction(dwarf::DW_CFA_remember_state);
    // The reason is explained in the `DWARFCFIState::update` method where
    // `dwarf::parseRows` is used.
    Context->reportWarning(Directive.getLoc(),
                           "this directive is not supported, ignoring it");
    break;
  case MCCFIInstruction::OpRestoreState:
    // TODO: restore state is not supported yet, the following line does not
    // work:
    // CFIP.addInstruction(dwarf::DW_CFA_restore_state);
    // The reason is explained in the `DWARFCFIState::update` method where
    // `dwarf::parseRows` is used.
    Context->reportWarning(Directive.getLoc(),
                           "this directive is not supported, ignoring it");
    break;
  case MCCFIInstruction::OpOffset:
    CFIP.addInstruction(dwarf::DW_CFA_offset, Directive.getRegister(),
                        Directive.getOffset());
    break;
  case MCCFIInstruction::OpLLVMDefAspaceCfa:
    CFIP.addInstruction(dwarf::DW_CFA_LLVM_def_aspace_cfa,
                        Directive.getRegister());
    break;
  case MCCFIInstruction::OpDefCfaRegister:
    CFIP.addInstruction(dwarf::DW_CFA_def_cfa_register,
                        Directive.getRegister());
    break;
  case MCCFIInstruction::OpDefCfaOffset:
    CFIP.addInstruction(dwarf::DW_CFA_def_cfa_offset, Directive.getOffset());
    break;
  case MCCFIInstruction::OpDefCfa:
    CFIP.addInstruction(dwarf::DW_CFA_def_cfa, Directive.getRegister(),
                        Directive.getOffset());
    break;
  case MCCFIInstruction::OpRelOffset:
    assert(
        IsInitiated &&
        "cannot define relative offset to a non-existing CFA unwinding rule");

    CFIP.addInstruction(dwarf::DW_CFA_offset, Directive.getRegister(),
                        Directive.getOffset() - Row.getCFAValue().getOffset());
    break;
  case MCCFIInstruction::OpAdjustCfaOffset:
    assert(IsInitiated &&
           "cannot adjust CFA offset of a non-existing CFA unwinding rule");

    CFIP.addInstruction(dwarf::DW_CFA_def_cfa_offset,
                        Directive.getOffset() + Row.getCFAValue().getOffset());
    break;
  case MCCFIInstruction::OpEscape:
    // TODO: DWARFExpressions are not supported yet, ignoring expression here.
    Context->reportWarning(Directive.getLoc(),
                           "this directive is not supported, ignoring it");
    break;
  case MCCFIInstruction::OpRestore:
    // The `.cfi_restore register` directive restores the register's unwinding
    // information to its CIE value. However, assemblers decide where CIE ends
    // and the FDE starts, so the functionality of this directive depends on the
    // assembler's decision and cannot be validated.
    Context->reportWarning(
        Directive.getLoc(),
        "this directive behavior depends on the assembler, ignoring it");
    break;
  case MCCFIInstruction::OpUndefined:
    CFIP.addInstruction(dwarf::DW_CFA_undefined, Directive.getRegister());
    break;
  case MCCFIInstruction::OpRegister:
    CFIP.addInstruction(dwarf::DW_CFA_register, Directive.getRegister(),
                        Directive.getRegister2());
    break;
  case MCCFIInstruction::OpWindowSave:
    CFIP.addInstruction(dwarf::DW_CFA_GNU_window_save);
    break;
  case MCCFIInstruction::OpNegateRAState:
    CFIP.addInstruction(dwarf::DW_CFA_AARCH64_negate_ra_state);
    break;
  case MCCFIInstruction::OpNegateRAStateWithPC:
    CFIP.addInstruction(dwarf::DW_CFA_AARCH64_negate_ra_state_with_pc);
    break;
  case MCCFIInstruction::OpGnuArgsSize:
    CFIP.addInstruction(dwarf::DW_CFA_GNU_args_size);
    break;
  case MCCFIInstruction::OpLabel:
    // `.cfi_label` does not have any functional effect on unwinding process.
    break;
  case MCCFIInstruction::OpValOffset:
    CFIP.addInstruction(dwarf::DW_CFA_val_offset, Directive.getRegister(),
                        Directive.getOffset());
    break;
  }

  return CFIP;
}
