// Copyright (C) 2020-2025 Free Software Foundation, Inc.

// This file is part of GCC.

// GCC is free software; you can redistribute it and/or modify it under
// the terms of the GNU General Public License as published by the Free
// Software Foundation; either version 3, or (at your option) any later
// version.

// GCC is distributed in the hope that it will be useful, but WITHOUT ANY
// WARRANTY; without even the implied warranty of MERCHANTABILITY or
// FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
// for more details.

// You should have received a copy of the GNU General Public License
// along with GCC; see the file COPYING3.  If not see
// <http://www.gnu.org/licenses/>.

#include "rust-export-metadata.h"
#include "rust-hir-visitor.h"
#include "rust-hir-full.h"
#include "rust-hir-map.h"
#include "rust-ast-dump.h"
#include "rust-abi.h"
#include "rust-item.h"
#include "rust-object-export.h"

#include "md5.h"
#include "rust-system.h"

namespace Rust {
namespace Metadata {

static const std::string extension_path = ".rox";

ExportContext::ExportContext () : mappings (Analysis::Mappings::get ()) {}

ExportContext::~ExportContext () {}

void
ExportContext::push_module_scope (const HIR::Module &module)
{
  module_stack.push_back (module);
}

const HIR::Module &
ExportContext::pop_module_scope ()
{
  rust_assert (!module_stack.empty ());
  const HIR::Module &poped = module_stack.back ();
  module_stack.pop_back ();
  return poped;
}

void
ExportContext::emit_trait (const HIR::Trait &trait)
{
  // lookup the AST node for this
  AST::Item *item
    = mappings.lookup_ast_item (trait.get_mappings ().get_nodeid ()).value ();

  std::stringstream oss;
  AST::Dump dumper (oss);
  dumper.go (*item);

  public_interface_buffer += oss.str ();
}

void
ExportContext::emit_function (const HIR::Function &fn)
{
  // lookup the AST node for this
  AST::Item *item
    = mappings.lookup_ast_item (fn.get_mappings ().get_nodeid ()).value ();

  // is this a CFG macro or not
  if (item->is_marked_for_strip ())
    return;

  // FIXME add assertion that item must be a vis_item;
  AST::VisItem &vis_item = static_cast<AST::VisItem &> (*item);

  // if its a generic function we need to output the full declaration
  // otherwise we can let people link against this

  std::stringstream oss;
  AST::Dump dumper (oss);
  if (!fn.has_generics ())
    {
      // FIXME assert that this is actually an AST::Function
      AST::Function &function = static_cast<AST::Function &> (vis_item);

      std::vector<std::unique_ptr<AST::ExternalItem>> external_items;
      external_items.push_back (std::unique_ptr<AST::ExternalItem> (
	static_cast<AST::ExternalItem *> (&function)));

      AST::ExternBlock extern_block (get_string_from_abi (Rust::ABI::RUST),
				     std::move (external_items),
				     vis_item.get_visibility (), {}, {},
				     fn.get_locus ());

      dumper.go (extern_block);
    }
  else
    {
      dumper.go (*item);
    }

  // store the dump
  public_interface_buffer += oss.str ();
}

void
ExportContext::emit_macro (NodeId macro)
{
  std::stringstream oss;
  AST::Dump dumper (oss);

  AST::Item *item = mappings.lookup_ast_item (macro).value ();

  dumper.go (*item);

  public_interface_buffer += oss.str ();
}

const std::string &
ExportContext::get_interface_buffer () const
{
  return public_interface_buffer;
}

// implicitly by using HIR nodes we know that these have passed CFG expansion
// and they exist in the compilation unit
class ExportVisItems : public HIR::HIRVisItemVisitor
{
public:
  ExportVisItems (ExportContext &context) : ctx (context) {}

  void visit (HIR::Module &) override {}
  void visit (HIR::ExternCrate &) override {}
  void visit (HIR::UseDeclaration &) override {}
  void visit (HIR::TypeAlias &) override {}
  void visit (HIR::StructStruct &) override {}
  void visit (HIR::TupleStruct &) override {}
  void visit (HIR::Enum &) override {}
  void visit (HIR::Union &) override {}
  void visit (HIR::ConstantItem &) override {}
  void visit (HIR::StaticItem &) override {}
  void visit (HIR::ImplBlock &) override {}
  void visit (HIR::ExternBlock &) override {}

  void visit (HIR::Trait &trait) override { ctx.emit_trait (trait); }

  void visit (HIR::Function &function) override
  {
    ctx.emit_function (function);
  }

private:
  ExportContext &ctx;
};

PublicInterface::PublicInterface (HIR::Crate &crate)
  : crate (crate), mappings (Analysis::Mappings::get ()), context ()
{}

void
PublicInterface::Export (HIR::Crate &crate)
{
  PublicInterface interface (crate);
  interface.gather_export_data ();
  interface.write_to_object_file ();
}

void
PublicInterface::ExportTo (HIR::Crate &crate, const std::string &output_path)
{
  PublicInterface interface (crate);
  interface.gather_export_data ();
  interface.write_to_path (output_path);
}

void
PublicInterface::gather_export_data ()
{
  ExportVisItems visitor (context);
  for (auto &item : crate.get_items ())
    {
      bool is_vis_item = item->get_hir_kind () == HIR::Node::BaseKind::VIS_ITEM;
      if (!is_vis_item)
	continue;

      HIR::VisItem &vis_item = static_cast<HIR::VisItem &> (*item.get ());
      if (is_crate_public (vis_item))
	vis_item.accept_vis (visitor);
    }

  for (const auto &macro : mappings.get_exported_macros ())
    context.emit_macro (macro);
}

void
PublicInterface::write_to_object_file () const
{
  // done
  const auto &buf = context.get_interface_buffer ();
  std::string size_buffer = std::to_string (buf.size ());

  // md5 this
  struct md5_ctx chksm;
  unsigned char checksum[16];

  md5_init_ctx (&chksm);
  md5_process_bytes (buf.c_str (), buf.size (), &chksm);
  md5_finish_ctx (&chksm, checksum);

  // MAGIC MD5 DLIM  DLIM buffer-size DELIM contents
  const std::string current_crate_name = mappings.get_current_crate_name ();

  // extern void
  rust_write_export_data (kMagicHeader, sizeof (kMagicHeader));
  rust_write_export_data ((const char *) checksum, sizeof (checksum));
  rust_write_export_data (kSzDelim, sizeof (kSzDelim));
  rust_write_export_data (current_crate_name.c_str (),
			  current_crate_name.size ());
  rust_write_export_data (kSzDelim, sizeof (kSzDelim));
  rust_write_export_data (size_buffer.c_str (), size_buffer.size ());
  rust_write_export_data (kSzDelim, sizeof (kSzDelim));
  rust_write_export_data (buf.c_str (), buf.size ());
}

void
PublicInterface::write_to_path (const std::string &path) const
{
  // validate path contains correct extension
  const std::string expected_file_name = expected_metadata_filename ();
  const char *path_base_name = lbasename (path.c_str ());
  if (strcmp (path_base_name, expected_file_name.c_str ()) != 0)
    {
      rust_error_at (UNDEF_LOCATION,
		     "expected metadata-output path to have base file name of: "
		     "%qs got %qs",
		     expected_file_name.c_str (), path_base_name);
      return;
    }

  // done
  const auto &buf = context.get_interface_buffer ();
  std::string size_buffer = std::to_string (buf.size ());

  // md5 this
  struct md5_ctx chksm;
  unsigned char checksum[16];

  md5_init_ctx (&chksm);
  md5_process_bytes (buf.c_str (), buf.size (), &chksm);
  md5_finish_ctx (&chksm, checksum);

  // MAGIC MD5 DLIM  DLIM buffer-size DELIM contents
  const std::string current_crate_name = mappings.get_current_crate_name ();

  // write to path
  FILE *nfd = fopen (path.c_str (), "wb");
  if (nfd == NULL)
    {
      rust_error_at (UNDEF_LOCATION,
		     "failed to open file %qs for writing: %s",
		     path.c_str (), xstrerror (errno));
      return;
    }

  // write data
  if (fwrite (kMagicHeader, sizeof (kMagicHeader), 1, nfd) < 1)
    {
      rust_error_at (UNDEF_LOCATION, "failed to write to file %qs: %s",
		     path.c_str (), xstrerror (errno));
      fclose (nfd);
      return;
    }

  if (fwrite (checksum, sizeof (checksum), 1, nfd) < 1)
    {
      rust_error_at (UNDEF_LOCATION, "failed to write to file %qs: %s",
		     path.c_str (), xstrerror (errno));
      fclose (nfd);
      return;
    }

  if (fwrite (kSzDelim, sizeof (kSzDelim), 1, nfd) < 1)
    {
      rust_error_at (UNDEF_LOCATION, "failed to write to file %qs: %s",
		     path.c_str (), xstrerror (errno));
      fclose (nfd);
      return;
    }

  if (fwrite (current_crate_name.c_str (), current_crate_name.size (), 1, nfd)
      < 1)
    {
      rust_error_at (UNDEF_LOCATION, "failed to write to file %qs: %s",
		     path.c_str (), xstrerror (errno));
      fclose (nfd);
      return;
    }

  if (fwrite (kSzDelim, sizeof (kSzDelim), 1, nfd) < 1)
    {
      rust_error_at (UNDEF_LOCATION, "failed to write to file %qs: %s",
		     path.c_str (), xstrerror (errno));
      fclose (nfd);
      return;
    }

  if (fwrite (size_buffer.c_str (), size_buffer.size (), 1, nfd) < 1)
    {
      rust_error_at (UNDEF_LOCATION, "failed to write to file %qs: %s",
		     path.c_str (), xstrerror (errno));
      fclose (nfd);
      return;
    }

  if (fwrite (kSzDelim, sizeof (kSzDelim), 1, nfd) < 1)
    {
      rust_error_at (UNDEF_LOCATION, "failed to write to file %qs: %s",
		     path.c_str (), xstrerror (errno));
      fclose (nfd);
      return;
    }

  if (!buf.empty ())
    if (fwrite (buf.c_str (), buf.size (), 1, nfd) < 1)
      {
	rust_error_at (UNDEF_LOCATION, "failed to write to file %qs: %s",
		       path.c_str (), xstrerror (errno));
	fclose (nfd);
	return;
      }

  // done
  fclose (nfd);
}

bool
PublicInterface::is_crate_public (const HIR::VisItem &item)
{
  const HIR::Visibility &visibility = item.get_visibility ();

  bool is_public
    = visibility.get_vis_type () == HIR::Visibility::VisType::PUBLIC;
  bool has_path = !visibility.get_path ().is_error ();

  // FIXME this might be pub(crate)
  // Arthur magic required here

  return is_public && !has_path;
}

std::string
PublicInterface::expected_metadata_filename ()
{
  auto &mappings = Analysis::Mappings::get ();

  const std::string current_crate_name = mappings.get_current_crate_name ();
  return current_crate_name + extension_path;
}

} // namespace Metadata
} // namespace Rust