/*
 * Copyright 2024 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 "flash_logging.h"

#include "console/dbgserial.h"
#include "drivers/flash.h"
#include "os/mutex.h"
#include "system/passert.h"
#include "util/shared_circular_buffer.h"

#include "services/common/system_task.h"

static SharedCircularBuffer s_buffer;
static SharedCircularBufferClient s_buffer_client;
// 526 bytes is enough to buffer up the worst case bunch of reboot messages - a watchdog reset with a timer
// callback. During normal operation, since log messages are hashed, most are only 30-40 bytes long with the longest
// being about 80 bytes, so this is enough for 7-15 or so messages.
static uint8_t s_buffer_storage[550];
static PebbleMutex *s_buffer_mutex = INVALID_MUTEX_HANDLE; //!< Protects s_buffer
static PebbleMutex *s_flash_write_mutex = INVALID_MUTEX_HANDLE; //!< Protects log line consistency
static bool s_is_flash_write_scheduled; //!< true if handle_buffer_sync KernelBG callback is scheduled

static void write_message(void) {
  // Note that we should enter this function with the buffer mutex held.

  const uint8_t *data_read;
  uint16_t read_length;

  // Read the header part
  bool result = shared_circular_buffer_read(&s_buffer, &s_buffer_client, sizeof(uint8_t), &data_read, &read_length);
  PBL_ASSERTN(result);
  PBL_ASSERT(read_length == sizeof(uint8_t), "read_length %u sizeof(uint8_t) %u", read_length, sizeof(uint8_t));
  uint8_t msg_length = *data_read;

  if (shared_circular_buffer_get_read_space_remaining(&s_buffer, &s_buffer_client) < msg_length + sizeof(uint8_t)) {
    return; // Not ready yet, consume nothing.
  }

  // Flash_logging_log_start can trigger a flash erase. Release the buffer mutex
  // to allow logging while the (slow) erase completes.
  mutex_unlock(s_buffer_mutex);
  uint32_t flash_addr = flash_logging_log_start(msg_length);
  mutex_lock(s_buffer_mutex);
  if (flash_addr == FLASH_LOG_INVALID_ADDR) {
    return;
  }

  shared_circular_buffer_consume(&s_buffer, &s_buffer_client, read_length);

  while (msg_length > 0) {
    mutex_unlock(s_buffer_mutex);

    // Note that this buffer read really should be done with the buffer mutex held.
    // This works only because writes to the buffer do not advance slackers.
    result = shared_circular_buffer_read(&s_buffer, &s_buffer_client, msg_length, &data_read, &read_length);
    PBL_ASSERTN(result);
    msg_length -= read_length;

    flash_logging_write(data_read, flash_addr, read_length);
    flash_addr += read_length;

    mutex_lock(s_buffer_mutex);
    shared_circular_buffer_consume(&s_buffer, &s_buffer_client, read_length);
  }

  // We should still be holding the buffer lock here...
}

static void handle_buffer_sync(void *data) {
  const bool is_async = (uintptr_t) data;

  mutex_lock(s_flash_write_mutex);
  mutex_lock(s_buffer_mutex);

  while (shared_circular_buffer_get_read_space_remaining(&s_buffer, &s_buffer_client) > 0) {
    write_message();
    // The above function mucks with the mutex
    mutex_assert_held_by_curr_task(s_buffer_mutex, true /* is_held */);
  }

  if (is_async) {
    s_is_flash_write_scheduled = false;
  }

  mutex_unlock(s_buffer_mutex);
  mutex_unlock(s_flash_write_mutex);
}


void advanced_logging_init(void) {
  flash_logging_init();

  shared_circular_buffer_init(&s_buffer, s_buffer_storage, sizeof(s_buffer_storage));
  shared_circular_buffer_add_client(&s_buffer, &s_buffer_client);

  s_buffer_mutex = mutex_create();
  s_flash_write_mutex = mutex_create();
}

// Return true on success
static bool write_buffer_locking(char* buffer, int length, bool async) {
  bool success = false;

  do {
    mutex_lock(s_buffer_mutex);
    if (shared_circular_buffer_get_write_space_remaining(&s_buffer) >= length + 1) {
      // Ideally we could figure out a way to skip out on this copy but then you'd potentially need to sniprintf
      // into a non-contiguous buffer... whatever, we have CPU to burn.
      uint8_t msg_length = length;

      // Do not advance slackers. Data loss and/or corruption will occur! See write_message()
      shared_circular_buffer_write(&s_buffer, &msg_length, sizeof(uint8_t), false /*advance_slackers*/);
      shared_circular_buffer_write(&s_buffer, (const uint8_t*) buffer, length, false /*advance_slackers*/);

      success = true;
    }
    mutex_unlock(s_buffer_mutex);

    // If we failed to buffer this message, flush the buffer to cache to make room.
    // Otherwise, if this is a sync message, flush this message to flash.
    if (!success || !async) {
      handle_buffer_sync((void *)(uintptr_t) false /* !is_async */);
    }
  } while (!success); // Loop until the buffer copy succeeds. If sync, also wait until this message
                      // is written to flash.
                      // It's highly unlikely that another task will win the race and completely
                      // fill the buffer between the flash write and the next buffer write attempt.
                      // If so, there are bigger issues.

  if (async) {
    mutex_lock(s_buffer_mutex);
    if (!s_is_flash_write_scheduled) {
      s_is_flash_write_scheduled = true;
      system_task_add_callback(handle_buffer_sync, (void *)(uintptr_t) true /* is_async */);
    }
    mutex_unlock(s_buffer_mutex);
  }

  return success;
}

void pbl_log_advanced(char* buffer, int length, bool async) {
  if (s_buffer_mutex == INVALID_MUTEX_HANDLE) {
    return;
  }
  write_buffer_locking(buffer, length, async);
}

char pbl_log_get_level_char(const uint8_t log_level) {
  switch (log_level) {
  case LOG_LEVEL_ALWAYS:
    return '*';
  case LOG_LEVEL_ERROR:
    return 'E';
  case LOG_LEVEL_WARNING:
    return 'W';
  case LOG_LEVEL_INFO:
    return 'I';
  case LOG_LEVEL_DEBUG:
    return 'D';
  case LOG_LEVEL_DEBUG_VERBOSE:
    return 'V';
  default:
    return '?';
  }
}

