"""Functions that are supposed to be run in development for example to generate a type hinter."""

import inspect
from types import FunctionType
from typing import Any

from .tcr_classfuncs import get_name_classname

print_ = print


def generate_function_argument_typehints(func: FunctionType) -> str:
  try:
    signature = inspect.signature(func)
  except ValueError:  # Signature not found
    return ''
  arguments_with_typehints = []
  for param in signature.parameters.values():
    if param.kind == inspect.Parameter.VAR_POSITIONAL:
      arguments_with_typehints.append('*' + param.name)
    elif param.kind == inspect.Parameter.VAR_KEYWORD:
      arguments_with_typehints.append('**' + param.name)
    else:
      argument = param.name
      if param.annotation != inspect.Parameter.empty:
        argument += f': {get_name_classname(param.annotation)}'
      if param.default == inspect.Parameter.empty:
        arguments_with_typehints.append(argument)
      else:
        arguments_with_typehints.append(f'{argument} = {param.default!r}')
  return ', '.join(arguments_with_typehints)


def generate_function_return_typehint(func: FunctionType) -> str:
  try:
    signature = inspect.signature(func)
  except ValueError:  # Signature not found
    return ''
  return_type = signature.return_annotation
  if return_type == inspect.Signature.empty:
    return ''
  else:
    return f' -> {get_name_classname(return_type)}'


def generate_type_hinter(
  obj: Any,
  /,
  *,
  clipboard: bool = False,
  print: bool = False,
  skip_leading_underscore: bool = False,
  attrib_whitelist: tuple[str] = (),
  attrib_blacklist: tuple[str] = ('sys', 'os', 'warnings'),
) -> str | None:
  """Generates a string class definition for typehinting based on the object's dir() and the types of its attributes.

  Args:
    obj (Any): The object to generate the type hint for.
    clipboard (bool, optional): Whether to copy the result to the clipboard. Defaults to False. (Will also suppress any output to not flood the console)
    print (bool, optional): Whether to print the result. Defaults to True. (Will also suppress any output to not flood the console)
    skip_leading_underscore (bool, optional): Whether to skip leading underscores in the generated type hints. When False, skip only __double_leading_underscore (except some).
    attrib_whitelist (tuple[str], optional): A tuple of attribute names to include in the type hint even if they don't match the specified `skip_leading_underscore` value (or none at all). Defaults to ().
    attrib_blacklist (tuple[str], optional): A tuple of attribute names to exclude from the type hint no matter what. Defaults to ('sys', 'os').
  """

  def sort_key(x):
    v = getattr(obj, x)
    if inspect.isclass(v):
      return 3
    if inspect.ismodule(v):
      return 2
    elif callable(v):
      return 1
    else:
      return 0

  hints = []
  processed_hints_dict = {}
  valid = (lambda x: not x.startswith('_')) if skip_leading_underscore else lambda x: not x.startswith('__')
  obj_dir = sorted([x for x in dir(obj) if ((valid(x)) or x in attrib_whitelist) and x not in attrib_blacklist], key=sort_key)
  for attr_name in obj_dir:
    attr_value = getattr(obj, attr_name)
    attr_type = get_name_classname(attr_value)
    if inspect.isclass(attr_value) or inspect.ismodule(attr_value):
      hints.append('\n  '.join(generate_type_hinter(attr_value).split('\n')))
    elif callable(attr_value):
      args_str = generate_function_argument_typehints(attr_value)
      hints.append(f'@staticmethod\n  def {attr_name}({args_str}){generate_function_return_typehint(attr_value)}: ...')
    else:
      hints.append(f'{attr_name}: {attr_type}')
    processed_hints_dict[attr_name] = attr_value

  if not obj_dir:
    hints.append('...')

  # hint types (imports)

  result = f'class {get_name_classname(obj).split(".")[-1]}:\n' + '\n'.join(f'  {x}' for x in hints)

  if print:
    if callable(print):
      print(result)
    else:
      print_(result)

  if clipboard:
    from .tcr_console import console

    try:
      import pyperclip
    except ImportError:
      print('\npyperclip not installed. Installing...')
      import subprocess as __sp

      try:
        __sp.check_call(['pip', 'install', 'pyperclip'])
      except __sp.CalledProcessError:
        console.error('Unable to install pyperclip... Install it manually, returning without copy')
      else:
        import pyperclip

        pyperclip.copy(result)
        console.log(f'Copied to clipboard! ({len(result)} characters)')
      del __sp
    else:
      pyperclip.copy(result)
      console.log(f'Copied to clipboard! ({len(result)} characters)')

  if not (clipboard or print):
    return result
  else:
    return None
