// Copyright 2019 Google
//
// 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 "Crashlytics/Crashlytics/Unwind/Compact/FIRCLSCompactUnwind_Private.h"
#include "Crashlytics/Crashlytics/Unwind/Dwarf/FIRCLSDataParsing.h"
#include "Crashlytics/Crashlytics/Helpers/FIRCLSDefines.h"
#include "Crashlytics/Crashlytics/Unwind/Dwarf/FIRCLSDwarfUnwind.h"
#include "Crashlytics/Crashlytics/Helpers/FIRCLSFeatures.h"
#include "Crashlytics/Crashlytics/Unwind/FIRCLSUnwind.h"
#include "Crashlytics/Crashlytics/Helpers/FIRCLSUtility.h"

#include <string.h>

#if CLS_COMPACT_UNWINDING_SUPPORTED

#pragma mark Parsing
bool FIRCLSCompactUnwindInit(FIRCLSCompactUnwindContext* context,
                             const void* unwindInfo,
                             const void* ehFrame,
                             uintptr_t loadAddress) {
  if (!FIRCLSIsValidPointer(context)) {
    FIRCLSSDKLog("Error: invalid context passed to compact unwind init");
    return false;
  }
  if (!FIRCLSIsValidPointer(unwindInfo)) {
    FIRCLSSDKLog("Error: invalid unwind info passed to compact unwind init");
    return false;
  }
  if (!FIRCLSIsValidPointer(loadAddress)) {
    FIRCLSSDKLog("Error: invalid load address passed to compact unwind init");
    return false;
  }

  memset(context, 0, sizeof(FIRCLSCompactUnwindContext));

  if (!FIRCLSReadMemory((vm_address_t)unwindInfo, &context->unwindHeader,
                        sizeof(struct unwind_info_section_header))) {
    FIRCLSSDKLog("Error: could not read memory contents of unwindInfo\n");
    return false;
  }

  if (context->unwindHeader.version != UNWIND_SECTION_VERSION) {
    FIRCLSSDKLog("Error: bad unwind_info structure version (%d != %d)\n",
                 context->unwindHeader.version, UNWIND_SECTION_VERSION);
    return false;
  }

  // copy in the values
  context->unwindInfo = unwindInfo;
  context->ehFrame = ehFrame;
  context->loadAddress = loadAddress;

  return true;
}

void* FIRCLSCompactUnwindGetIndexData(FIRCLSCompactUnwindContext* context) {
  return (void*)((uintptr_t)context->unwindInfo +
                 (uintptr_t)context->unwindHeader.indexSectionOffset);
}

compact_unwind_encoding_t* FIRCLSCompactUnwindGetCommonEncodings(
    FIRCLSCompactUnwindContext* context) {
  return (compact_unwind_encoding_t*)((uintptr_t)context->unwindInfo +
                                      (uintptr_t)
                                          context->unwindHeader.commonEncodingsArraySectionOffset);
}

void* FIRCLSCompactUnwindGetSecondLevelData(FIRCLSCompactUnwindContext* context) {
  return (void*)((uintptr_t)context->unwindInfo +
                 context->indexHeader.secondLevelPagesSectionOffset);
}

uintptr_t FIRCLSCompactUnwindGetIndexFunctionOffset(FIRCLSCompactUnwindContext* context) {
  return context->loadAddress + context->indexHeader.functionOffset;
}
uintptr_t FIRCLSCompactUnwindGetTargetAddress(FIRCLSCompactUnwindContext* context, uintptr_t pc) {
  uintptr_t offset = FIRCLSCompactUnwindGetIndexFunctionOffset(context);

  if (pc <= offset) {
    FIRCLSSDKLog("Error: PC is invalid\n");
    return 0;
  }

  return pc - offset;
}

#pragma mark - Parsing and Lookup
bool FIRCLSCompactUnwindLookupFirstLevel(FIRCLSCompactUnwindContext* context, uintptr_t address) {
  if (!context) {
    return false;
  }

  // In practice, it appears that there always one more first level entry
  // than required. This actually makes sense, since we have to use this
  // info to check if we are in range. This implies there must be
  // at least 2 indices at a minimum.

  uint32_t indexCount = context->unwindHeader.indexCount;
  if (indexCount < 2) {
    return false;
  }

  // make sure our address is valid
  if (address < context->loadAddress) {
    return false;
  }

  struct unwind_info_section_header_index_entry* indexEntries =
      FIRCLSCompactUnwindGetIndexData(context);
  if (!indexEntries) {
    return false;
  }

  address -= context->loadAddress;  // search relative to zero

  // minus one because of the extra entry - see comment above
  for (uint32_t index = 0; index < indexCount - 1; ++index) {
    uint32_t value = indexEntries[index].functionOffset;
    uint32_t nextValue = indexEntries[index + 1].functionOffset;

    if (address >= value && address < nextValue) {
      context->firstLevelNextFunctionOffset = nextValue;
      context->indexHeader = indexEntries[index];
      return true;
    }
  }

  return false;
}

uint32_t FIRCLSCompactUnwindGetSecondLevelPageKind(FIRCLSCompactUnwindContext* context) {
  if (!context) {
    return 0;
  }

  return *(uint32_t*)FIRCLSCompactUnwindGetSecondLevelData(context);
}

bool FIRCLSCompactUnwindLookupSecondLevelRegular(FIRCLSCompactUnwindContext* context,
                                                 uintptr_t pc,
                                                 FIRCLSCompactUnwindResult* result) {
  FIRCLSSDKLog("Encountered a regular second-level page\n");
  return false;
}

// this only works for compressed entries right now
bool FIRCLSCompactUnwindBinarySearchSecondLevel(uintptr_t address,
                                                uint32_t* index,
                                                uint16_t entryCount,
                                                uint32_t* entryArray) {
  if (!index || !entryArray) {
    return false;
  }

  if (entryCount == 0) {
    return false;
  }

  if (address == 0) {
    return false;
  }

  uint32_t highIndex = entryCount;
  *index = 0;

  while (*index < highIndex) {
    uint32_t midIndex = (*index + highIndex) / 2;

    //        FIRCLSSDKLog("%u %u %u\n", *index, midIndex, highIndex);

    uintptr_t value = UNWIND_INFO_COMPRESSED_ENTRY_FUNC_OFFSET(entryArray[midIndex]);

    if (value > address) {
      if (highIndex == midIndex) {
        return false;
      }

      highIndex = midIndex;
      continue;
    }

    *index = midIndex;

    // are we at the end of the array?
    if (midIndex == entryCount - 1) {
      return false;
    }

    uintptr_t nextValue = UNWIND_INFO_COMPRESSED_ENTRY_FUNC_OFFSET(entryArray[midIndex + 1]);
    if (nextValue > address) {
      // we've found it
      break;
    }

    *index += 1;
  }

  // check to make sure we're still within bounds
  return *index < entryCount;
}

bool FIRCLSCompactUnwindLookupSecondLevelCompressed(FIRCLSCompactUnwindContext* context,
                                                    uintptr_t pc,
                                                    FIRCLSCompactUnwindResult* result) {
  if (!context || !result) {
    return false;
  }

  void* ptr = FIRCLSCompactUnwindGetSecondLevelData(context);

  if (!ptr) {
    return false;
  }

  memset(result, 0, sizeof(FIRCLSCompactUnwindResult));

  struct unwind_info_compressed_second_level_page_header* header =
      (struct unwind_info_compressed_second_level_page_header*)ptr;

  // adjust address
  uintptr_t targetAddress = FIRCLSCompactUnwindGetTargetAddress(context, pc);

  uint32_t* entryArray = ptr + header->entryPageOffset;

  uint32_t index = 0;

  if (!FIRCLSCompactUnwindBinarySearchSecondLevel(targetAddress, &index, header->entryCount,
                                                  entryArray)) {
    FIRCLSSDKLogInfo("Unable to find PC in second level\n");
    return false;
  }

  uint32_t entry = entryArray[index];

  // Computing the fuction start address is easy
  result->functionStart = UNWIND_INFO_COMPRESSED_ENTRY_FUNC_OFFSET(entry) +
                          FIRCLSCompactUnwindGetIndexFunctionOffset(context);

  // Computing the end is more complex, because we could be on the last entry. In that case, we
  // cannot use the next value as the end.
  result->functionEnd = context->loadAddress;
  if (index < header->entryCount - 1) {
    result->functionEnd += UNWIND_INFO_COMPRESSED_ENTRY_FUNC_OFFSET(entryArray[index + 1]) +
                           context->indexHeader.functionOffset;
  } else {
    result->functionEnd += context->firstLevelNextFunctionOffset;
  }

  //    FIRCLSSDKLog("Located %lx => %lx %lx\n", pc, result->functionStart, result->functionEnd);

  if ((pc < result->functionStart) || (pc >= result->functionEnd)) {
    FIRCLSSDKLog("PC does not match computed function range\n");
    return false;
  }

  uint32_t encodingIndex = UNWIND_INFO_COMPRESSED_ENTRY_ENCODING_INDEX(entry);

  // encoding could be in the common array
  if (encodingIndex < context->unwindHeader.commonEncodingsArrayCount) {
    result->encoding = FIRCLSCompactUnwindGetCommonEncodings(context)[encodingIndex];

    //        FIRCLSSDKLog("Entry has common encoding: 0x%x\n", result->encoding);
  } else {
    encodingIndex = encodingIndex - context->unwindHeader.commonEncodingsArrayCount;

    compact_unwind_encoding_t* encodings = ptr + header->encodingsPageOffset;

    result->encoding = encodings[encodingIndex];

    //        FIRCLSSDKLog("Entry has compressed encoding: 0x%x\n", result->encoding);
  }

  if (result->encoding == 0) {
    FIRCLSSDKLogInfo("Entry has has no unwind info\n");
    return false;
  }

  return true;
}

bool FIRCLSCompactUnwindLookupSecondLevel(FIRCLSCompactUnwindContext* context,
                                          uintptr_t pc,
                                          FIRCLSCompactUnwindResult* result) {
  switch (FIRCLSCompactUnwindGetSecondLevelPageKind(context)) {
    case UNWIND_SECOND_LEVEL_REGULAR:
      FIRCLSSDKLogInfo("Found a second level regular header\n");
      if (FIRCLSCompactUnwindLookupSecondLevelRegular(context, pc, result)) {
        return true;
      }
      break;
    case UNWIND_SECOND_LEVEL_COMPRESSED:
      FIRCLSSDKLogInfo("Found a second level compressed header\n");
      if (FIRCLSCompactUnwindLookupSecondLevelCompressed(context, pc, result)) {
        return true;
      }
      break;
    default:
      FIRCLSSDKLogError("Unrecognized header kind - unable to continue\n");
      break;
  }

  return false;
}

bool FIRCLSCompactUnwindLookup(FIRCLSCompactUnwindContext* context,
                               uintptr_t pc,
                               FIRCLSCompactUnwindResult* result) {
  if (!context || !result) {
    return false;
  }

  // step 1 - find the pc in the first-level index
  if (!FIRCLSCompactUnwindLookupFirstLevel(context, pc)) {
    FIRCLSSDKLogWarn("Unable to find pc in first level\n");
    return false;
  }

  FIRCLSSDKLogDebug("Found first level (second => %u)\n",
                    context->indexHeader.secondLevelPagesSectionOffset);

  // step 2 - use that info to find the second-level information
  // that second actually has the encoding info we're looking for.
  if (!FIRCLSCompactUnwindLookupSecondLevel(context, pc, result)) {
    FIRCLSSDKLogInfo("Second-level PC lookup failed\n");
    return false;
  }

  return true;
}

#pragma mark - Unwinding
bool FIRCLSCompactUnwindLookupAndCompute(FIRCLSCompactUnwindContext* context,
                                         FIRCLSThreadContext* registers) {
  if (!context || !registers) {
    return false;
  }

  uintptr_t pc = FIRCLSThreadContextGetPC(registers);

  // little sanity check
  if (pc < context->loadAddress) {
    return false;
  }

  FIRCLSCompactUnwindResult result;

  memset(&result, 0, sizeof(result));

  if (!FIRCLSCompactUnwindLookup(context, pc, &result)) {
    FIRCLSSDKLogInfo("Unable to lookup compact unwind for pc %p\n", (void*)pc);
    return false;
  }

  // Ok, armed with the encoding, we can actually attempt to modify the registers. Because
  // the encoding is arch-specific, this function has to be defined per-arch.
  if (!FIRCLSCompactUnwindComputeRegisters(context, &result, registers)) {
    FIRCLSSDKLogError("Failed to compute registers\n");
    return false;
  }

  return true;
}

#if CLS_DWARF_UNWINDING_SUPPORTED
bool FIRCLSCompactUnwindDwarfFrame(FIRCLSCompactUnwindContext* context,
                                   uintptr_t dwarfOffset,
                                   FIRCLSThreadContext* registers) {
  if (!context || !registers) {
    return false;
  }

  // Everyone's favorite! Dwarf unwinding!
  FIRCLSSDKLogInfo("Trying to read dwarf data with offset %lx\n", dwarfOffset);

  FIRCLSDwarfCFIRecord record;

  if (!FIRCLSDwarfParseCFIFromFDERecordOffset(&record, context->ehFrame, dwarfOffset)) {
    FIRCLSSDKLogError("Unable to init FDE\n");
    return false;
  }

  if (!FIRCLSDwarfUnwindComputeRegisters(&record, registers)) {
    FIRCLSSDKLogError("Failed to compute DWARF registers\n");
    return false;
  }

  return true;
}
#endif

#else
INJECT_STRIP_SYMBOL(compact_unwind)
#endif
