import ast
import collections
import contextlib
import copy
import enum
import functools
import inspect
import re
import textwrap

import py_misc_utils.alog as alog
import py_misc_utils.ast_utils as asu
import py_misc_utils.core_utils as pycu
import py_misc_utils.inspect_utils as pyiu
import py_misc_utils.thread_context as pytc
import py_misc_utils.utils as pyu

from .ast_utils import *
from .common_defs import *
from .decorators import *
from .entity import *
from .interface import *
from .types import *
from .vars import *
from .utils import *
from .wrap import *


_LoopContext = collections.namedtuple('LoopContext', 'mode')
_ForLoop = collections.namedtuple('ForLoop', 'data, ivar, start, end, step',
                                  defaults=(None, None, None, None))
_Return = collections.namedtuple('Return', 'value, placement')
_MatchCase = collections.namedtuple('MatchCase', 'pattern, scope')

_CGENCTX = 'pyxhdl.CodeGen'
_CODEFMT_RX = r'(?<!\{)\{([^{][^}]*(\}\}[^}]+)*)\}'


class Variable:

  __slots__ = ('dtype', 'isreg', 'init', 'vspec')

  def __init__(self, dtype, isreg, init=None, vspec=None):
    self.dtype = dtype
    self.isreg = isreg
    self.init = init
    self.vspec = vspec

  def is_const(self):
    return self.vspec is not None and self.vspec.const

  def __repr__(self):
    return f'{pyiu.cname(self)}({self.dtype}, {self.isreg}, {self.init}, {self.vspec})'


class _LoopModes(enum.IntEnum):
  UNROLLED = enum.auto()
  HDL = enum.auto()


class _Exception(Exception):
  pass


class _ReturnException(_Exception):

  def __init__(self, value):
    super().__init__()
    self.value = value


class _BreakException(_Exception):
  pass


class _ContinueException(_Exception):
  pass


class _SourceLocation:

  def __init__(self, filename, base_lineno):
    self.filename = filename
    self.base_lineno = base_lineno
    self.lineno = base_lineno

  def set_lineno(self, lineno):
    self.lineno = self.base_lineno + lineno - 1


class _Storer:

  def __init__(self, fn):
    self.fn = fn

  def store(self, value):
    self.fn(value)


class _HdlChecker(ast.NodeVisitor):

  HDL_TYPES = (Value, Interface, InterfaceView)

  def __init__(self, globs, locs, hdl_required=('Yield', 'YieldFrom')):
    super().__init__()
    self._globs = globs
    self._locs = locs
    self._scope = 0
    self._stack = []
    self.count = 0
    for name in pyu.expand_strings(hdl_required):
      setattr(self, f'visit_{name}', self._checker)

  def _checker(self, node):
    self.count += 1

  @contextlib.contextmanager
  def _scope_in(self):
    self._scope += 1
    try:
      yield self
    finally:
      self._scope -= 1
      if self._scope == 0:
        self._stack = []

  def _pop_value(self):
    return self._stack.pop() if self._stack else NONE

  def _push_value(self, value):
    if self._scope > 0:
      self._stack.append(value)
    if isinstance(value, self.HDL_TYPES):
      self.count += 1

  def _needs_hdl_call(self, func):
    if is_hdl_function(func):
      return True
    if inspect.isfunction(func):
      sig = inspect.signature(func)
      if sig.return_annotation in self.HDL_TYPES:
        return True
    if not inspect.isclass(func):
      return False

    return (pyiu.is_subclass(func, Entity) or func in self.HDL_TYPES or
            is_hdl_function(getattr(func, '__init__', None)))

  def visit_Attribute(self, node):
    with self._scope_in():
      self.visit(node.value)
      if (avalue := getattr(self._pop_value(), node.attr, NONE)) is not NONE:
        self._push_value(avalue)

  def visit_Name(self, node):
    value = vload(node.id, self._globs, self._locs)
    self._push_value(value)

  def visit_Call(self, node):
    with self._scope_in():
      self.visit(node.func)
      if self._needs_hdl_call(self._pop_value()):
        self.count += 1

    for carg in node.args:
      self.visit(carg)
    for kwarg in node.keywords:
      self.visit(kwarg)


class _Frame:

  def __init__(self, fglobals, flocals, location):
    self.fglobals = fglobals
    self.flocals = flocals
    self.location = location
    self.yields = []
    self.global_names = set()
    self.return_capture = 0
    self.in_hdl = 0
    self.return_values = []
    self.retval = None
    self.loops = []
    self.loop_modes = []

  def new_locals(self, flocals):
    return pycu.new_with(self, flocals=flocals)


class _VoidResult:
  pass


class _ExecVisitor(ast.NodeVisitor):

  def __init__(self, vglobals, vlocals=None):
    super().__init__()
    self._default_loop_mode = _LoopModes.UNROLLED
    self._frames = [_Frame(vglobals, vlocals or dict(), _SourceLocation('NOFILE', 0))]
    self._variables = []
    self._results = []
    self._revgen = pycu.RevGen(fmt='{name}{ver}')

  @property
  def frame(self):
    return self._frames[-1]

  @property
  def locals(self):
    return self.frame.flocals

  @property
  def globals(self):
    return self.frame.fglobals

  @property
  def global_names(self):
    return self.frame.global_names

  @property
  def location(self):
    return self.frame.location

  @property
  def yields(self):
    return self.frame.yields

  @property
  def variables(self):
    return self._variables[-1]

  @property
  def results(self):
    return self._results[-1] if self._results else None

  @property
  def loop(self):
    return self.frame.loops[-1]

  @property
  def loop_mode(self):
    frame = self.frame
    return frame.loop_modes[-1] if frame.loop_modes else self._default_loop_mode

  def visit(self, node):
    if self.frame.in_hdl >= 0:
      method = 'visit_' + pyiu.cname(node)
      visitor = getattr(self, method, self._visit_default)
    else:
      visitor = self._static_eval

    visitor(node)

  @contextlib.contextmanager
  def _frame(self, frame):
    self._frames.append(frame)
    try:
      yield self
    finally:
      self._frames.pop()

  @contextlib.contextmanager
  def _force_hdl(self, step):
    frame = self.frame
    frame.in_hdl += step
    try:
      yield self
    finally:
      frame.in_hdl -= step

  @contextlib.contextmanager
  def _return_capture(self):
    frame = self.frame
    frame.return_capture += 1
    try:
      yield self
    finally:
      frame.return_capture -= 1

  @contextlib.contextmanager
  def _loop(self, loop):
    frame = self.frame
    frame.loops.append(loop)
    try:
      yield self
    finally:
      frame.loops.pop()

  @contextlib.contextmanager
  def _loop_mode(self, mode):
    frame = self.frame
    frame.loop_modes.append(mode)
    try:
      yield self
    finally:
      frame.loop_modes.pop()

  @contextlib.contextmanager
  def _exec_locals(self, tmp_values):
    # Override temporary values, but keep the changes to the locals.
    save_dict = dict()

    ref_dict = self.locals
    for k, v in tmp_values.items():
      save_dict[k] = ref_dict.get(k, NONE)
      ref_dict[k] = v

    try:
      yield self
    finally:
      for k, v in save_dict.items():
        if v is NONE:
          ref_dict.pop(k, None)
        else:
          ref_dict[k] = v

  @contextlib.contextmanager
  def _eval_locals(self, tmp_values):
    # Apply temporary changes during the eval operation, but revert to previous
    # status afterwards (do not persist eventual changes).
    save_dict = self.locals.copy()

    ref_dict = self.locals
    for k, v in tmp_values.items():
      ref_dict[k] = v

    try:
      yield self
    finally:
      self.locals.clear()
      self.locals.update(save_dict)

  def _store_value(self, name, value):
    if name in self.global_names:
      self.globals[name] = value
    else:
      self.locals[name] = value

  def _add_variable(self, name, dtype, isreg, init=None, vspec=None):
    alog.debug(lambda: f'NEW VAR: {valkind(isreg)} {dtype}\t{name}\t{init}\t{vspec}')

    self.variables[name] = Variable(dtype, isreg, init=init, vspec=vspec)

  def _static_eval(self, node):
    self.location.set_lineno(node.lineno)

    return asu.static_eval(node, self.globals, self.locals,
                           filename=self.location.filename)

  def _eval_node(self, node, visit_node=True):
    self.location.set_lineno(node.lineno)

    results = []
    self._results.append(results)
    try:
      if visit_node:
        self.visit(node)
      else:
        alog.debug(lambda: asu.dump(node))
        self.generic_visit(node)
    finally:
      self._results.pop()

    return results[0] if len(results) == 1 else results if results else _VoidResult()

  def _annotated_exception(self, ex):
    noted = getattr(ex, '_noted', False)
    if not noted:
      # Discard the 0-th entry as it is the NOFILE dummy one.
      locs = []
      for f in self._frames[1: ]:
        locs.append(f'{f.location.filename}:{f.location.lineno}')

      ex = pycu.rewrited_exception(ex, f'\nError stack:\n' + '\n'.join(locs))
      setattr(ex, '_noted', True)

    return ex

  def eval_node(self, node, visit_node=True):
    try:
      return self._eval_node(node, visit_node=visit_node)
    except _Exception:
      # These are internal exceptions meant to be left as is.
      raise
    except Exception as ex:
      raise self._annotated_exception(ex) from None

  def push_result(self, result):
    results = self.results
    if results is not None and not isinstance(result, _VoidResult):
      results.append(result)

  def push_yield(self, value):
    yields = self.yields
    if yields is not None:
      yields.append(value)

  def _get_function_body(self, name, node):
    if isinstance(node, ast.Module):
      for mnode in node.body:
        if isinstance(mnode, ast.FunctionDef) and mnode.name == name:
          return mnode.body

    alog.warning(f'Function "{name}" body not found in node: {asu.dump(node)}')

    return [node]

  def _populate_args_locals(self, sig, args, kwargs, func_locals):
    alog.debug(lambda: f'Build Args: ARGS={args}\tKWARGS={kwargs}\tFNLOCALS={func_locals}')

    n, xkwargs = 0, kwargs.copy()
    for param in sig.parameters.values():
      if param.kind == inspect.Parameter.POSITIONAL_ONLY:
        func_locals[param.name] = args[n]
        n += 1
      elif param.kind == inspect.Parameter.VAR_POSITIONAL:
        func_locals[param.name] = args[n: ]
        n = len(args)
      elif param.kind == inspect.Parameter.POSITIONAL_OR_KEYWORD:
        pvalue = xkwargs.pop(param.name, NONE)
        if pvalue is not NONE:
          func_locals[param.name] = pvalue
        elif param.default != inspect.Parameter.empty:
          func_locals[param.name] = param.default
        else:
          func_locals[param.name] = args[n]
          n += 1
      elif param.kind == inspect.Parameter.KEYWORD_ONLY:
        pvalue = xkwargs.pop(param.name, NONE)
        if pvalue is NONE and param.default != inspect.Parameter.empty:
          pvalue = param.default

        func_locals[param.name] = pvalue
      elif param.kind == inspect.Parameter.VAR_KEYWORD:
        func_locals[param.name] = xkwargs
        xkwargs = dict()

    return xkwargs

  def _generate_retval_result(self, fname, value, vindex, tmp_names):
    if isinstance(value, (list, tuple)):
      results = []
      for lvalue in value:
        rvalue, vindex = self._generate_retval_result(fname, lvalue, vindex, tmp_names)
        results.append(rvalue)

      return type(value)(results), vindex
    elif pycu.isdict(value):
      rdict = dict()
      for k, dvalue in value.items():
        rvalue, vindex = self._generate_retval_result(fname, dvalue, vindex, tmp_names)
        rdict[k] = rvalue

      return rdict, vindex

    if vindex == len(tmp_names):
      rvname = self._revgen.newname(fname)
      tmp_names.append(rvname)
    elif vindex < len(tmp_names):
      rvname = tmp_names[vindex]
    else:
      fatal(f'Value index out of range: {vindex} vs {len(tmp_names)}')

    var = self.load_var(rvname, ctx=ast.Store())
    if var is NONE and isinstance(value, Value):
      var = self._new_variable(rvname, value)

    self._assign_value(var, value, rvname)

    return self.load_var(rvname, ctx=ast.Load()), vindex + 1

  def _generate_retlist_result(self, fname, return_values):
    tmp_names, results, sig = [], [], None
    for i, retval in enumerate(return_values):
      with self.emitter.placement(retval.placement):
        vsig = pycu.signature(retval.value)
        if sig is not None and not pycu.equal_signature(sig, vsig):
          fatal(f'Return values signature mismatch: {vsig} vs. {sig}')

        sig = vsig

        rvalue, _ = self._generate_retval_result(fname, retval.value, 0, tmp_names)
        results.append(rvalue)

    return results[0]

  def _run_function_helper(self, func, args, kwargs):
    func_self = getattr(func, '__self__', None)
    func = getattr(func, '__wrapped__', func)

    sig = inspect.signature(func)
    alog.debug(lambda: f'Signature: {sig}')

    fninfo = get_function_info(func)
    alog.debug(lambda: f'Source: {fninfo.filename} @ {fninfo.lineno}\n{fninfo.source}')

    func_node = ast.parse(fninfo.source, filename=fninfo.filename, mode='exec')

    func_node = ast_hdl_transform(func_node)
    alog.debug(lambda: f'FUNC AST: {asu.dump(func_node)}')

    func_locals = dict()
    if func_self is not None:
      args = [func_self] + args

    kwargs = self._populate_args_locals(sig, args, kwargs, func_locals)
    func_locals.update(**kwargs)

    func_body = self._get_function_body(pyiu.func_name(func), func_node)

    frame = _Frame(get_obj_globals(func),
                   func_locals,
                   _SourceLocation(fninfo.filename, fninfo.lineno))
    with self._frame(frame):
      try:
        for bnode in func_body:
          self.visit(bnode)
      except _ReturnException as rex:
        frame.retval = rex.value

    if frame.return_values:
      # This handles the case of return statements from within an HDL function.
      # As all the HDL functions gets inlined, there is not really a "return", so
      # the returned values get stored into a temporary, and a "load" of that temporary
      # returned from this API.
      result = self._generate_retlist_result(pyiu.func_name(func), frame.return_values)
    else:
      result = frame.yields if frame.yields else frame.retval

    alog.debug(lambda: f'RESULT: {result}\tEXIT LOCALS: {pyu.stri(func_locals)}')

    return result

  def _run_class_function(self, func, args, kwargs):
    alog.debug(lambda: f'OBJECT CREATE: class={pyiu.func_name(func)} args={pyu.stri(args)} ' \
               f'kwargs={pyu.stri(kwargs)}')
    obj = func.__new__(func, *args, **kwargs)
    init = getattr(func, '__init__', None)
    if init is not None:
      self._run_function_helper(init, [obj] + args, kwargs)

    return obj

  def _call_direct(self, func, args, kwargs):
    alog.debug(lambda: f'DIRECT CALL: function={pyiu.func_name(func)} args={pyu.stri(args)} ' \
               f'kwargs={pyu.stri(kwargs)}')

    # We cannot call func(*args, **kwargs) directly as we need to insert the current
    # locals and globals.
    self.locals.update(__func=func, __args=args, __kwargs=kwargs)

    return eval('__func(*__args, **__kwargs)', self.globals, self.locals)

  def _needs_hdl_processing(self, func):
    if is_hdl_function(func):
      return True
    elif not inspect.isclass(func):
      return False

    return is_hdl_function(getattr(func, '__init__', None))

  def run_function(self, func, args, kwargs=None):
    kwargs = kwargs or dict()
    if self._needs_hdl_processing(func):
      if inspect.isclass(func):
        # Running a function with a class function object, means object creation.
        result = self._run_class_function(func, args, kwargs)
      else:
        result = self._run_function_helper(func, args, kwargs)

      return result

    return self._call_direct(func, args, kwargs)

  def _is_hdl_tree(self, node):
    checker = _HdlChecker(self.globals, self.locals)
    checker.visit(node)

    return checker.count > 0


class CodeGen(_ExecVisitor):

  def __init__(self, emitter, globs):
    super().__init__(globs)
    self.emitter = emitter
    self._module_decls_place = emitter.emit_placement()
    self._used_entities = set()
    self._generated_entities = set()
    self._ent_versions = EntityVersions()
    self._vars_places = []
    self._root_vars = dict()
    self._process_kind = None
    self._process_args = None
    self._setup_handlers()

  def _setup_handlers(self):
    self.visit_IfExp = functools.partial(self._hdl_visitor, 'IfExp', pushres=True)
    self.visit_If = functools.partial(self._hdl_visitor, 'If')
    self.visit_For = functools.partial(self._hdl_visitor, 'For')
    self.visit_While = functools.partial(self._hdl_visitor, 'While')
    self.visit_Match = functools.partial(self._hdl_visitor, 'Match')
    self.visit_Try = functools.partial(self._hdl_visitor, 'Try')
    self.visit_With = functools.partial(self._hdl_visitor, 'With')
    self.visit_Call = functools.partial(self._hdl_visitor, 'Call', pushres=True)

  def _hdl_visitor(self, name, node, pushres=False):
    if self.frame.in_hdl > 0 or self._is_hdl_tree(node):
      handler = getattr(self, f'_handle_{name}')
      handler(node)
    else:
      result = self._static_eval(node)
      if pushres:
        self.push_result(result)

  def _get_format_parts(self, fmt, kwargs):

    def mapfn(tok):
      arg = self.emitter.eval_token(tok)
      if arg is None:
        varg = self.run_code(tok, kwargs, 'eval',
                             filename=self.location.filename,
                             lineno=self.location.lineno)
        return self.emitter.eval_tostring(varg)

      return arg

    def nmapfn(tok):
      return self.emitter.quote_string(tok)

    return pyu.sreplace(_CODEFMT_RX, fmt, mapfn, nmapfn=nmapfn, join=False)

  def _resolve_code(self, code, kwargs):

    def mapfn(tok):
      arg = self.emitter.eval_token(tok)
      if arg is None:
        varg = self.run_code(tok, kwargs, 'eval',
                             filename=self.location.filename,
                             lineno=self.location.lineno)
        return self.emitter.svalue(varg)

      return arg

    return pyu.sreplace(_CODEFMT_RX, code, mapfn)

  def _visit_default(self, node):
    # This is the default handler when a specific visit_XXX() method does not exist.
    # If something is not working, look for 'FAST STATIC' within the logs, which will
    # tell which node is escaping from the PyXHDL interpreter.
    alog.debug(lambda: f'FAST STATIC: {asu.dump(node)}')
    self._static_eval(node)

  def load_var(self, name, ctx=ast.Load()):
    var = vload(name, self.globals, self.locals)
    if var is NONE:
      shvar = self._root_vars.get(name)
      if shvar is not None:
        var = Value(shvar.dtype, Ref(name, vspec=shvar.vspec, vname=name),
                    isreg=shvar.isreg)
      elif isinstance(ctx, ast.Load):
        fatal(f'Undefined variable: {name}')

    return self.emitter.var_remap(var, isinstance(ctx, ast.Store))

  def _new_variable(self, name, value):
    isreg = value.isreg if value.isreg is not None else False
    base_name = None

    vinit = value.init
    if vinit is not None:
      init, vspec, base_name = vinit.value, vinit.vspec, vinit.name
    else:
      init, vspec = None, None

      vref = value.ref
      if vref is not None:
        vspec = vref.vspec
        if vref.cname is not None:
          base_name = vref.cname

    if vspec is not None:
      vspec = vspec.for_new_variable()

    vname = self._revgen.newname(base_name or name, shortzero=True)

    var = Value(value.dtype, Ref(vname, vspec=vspec, vname=vname), isreg=isreg)
    self._add_variable(vname, var.dtype, var.isreg, init=init, vspec=vspec)
    self._store_value(name, var)

    return var

  def _assign_variable(self, var, value, name):
    if isinstance(value, Value) and isinstance(value.value, Init):
      if value.isreg != var.isreg:
        alog.warning(f'Cannot create "{var.name}" as {valkind(value.isreg)}, ' \
                     f'will be {valkind(var.isreg)}')
      if isinstance(value.value, Init):
        alog.debug(lambda: f'ASSIGN CREATE: {name} is {value.dtype} = {value.value}')
      else:
        alog.debug(lambda: f'ASSIGN CREATE: {name} is {value.dtype}')
    else:
      if is_ro_ref(var):
        fatal(f'Trying to assign {var.name} which is read-only')

      self.emitter.emit_assign(var, name, value)

  def _assign_value(self, var, value, name):
    alog.debug(lambda: f'ASSIGN: {var} ({name}) = {value}')

    if not isinstance(var, Value):
      if isinstance(value, Value):
        # Two cases of creating new variables.
        if value.init is not None:
          # The mkwire(name=None), mkreg(name=None), mkvwire() and mkvreg() APIs
          # have the "init" member populated.
          var = self._new_variable(name, value)
        elif (ref := value.ref) and ref.cname is not None:
          # The mkwire(name=XYZ) and mkreg(name=XYZ) APIs have the "ref" populated,
          # with ref.cname not None.
          var = self._new_variable(name, value)
          return
    elif name is not None and var.ref is None:
      var = None

    if isinstance(var, Value):
      self._assign_variable(var, value, name)
    elif name is not None:
      self._store_value(name, value)

  def _multi_assign_inner(self, target, value):
    elts = elements(target)
    if elts is not None:
      for el, val in zip(elts, value):
        self._multi_assign_inner(el, val)
    else:
      var = self.eval_node(target)
      if isinstance(var, _Storer):
        var.store(value)
      else:
        name = target.id if isinstance(target, ast.Name) else None
        self._assign_value(var, value, name)

  def _multi_assign(self, targets, value):
    for target in targets:
      self._multi_assign_inner(target, value)

  def assign_value(self, name, value):
    self._assign_value(self.load_var(name, ctx=ast.Store()), value, name)

  def _unpack_value(self, targets, values, dest):
    if isinstance(targets, ast.Name):
      dest[targets.id] = values
    else:
      elts = elements(targets)
      if elts:
        for t, v in zip(elts, values):
          self._unpack_value(t, v, dest)

  def _handle_array(self, node, atype):
    alog.debug(lambda: asu.dump(node))
    values = [self.eval_node(n) for n in node.elts]

    self.push_result(atype(values))

  def _handle_call(self, func, args, kwargs):
    if pyiu.is_subclass(func, Entity):
      alog.debug(lambda: f'Entity instantiation: {pyiu.func_name(func)} args={pyu.stri(args)} ' \
                 f'kwargs={pyu.stri(kwargs)}')
      # We do not run the Entity init code with run_function() as this is not what
      # we want. Doing so will end up emitting HDL code.
      # Entities constructors cannot perform operations which lead to HDL code emission.
      # Note that HDL data type manipulation (like adding values to containers and such)
      # is OK, as this will not emit HDL code.
      result = func(*args, **kwargs)

      ent_name = self._register_entity(func, kwargs)

      self.emitter.emit_entity(result, kwargs, ent_name=ent_name)
    else:
      result = self.run_function(func, args, kwargs=kwargs)

    return result

  def _process_scope_enter(self, vars_scope, process_kind, process_args):
    self._variables.append(dict())
    self._vars_places.append(vars_scope)
    self._process_kind = process_kind
    self._process_args = process_args

  def _process_scope_exit(self):
    alog.debug(lambda: f'Variables stack is {len(self._variables)} deep')

    svars = self._variables.pop()
    place = self._vars_places.pop()

    root_vars = dict()
    with self.emitter.placement(place) as emt:
      for name, var in svars.items():
        if emt.is_root_variable(var):
          root_vars[name] = var
        else:
          alog.debug(lambda: f'VARIABLE: {valkind(var.isreg)} {var.dtype} {name}')
          emt.emit_declare_variable(name, var)

    with self.emitter.placement(self.emitter.module_vars_place) as emt:
      for name, var in root_vars.items():
        shv = self._root_vars.get(name)
        if shv is not None:
          if shv != var:
            fatal(f'Root variable declaration mismatch: {pyu.stri(var)} vs. {pyu.stri(shv)}')
        else:
          alog.debug(lambda: f'ROOT VARIABLE: {valkind(var.isreg)} {var.dtype} {name}')
          emt.emit_declare_variable(name, var)

    self._root_vars.update(**root_vars)

  def _register_entity(self, eclass, kwargs, generated=False):
    pargs, rkwargs = dict(), kwargs.copy()
    for pin in eclass.PORTS:
      arg = rkwargs.pop(pin.name, None)
      if arg is None:
        fatal(f'Missing entity port "{pin.name}" binding for entity {eclass.__name__}')

      pin.verify_arg(arg)
      if pin.is_ifc():
        pargs[pin.name] = pin.ifc_view(arg)
      else:
        pargs[pin.name] = arg.new_value(make_port_ref(pin))

    for kwarg_name, arg in eclass.ARGS.items():
      rkwargs[kwarg_name] = rkwargs.get(kwarg_name, arg)

    ename = eclass.NAME
    if ename is None:
      # External entities (for which the NAME field is provided within the Entity
      # subclass declaration) do not need to be regsitered, as no generation needs
      # to happen for them.
      ename, erec = self._ent_versions.getname(eclass, pargs, {k: wrap(v) for k, v in rkwargs.items()})
      self._used_entities.add(erec)
      if generated:
        self._generated_entities.add(erec)
    else:
      # External libraries can provide a specific library to be included (where
      # such entity is defined).
      if eclass.LIBNAME:
        self.emitter.add_extra_library(eclass.LIBNAME)

    return ename

  def _get_sensitivity(self, hdl_args, din):
    def expand(v, dest):
      if pycu.isdict(v):
        dest.update(v)
      elif isinstance(v, str):
        for port in pyu.resplit(v, ','):
          sargs = dict()
          if port.startswith('+'):
            sargs['trigger'] = POSEDGE
            port = port[1: ]
          elif port.startswith('-'):
            sargs['trigger'] = NEGEDGE
            port = port[1: ]

          dest[port] = Sens(**sargs)
      elif isinstance(v, (list, tuple)):
        for x in v:
          expand(x, dest)

      return dest

    sstmp = expand(hdl_args.get('sens', dict()), dict())

    sensitivity = dict()
    for name, sens in sstmp.items():
      m = re.match(r'(\w+)(\.(\w+))?', name)
      if not m:
        fatal(f'Invalid sensitivity port name: {name}')

      pname, fname = m.group(1), m.group(3)

      if fname:
        pin = din.get(pname)
        if pin is None:
          fatal(f'Sensitivity source interface is not a port: {pname}')
        if not pin.is_ifc():
          fatal(f'Sensitivity source interface is not an interface: {pin}')

        pname = pin.ifc_field_name(fname)
      else:
        if pname not in din and pname not in self._root_vars:
          fatal(f'Sensitivity source is not a port: {pname}')

      sensitivity[pname] = sens

      alog.debug(lambda: f'Sensitivity: {pname} {sens.trigger}')

    return sensitivity

  def generate_entity(self, eclass, eargs):
    alog.debug(lambda: f'Entity {eclass.__name__}')

    ent_name = self._register_entity(eclass, eargs, generated=True)

    kwargs = eargs.copy()
    din, cargs = dict(), dict()
    for pin in eclass.PORTS:
      alog.debug(lambda: f'Port: {pin.name} {pin.idir}')

      arg = kwargs.pop(pin.name, None)
      if arg is None:
        fatal(f'Missing argument "{pin.name}" for Entity "{eclass.__name__}"')

      din[pin.name] = pin
      if pin.is_ifc():
        cargs[pin.name] = pin.ifc_view(arg)
      else:
        ref = make_port_ref(pin)
        if isinstance(arg, Value):
          port_arg = arg.new_value(ref)
        else:
          if not isinstance(arg, Type):
            fatal(f'Argument must be Type at this point: {arg}')

          port_arg = mkwire(arg, name=ref)

        cargs[pin.name] = self.emitter.make_port_arg(port_arg)

    for kwarg_name, arg in eclass.ARGS.items():
      rarg = kwargs.get(kwarg_name, arg)
      kwargs[kwarg_name] = rarg

      alog.debug(lambda: f'Arg: {kwarg_name} = {rarg}')

    uw_kwargs = {k: unwrap(v) for k, v in kwargs.items()}
    ent_args = pycu.dmerge(cargs, uw_kwargs)

    ent = eclass(**ent_args)

    ecomm = f'Entity "{ent_name}" is "{eclass.__name__}" with:\n' \
      f'\targs={({k: pyu.stri(v.dtype if isinstance(v, Value) else v) for k, v in cargs.items()})}\n' \
      f'\tkwargs={pyu.stri(ent.kwargs)}'

    with self.emitter.placement(self._module_decls_place) as emt:
      emt.emit_module_def(ent_name, ent, comment=ecomm)

    self.emitter.emit_module_decl(ent_name, ent)

    for func in ent.enum_processes():
      hdl_args = get_hdl_args(func) or dict()

      alog.debug(lambda: f'Process function: {pyiu.func_name(func)}')

      sensitivity = self._get_sensitivity(hdl_args, din)
      process_kind = hdl_args.get('kind')

      with self.emitter.indent():
        # Process functions will automatically see port names as locals, so the
        # position arguments list is empty. Process functions can still have keyword
        # arguments, which will be correctly populated.
        self.generate_process(func, [ent],
                              kwargs=ent_args,
                              sensitivity=sensitivity,
                              process_kind=process_kind,
                              process_args=hdl_args)

    self.emitter.emit_module_end()

    self._reset_entity_context()

  def generate_process(self, func, args, kwargs=None, sensitivity=None,
                       process_kind=None, process_args=None):
    if process_kind == ROOT_PROCESS:
      self._process_scope_enter(self.emitter.module_vars_place, process_kind,
                                process_args)
      try:
        result = self.run_function(func, args, kwargs=kwargs)
      finally:
        self._process_scope_exit()
    else:
      self.emitter.emit_process_decl(pyiu.func_name(func),
                                     sensitivity=sensitivity,
                                     process_kind=process_kind,
                                     process_args=process_args)

      self.emitter.emit_process_begin()
      self._process_scope_enter(self.emitter.process_vars_place, process_kind,
                                process_args)
      try:
        with self.emitter.indent():
          result = self.run_function(func, args, kwargs=kwargs)
      finally:
        self._process_scope_exit()
      self.emitter.emit_process_end()

    return result

  def _reset_entity_context(self):
    self._root_vars = dict()
    self._revgen = pycu.RevGen(fmt='{name}{ver}')

  def _flush_generation(self):
    while True:
      used_entities = self._used_entities.copy()
      generated = 0
      for erec in used_entities:
        if erec not in self._generated_entities:
          self.generate_entity(erec.eclass, pycu.dmerge(erec.pargs, erec.kwargs))
          generated += 1

      if generated == 0:
        break

  def visit_Constant(self, node):
    self.push_result(node.value)

  def visit_FormattedValue(self, node):
    self.push_result(self._static_eval(node))

  def visit_JoinedStr(self, node):
    self.push_result(self._static_eval(node))

  def visit_Tuple(self, node):
    self._handle_array(node, tuple)

  def visit_List(self, node):
    self._handle_array(node, list)

  def visit_Set(self, node):
    alog.debug(lambda: asu.dump(node))
    result = set()
    for snode in node.elts:
      result.add(self.eval_node(snode))

    self.push_result(result)

  def visit_Dict(self, node):
    alog.debug(lambda: asu.dump(node))
    result = dict()
    for knode, vnode in zip(node.keys, node.values):
      k = self.eval_node(knode)
      v = self.eval_node(vnode)
      result[k] = v

    self.push_result(result)

  def visit_Name(self, node):
    alog.debug(lambda: asu.dump(node))
    result = self.load_var(node.id, ctx=node.ctx)
    self.push_result(result)

  def visit_Expression(self, node):
    value = self.eval_node(node.body)
    self.push_result(value)

  def visit_Expr(self, node):
    value = self.eval_node(node.value)
    self.push_result(value)

  def visit_UnaryOp(self, node):
    operand = self.eval_node(node.operand)
    if has_hdl_vars(operand):
      result = self.emitter.eval_UnaryOp(node.op, operand)
    else:
      result = self._static_eval(node)

    self.push_result(result)

  def _neval(self, ref_node, cls, **kwargs):
    snode = cls(**kwargs)
    ast.copy_location(snode, ref_node)

    return self._static_eval(snode)

  def visit_BinOp(self, node):
    left = self.eval_node(node.left)
    right = self.eval_node(node.right)
    if has_hdl_vars((left, right)):
      result = self.emitter.eval_BinOp(node.op, left, right)
    else:
      result = self._static_eval(node)

    self.push_result(result)

  def visit_BoolOp(self, node):
    alog.debug(lambda: asu.dump(node))

    # Implement AND(False, ...) and OR(True, ...) shortcutting.
    result, values = None, []
    for val in node.values:
      xval = self.eval_node(val)
      if xval is True:
        if isinstance(node.op, ast.Or):
          result = True
          break
      elif xval is False:
        if isinstance(node.op, ast.And):
          result = False
          break
      else:
        values.append(xval)

    if result is None:
      if has_hdl_vars(values):
        result = self.emitter.eval_BoolOp(node.op, values)
      else:
        result = self._static_eval(node)

    self.push_result(result)

  def visit_Compare(self, node):
    left = self.eval_node(node.left)
    comparators = []
    for comp in node.comparators:
      xcomp = self.eval_node(comp)
      comparators.append(xcomp)

    if has_hdl_vars((left, comparators)):
      result = self.emitter.eval_Compare(left, node.ops, comparators)
    else:
      result = self._static_eval(node)

    self.push_result(result)

  def visit_Lambda(self, node):
    alog.debug(lambda: asu.dump(node))

    flocals = self.locals

    def lambda_runner(*args, **kwargs):
      func_locals = flocals.copy()

      for p, arg in zip(node.args.args, args):
        func_locals[p.arg] = arg

      func_locals.update(**kwargs)

      with self._frame(self.frame.new_locals(func_locals)):
        result = self.eval_node(node.body)

      alog.debug(lambda: f'LM RESULT {result}\tLOCALS: {pyu.stri(func_locals)}')

      return result

    self.push_result(lambda_runner)

  def _handle_Call(self, node):
    func = self.eval_node(node.func)

    args = []
    for carg in node.args:
      if isinstance(carg, ast.Starred):
        value = self.eval_node(carg.value)
        for svalue in elements(value):
          args.append(svalue)
      else:
        value = self.eval_node(carg)
        args.append(value)

    kwargs = dict()
    for kwarg in node.keywords:
      kwval = self.eval_node(kwarg.value)
      if kwarg.arg:
        kwargs[kwarg.arg] = kwval
      else:
        if not pycu.isdict(kwval):
          fatal(f'Wrong type: {type(kwval)}')

        for name, value in kwval.items():
          kwargs[name] = value

    alog.debug(lambda: f'CALL: {func}\t{pyu.stri(args)}\t{pyu.stri(kwargs)}')

    result = self._handle_call(func, args, kwargs)

    self.push_result(result)

  def _load_subs_attr(self, node, ctx):
    if isinstance(node, ast.Name):
      return self.load_var(node.id, ctx=ctx)

    return self.eval_node(node)

  def _attr_storefn(self, target, name):

    def setfn(value):
      var = getattr(target, name, None)
      if isinstance(var, Value) and var.ref is not None:
        self._assign_value(var, value, name)
      else:
        setattr(target, name, value)

    return setfn

  def visit_Attribute(self, node):
    value = self._load_subs_attr(node.value, node.ctx)
    if isinstance(node.ctx, ast.Load):
      result = getattr(value, node.attr)
    elif isinstance(node.ctx, ast.Store):
      if isinstance(value, Value) and is_ro_ref(value):
        fatal(f'Trying to assign attribute {node.attr} of {value.name}, which is read-only')

      result = _Storer(self._attr_storefn(value, node.attr))
    else:
      fatal(f'Unknown subscript context type: {asu.dump(node.ctx)}')

    self.push_result(result)

  def visit_NamedExpr(self, node):
    value = self.eval_node(node.value)
    var = self.load_var(node.target.id, ctx=node.target.ctx)
    self._assign_value(var, value, node.target.id)

  def visit_Subscript(self, node):
    value = self._load_subs_attr(node.value, node.ctx)
    sv = self.eval_node(node.slice)

    if isinstance(value, Value) and isinstance(node.ctx, ast.Store) and is_ro_ref(value):
      fatal(f'Trying to assign subscript {sv} of {value.name}, which is read-only')

    if isinstance(value, Value):
      result = self.emitter.eval_Subscript(value, sv)
    elif isinstance(node.ctx, ast.Load):
      result = value[sv]
    elif isinstance(node.ctx, ast.Store):
      result = _Storer(subscript_setter(value, sv))
    else:
      fatal(f'Unknown subscript context type: {asu.dump(node.ctx)}')

    self.push_result(result)

  def visit_Slice(self, node):
    lower = self.eval_node(node.lower) if node.lower is not None else None
    upper = self.eval_node(node.upper) if node.upper is not None else None
    step = self.eval_node(node.step) if node.step is not None else None
    if has_hdl_vars((upper, step)):
      fatal(f'Slice cannot have HDL arguments: {asu.dump(node)}')

    self.push_result(slice(lower, upper, step))

  def _walk_generator(self, node, fn):
    for gen in node.generators:
      gen_iter = self.eval_node(gen.iter)
      for xv in gen_iter:
        self.locals[gen.target.id] = xv

        ifv = True
        for ifn in gen.ifs:
          ifv = self.eval_node(ifn)
          if not ifv:
            break

        if ifv:
          fn(node)

  def visit_DictComp(self, node):
    result = dict()

    def walker(node):
      kv = self.eval_node(node.key)
      vv = self.eval_node(node.value)
      result[kv] = vv

    self._walk_generator(node, walker)

    self.push_result(result)

  def _handle_container_comp(self, node, ctype):
    result = []

    def walker(node):
      cv = self.eval_node(node.elt)
      result.append(cv)

    self._walk_generator(node, walker)

    if not isinstance(result, ctype):
      result = ctype(result)

    self.push_result(result)

  def visit_ListComp(self, node):
    self._handle_container_comp(node, list)

  def visit_SetComp(self, node):
    self._handle_container_comp(node, set)

  def visit_GeneratorExp(self, node):
    self._handle_container_comp(node, tuple)

  def visit_Assign(self, node):
    value = self.eval_node(node.value)
    self._multi_assign(node.targets, value)

  def visit_AugAssign(self, node):
    ltarget = as_loading(node.target)
    ref_value = self.eval_node(ltarget)
    value = self.eval_node(node.value)

    if has_hdl_vars((ref_value, value)):
      result = self.emitter.eval_BinOp(node.op, ref_value, value)
    else:
      result = self._neval(node, ast.BinOp, op=node.op, left=ltarget, right=node.value)

    self._multi_assign([node.target], result)

  def visit_Raise(self, node):
    exc = self.eval_node(node.exc)
    raise exc

  def visit_Assert(self, node):
    test = self.eval_node(node.test)
    msg = self.eval_node(node.msg) if node.msg is not None else None

    if has_hdl_vars(test):
      parts = self._get_format_parts(msg, dict()) if msg else None
      self.emitter.emit_Assert(test, parts)
    elif not test:
      fatal(msg, exc=AssertionError)

  def visit_Module(self, node):
    # This handler simply dive into inner nodes by calling the AST Visitor
    # generic_visit() API (after some logging we do to make sense of what is
    # being processed).
    self.eval_node(node, visit_node=False)

  def _handle_IfExp(self, node):
    test = self.eval_node(node.test)
    if has_hdl_vars(test):
      body = self.eval_node(node.body)
      orelse = self.eval_node(node.orelse)
      result = self.emitter.eval_IfExp(test, body, orelse)
    else:
      if test:
        result = self.eval_node(node.body)
      else:
        result = self.eval_node(node.orelse)

    self.push_result(result)

  def _handle_If(self, node):
    test = self.eval_node(node.test)

    if has_hdl_vars(test):
      with self._return_capture():
        self.emitter.emit_If(test)
        with self.emitter.indent():
          for insn in node.body:
            self.eval_node(insn)

        enode, enodes = node, []
        while len(enode.orelse) == 1 and isinstance(enode.orelse[0], ast.If):
          enode = enode.orelse[0]
          etest = self.eval_node(enode.test)
          if has_hdl_vars(etest):
            self.emitter.emit_Elif(etest)
            with self.emitter.indent():
              for insn in enode.body:
                self.eval_node(insn)
          elif etest:
            # Instructions which are part of a statically (no HDL) True "elif" branch
            # are emitted after the "if" instruction.
            enodes.extend(enode.body)

        if enode.orelse or enodes:
          self.emitter.emit_Else()
          with self.emitter.indent():
            for insn in enodes + (enode.orelse or []):
              self.eval_node(insn)

        self.emitter.emit_EndIf()
    else:
      alog.debug(lambda: f'Resolving static If test: {asu.dump(node.test)}')
      if test:
        for insn in node.body:
          self.eval_node(insn)
      else:
        for insn in node.orelse:
          self.eval_node(insn)

  def _decode_for_loop(self, node):
    idata = self.eval_node(node.iter)

    if isinstance(node.target, ast.Name):
      idata = tuple(idata)
      if idata and all(isinstance(x, int) for x in idata):
        steps = set(idata[i + 1] - idata[i] for i in range(len(idata) - 1))
        if len(steps) <= 1:
          return _ForLoop(data=idata,
                          ivar=node.target.id,
                          start=idata[0],
                          end=idata[-1],
                          step=steps.pop() if steps else 1)

    return _ForLoop(data=idata)

  def _unrolled_For(self, node, floop):
    with self._loop(_LoopContext(mode=_LoopModes.UNROLLED)):
      for t in floop.data:
        self._unpack_value(node.target, t, self.locals)

        got_break = False
        for insn in node.body:
          try:
            self.eval_node(insn)
          except _BreakException:
            got_break = True
            break
          except _ContinueException:
            break

        if got_break:
          break

  def _hdl_For(self, node, floop):
    loop_var = mkwire(INT, name=floop.ivar, const=True)

    with (self._loop(_LoopContext(mode=_LoopModes.HDL)),
          self._exec_locals({floop.ivar: loop_var})):
      self.emitter.emit_For(floop.ivar, floop.start, floop.end, floop.step)
      with self.emitter.indent():
        for insn in node.body:
          self.eval_node(insn)

      self.emitter.emit_EndFor()

  def _handle_For(self, node):
    alog.debug(lambda: asu.dump(node))

    floop = self._decode_for_loop(node)

    if floop.ivar is None or self.loop_mode != _LoopModes.HDL:
      self._unrolled_For(node, floop)
    else:
      self._hdl_For(node, floop)

  def _handle_While(self, node):
    alog.debug(lambda: asu.dump(node))

    with self._loop(_LoopContext(mode=_LoopModes.UNROLLED)):
      while True:
        test = self.eval_node(node.test)
        if has_hdl_vars(test):
          fatal(f'While test cannot have HDL vars: {asu.dump(node.test)}')

        if test:
          got_break = False
          for insn in node.body:
            try:
              self.eval_node(insn)
            except _BreakException:
              got_break = True
              break
            except _ContinueException:
              break

          if got_break:
            break
        else:
          break

  def visit_Break(self, node):
    alog.debug(lambda: asu.dump(node))

    loop = self.loop
    if loop.mode == _LoopModes.UNROLLED:
      raise _BreakException()
    else:
      self.emitter.emit_Break()

  def visit_Continue(self, node):
    alog.debug(lambda: asu.dump(node))

    loop = self.loop
    if loop.mode == _LoopModes.UNROLLED:
      raise _ContinueException()
    else:
      self.emitter.emit_Continue()

  def _handle_Try(self, node):
    try:
      for bnode in node.body:
        self.visit(bnode)
    except Exception as e:
      alog.debug(lambda: f'Caught exception: {e}')
      for xhand in node.handlers:
        xtype = self.eval_node(xhand.type)
        if isinstance(e, xtype):
          if xhand.name:
            self.locals[xhand.name] = e
          for bnode in xhand.body:
            self.visit(bnode)

          break
    else:
      for enode in node.orelse:
        self.visit(enode)
    finally:
      for fnode in node.finalbody:
        self.visit(fnode)

  def _handle_With(self, node):
    items, names = [], []
    for inode in node.items:
      item = self.eval_node(inode.context_expr)
      name = inode.optional_vars.id if isinstance(inode.optional_vars, ast.Name) else None
      items.append(item)
      names.append(name)

    bi, bitems, rex = 0, [], None
    try:
      while bi < len(items):
        bitem = items[bi].__enter__()
        bitems.append(bitem)
        bi += 1

      for name, bitem in zip(names, bitems):
        if name is not None:
          self.locals[name] = bitem

      for bnode in node.body:
        self.eval_node(bnode)

    except Exception as ex:
      rex = ex

    wex = None
    bi -= 1
    while bi >= 0:
      if rex is not None:
        if not items[bi].__exit__(type(rex), rex, rex.__traceback__):
          wex = rex
      else:
        items[bi].__exit__(None, None, None)
      bi -= 1

    if wex is not None:
      raise wex

  def visit_Return(self, node):
    alog.debug(lambda: asu.dump(node))
    value = self.eval_node(node.value) if node.value is not None else None
    if self.frame.return_capture:
      retval = _Return(value=value, placement=self.emitter.emit_placement())
      self.frame.return_values.append(retval)
    else:
      raise _ReturnException(value)

  def visit_Yield(self, node):
    # Yielded values are accumulated into yields list setup by the function call
    # processing. This is different from how they are implemented in CPython but
    # they are much easier to implement.
    alog.debug(lambda: asu.dump(node))
    value = self.eval_node(node.value)
    self.push_yield(value)

  def visit_YieldFrom(self, node):
    # Yielded values are accumulated into yields list setup by the function call
    # processing. This is different from how they are implemented in CPython but
    # they are much easier to implement.
    alog.debug(lambda: asu.dump(node))
    yiter = self.eval_node(node.value)
    for value in yiter:
      self.push_yield(value)

  def visit_Global(self, node):
    for name in node.names:
      self.global_names.add(name)

  def visit_Pass(self, node):
    pass

  def _handle_Match(self, node):
    subject = self.eval_node(node.subject)
    cases = []
    with self._return_capture():
      for mc in node.cases:
        pattern = self.eval_node(mc.pattern)
        scope = self.emitter.create_placement(extra_indent=2)
        with self.emitter.placement(scope):
          for insn in mc.body:
            self.eval_node(insn)

        for ptrn in pyu.as_sequence(pattern, t=(tuple, list)):
          cases.append(_MatchCase(pattern=ptrn, scope=scope))

    self.emitter.emit_match_cases(subject, cases)

  def visit_MatchAs(self, node):
    alog.debug(lambda: asu.dump(node))
    if node.pattern:
      result = self.eval_node(node.pattern)
      assert node.name is None, 'TBH'
    elif node.name:
      result = self.load_var(node.name, ctx=ast.Load())
    else:
      # None match means "everything else".
      result =  None

    self.push_result(result)

  def visit_MatchValue(self, node):
    alog.debug(lambda: asu.dump(node))
    result = self.eval_node(node.value)
    self.push_result(result)

  def visit_MatchSequence(self, node):
    alog.debug(lambda: asu.dump(node))
    patterns = []
    for ptrn in node.patterns:
      value = self.eval_node(ptrn)
      patterns.append(value)

    self.push_result(patterns)

  @staticmethod
  def current():
    return pytc.get_context(_CGENCTX)

  def context(self):
    return pytc.Context(_CGENCTX, self)

  def run_code(self, code, args, mode, filename='<string>', lineno=1):
    dcode = textwrap.dedent(code)

    try:
      cnode = ast.parse(dcode, filename=filename, mode=mode)
    except Exception as ex:
      raise pycu.rewrited_exception(ex, f'\nCode:\n{dcode}') from None

    cnode.lineno = lineno
    ast.fix_missing_locations(cnode)

    alog.debug(lambda: f'RUN CODE: {asu.dump(cnode)}')

    if mode == 'exec':
      with self._exec_locals(args):
        return self.eval_node(cnode)
    elif mode == 'eval':
      with self._eval_locals(args):
        return self.eval_node(cnode)
    else:
      fatal(f'Invalid mode: {mode}')

  def flush(self):
    self._flush_generation()
    return self.emitter.flush()

  def emit_code(self, code, **kwargs):
    dcode = textwrap.dedent(code)
    alog.debug(lambda: f'INLINE CODE:\n{dcode}')

    hcode = self._resolve_code(dcode, kwargs)
    self.emitter.emit_code(hcode)

  def emit_report(self, fmt, **kwargs):
    parts = self._get_format_parts(fmt, kwargs)

    self.emitter.emit_report(parts, **kwargs)

  def emit_write(self, fmt, **kwargs):
    parts = self._get_format_parts(fmt, kwargs)

    self.emitter.emit_write(parts, **kwargs)

  def emit_call(self, fname, args, dtype):
    return self.emitter.emit_call(fname, args, dtype)

  def emitter_context(self, **kwargs):
    return self.emitter.context(kwargs)

  def no_hdl(self):
    return self._force_hdl(-1)

  def force_hdl(self):
    return self._force_hdl(1)

  def set_loop_mode(self, mode):
    match mode:
      case 'hdl':
        lmode = _LoopModes.HDL

      case 'unrolled':
        lmode = _LoopModes.UNROLLED

      case _:
        fatal(f'Invalid loop mode: {mode}')

    return self._loop_mode(lmode)

  def generate_name(self, name, shortzero=False):
    return self._revgen.newname(name, shortzero=shortzero)

  def get_frames(self):
    return tuple((f.location.filename, f.location.lineno) for f in self._frames)

