import inspect
import warnings
from contextlib import contextmanager
import itertools
from vcd import VCDWriter
from vcd.gtkw import GTKWSave

from .._utils import deprecated
from ..hdl.ast import *
from ..hdl.cd import *
from ..hdl.ir import *
from ..hdl.xfrm import ValueVisitor, StatementVisitor, LHSGroupFilter


class Command:
    pass


class Settle(Command):
    def __repr__(self):
        return "(settle)"


class Delay(Command):
    def __init__(self, interval=None):
        self.interval = None if interval is None else float(interval)

    def __repr__(self):
        if self.interval is None:
            return "(delay ε)"
        else:
            return "(delay {:.3}us)".format(self.interval * 1e6)


class Tick(Command):
    def __init__(self, domain="sync"):
        if not isinstance(domain, (str, ClockDomain)):
            raise TypeError("Domain must be a string or a ClockDomain instance, not {!r}"
                            .format(domain))
        assert domain != "comb"
        self.domain = domain

    def __repr__(self):
        return "(tick {})".format(self.domain)


class Passive(Command):
    def __repr__(self):
        return "(passive)"


class Active(Command):
    def __repr__(self):
        return "(active)"


class _WaveformWriter:
    def update(self, timestamp, signal, value):
        raise NotImplementedError # :nocov:

    def close(self, timestamp):
        raise NotImplementedError # :nocov:


class _VCDWaveformWriter(_WaveformWriter):
    @staticmethod
    def timestamp_to_vcd(timestamp):
        return timestamp * (10 ** 10) # 1/(100 ps)

    @staticmethod
    def decode_to_vcd(signal, value):
        return signal.decoder(value).expandtabs().replace(" ", "_")

    def __init__(self, signal_names, *, vcd_file, gtkw_file=None, traces=()):
        if isinstance(vcd_file, str):
            vcd_file = open(vcd_file, "wt")
        if isinstance(gtkw_file, str):
            gtkw_file = open(gtkw_file, "wt")

        self.vcd_vars = SignalDict()
        self.vcd_file = vcd_file
        self.vcd_writer = vcd_file and VCDWriter(self.vcd_file,
            timescale="100 ps", comment="Generated by nMigen")

        self.gtkw_names = SignalDict()
        self.gtkw_file = gtkw_file
        self.gtkw_save = gtkw_file and GTKWSave(self.gtkw_file)

        self.traces = []

        trace_names = SignalDict()
        for trace in traces:
            if trace not in signal_names:
                trace_names[trace] = trace.name
            self.traces.append(trace)

        if self.vcd_writer is None:
            return

        for signal, names in itertools.chain(signal_names.items(), trace_names.items()):
            if signal.decoder:
                var_type = "string"
                var_size = 1
                var_init = self.decode_to_vcd(signal, signal.reset)
            else:
                var_type = "wire"
                var_size = signal.width
                var_init = signal.reset

            for (*var_scope, var_name) in names:
                suffix = None
                while True:
                    try:
                        if suffix is None:
                            var_name_suffix = var_name
                        else:
                            var_name_suffix = "{}${}".format(var_name, suffix)
                        vcd_var = self.vcd_writer.register_var(
                            scope=var_scope, name=var_name_suffix,
                            var_type=var_type, size=var_size, init=var_init)
                        break
                    except KeyError:
                        suffix = (suffix or 0) + 1

                if signal not in self.vcd_vars:
                    self.vcd_vars[signal] = set()
                self.vcd_vars[signal].add(vcd_var)

                if signal not in self.gtkw_names:
                    self.gtkw_names[signal] = (*var_scope, var_name_suffix)

    def update(self, timestamp, signal, value):
        if signal not in self.vcd_vars:
            return

        vcd_timestamp = self.timestamp_to_vcd(timestamp)
        if signal.decoder:
            var_value = self.decode_to_vcd(signal, value)
        else:
            var_value = value
        for vcd_var in self.vcd_vars[signal]:
            self.vcd_writer.change(vcd_var, vcd_timestamp, var_value)

    def close(self, timestamp):
        if self.vcd_writer is not None:
            self.vcd_writer.close(self.timestamp_to_vcd(timestamp))

        if self.gtkw_save is not None:
            self.gtkw_save.dumpfile(self.vcd_file.name)
            self.gtkw_save.dumpfile_size(self.vcd_file.tell())

            self.gtkw_save.treeopen("top")
            for signal in self.traces:
                if len(signal) > 1 and not signal.decoder:
                    suffix = "[{}:0]".format(len(signal) - 1)
                else:
                    suffix = ""
                self.gtkw_save.trace(".".join(self.gtkw_names[signal]) + suffix)

        if self.vcd_file is not None:
            self.vcd_file.close()
        if self.gtkw_file is not None:
            self.gtkw_file.close()


class _Process:
    __slots__ = ("runnable", "passive")

    def reset(self):
        raise NotImplementedError # :nocov:

    def run(self):
        raise NotImplementedError # :nocov:

    @property
    def name(self):
        raise NotImplementedError # :nocov:


class _SignalState:
    __slots__ = ("signal", "curr", "next", "waiters", "pending")

    def __init__(self, signal, pending):
        self.signal = signal
        self.pending = pending
        self.waiters = dict()
        self.reset()

    def reset(self):
        self.curr = self.next = self.signal.reset

    def set(self, value):
        if self.next == value:
            return
        self.next = value
        self.pending.add(self)

    def wait(self, task, *, trigger=None):
        assert task not in self.waiters
        self.waiters[task] = trigger

    def commit(self):
        if self.curr == self.next:
            return False
        self.curr = self.next
        return True

    def wakeup(self):
        awoken_any = False
        for process, trigger in self.waiters.items():
            if trigger is None or trigger == self.curr:
                process.runnable = awoken_any = True
        return awoken_any


class _SimulatorState:
    def __init__(self):
        self.signals = SignalDict()
        self.pending = set()

        self.timestamp = 0.0
        self.deadlines = dict()

        self.waveform_writer = None

    def reset(self):
        for signal_state in self.signals.values():
            signal_state.reset()
        self.pending.clear()

        self.timestamp = 0.0
        self.deadlines.clear()

    def for_signal(self, signal):
        try:
            return self.signals[signal]
        except KeyError:
            signal_state = _SignalState(signal, self.pending)
            self.signals[signal] = signal_state
            return signal_state

    def commit(self):
        awoken_any = False
        for signal_state in self.pending:
            if signal_state.commit():
                if signal_state.wakeup():
                    awoken_any = True
                if self.waveform_writer is not None:
                    self.waveform_writer.update(self.timestamp,
                        signal_state.signal, signal_state.curr)
        return awoken_any

    def advance(self):
        nearest_processes = set()
        nearest_deadline = None
        for process, deadline in self.deadlines.items():
            if deadline is None:
                if nearest_deadline is not None:
                    nearest_processes.clear()
                nearest_processes.add(process)
                nearest_deadline = self.timestamp
                break
            elif nearest_deadline is None or deadline <= nearest_deadline:
                assert deadline >= self.timestamp
                if nearest_deadline is not None and deadline < nearest_deadline:
                    nearest_processes.clear()
                nearest_processes.add(process)
                nearest_deadline = deadline

        if not nearest_processes:
            return False

        for process in nearest_processes:
            process.runnable = True
            del self.deadlines[process]
        self.timestamp = nearest_deadline

        return True

    def start_waveform(self, waveform_writer):
        if self.timestamp != 0.0:
            raise ValueError("Cannot start writing waveforms after advancing simulation time")
        if self.waveform_writer is not None:
            raise ValueError("Already writing waveforms to {!r}"
                             .format(self.waveform_writer))
        self.waveform_writer = waveform_writer

    def finish_waveform(self):
        if self.waveform_writer is None:
            return
        self.waveform_writer.close(self.timestamp)
        self.waveform_writer = None


class _EvalContext:
    __slots__ = ("state", "indexes", "slots")

    def __init__(self, state):
        self.state = state
        self.indexes = SignalDict()
        self.slots = []

    def get_signal(self, signal):
        try:
            return self.indexes[signal]
        except KeyError:
            index = len(self.slots)
            self.slots.append(self.state.for_signal(signal))
            self.indexes[signal] = index
            return index

    def get_in_signal(self, signal, *, trigger=None):
        index = self.get_signal(signal)
        self.slots[index].waiters[self] = trigger
        return index

    def get_out_signal(self, signal):
        return self.get_signal(signal)


class _Emitter:
    def __init__(self):
        self._buffer = []
        self._suffix = 0
        self._level  = 0

    def append(self, code):
        self._buffer.append("    " * self._level)
        self._buffer.append(code)
        self._buffer.append("\n")

    @contextmanager
    def indent(self):
        self._level += 1
        yield
        self._level -= 1

    def flush(self, indent=""):
        code = "".join(self._buffer)
        self._buffer.clear()
        return code

    def gen_var(self, prefix):
        name = f"{prefix}_{self._suffix}"
        self._suffix += 1
        return name

    def def_var(self, prefix, value):
        name = self.gen_var(prefix)
        self.append(f"{name} = {value}")
        return name


class _Compiler:
    def __init__(self, context, emitter):
        self.context = context
        self.emitter = emitter


class _ValueCompiler(ValueVisitor, _Compiler):
    helpers = {
        "sign": lambda value, sign: value | sign if value & sign else value,
        "zdiv": lambda lhs, rhs: 0 if rhs == 0 else lhs // rhs,
    }

    def on_ClockSignal(self, value):
        raise NotImplementedError # :nocov:

    def on_ResetSignal(self, value):
        raise NotImplementedError # :nocov:

    def on_Record(self, value):
        return self(Cat(value.fields.values()))

    def on_AnyConst(self, value):
        raise NotImplementedError # :nocov:

    def on_AnySeq(self, value):
        raise NotImplementedError # :nocov:

    def on_Sample(self, value):
        raise NotImplementedError # :nocov:

    def on_Initial(self, value):
        raise NotImplementedError # :nocov:


class _RHSValueCompiler(_ValueCompiler):
    def __init__(self, context, emitter, *, mode, inputs=None):
        super().__init__(context, emitter)
        assert mode in ("curr", "next")
        self.mode = mode
        # If not None, `inputs` gets populated with RHS signals.
        self.inputs = inputs

    def on_Const(self, value):
        return f"{value.value}"

    def on_Signal(self, value):
        if self.inputs is not None:
            self.inputs.add(value)

        if self.mode == "curr":
            return f"slots[{self.context.get_signal(value)}].{self.mode}"
        else:
            return f"next_{self.context.get_signal(value)}"

    def on_Operator(self, value):
        def mask(value):
            value_mask = (1 << len(value)) - 1
            return f"({self(value)} & {value_mask})"

        def sign(value):
            if value.shape().signed:
                return f"sign({mask(value)}, {-1 << (len(value) - 1)})"
            else: # unsigned
                return mask(value)

        if len(value.operands) == 1:
            arg, = value.operands
            if value.operator == "~":
                return f"(~{self(arg)})"
            if value.operator == "-":
                return f"(-{self(arg)})"
            if value.operator == "b":
                return f"bool({mask(arg)})"
            if value.operator == "r|":
                return f"({mask(arg)} != 0)"
            if value.operator == "r&":
                return f"({mask(arg)} == {(1 << len(arg)) - 1})"
            if value.operator == "r^":
                # Believe it or not, this is the fastest way to compute a sideways XOR in Python.
                return f"(format({mask(arg)}, 'b').count('1') % 2)"
            if value.operator in ("u", "s"):
                # These operators don't change the bit pattern, only its interpretation.
                return self(arg)
        elif len(value.operands) == 2:
            lhs, rhs = value.operands
            lhs_mask = (1 << len(lhs)) - 1
            rhs_mask = (1 << len(rhs)) - 1
            if value.operator == "+":
                return f"({sign(lhs)} + {sign(rhs)})"
            if value.operator == "-":
                return f"({sign(lhs)} - {sign(rhs)})"
            if value.operator == "*":
                return f"({sign(lhs)} * {sign(rhs)})"
            if value.operator == "//":
                return f"zdiv({sign(lhs)}, {sign(rhs)})"
            if value.operator == "&":
                return f"({self(lhs)} & {self(rhs)})"
            if value.operator == "|":
                return f"({self(lhs)} | {self(rhs)})"
            if value.operator == "^":
                return f"({self(lhs)} ^ {self(rhs)})"
            if value.operator == "<<":
                return f"({sign(lhs)} << {sign(rhs)})"
            if value.operator == ">>":
                return f"({sign(lhs)} >> {sign(rhs)})"
            if value.operator == "==":
                return f"({sign(lhs)} == {sign(rhs)})"
            if value.operator == "!=":
                return f"({sign(lhs)} != {sign(rhs)})"
            if value.operator == "<":
                return f"({sign(lhs)} < {sign(rhs)})"
            if value.operator == "<=":
                return f"({sign(lhs)} <= {sign(rhs)})"
            if value.operator == ">":
                return f"({sign(lhs)} > {sign(rhs)})"
            if value.operator == ">=":
                return f"({sign(lhs)} >= {sign(rhs)})"
        elif len(value.operands) == 3:
            if value.operator == "m":
                sel, val1, val0 = value.operands
                return f"({self(val1)} if {self(sel)} else {self(val0)})"
        raise NotImplementedError("Operator '{}' not implemented".format(value.operator)) # :nocov:

    def on_Slice(self, value):
        return f"(({self(value.value)} >> {value.start}) & {(1 << len(value)) - 1})"

    def on_Part(self, value):
        offset_mask = (1 << len(value.offset)) - 1
        offset = f"(({self(value.offset)} & {offset_mask}) * {value.stride})"
        return f"({self(value.value)} >> {offset} & " \
               f"{(1 << value.width) - 1})"

    def on_Cat(self, value):
        gen_parts = []
        offset = 0
        for part in value.parts:
            part_mask = (1 << len(part)) - 1
            gen_parts.append(f"(({self(part)} & {part_mask}) << {offset})")
            offset += len(part)
        return f"({' | '.join(gen_parts)})"

    def on_Repl(self, value):
        part_mask = (1 << len(value.value)) - 1
        gen_part = self.emitter.def_var("repl", f"{self(value.value)} & {part_mask}")
        gen_parts = []
        offset = 0
        for _ in range(value.count):
            gen_parts.append(f"({gen_part} << {offset})")
            offset += len(value.value)
        return f"({' | '.join(gen_parts)})"

    def on_ArrayProxy(self, value):
        index_mask = (1 << len(value.index)) - 1
        gen_index = self.emitter.def_var("rhs_index", f"{self(value.index)} & {index_mask}")
        gen_value = self.emitter.gen_var("rhs_proxy")
        if value.elems:
            gen_elems = []
            for index, elem in enumerate(value.elems):
                if index == 0:
                    self.emitter.append(f"if {gen_index} == {index}:")
                else:
                    self.emitter.append(f"elif {gen_index} == {index}:")
                with self.emitter.indent():
                    self.emitter.append(f"{gen_value} = {self(elem)}")
            self.emitter.append(f"else:")
            with self.emitter.indent():
                self.emitter.append(f"{gen_value} = {self(value.elems[-1])}")
            return gen_value
        else:
            return f"0"

    @classmethod
    def compile(cls, context, value, *, mode, inputs=None):
        emitter = _Emitter()
        compiler = cls(context, emitter, mode=mode, inputs=inputs)
        emitter.append(f"result = {compiler(value)}")
        return emitter.flush()


class _LHSValueCompiler(_ValueCompiler):
    def __init__(self, context, emitter, *, rhs, outputs=None):
        super().__init__(context, emitter)
        # `rrhs` is used to translate rvalues that are syntactically a part of an lvalue, e.g.
        # the offset of a Part.
        self.rrhs = rhs
        # `lrhs` is used to translate the read part of a read-modify-write cycle during partial
        # update of an lvalue.
        self.lrhs = _RHSValueCompiler(context, emitter, mode="next", inputs=None)
        # If not None, `outputs` gets populated with signals on LHS.
        self.outputs = outputs

    def on_Const(self, value):
        raise TypeError # :nocov:

    def on_Signal(self, value):
        if self.outputs is not None:
            self.outputs.add(value)

        def gen(arg):
            value_mask = (1 << len(value)) - 1
            if value.shape().signed:
                value_sign = f"sign({arg} & {value_mask}, {-1 << (len(value) - 1)})"
            else: # unsigned
                value_sign = f"{arg} & {value_mask}"
            self.emitter.append(f"next_{self.context.get_out_signal(value)} = {value_sign}")
        return gen

    def on_Operator(self, value):
        raise TypeError # :nocov:

    def on_Slice(self, value):
        def gen(arg):
            width_mask = (1 << (value.stop - value.start)) - 1
            self(value.value)(f"({self.lrhs(value.value)} & " \
                f"{~(width_mask << value.start)} | " \
                f"(({arg} & {width_mask}) << {value.start}))")
        return gen

    def on_Part(self, value):
        def gen(arg):
            width_mask = (1 << value.width) - 1
            offset_mask = (1 << len(value.offset)) - 1
            offset = f"(({self.rrhs(value.offset)} & {offset_mask}) * {value.stride})"
            self(value.value)(f"({self.lrhs(value.value)} & " \
                f"~({width_mask} << {offset}) | " \
                f"(({arg} & {width_mask}) << {offset}))")
        return gen

    def on_Cat(self, value):
        def gen(arg):
            gen_arg = self.emitter.def_var("cat", arg)
            gen_parts = []
            offset = 0
            for part in value.parts:
                part_mask = (1 << len(part)) - 1
                self(part)(f"(({gen_arg} >> {offset}) & {part_mask})")
                offset += len(part)
        return gen

    def on_Repl(self, value):
        raise TypeError # :nocov:

    def on_ArrayProxy(self, value):
        def gen(arg):
            index_mask = (1 << len(value.index)) - 1
            gen_index = self.emitter.def_var("index", f"{self.rrhs(value.index)} & {index_mask}")
            if value.elems:
                gen_elems = []
                for index, elem in enumerate(value.elems):
                    if index == 0:
                        self.emitter.append(f"if {gen_index} == {index}:")
                    else:
                        self.emitter.append(f"elif {gen_index} == {index}:")
                    with self.emitter.indent():
                        self(elem)(arg)
                self.emitter.append(f"else:")
                with self.emitter.indent():
                    self(value.elems[-1])(arg)
            else:
                self.emitter.append(f"pass")
        return gen

    @classmethod
    def compile(cls, context, stmt, *, inputs=None, outputs=None):
        emitter = _Emitter()
        compiler = cls(context, emitter, inputs=inputs, outputs=outputs)
        compiler(stmt)
        return emitter.flush()


class _StatementCompiler(StatementVisitor, _Compiler):
    def __init__(self, context, emitter, *, inputs=None, outputs=None):
        super().__init__(context, emitter)
        self.rhs = _RHSValueCompiler(context, emitter, mode="curr", inputs=inputs)
        self.lhs = _LHSValueCompiler(context, emitter, rhs=self.rhs, outputs=outputs)

    def on_statements(self, stmts):
        for stmt in stmts:
            self(stmt)
        if not stmts:
            self.emitter.append("pass")

    def on_Assign(self, stmt):
        return self.lhs(stmt.lhs)(self.rhs(stmt.rhs))

    def on_Switch(self, stmt):
        gen_test = self.emitter.def_var("test",
            f"{self.rhs(stmt.test)} & {(1 << len(stmt.test)) - 1}")
        for index, (patterns, stmts) in enumerate(stmt.cases.items()):
            gen_checks = []
            if not patterns:
                gen_checks.append(f"True")
            else:
                for pattern in patterns:
                    if "-" in pattern:
                        mask  = int("".join("0" if b == "-" else "1" for b in pattern), 2)
                        value = int("".join("0" if b == "-" else  b  for b in pattern), 2)
                        gen_checks.append(f"({gen_test} & {mask}) == {value}")
                    else:
                        value = int(pattern, 2)
                        gen_checks.append(f"{gen_test} == {value}")
            if index == 0:
                self.emitter.append(f"if {' or '.join(gen_checks)}:")
            else:
                self.emitter.append(f"elif {' or '.join(gen_checks)}:")
            with self.emitter.indent():
                self(stmts)

    def on_Assert(self, stmt):
        raise NotImplementedError # :nocov:

    def on_Assume(self, stmt):
        raise NotImplementedError # :nocov:

    def on_Cover(self, stmt):
        raise NotImplementedError # :nocov:

    @classmethod
    def compile(cls, context, stmt, *, inputs=None, outputs=None):
        output_indexes = [context.get_signal(signal) for signal in stmt._lhs_signals()]
        emitter = _Emitter()
        for signal_index in output_indexes:
            emitter.append(f"next_{signal_index} = slots[{signal_index}].next")
        compiler = cls(context, emitter, inputs=inputs, outputs=outputs)
        compiler(stmt)
        for signal_index in output_indexes:
            emitter.append(f"slots[{signal_index}].set(next_{signal_index})")
        return emitter.flush()


class _CompiledProcess(_Process):
    __slots__ = ("context", "comb", "name", "run")

    def __init__(self, state, *, comb, name):
        self.context = _EvalContext(state)
        self.comb = comb
        self.name = name
        self.run = None # set by _FragmentCompiler
        self.reset()

    def reset(self):
        self.runnable = self.comb
        self.passive = True


class _FragmentCompiler:
    def __init__(self, state, signal_names):
        self.state = state
        self.signal_names = signal_names

    def __call__(self, fragment, *, hierarchy=("top",)):
        processes = set()

        def add_signal_name(signal):
            hierarchical_signal_name = (*hierarchy, signal.name)
            if signal not in self.signal_names:
                self.signal_names[signal] = {hierarchical_signal_name}
            else:
                self.signal_names[signal].add(hierarchical_signal_name)

        for domain_name, domain_signals in fragment.drivers.items():
            domain_stmts = LHSGroupFilter(domain_signals)(fragment.statements)
            domain_process = _CompiledProcess(self.state, comb=domain_name is None,
                name=".".join((*hierarchy, "<{}>".format(domain_name or "comb"))))

            emitter = _Emitter()
            emitter.append(f"def run():")
            emitter._level += 1

            if domain_name is None:
                for signal in domain_signals:
                    signal_index = domain_process.context.get_signal(signal)
                    emitter.append(f"next_{signal_index} = {signal.reset}")

                inputs = SignalSet()
                _StatementCompiler(domain_process.context, emitter, inputs=inputs)(domain_stmts)

                for input in inputs:
                    self.state.for_signal(input).wait(domain_process)

            else:
                domain = fragment.domains[domain_name]
                add_signal_name(domain.clk)
                if domain.rst is not None:
                    add_signal_name(domain.rst)

                clk_trigger = 1 if domain.clk_edge == "pos" else 0
                self.state.for_signal(domain.clk).wait(domain_process, trigger=clk_trigger)
                if domain.rst is not None and domain.async_reset:
                    rst_trigger = 1
                    self.state.for_signal(domain.rst).wait(domain_process, trigger=rst_trigger)

                gen_asserts = []
                clk_index = domain_process.context.get_signal(domain.clk)
                gen_asserts.append(f"slots[{clk_index}].curr == {clk_trigger}")
                if domain.rst is not None and domain.async_reset:
                    rst_index = domain_process.context.get_signal(domain.rst)
                    gen_asserts.append(f"slots[{rst_index}].curr == {rst_trigger}")
                emitter.append(f"assert {' or '.join(gen_asserts)}")

                for signal in domain_signals:
                    signal_index = domain_process.context.get_signal(signal)
                    emitter.append(f"next_{signal_index} = slots[{signal_index}].next")

                _StatementCompiler(domain_process.context, emitter)(domain_stmts)

            for signal in domain_signals:
                signal_index = domain_process.context.get_signal(signal)
                emitter.append(f"slots[{signal_index}].set(next_{signal_index})")

            exec_locals = {"slots": domain_process.context.slots, **_ValueCompiler.helpers}
            exec(emitter.flush(), exec_locals)
            domain_process.run = exec_locals["run"]

            processes.add(domain_process)

            for used_signal in domain_process.context.indexes:
                add_signal_name(used_signal)

        for subfragment_index, (subfragment, subfragment_name) in enumerate(fragment.subfragments):
            if subfragment_name is None:
                subfragment_name = "U${}".format(subfragment_index)
            processes.update(self(subfragment, hierarchy=(*hierarchy, subfragment_name)))

        return processes


class _CoroutineProcess(_Process):
    def __init__(self, state, domains, constructor, *, default_cmd=None):
        self.state = state
        self.domains = domains
        self.constructor = constructor
        self.default_cmd = default_cmd
        self.reset()

    def reset(self):
        self.runnable = True
        self.passive = False
        self.coroutine = self.constructor()
        self.eval_context = _EvalContext(self.state)
        self.exec_locals = {
            "slots": self.eval_context.slots,
            "result": None,
            **_ValueCompiler.helpers
        }
        self.waits_on = set()

    @property
    def name(self):
        coroutine = self.coroutine
        while coroutine.gi_yieldfrom is not None:
            coroutine = coroutine.gi_yieldfrom
        if inspect.isgenerator(coroutine):
            frame = coroutine.gi_frame
        if inspect.iscoroutine(coroutine):
            frame = coroutine.cr_frame
        return "{}:{}".format(inspect.getfile(frame), inspect.getlineno(frame))

    def get_in_signal(self, signal, *, trigger=None):
        signal_state = self.state.for_signal(signal)
        assert self not in signal_state.waiters
        signal_state.waiters[self] = trigger
        self.waits_on.add(signal_state)
        return signal_state

    def run(self):
        if self.coroutine is None:
            return

        if self.waits_on:
            for signal_state in self.waits_on:
                del signal_state.waiters[self]
            self.waits_on.clear()

        response = None
        while True:
            try:
                command = self.coroutine.send(response)
                if command is None:
                    command = self.default_cmd
                response = None

                if isinstance(command, Value):
                    exec(_RHSValueCompiler.compile(self.eval_context, command, mode="curr"),
                        self.exec_locals)
                    response = Const.normalize(self.exec_locals["result"], command.shape())

                elif isinstance(command, Statement):
                    exec(_StatementCompiler.compile(self.eval_context, command),
                        self.exec_locals)

                elif type(command) is Tick:
                    domain = command.domain
                    if isinstance(domain, ClockDomain):
                        pass
                    elif domain in self.domains:
                        domain = self.domains[domain]
                    else:
                        raise NameError("Received command {!r} that refers to a nonexistent "
                                        "domain {!r} from process {!r}"
                                        .format(command, command.domain, self.name))
                    self.get_in_signal(domain.clk, trigger=1 if domain.clk_edge == "pos" else 0)
                    if domain.rst is not None and domain.async_reset:
                        self.get_in_signal(domain.rst, trigger=1)
                    return

                elif type(command) is Settle:
                    self.state.deadlines[self] = None
                    return

                elif type(command) is Delay:
                    if command.interval is None:
                        self.state.deadlines[self] = None
                    else:
                        self.state.deadlines[self] = self.state.timestamp + command.interval
                    return

                elif type(command) is Passive:
                    self.passive = True

                elif type(command) is Active:
                    self.passive = False

                elif command is None: # only possible if self.default_cmd is None
                    raise TypeError("Received default command from process {!r} that was added "
                                    "with add_process(); did you mean to add this process with "
                                    "add_sync_process() instead?"
                                    .format(self.name))

                else:
                    raise TypeError("Received unsupported command {!r} from process {!r}"
                                    .format(command, self.name))

            except StopIteration:
                self.passive = True
                self.coroutine = None
                return

            except Exception as exn:
                self.coroutine.throw(exn)


class _WaveformContextManager:
    def __init__(self, state, waveform_writer):
        self._state = state
        self._waveform_writer = waveform_writer

    def __enter__(self):
        try:
            self._state.start_waveform(self._waveform_writer)
        except:
            self._waveform_writer.close(0)
            raise

    def __exit__(self, *args):
        self._state.finish_waveform()


class Simulator:
    def __init__(self, fragment, **kwargs):
        self._state = _SimulatorState()
        self._signal_names = SignalDict()
        self._fragment = Fragment.get(fragment, platform=None).prepare()
        self._processes = _FragmentCompiler(self._state, self._signal_names)(self._fragment)
        if kwargs: # :nocov:
            # TODO(nmigen-0.3): remove
            self._state.start_waveform(_VCDWaveformWriter(self._signal_names, **kwargs))
        self._clocked = set()

    def _check_process(self, process):
        if not (inspect.isgeneratorfunction(process) or inspect.iscoroutinefunction(process)):
            if inspect.isgenerator(process) or inspect.iscoroutine(process):
                warnings.warn("instead of generators, use generator functions as processes; "
                              "this allows the simulator to be repeatedly reset",
                              DeprecationWarning, stacklevel=3)
                def wrapper():
                    yield from process
                return wrapper
            else:
                raise TypeError("Cannot add a process {!r} because it is not a generator function"
                                .format(process))
        return process

    def _add_coroutine_process(self, process, *, default_cmd):
        self._processes.add(_CoroutineProcess(self._state, self._fragment.domains, process,
                                              default_cmd=default_cmd))

    def add_process(self, process):
        process = self._check_process(process)
        def wrapper():
            # Only start a bench process after comb settling, so that the reset values are correct.
            yield Settle()
            yield from process()
        self._add_coroutine_process(wrapper, default_cmd=None)

    def add_sync_process(self, process, *, domain="sync"):
        process = self._check_process(process)
        def wrapper():
            # Only start a sync process after the first clock edge (or reset edge, if the domain
            # uses an asynchronous reset). This matches the behavior of synchronous FFs.
            yield Tick(domain)
            yield from process()
        return self._add_coroutine_process(wrapper, default_cmd=Tick(domain))

    def add_clock(self, period, *, phase=None, domain="sync", if_exists=False):
        """Add a clock process.

        Adds a process that drives the clock signal of ``domain`` at a 50% duty cycle.

        Arguments
        ---------
        period : float
            Clock period. The process will toggle the ``domain`` clock signal every ``period / 2``
            seconds.
        phase : None or float
            Clock phase. The process will wait ``phase`` seconds before the first clock transition.
            If not specified, defaults to ``period / 2``.
        domain : str or ClockDomain
            Driven clock domain. If specified as a string, the domain with that name is looked up
            in the root fragment of the simulation.
        if_exists : bool
            If ``False`` (the default), raise an error if the driven domain is specified as
            a string and the root fragment does not have such a domain. If ``True``, do nothing
            in this case.
        """
        if isinstance(domain, ClockDomain):
            pass
        elif domain in self._fragment.domains:
            domain = self._fragment.domains[domain]
        elif if_exists:
            return
        else:
            raise ValueError("Domain {!r} is not present in simulation"
                             .format(domain))
        if domain in self._clocked:
            raise ValueError("Domain {!r} already has a clock driving it"
                             .format(domain.name))

        half_period = period / 2
        if phase is None:
            # By default, delay the first edge by half period. This causes any synchronous activity
            # to happen at a non-zero time, distinguishing it from the reset values in the waveform
            # viewer.
            phase = half_period
        def clk_process():
            yield Passive()
            yield Delay(phase)
            # Behave correctly if the process is added after the clock signal is manipulated, or if
            # its reset state is high.
            initial = (yield domain.clk)
            while True:
                yield domain.clk.eq(~initial)
                yield Delay(half_period)
                yield domain.clk.eq(initial)
                yield Delay(half_period)
        self._add_coroutine_process(clk_process, default_cmd=None)
        self._clocked.add(domain)

    def reset(self):
        """Reset the simulation.

        Assign the reset value to every signal in the simulation, and restart every user process.
        """
        self._state.reset()
        for process in self._processes:
            process.reset()

    def _delta(self):
        """Perform a delta cycle.

        Performs the two phases of a delta cycle:
            1. run and suspend every non-waiting process once, queueing signal changes;
            2. commit every queued signal change, waking up any waiting process.
        """
        for process in self._processes:
            if process.runnable:
                process.runnable = False
                process.run()

        return self._state.commit()

    def _settle(self):
        """Settle the simulation.

        Run every process and commit changes until a fixed point is reached. If there is
        an unstable combinatorial loop, this function will never return.
        """
        while self._delta():
            pass

    def step(self):
        """Step the simulation.

        Run every process and commit changes until a fixed point is reached, then advance time
        to the closest deadline (if any). If there is an unstable combinatorial loop,
        this function will never return.

        Returns ``True`` if there are any active processes, ``False`` otherwise.
        """
        self._settle()
        self._state.advance()
        return any(not process.passive for process in self._processes)

    def run(self):
        """Run the simulation while any processes are active.

        Processes added with :meth:`add_process` and :meth:`add_sync_process` are initially active,
        and may change their status using the ``yield Passive()`` and ``yield Active()`` commands.
        Processes compiled from HDL and added with :meth:`add_clock` are always passive.
        """
        while self.step():
            pass

    def run_until(self, deadline, *, run_passive=False):
        """Run the simulation until it advances to ``deadline``.

        If ``run_passive`` is ``False``, the simulation also stops when there are no active
        processes, similar to :meth:`run`. Otherwise, the simulation will stop only after it
        advances to or past ``deadline``.

        If the simulation stops advancing, this function will never return.
        """
        assert self._state.timestamp <= deadline
        while (self.step() or run_passive) and self._state.timestamp < deadline:
            pass

    def write_vcd(self, vcd_file, gtkw_file=None, *, traces=()):
        """Write waveforms to a Value Change Dump file, optionally populating a GTKWave save file.

        This method returns a context manager. It can be used as: ::

            sim = Simulator(frag)
            sim.add_clock(1e-6)
            with sim.write_vcd("dump.vcd", "dump.gtkw"):
                sim.run_until(1e-3)

        Arguments
        ---------
        vcd_file : str or file-like object
            Verilog Value Change Dump file or filename.
        gtkw_file : str or file-like object
            GTKWave save file or filename.
        traces : iterable of Signal
            Signals to display traces for.
        """
        waveform_writer = _VCDWaveformWriter(self._signal_names,
            vcd_file=vcd_file, gtkw_file=gtkw_file, traces=traces)
        return _WaveformContextManager(self._state, waveform_writer)

    # TODO(nmigen-0.3): remove
    @deprecated("instead of `with Simulator(fragment, ...) as sim:`, use "
                "`sim = Simulator(fragment); with sim.write_vcd(...):`")
    def __enter__(self): # :nocov:
        return self

    # TODO(nmigen-0.3): remove
    def __exit__(self, *args): # :nocov:
        self._state.finish_waveform()
