/*
 * Copyright © 2022 Imagination Technologies Ltd.
 *
 * Permission is hereby granted, free of charge, to any person obtaining a copy
 * of this software and associated documentation files (the "Software"), to deal
 * in the Software without restriction, including without limitation the rights
 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 * copies of the Software, and to permit persons to whom the Software is
 * furnished to do so, subject to the following conditions:
 *
 * The above copyright notice and this permission notice (including the next
 * paragraph) shall be included in all copies or substantial portions of the
 * Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
 * SOFTWARE.
 */

#ifndef PVR_DUMP_H
#define PVR_DUMP_H

#include <inttypes.h>
#include <stdarg.h>
#include <stdbool.h>
#include <stdint.h>
#include <stdio.h>

#include "pvr_types.h"
#include "pvr_util.h"
#include "util/macros.h"
#include "util/u_math.h"

/** BASIC PRINTING **/

#define PVR_DUMP_OFFSET_PREFIX "[%0*" PRIx64 "] "

/** CONTEXTS **/

#define PVR_DUMP_INDENT_SIZE 2U
#define PVR_DUMP_FIELD_COLUMN_WIDTH 36U

/* This is an invalid context used to permanently mark popped contexts as
 * unusable. All operations on a context check that it's the "top" context
 * by ensuring it has no active child. The only way to remove the active child
 * of a context is by popping the active child directly. Assigning an invalid
 * context as the active child of a context therefore makes it impossible to
 * use.
 */
extern const struct pvr_dump_ctx __pvr_dump_ctx_invalid;

struct pvr_dump_ctx {
   /* This is const because only the "top" context should ever be modified. It's
    * fine to extract information from the parent context, but not to modify it.
    * There is *one* exception: pvr_dump_ctx_pop() must cast away the const to
    * return the parent context as the new "top" context. This is considered
    * sound because the parent context was not const when assigned here in
    * pvr_dump_ctx_push().
    */
   const struct pvr_dump_ctx *parent;

   /* This is const because it's not meant to be used for access - it's just a
    * way of checking if this context is the "top" context (see the comment on
    * __pvr_dump_ctx_invalid for more details). Unlike parent, the const
    * qualifier here should never be cast away.
    */
   const struct pvr_dump_ctx *active_child;

   FILE *file;
   const char *name;

   uint32_t allowed_child_depth;
   uint32_t parent_indent;

   /* User-modifiable values */
   uint32_t indent;
   bool ok;
};

static inline uint32_t
__pvr_dump_ctx_get_indent(const struct pvr_dump_ctx *const ctx)
{
   return (ctx->parent_indent + ctx->indent) * PVR_DUMP_INDENT_SIZE;
}

struct pvr_dump_buffer_ctx {
   struct pvr_dump_ctx base;

   const void *initial_ptr;
   uint64_t capacity;

   /* User-modifiable values */
   const void *ptr;
   uint64_t remaining_size;
};

#define pvr_dump_printf(ctx, format, args...)           \
   pvr_dump_printf_cont(ctx,                            \
                        "%*s" format,                   \
                        __pvr_dump_ctx_get_indent(ctx), \
                        "",                             \
                        ##args)

/* Same as pvr_dump_printf(), but with no indent.
 * Intended for continuation lines.
 */
#define pvr_dump_printf_cont(ctx, format, args...) \
   fprintf((ctx)->file, format, ##args)

#define pvr_dump_println(ctx, format, args...) \
   pvr_dump_printf(ctx, format "\n", ##args)

#define pvr_dump_println_cont(ctx, format, args...) \
   pvr_dump_printf_cont(ctx, format "\n", ##args)

#define pvr_dump_print_eol(ctx) fprintf((ctx)->file, "\n")

#define pvr_dump_mark_section(ctx, format, args...)                \
   do {                                                            \
      pvr_dump_print_eol(ctx);                                     \
      pvr_dump_println(ctx, "------- " format " -------", ##args); \
   } while (0)

#define pvr_dump_buffer_print_header_prefix(ctx)                            \
   do {                                                                     \
      struct pvr_dump_buffer_ctx *_prefix_ctx = (ctx);                      \
      pvr_dump_printf(&_prefix_ctx->base,                                   \
                      PVR_DUMP_OFFSET_PREFIX,                               \
                      u64_dec_digits(_prefix_ctx->capacity),                \
                      _prefix_ctx->capacity - _prefix_ctx->remaining_size); \
   } while (0)

#define pvr_dump_buffer_print_header_line(ctx, format, args...) \
   do {                                                         \
      struct pvr_dump_buffer_ctx *_ctx = (ctx);                 \
      pvr_dump_buffer_print_header_prefix(_ctx);                \
      pvr_dump_printf_cont(&_ctx->base, format "\n", ##args);   \
   } while (0)

#define pvr_dump_msg(ctx, prefix, ret, format, args...)            \
   ({                                                              \
      bool _ret = (ret);                                           \
      struct pvr_dump_ctx *_ctx = (ctx);                           \
      pvr_dump_println(_ctx, "<!" prefix "! " format ">", ##args); \
      if (!_ret)                                                   \
         _ctx->ok = _ret;                                          \
      _ret;                                                        \
   })

#define pvr_dump_error(ctx, format, args...) \
   pvr_dump_msg(ctx, "ERROR", false, format, ##args)

#define pvr_dump_warn(ctx, format, args...) \
   pvr_dump_msg(ctx, "WARN", true, format, ##args)

static inline bool pvr_dump_ctx_require_top(struct pvr_dump_ctx *const ctx)
{
   if (ctx->active_child != NULL)
      return pvr_dump_error(ctx, "use of non-top context");

   return true;
}

static inline void pvr_dump_indent(struct pvr_dump_ctx *const ctx)
{
   ctx->indent++;
}

static inline void pvr_dump_dedent(struct pvr_dump_ctx *const ctx)
{
   if (ctx->indent)
      ctx->indent--;
}

static inline void __pvr_dump_ctx_init(struct pvr_dump_ctx *const ctx,
                                       const struct pvr_dump_ctx *const parent,
                                       FILE *const file,
                                       const char *const name,
                                       const uint32_t allowed_child_depth,
                                       const uint32_t parent_indent)
{
   ctx->parent = parent;
   ctx->active_child = NULL;

   ctx->file = file;
   ctx->name = name;

   ctx->allowed_child_depth = allowed_child_depth;
   ctx->parent_indent = parent_indent;
   ctx->indent = 0;
   ctx->ok = true;
}

static inline void __pvr_dump_ctx_mark_popped(struct pvr_dump_ctx *const ctx)
{
   ctx->active_child = &__pvr_dump_ctx_invalid;
}

static inline void pvr_dump_begin(struct pvr_dump_ctx *const root_ctx,
                                  FILE *const file,
                                  const char *const name,
                                  const uint32_t max_depth)
{
   __pvr_dump_ctx_init(root_ctx, NULL, file, name, max_depth, 0);

   flockfile(file);
   pvr_dump_println(root_ctx, "======= BEGIN %s =======", name);
}

static inline bool pvr_dump_end(struct pvr_dump_ctx *const root_ctx)
{
   /* In order to end a dump, we must be in a root context (no parent) and have
    * no active child context.
    */
   if (!pvr_dump_ctx_require_top(root_ctx))
      return false;

   if (root_ctx->parent)
      return pvr_dump_error(root_ctx, "ending non-root context");

   pvr_dump_println(root_ctx, "======= END %s =======", root_ctx->name);
   funlockfile(root_ctx->file);

   __pvr_dump_ctx_mark_popped(root_ctx);

   return true;
}

static inline bool pvr_dump_ctx_push(struct pvr_dump_ctx *const ctx,
                                     struct pvr_dump_ctx *const parent_ctx)
{
   if (!parent_ctx->ok)
      return false;

   if (!parent_ctx->allowed_child_depth)
      return pvr_dump_error(parent_ctx, "context stack depth limit reached");

   __pvr_dump_ctx_init(ctx,
                       parent_ctx,
                       parent_ctx->file,
                       parent_ctx->name,
                       parent_ctx->allowed_child_depth - 1,
                       parent_ctx->parent_indent + parent_ctx->indent);

   parent_ctx->active_child = ctx;

   return true;
}

static inline struct pvr_dump_ctx *
pvr_dump_ctx_pop(struct pvr_dump_ctx *const ctx)
{
   struct pvr_dump_ctx *const parent = (struct pvr_dump_ctx *)ctx->parent;

   if (!pvr_dump_ctx_require_top(ctx))
      return NULL;

   if (!parent) {
      pvr_dump_error(ctx, "popped root context");
      return NULL;
   }

   parent->active_child = NULL;

   __pvr_dump_ctx_mark_popped(ctx);

   return parent;
}

static inline bool
pvr_dump_buffer_ctx_push(struct pvr_dump_buffer_ctx *const ctx,
                         struct pvr_dump_ctx *const parent_ctx,
                         const void *const initial_ptr,
                         const uint64_t size)
{
   if (!pvr_dump_ctx_push(&ctx->base, parent_ctx))
      return false;

   ctx->initial_ptr = initial_ptr;
   ctx->capacity = size;

   ctx->ptr = initial_ptr;
   ctx->remaining_size = size;

   return true;
}

static inline struct pvr_dump_ctx *
pvr_dump_buffer_ctx_pop(struct pvr_dump_buffer_ctx *const ctx)
{
   return pvr_dump_ctx_pop(&ctx->base);
}

bool pvr_dump_buffer_hex(struct pvr_dump_buffer_ctx *ctx, uint64_t nr_bytes);

static inline void __pvr_dump_buffer_advance(struct pvr_dump_buffer_ctx *ctx,
                                             const uint64_t nr_bytes)
{
   ctx->ptr += nr_bytes;
   ctx->remaining_size -= nr_bytes;
}

static inline bool pvr_dump_buffer_advance(struct pvr_dump_buffer_ctx *ctx,
                                           const uint64_t nr_bytes)
{
   if (!ctx->base.ok || !pvr_dump_ctx_require_top(&ctx->base))
      return false;

   if (nr_bytes > ctx->remaining_size)
      return pvr_dump_error(&ctx->base, "advanced past end of context buffer");

   __pvr_dump_buffer_advance(ctx, nr_bytes);

   return true;
}

static inline void __pvr_dump_buffer_rewind(struct pvr_dump_buffer_ctx *ctx,
                                            const uint32_t nr_bytes)
{
   ctx->ptr -= nr_bytes;
   ctx->remaining_size += nr_bytes;
}

static inline bool pvr_dump_buffer_rewind(struct pvr_dump_buffer_ctx *ctx,
                                          const uint32_t nr_bytes)
{
   if (!ctx->base.ok || !pvr_dump_ctx_require_top(&ctx->base))
      return false;

   if (nr_bytes > ctx->capacity - ctx->remaining_size)
      return pvr_dump_error(&ctx->base, "rewound past start of context buffer");

   __pvr_dump_buffer_rewind(ctx, nr_bytes);

   return true;
}

static inline bool pvr_dump_buffer_truncate(struct pvr_dump_buffer_ctx *ctx,
                                            const uint64_t remaining_size)
{
   if (!ctx->base.ok || !pvr_dump_ctx_require_top(&ctx->base))
      return false;

   if (remaining_size > ctx->remaining_size)
      return pvr_dump_error(&ctx->base, "truncated to larger size");

   ctx->remaining_size = remaining_size;

   return true;
}

static inline const void *restrict
pvr_dump_buffer_peek(struct pvr_dump_buffer_ctx *const restrict ctx,
                     const uint64_t nr_bytes)
{
   if (!ctx->base.ok || !pvr_dump_ctx_require_top(&ctx->base))
      return NULL;

   if (nr_bytes > ctx->remaining_size) {
      pvr_dump_error(&ctx->base, "peeked past end of context buffer");
      return NULL;
   }

   return ctx->ptr;
}

static inline const void *restrict
pvr_dump_buffer_take(struct pvr_dump_buffer_ctx *const restrict ctx,
                     const uint64_t nr_bytes)
{
   const void *const ptr = pvr_dump_buffer_peek(ctx, nr_bytes);

   if (ptr)
      __pvr_dump_buffer_advance(ctx, nr_bytes);

   return ptr;
}

static inline void
pvr_dump_buffer_restart(struct pvr_dump_buffer_ctx *const ctx)
{
   ctx->ptr = ctx->initial_ptr;
   ctx->remaining_size = ctx->capacity;
}

/*****************************************************************************
   Field printers
*****************************************************************************/

#define pvr_dump_field(ctx, name, format, args...)     \
   pvr_dump_println(ctx,                               \
                    "%-*s : " format,                  \
                    PVR_DUMP_FIELD_COLUMN_WIDTH -      \
                       __pvr_dump_ctx_get_indent(ctx), \
                    name,                              \
                    ##args)

#define pvr_dump_field_computed(ctx, name, format, raw_format, args...) \
   pvr_dump_field(ctx, name, format " (" raw_format ")", ##args)

#define pvr_dump_field_error(ctx, format, args...)               \
   ({                                                            \
      struct pvr_dump_ctx *_ctx = (ctx);                         \
      pvr_dump_field(_ctx, "<!ERROR!>", "<" format ">", ##args); \
      _ctx->ok = false;                                          \
      false;                                                     \
   })

/*****************************************************************************
   Field printers: integers
*****************************************************************************/

static inline void pvr_dump_field_u32(struct pvr_dump_ctx *const ctx,
                                      const char *const name,
                                      const uint32_t value)
{
   pvr_dump_field(ctx, name, "%" PRIu32, value);
}

static inline void pvr_dump_field_u32_units(struct pvr_dump_ctx *const ctx,
                                            const char *const name,
                                            const uint32_t value,
                                            const char *const units)
{
   pvr_dump_field(ctx, name, "%" PRIu32 " %s", value, units);
}

static inline void pvr_dump_field_u32_offset(struct pvr_dump_ctx *const ctx,
                                             const char *const name,
                                             const uint32_t value,
                                             const uint32_t offset)
{
   pvr_dump_field_computed(ctx,
                           name,
                           "%" PRIu32,
                           "%" PRIu32 " + %" PRIu32,
                           value + offset,
                           value,
                           offset);
}

static inline void pvr_dump_field_u32_scaled(struct pvr_dump_ctx *const ctx,
                                             const char *const name,
                                             const uint32_t value,
                                             const uint32_t scale)
{
   pvr_dump_field_computed(ctx,
                           name,
                           "%" PRIu32,
                           "%" PRIu32 " x %" PRIu32,
                           value * scale,
                           value,
                           scale);
}

static inline void
pvr_dump_field_u32_scaled_units(struct pvr_dump_ctx *const ctx,
                                const char *const name,
                                const uint32_t value,
                                const uint32_t scale,
                                const char *const units)
{
   pvr_dump_field_computed(ctx,
                           name,
                           "%" PRIu32 " %s",
                           "%" PRIu32 " x %" PRIu32 " %s",
                           value * scale,
                           units,
                           value,
                           scale,
                           units);
}

static inline void pvr_dump_field_u32_zero(struct pvr_dump_ctx *const ctx,
                                           const char *const name,
                                           const uint32_t value,
                                           const uint32_t zero_value)
{
   if (value)
      pvr_dump_field_u32(ctx, name, value);
   else
      pvr_dump_field_computed(ctx, name, "%" PRIu32, "0", zero_value);
}

static inline void pvr_dump_field_x32(struct pvr_dump_ctx *const ctx,
                                      const char *const name,
                                      const uint32_t value,
                                      const uint32_t chars)
{
   pvr_dump_field(ctx,
                  name,
                  "0x%0*" PRIx32,
                  chars,
                  value & BITFIELD_MASK(chars * 4));
}

/*****************************************************************************
   Field printers: floating point
*****************************************************************************/

static inline void pvr_dump_field_f32(struct pvr_dump_ctx *const ctx,
                                      const char *const name,
                                      const float value)
{
   pvr_dump_field_computed(ctx, name, "%f", "0x%08" PRIx32, value, fui(value));
}

/*****************************************************************************
   Field printers: fixed point
*****************************************************************************/

/* clang-format off */
static const char *const __fixed_frac_str_table_4[1 << 4] = {
   "0", "0625", "125", "1875", "25", "3125", "375", "4375",
   "5", "5625", "625", "6875", "75", "8125", "875", "9375",
};
/* clang-format on */

static inline void pvr_dump_field_uq4_4(struct pvr_dump_ctx *const ctx,
                                        const char *const name,
                                        const uint32_t raw_value)
{
   const uint32_t int_part = (raw_value & BITFIELD_RANGE(4, 4)) >> 4;
   const uint32_t frac_part = raw_value & BITFIELD_MASK(4);

   pvr_dump_field_computed(ctx,
                           name,
                           "%" PRIu32 ".%s",
                           "0x%02" PRIx32, /* Or %0*x where *=(nr_bits+3)/4 */
                           int_part,
                           __fixed_frac_str_table_4[frac_part],
                           raw_value & BITFIELD_MASK(8));
}

static inline void pvr_dump_field_uq4_4_offset(struct pvr_dump_ctx *const ctx,
                                               const char *const name,
                                               const uint32_t raw_value,
                                               const uint32_t raw_offset)
{
   const uint32_t raw_offset_value = raw_value + raw_offset;

   const uint32_t int_part = (raw_offset_value & BITFIELD_RANGE(4, 4)) >> 4;
   const uint32_t frac_part = raw_offset_value & BITFIELD_MASK(4);

   pvr_dump_field_computed(ctx,
                           name,
                           "%" PRIu32 ".%s",
                           "0x%02" PRIx32 " + 0x%02" PRIx32,
                           int_part,
                           __fixed_frac_str_table_4[frac_part],
                           raw_value & BITFIELD_MASK(8),
                           raw_offset);
}

/*****************************************************************************
   Field printers: device address
*****************************************************************************/

static inline void pvr_dump_field_addr_non_null(struct pvr_dump_ctx *const ctx,
                                                const char *const name,
                                                const pvr_dev_addr_t value)
{
   pvr_dump_field(ctx, name, PVR_DEV_ADDR_FMT, value.addr);
}

static inline void pvr_dump_field_addr(struct pvr_dump_ctx *const ctx,
                                       const char *const name,
                                       const pvr_dev_addr_t value)
{
   if (value.addr)
      pvr_dump_field_addr_non_null(ctx, name, value);
   else
      pvr_dump_field(ctx, name, "<null>");
}

static inline void pvr_dump_field_addr_split(struct pvr_dump_ctx *const ctx,
                                             const char *const name,
                                             const pvr_dev_addr_t msb,
                                             const pvr_dev_addr_t lsb)
{
   pvr_dump_field_addr(ctx, name, PVR_DEV_ADDR(msb.addr | lsb.addr));

   pvr_dump_indent(ctx);
   pvr_dump_field_addr_non_null(ctx, "msb", msb);
   pvr_dump_field_addr_non_null(ctx, "lsb", lsb);
   pvr_dump_dedent(ctx);
}

static inline void pvr_dump_field_addr_offset(struct pvr_dump_ctx *const ctx,
                                              const char *const name,
                                              const pvr_dev_addr_t value,
                                              const pvr_dev_addr_t base)
{
   pvr_dump_field_computed(ctx,
                           name,
                           PVR_DEV_ADDR_FMT,
                           PVR_DEV_ADDR_FMT " + " PVR_DEV_ADDR_FMT,
                           PVR_DEV_ADDR_OFFSET(base, value.addr).addr,
                           base.addr,
                           value.addr);
}

/*****************************************************************************
   Field printers: enums
*****************************************************************************/

#define pvr_dump_field_enum(ctx, name, value, to_str)               \
   do {                                                             \
      __typeof__(value) _value = (value);                           \
      const char *_str = to_str(_value);                            \
      if (!_str)                                                    \
         _str = "<unknown>";                                        \
      pvr_dump_field_computed(ctx, name, "%s", "%u", _str, _value); \
   } while (0)

static inline const char *__bool_to_str(const bool b)
{
   return b ? "yes" : "no";
}

/* A bool is just an enum with two values. */
static inline void pvr_dump_field_bool(struct pvr_dump_ctx *const ctx,
                                       const char *const name,
                                       const bool value)
{
   pvr_dump_field_enum(ctx, name, value, __bool_to_str);
}

/*****************************************************************************
   Field printers: not present
*****************************************************************************/

static inline void pvr_dump_field_no_fields(struct pvr_dump_ctx *const ctx)
{
   pvr_dump_println(ctx, "<no fields>");
}

static inline void pvr_dump_field_not_present(struct pvr_dump_ctx *const ctx,
                                              const char *const name)
{
   pvr_dump_field(ctx, name, "<not present>");
}

/*****************************************************************************
   Field printers: helpers for members
*****************************************************************************/

/* clang-format off */

#define pvr_dump_field_member_u32(ctx, compound, member) \
   pvr_dump_field_u32(ctx, #member, (compound)->member)

#define pvr_dump_field_member_u32_units(ctx, compound, member, units) \
   pvr_dump_field_u32_units(ctx, #member, (compound)->member, units)

#define pvr_dump_field_member_u32_offset(ctx, compound, member, offset) \
   pvr_dump_field_u32_offset(ctx, #member, (compound)->member, offset)

#define pvr_dump_field_member_u32_scaled(ctx, compound, member, scale) \
   pvr_dump_field_u32_scaled(ctx, #member, (compound)->member, scale)

#define pvr_dump_field_member_u32_scaled_units(ctx, compound, member, scale, units) \
   pvr_dump_field_u32_scaled_units(ctx, #member, (compound)->member, scale, units)

#define pvr_dump_field_member_u32_zero(ctx, compound, member, zero_value) \
   pvr_dump_field_u32_zero(ctx, #member, (compound)->member, zero_value)

#define pvr_dump_field_member_x32(ctx, compound, member, chars) \
   pvr_dump_field_x32(ctx, #member, (compound)->member, chars)

#define pvr_dump_field_member_f32(ctx, compound, member) \
   pvr_dump_field_f32(ctx, #member, (compound)->member)

#define pvr_dump_field_member_uq4_4(ctx, compound, member) \
   pvr_dump_field_uq4_4(ctx, #member, (compound)->member)

#define pvr_dump_field_member_uq4_4_offset(ctx, compound, member, raw_offset) \
   pvr_dump_field_uq4_4_offset(ctx, #member, (compound)->member, raw_offset)

#define pvr_dump_field_member_addr(ctx, compound, member) \
   pvr_dump_field_addr(ctx, #member, (compound)->member)

#define pvr_dump_field_member_addr_offset(ctx, compound, member, base) \
   pvr_dump_field_addr_offset(ctx, #member, (compound)->member, base)

#define pvr_dump_field_member_enum(ctx, compound, member, to_str) \
   pvr_dump_field_enum(ctx, #member, (compound)->member, to_str)

#define pvr_dump_field_member_bool(ctx, compound, member) \
   pvr_dump_field_bool(ctx, #member, (compound)->member)

/* clang-format on */

#define pvr_dump_field_member_not_present(ctx, compound, member) \
   do {                                                          \
      (void)&(compound)->member;                                 \
      pvr_dump_field_not_present(ctx, #member);                  \
   } while (0)

#endif /* PVR_DUMP_H */
