# Copyright 2025 DeepMind Technologies Limited
#
# 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.

"""Utility functions for code generation."""

import os
from introspect import ast_nodes


def write_to_file(filepath: str, content: str) -> None:
  """Writes content to a file."""
  output_dir = os.path.dirname(filepath)

  try:
    if output_dir:
      os.makedirs(output_dir, exist_ok=True)
    with open(filepath, "w") as f:
      chars = f.write(content)
      print(f"wrote {chars} characters to file '{filepath}'")
  except IOError as e:
    print(f"Error writing to output file: {filepath} - {e}")


def decapitalize(input_string: str) -> str:
  """Lowercases the first letter of a string."""
  return input_string[:1].lower() + input_string[1:]


def capitalize(input_string: str) -> str:
  """Uppercases the first letter of a string."""
  return input_string[:1].upper() + input_string[1:]


def replace_lines_containing_marker(
    lines: list[str],
    marker: str,
    content: list[str],
) -> list[str]:
  """Replaces lines containing a specific marker with new content."""
  for i, line in enumerate(lines):
    if marker in line:
      indent = line[: len(line) - len(line.lstrip(" "))]
      replacement_lines = []
      for text in content:
        if text.strip():
          # Prepend indent to ensure the first replacement line matches the
          # indentation of the marker and also ensure that text containing
          # newlines is also indented correctly.
          # TODO(matijak): This is working around an upstream problem, we should
          # make it a precondition that content elements do not contain newlines
          # and fix callers to ensure that.
          replacement_lines.append(
              indent + text.replace("\n", f"\n{indent}") + "\n"
          )
      return lines[:i] + replacement_lines + lines[i + 1 :]
  return lines


def get_pointer_return_inner_value_type(
    func: ast_nodes.FunctionDecl,
) -> ast_nodes.ValueType | None:
  if not isinstance(func.return_type, ast_nodes.PointerType):
    return None
  if not isinstance(func.return_type.inner_type, ast_nodes.ValueType):
    return None
  return func.return_type.inner_type


def get_inner_value_type(
    param: ast_nodes.FunctionParameterDecl,
) -> ast_nodes.ValueType | None:
  if not isinstance(param.type, (ast_nodes.PointerType, ast_nodes.ArrayType)):
    return None
  if not isinstance(param.type.inner_type, ast_nodes.ValueType):
    return None
  return param.type.inner_type


def should_be_wrapped(func: ast_nodes.FunctionDecl) -> bool:
  """Checks if a MuJoCo function needs a wrapper function."""
  if get_pointer_return_inner_value_type(func):
    return True
  for param in func.parameters:
    if get_inner_value_type(param):
      return True
  return False


def wrapped_struct_name(c_struct_name: str) -> str:
  """Returns the name of the struct wrapping the given C struct."""
  return capitalize(c_struct_name)


def wrapped_function_name(func: ast_nodes.FunctionDecl) -> str:
  """Returns the name of the function wrapping the given C function.

  Hard-coded wrappers in the template need changing if the implementation of the
  wrapped name is changed

  Args:
    func: The FunctionDecl of function to wrap.
  """
  if should_be_wrapped(func):
    return f"{func.name}_wrapper"
  return func.name
