#!/usr/bin/env python3
# -*- coding: utf-8 -*-

import os
import shutil

from hwt.hdl.architecture import Architecture
from hwt.hdl.entity import Entity
from hwt.serializer.exceptions import SerializerException
from hwt.serializer.generic.serializer import GenericSerializer
from hwt.serializer.vhdl.serializer import VhdlSerializer
from hwt.pyUtils.uniqList import UniqList
from hwt.synthesizer.unit import Unit
from hwt.synthesizer.dummyPlatform import DummyPlatform

def toRtl(unitOrCls: Unit, name: str=None,
          serializer: GenericSerializer=VhdlSerializer,
          targetPlatform=DummyPlatform(), saveTo: str=None):
    """
    Convert unit to RTL using specified serializer

    :param unitOrCls: unit instance or class, which should be converted
    :param name: name override of top unit (if is None name is derived
        form class name)
    :param serializer: serializer which should be used for to RTL conversion
    :param targetPlatform: metainformatins about target platform, distributed
        on every unit under _targetPlatform attribute
        before Unit._impl() is called
    :param saveTo: directory where files should be stored
        If None RTL is returned as string.
    :raturn: if saveTo returns RTL string else returns list of file names
        which were created
    """
    if not isinstance(unitOrCls, Unit):
        u = unitOrCls()
    else:
        u = unitOrCls

    u._loadDeclarations()
    if name is not None:
        assert isinstance(name, str)
        u._name = name

    globScope = serializer.getBaseNameScope()
    mouduleScopes = {}

    # unitCls : unitobj
    serializedClasses = {}

    # (unitCls, paramsValues) : unitObj
    # where paramsValues are dict name:value
    serializedConfiguredUnits = {}

    doSerialize = True

    createFiles = saveTo is not None
    if createFiles:
        os.makedirs(saveTo, exist_ok=True)
        files = UniqList()
    else:
        codeBuff = []

    for obj in u._toRtl(targetPlatform):
        doSerialize = serializer.serializationDecision(
            obj,
            serializedClasses,
            serializedConfiguredUnits)
        if doSerialize:
            if isinstance(obj, Entity):
                s = globScope.fork(1)
                s.setLevel(2)
                ctx = serializer.getBaseContext()
                ctx.scope = s
                mouduleScopes[obj] = ctx
                ctx.currentUnit = obj.origin

                sc = serializer.Entity(obj, ctx)
                if createFiles:
                    fName = obj.name + serializer.fileExtension
                    fileMode = 'w'

            elif isinstance(obj, Architecture):
                try:
                    ctx = mouduleScopes[obj.entity]
                except KeyError:
                    raise SerializerException(
                        "Entity should be serialized"
                        " before architecture of %s"
                        % (obj.getEntityName()))

                sc = serializer.Architecture(obj, ctx)
                if createFiles:
                    fName = obj.getEntityName() + serializer.fileExtension
                    fileMode = 'a'
            else:
                if hasattr(obj, "_hdlSources"):
                    for fn in obj._hdlSources:
                        if isinstance(fn, str):
                            shutil.copy2(fn, saveTo)
                            files.append(fn)
                            continue
                else:
                    sc = serializer.asHdl(obj)

            if sc:
                if createFiles:
                    fp = os.path.join(saveTo, fName)
                    files.append(fp)
                    with open(fp, fileMode) as f:
                        if fileMode == 'a':
                            f.write("\n")

                        f.write(
                            serializer.formatter(sc)
                        )
                else:
                    codeBuff.append(sc)

        elif not createFiles:
            try:
                name = '"%s"' % obj.name
            except AttributeError:
                name = ""
            codeBuff.append(serializer.comment(
                "Object of class %s, %s was not serialized as specified" % (
                    obj.__class__.__name__, name)))

    if createFiles:
        return files
    else:
        return serializer.formatter(
            "\n".join(codeBuff)
        )

def serializeAsIpcore(unit, folderName=".", name=None,
                      serializer: GenericSerializer=VhdlSerializer,
                      targetPlatform=DummyPlatform()):
    from hwt.serializer.ip_packager import IpPackager
    p = IpPackager(unit, name=name,
                   serializer=serializer,
                   targetPlatform=targetPlatform)
    p.createPackage(folderName)
    return p


from copy import copy
from itertools import compress
from typing import List, Generator

from hwt.code import If
from hwt.hdl.architecture import Architecture
from hwt.hdl.assignment import Assignment
from hwt.hdl.entity import Entity
from hwt.hdl.operator import Operator
from hwt.hdl.portItem import PortItem
from hwt.hdl.process import HWProcess
from hwt.hdl.statementUtils import fill_stm_list_with_enclosure
from hwt.hdl.statements import HdlStatement, HwtSyntaxError
from hwt.hdl.types.defs import BIT
from hwt.hdl.value import Value
from hwt.pyUtils.arrayQuery import distinctBy, where
from hwt.pyUtils.uniqList import UniqList
from hwt.synthesizer.exceptions import SigLvlConfErr
from hwt.synthesizer.interfaceLevel.mainBases import InterfaceBase
from hwt.synthesizer.rtlLevel.memory import RtlSyncSignal
from hwt.synthesizer.rtlLevel.optimalizator import removeUnconnectedSignals, \
    reduceProcesses
from hwt.synthesizer.rtlLevel.rtlSignal import RtlSignal
from hwt.synthesizer.rtlLevel.signalUtils.exceptions import MultipleDriversErr,\
    NoDriverErr
from hwt.synthesizer.rtlLevel.utils import portItemfromSignal
from hwt.doc_markers import internal

@internal
def name_for_process_and_mark_outputs(statements: List[HdlStatement])\
        -> str:
    """
    Resolve name for process and mark outputs of statemens as not hidden
    """
    out_names = []
    for stm in statements:
        for sig in stm._outputs:
            if not sig.hasGenericName:
                out_names.append(sig.name)

    if out_names:
        return min(out_names)
    else:
        return ""

@internal
def cut_off_drivers_of(dstSignal, statements):
    """
    Cut off drivers from statements
    """
    separated = []
    stm_filter = []
    for stm in statements:
        stm._clean_signal_meta()
        d = stm._cut_off_drivers_of(dstSignal)
        if d is not None:
            separated.append(d)

        f = d is not stm
        stm_filter.append(f)

    return list(compress(statements, stm_filter)), separated

@internal
def _statements_to_HWProcesses(_statements, tryToSolveCombLoops)\
        -> Generator[HWProcess, None, None]:
    assert _statements
    # try to simplify statements
    proc_statements = []
    for _stm in _statements:
        stms, _ = _stm._try_reduce()
        proc_statements.extend(stms)

    outputs = UniqList()
    _inputs = UniqList()
    sensitivity = UniqList()
    enclosed_for = set()
    for _stm in proc_statements:
        seen = set()
        _stm._discover_sensitivity(seen)
        _stm._discover_enclosure()
        outputs.extend(_stm._outputs)
        _inputs.extend(_stm._inputs)
        sensitivity.extend(_stm._sensitivity)
        enclosed_for.update(_stm._enclosed_for)

    enclosure_values = {}
    for sig in outputs:
        # inject nopVal if needed
        if sig._useNopVal:
            n = sig._nopVal
            enclosure_values[sig] = n

    if enclosure_values:
        do_enclose_for = list(where(outputs,
                                    lambda o: o in enclosure_values))
        fill_stm_list_with_enclosure(None, enclosed_for, proc_statements,
                                     do_enclose_for, enclosure_values)

    if proc_statements:
        for o in outputs:
            assert not o.hidden, o
        seen = set()
        inputs = UniqList()
        for i in _inputs:
            inputs.extend(i._walk_public_drivers(seen))

        intersect = outputs.intersection_set(sensitivity)
        if intersect:
            if not tryToSolveCombLoops:
                raise HwtSyntaxError(
                    "Combinational loop on signal(s)", intersect)

            # try to solve combinational loops by separating drivers of signals
            # from statements
            for sig in intersect:
                proc_statements, proc_stms_select = cut_off_drivers_of(
                    sig, proc_statements)
                yield from _statements_to_HWProcesses(proc_stms_select, False)

            if proc_statements:
                yield from _statements_to_HWProcesses(proc_statements, False)
        else:
            name = name_for_process_and_mark_outputs(proc_statements)
            yield HWProcess("assig_process_" + name,
                            proc_statements, sensitivity,
                            inputs, outputs)
    else:
        assert not outputs
        # this can happend f.e. when If does not contains any Assignment
        pass

@internal
def statements_to_HWProcesses(statements: List[HdlStatement])\
        -> Generator[HWProcess, None, None]:
    """
    Pack statements into HWProcess instances,
    * for each out signal resolve it's drivers and collect them
    * split statements if there is and combinational loop
    * merge statements if it is possible
    * resolve sensitivitilists
    * wrap into HWProcess instance
    * for every IO of process generate name if signal has not any
    """
    # create copy because this set will be reduced
    statements = copy(statements)

    # process ranks = how many assignments is probably in process
    # used to minimize number of merge tries
    processes = []
    while statements:
        stm = statements.pop()
        proc_statements = [stm, ]
        ps = _statements_to_HWProcesses(proc_statements, True)
        processes.extend(ps)

    yield from reduceProcesses(processes)

@internal
def walk_assignments(stm: HdlStatement, dst: RtlSignal) -> Generator[Assignment, None, None]:
    if isinstance(stm, Assignment):
        if dst is stm.dst:
            yield stm
    else:
        for _stm in stm._iter_stms():
            yield from walk_assignments(_stm, dst)

@internal
def markVisibilityOfSignals(ctx, ctxName, signals, interfaceSignals):
    """
    * check if all signals are driven by something
    * mark signals with hidden = False if they are connecting statements
      or if they are external interface
    """
    for sig in signals:
        driver_cnt = len(sig.drivers)
        has_comb_driver = False
        if driver_cnt > 1:
            sig.hidden = False
            for d in sig.drivers:
                if not isinstance(d, Operator):
                    sig.hidden = False

                is_comb_driver = False

                if isinstance(d, PortItem):
                    is_comb_driver = True
                elif not d._now_is_event_dependent:
                    for a in walk_assignments(d, sig):
                        if not a.indexes\
                                and not a._is_completly_event_dependent:
                            is_comb_driver = True
                            break

                if has_comb_driver and is_comb_driver:
                    raise MultipleDriversErr(
                        "%s: Signal %r has multiple combinational drivers" %
                        (ctx.getDebugScopeName(), sig))

                has_comb_driver |= is_comb_driver
        elif driver_cnt == 1:
            if not isinstance(sig.drivers[0], Operator):
                sig.hidden = False
        else:
            sig.hidden = False
            if sig not in interfaceSignals:
                if not sig.defVal._isFullVld():
                    raise NoDriverErr(
                        sig, "Signal without any driver or valid value in ", ctxName)
                sig._const = True

class RtlNetlist():
    """
    Hierarchical container for signals

    :ivar parentForDebug: optional parent for debug
        (has to have ._name and can have ._parent attribute)
    :ivar params: dictionary {name: Param instance}
    :ivar signals: set of all signals in this context
    :ivar statements: list of all statements which are connected to signals in this context
    :ivar subUnits: is set of all units in this context
    :ivar synthesised: flag, True if synthesize method was called
    """

    def __init__(self, parentForDebug=None):
        self.parentForDebug = parentForDebug
        self.params = {}
        self.signals = set()
        self.statements = set()
        self.subUnits = set()
        self.synthesised = False

    def sig(self, name, dtype=BIT, clk=None, syncRst=None, defVal=None):
        """
        Create new signal in this context

        :param clk: clk signal, if specified signal is synthesized
            as SyncSignal
        :param syncRst: synchronous reset signal
        """
        if isinstance(defVal, RtlSignal):
            assert defVal._const, \
                "Initial value of register has to be constant"
            _defVal = defVal._auto_cast(dtype)
        elif isinstance(defVal, Value):
            _defVal = defVal._auto_cast(dtype)
        elif isinstance(defVal, InterfaceBase):
            _defVal = defVal._sig
        else:
            _defVal = dtype.fromPy(defVal)

        if clk is not None:
            s = RtlSyncSignal(self, name, dtype, _defVal)
            if syncRst is not None and defVal is None:
                raise SigLvlConfErr(
                    "Probably forgotten default value on sync signal %s", name)
            if syncRst is not None:
                r = If(syncRst._isOn(),
                       RtlSignal.__call__(s, _defVal)
                       ).Else(
                    RtlSignal.__call__(s, s.next)
                )
            else:
                r = [RtlSignal.__call__(s, s.next)]

            If(clk._onRisingEdge(),
               r
               )
        else:
            if syncRst:
                raise SigLvlConfErr(
                    "Signal %s has reset but has no clk" % name)
            s = RtlSignal(self, name, dtype, defVal=_defVal)

        self.signals.add(s)

        return s

    def synthesize(self, name, interfaces, targetPlatform):
        """
        Build Entity and Architecture instance out of netlist representation
        """
        ent = Entity(name)
        ent._name = name + "_inst"  # instance name

        # create generics
        for _, v in self.params.items():
            ent.generics.append(v)

        # interface set for faster lookup
        if isinstance(interfaces, set):
            intfSet = interfaces
        else:
            intfSet = set(interfaces)

        # create ports
        for s in interfaces:
            pi = portItemfromSignal(s, ent)
            pi.registerInternSig(s)
            ent.ports.append(pi)
            s.hidden = False

        removeUnconnectedSignals(self)
        markVisibilityOfSignals(self, name, self.signals, intfSet)

        for proc in targetPlatform.beforeHdlArchGeneration:
            proc(self)

        arch = Architecture(ent)
        for p in statements_to_HWProcesses(self.statements):
            arch.processes.append(p)

        # add signals, variables etc. in architecture
        for s in self.signals:
            if s not in intfSet and not s.hidden:
                arch.variables.append(s)

        # instantiate subUnits in architecture
        for u in self.subUnits:
            arch.componentInstances.append(u)

        # add components in architecture
        for su in distinctBy(self.subUnits, lambda x: x.name):
            arch.components.append(su)

        self.synthesised = True

        return [ent, arch]

    def getDebugScopeName(self):
        scope = []
        p = self.parentForDebug
        while p is not None:
            scope.append(p._name)
            try:
                p = p._parent
            except AttributeError:
                break

        return ".".join(reversed(scope))


from typing import Optional, Any

from hwt.hdl.types.defs import INT, STR, BOOL
from hwt.hdl.types.hdlType import HdlType
from hwt.hdl.value import Value
from hwt.hdl.variables import SignalItem
from hwt.synthesizer.interfaceLevel.mainBases import InterfaceBase

defaultPyConversions = {int: INT,
                        str: STR,
                        bool: BOOL}

def toHVal(op: Any, suggestedType: Optional[HdlType]=None):
    """Convert python or hdl value/signal object to hdl value/signal object"""
    if isinstance(op, Value) or isinstance(op, SignalItem):
        return op
    elif isinstance(op, InterfaceBase):
        return op._sig
    else:
        if isinstance(op, int):
            if suggestedType is not None:
                return suggestedType.fromPy(op)

            if op >= 1 << 31:
                raise TypeError(
                    "Number %d is too big to fit in 32 bit integer of HDL"
                    " use Bits type instead" % op)
            elif op < -(1 << 31):
                raise TypeError(
                    "Number %d is too small to fit in 32 bit integer"
                    " of HDL use Bits type instead" % op)
        try:
            hType = defaultPyConversions[type(op)]
        except KeyError:
            hType = None

        if hType is None:
            raise TypeError("Unknown hardware type for %s" % (op.__class__))

        return hType.fromPy(op)


from hwt.hdl.types.array import HArray
from hwt.hdl.types.bits import Bits
from hwt.hdl.types.bool import HBool
from hwt.hdl.types.enum import HEnum
from hwt.hdl.types.integer import Integer
from hwt.hdl.types.slice import Slice
from hwt.hdl.types.string import String
from hwt.synthesizer.rtlLevel.mainBases import RtlSignalBase
from hwt.serializer.exceptions import SerializerException
from hwt.synthesizer.param import Param
from hwt.serializer.generic.context import SerializerCtx

class GenericSerializer_Value():
    @classmethod
    def Value(cls, val, ctx: SerializerCtx):
        """
        :param dst: is signal connected with value
        :param val: value object, can be instance of Signal or Value
        """
        t = val._dtype

        if isinstance(val, RtlSignalBase):
            return cls.SignalItem(val, ctx)

        c = cls.Value_try_extract_as_const(val, ctx)
        if c:
            return c

        if isinstance(t, Slice):
            return cls.Slice_valAsHdl(t, val, ctx)
        elif isinstance(t, HArray):
            return cls.HArrayValAsHdl(t, val, ctx)
        elif isinstance(t, Bits):
            return cls.Bits_valAsHdl(t, val, ctx)
        elif isinstance(t, HBool):
            return cls.Bool_valAsHdl(t, val, ctx)
        elif isinstance(t, HEnum):
            return cls.HEnumValAsHdl(t, val, ctx)
        elif isinstance(t, Integer):
            return cls.Integer_valAsHdl(t, val, ctx)
        elif isinstance(t, String):
            return cls.String_valAsHdl(t, val, ctx)
        else:
            raise SerializerException(
                "can not resolve value serialization for %r"
                % (val))

    @classmethod
    def Value_try_extract_as_const(cls, val, ctx: SerializerCtx):
        return None

    @classmethod
    def Integer_valAsHdl(cls, dtype, val, ctx: SerializerCtx):
        return str(int(val.val))

    @classmethod
    def Bits_valAsHdl(cls, dtype, val, ctx: SerializerCtx):
        w = dtype.bit_length()
        if dtype.signed is None:
            if dtype.forceVector or w > 1:
                return cls.BitString(val.val, w, val.vldMask)
            else:
                return cls.BitLiteral(val.val, val.vldMask)
        elif dtype.signed:
            return cls.SignedBitString(val.val, w, dtype.forceVector,
                                       val.vldMask)
        else:
            return cls.UnsignedBitString(val.val, w, dtype.forceVector,
                                         val.vldMask)

    @classmethod
    def get_signal_name(cls, si, ctx: SerializerCtx):
        if si.hidden and hasattr(si, "origin"):
            # hidden signal, render it's driver instead
            return cls.asHdl(si.origin, ctx)
        elif isinstance(si, Param) and ctx.currentUnit is not None:
            try:
                return si.getName(ctx.currentUnit)
            except KeyError:
                pass
            # parameter was taken from other place and has not
            # any name in this scope, use value only
            return cls.asHdl(si.staticEval(), ctx)
        return si.name


from hwt.hdl.assignment import Assignment
from hwt.hdl.waitStm import WaitStm
from hwt.doc_markers import internal

@internal
def getMaxStmIdForStm(stm):
    """
    Get maximum _instId from all assigments in statement
    """
    maxId = 0
    if isinstance(stm, Assignment):
        return stm._instId
    elif isinstance(stm, WaitStm):
        return maxId
    else:
        for _stm in stm._iter_stms():
            maxId = max(maxId, getMaxStmIdForStm(_stm))
        return maxId

@internal
def maxStmId(proc):
    """
    get max statement id,
    used for sorting of processes in architecture
    """
    maxId = 0
    for stm in proc.statements:
        maxId = max(maxId, getMaxStmIdForStm(stm))
    return maxId


from collections import deque

from hwt.hdl.constants import NOP
from hwt.simulator.agentBase import SyncAgentBase

class RdSyncedAgent(SyncAgentBase):
    """
    Simulation/verification agent for RdSynced interface
    """
    def __init__(self, intf, allowNoReset=True):
        super().__init__(intf, allowNoReset=allowNoReset)
        self.actualData = NOP
        self.data = deque()
        self._rd = self.getRd(intf)

    def getRd(self, intf):
        return intf.rd

    def isRd(self, readFn):
        return readFn(self._rd)

    def wrRd(self, writeFn, val):
        writeFn(val, self._rd)

    def setEnable_asMonitor(self, en, sim):
        super(RdSyncedAgent, self).setEnable_asMonitor(en, sim)
        if not en:
            self.wrRd(sim.write, 0)

    def monitor(self, sim):
        """Collect data from interface"""
        if self.notReset(sim) and self._enabled:
            self.wrRd(sim.write, 1)

            yield sim.waitOnCombUpdate()

            d = self.doRead(sim)
            self.data.append(d)
        else:
            self.wrRd(sim.write, 0)

    def doRead(self, sim):
        """extract data from interface"""
        return sim.read(self.intf.data)

    def doWrite(self, sim, data):
        """write data to interface"""
        sim.write(data, self.intf.data)

    def driver(self, sim):
        """Push data to interface"""
        r = sim.read
        if self.actualData is NOP and self.data:
            self.actualData = self.data.popleft()

        do = self.actualData is not NOP

        if do:
            self.doWrite(sim, self.actualData)
        else:
            self.doWrite(sim, None)

        en = self.notReset(sim) and self._enabled
        if not (en and do):
            return

        yield sim.waitOnCombUpdate()

        rd = self.isRd(r)
        if en:
            assert rd.vldMask, (
                ("%r: ready signal for interface %r is in invalid state,"
                 " this would cause desynchronization") %
                (sim.now, self.intf))
        if rd.val:
            if self._debugOutput is not None:
                self._debugOutput.write("%s, wrote, %d: %r\n" % (
                                           self.intf._getFullName(),
                                           sim.now, self.actualData))
            if self.data:
                self.actualData = self.data.popleft()
            else:
                self.actualData = NOP


from hwt.hdl.value import Value, areValues
from hwt.hdl.types.defs import BOOL, INT, SLICE
from hwt.hdl.operator import Operator
from hwt.hdl.operatorDefs import AllOps
from hwt.hdl.types.typeCast import toHVal
from hwt.hdl.types.integer import Integer
from operator import pow, eq
from hwt.doc_markers import internal

BoolVal = BOOL.getValueCls()
SliceVal = SLICE.getValueCls()

@internal
def intOp__val(self, other, resT, evalFn):
    v = evalFn(self.val, other.val)
    vldMask = int(self.vldMask and other.vldMask)
    updateTime = max(self.updateTime, other.updateTime)
    return resT.getValueCls()(v, resT, vldMask, updateTime)

@internal
def intOp(self, other, op, resT, evalFn=None):
    if evalFn is None:
        evalFn = op._evalFn

    other = toHVal(other)._auto_cast(INT)
    if areValues(self, other):
        return intOp__val(self, other, resT, evalFn)
    else:
        return Operator.withRes(op, [self, other], resT)

@internal
def intAritmeticOp(self, other, op):
    return intOp(self, other, op, INT)

@internal
def intCmpOp(self, other, op, evalFn=None):
    return intOp(self, other, op, BOOL, evalFn=evalFn)

class IntegerVal(Value):

    def _isFullVld(self):
        return bool(self.vldMask)

    @classmethod
    def fromPy(cls, val, typeObj, vldMask=None):
        """
        :param val: value of python type int or None
        :param typeObj: instance of Integer
        :param vldMask: None vldMask is resolved from val,
            if is 0 value is invalidated
            if is 1 value has to be valid
        """
        assert isinstance(typeObj, Integer)
        vld = int(val is not None)
        if not vld:
            assert vldMask is None or vldMask == 0
            val = 0
        else:
            if vldMask == 0:
                val = False
                vld = 0
            else:
                val = int(val)

        return cls(val, typeObj, vld)

    def toPy(self):
        return int(self)

    # arithmetic
    @internal
    def _neg__val(self):
        v = self.clone()
        v.val = -self.val
        return v

    def __neg__(self):
        if isinstance(self, Value):
            return self._neg__val()
        else:
            return Operator.withRes(AllOps.UN_MINUS, [self], INT)

    def __add__(self, other):
        return intAritmeticOp(self, other, AllOps.ADD)

    def __sub__(self, other):
        return intAritmeticOp(self, other, AllOps.SUB)

    def __mul__(self, other):
        return intAritmeticOp(self, other, AllOps.MUL)

    def _pow(self, other):
        return intOp(self, other, AllOps.POW, INT, pow)

    def __floordiv__(self, other):
        return intAritmeticOp(self, other, AllOps.DIV)

    def _downto__val(self, other):
        vldMask = int(self.vldMask and other.vldMask)
        updateTime = max(self.updateTime, other.updateTime)
        return SliceVal((self, other), SLICE, vldMask, updateTime)

    def _downto(self, other):
        other = toHVal(other)._auto_cast(INT)
        if areValues(self, other):
            return self._downto__val(other)
        else:
            return Operator.withRes(AllOps.DOWNTO, [self, other], SLICE)

    # comparisons
    def _eq(self, other):
        return intCmpOp(self, other, AllOps.EQ, eq)

    def __ne__(self, other):
        return intCmpOp(self, other, AllOps.NEQ)

    def __le__(self, other):
        return intCmpOp(self, other, AllOps.LE)

    def __lt__(self, other):
        return intCmpOp(self, other, AllOps.LT)

    def __ge__(self, other):
        return intCmpOp(self, other, AllOps.GE)

    def __gt__(self, other):
        return intCmpOp(self, other, AllOps.GT)


from hwt.doc_markers import internal
from hwt.hdl.constants import DIRECTION, INTF_DIRECTION
from hwt.hdl.types.typeCast import toHVal
from hwt.synthesizer.exceptions import IntfLvlConfErr
from hwt.synthesizer.hObjList import HObjList
from hwt.synthesizer.interfaceLevel.interfaceUtils.directionFns import \
    InterfaceDirectionFns
from hwt.synthesizer.interfaceLevel.interfaceUtils.implDependent import\
    InterfaceceImplDependentFns
from hwt.synthesizer.interfaceLevel.mainBases import InterfaceBase
from hwt.synthesizer.interfaceLevel.propDeclrCollector import\
    PropDeclrCollector
from hwt.synthesizer.vectorUtils import fitTo

def _default_param_updater(self, myP, parentP):
    self._replaceParam(myP, parentP)

class Interface(InterfaceBase, InterfaceceImplDependentFns,
                PropDeclrCollector, InterfaceDirectionFns):
    """
    Base class for all interfaces in interface synthesizer

    :cvar _NAME_SEPARATOR: separator for nested interface names
    :ivar _params: [] of parameter
    :ivar _interfaces: [] sub interfaces
    :ivar _name: name assigned during synthesis
    :ivar _parent: parent object (Unit or Interface instance)
    :ivar _isExtern: If true synthesizer sets it as external port of unit
    :ivar _associatedClk: clock Signal (interface) associated with
        this interface if is none simulation agent try to search it on parent
    :ivar _associatedRst: rst(_n) Signal (interface) associated
        with this interface if is none simulation agent try to search
        it on parent

    :note: only interfaces without _interfaces have

    :ivar _sig: rtl level signal instance
    :ivar _sigInside: _sig after toRtl conversion is made
        (after toRtl conversion _sig is signal for parent unit
        and _sigInside is signal in original unit, this separates process
        of translating units)
    :ivar _boundedEntityPort: entityPort for which was this interface created
    :ivar _boundedSigLvlUnit: RTL unit for which was this interface created

    Agenda of direction

    :ivar _masterDir: specifies which direction has this interface at master
    :ivar _direction: means actual direction of this interface resolved
        by its drivers
    :ivar _ctx: rtl netlist context of all signals and params
        on this interface after interface is registered on parent _ctx
        is merged

    Agenda of simulations

    :ivar _ag: agent object connected to this interface
        (initialized only before simultion)
    """

    _NAME_SEPARATOR = "_"

    def __init__(self, masterDir=DIRECTION.OUT,
                 loadConfig=True):
        """
        This constructor is called when constructing new interface,
        it is usually done manually while creating Unit or
        automatically while extracting interfaces from UnitWithSoure

        :param masterDir: direction which this interface should have for master
        :param multiplyedBy: this can be instance of integer or Param,
            this mean the interface is array of the interfaces
            where multiplyedBy is the size
        :param loadConfig: do load config in __init__
        """
        self._setAttrListener = None
        self._associatedClk = None
        self._associatedRst = None
        self._parent = None

        super().__init__()
        self._masterDir = masterDir
        # Interface is instanciated inside of Unit first,
        # master direction actually means slave from outside view
        self._direction = INTF_DIRECTION.UNKNOWN

        self._ctx = None

        if loadConfig:
            self._loadConfig()

        # flags for better design error detection
        self._isExtern = False
        self._isAccessible = True
        self._ag = None

    def _m(self):
        """
        Note that this interface will be master

        :return: self
        """
        assert not hasattr(self, "_interfaces") or not self._interfaces, \
            "Too late to change direction of interface"
        self._direction = DIRECTION.asIntfDirection(DIRECTION.opposite(self._masterDir))

        return self

    def __call__(self, other):
        """
        :attention: it is not call of function it is operator of assignment
        """
        return self._connectTo(other)

    @internal
    def _loadDeclarations(self):
        """
        load declaratoins from _declr method
        This function is called first for parent and then for children
        """
        if not hasattr(self, "_interfaces"):
            self._interfaces = []
        self._setAttrListener = self._declrCollector
        self._declr()
        self._setAttrListener = None

        for i in self._interfaces:
            i._isExtern = self._isExtern
            i._loadDeclarations()

        for p in self._params:
            p.setReadOnly()

        if self._isExtern:
            # direction from inside of unit (reverset compared to outside direction)
            if self._direction == INTF_DIRECTION.UNKNOWN:
                self._direction = INTF_DIRECTION.MASTER
            self._setDirectionsLikeIn(self._direction)

    @internal
    def _clean(self, rmConnetions=True, lockNonExternal=True):
        """
        Remove all signals from this interface (used after unit is synthesized
        and its parent is connecting its interface to this unit)
        """

        if self._interfaces:
            for i in self._interfaces:
                i._clean(rmConnetions=rmConnetions,
                         lockNonExternal=lockNonExternal)
        else:
            self._sigInside = self._sig
            del self._sig

        if lockNonExternal and not self._isExtern:
            self._isAccessible = False  # [TODO] mv to signal lock

    @internal
    def _connectToIter(self, master, exclude=None, fit=False):
        if exclude and (self in exclude or master in exclude):
            return

        if self._interfaces:
            for ifc in self._interfaces:
                if exclude and ifc in exclude:
                    continue

                mIfc = getattr(master, ifc._name)
                if exclude and mIfc in exclude:
                    continue

                if mIfc._masterDir == DIRECTION.OUT:
                    if ifc._masterDir != mIfc._masterDir:
                        raise IntfLvlConfErr(
                            "Invalid connection %r <= %r" % (ifc, mIfc))

                    yield from ifc._connectTo(mIfc,
                                              exclude=exclude,
                                              fit=fit)
                else:
                    if ifc._masterDir != mIfc._masterDir:
                        raise IntfLvlConfErr(
                            "Invalid connection %r <= %r" % (mIfc, ifc))

                    yield from mIfc._connectTo(ifc,
                                               exclude=exclude,
                                               fit=fit)
        else:
            dstSig = toHVal(self)
            srcSig = toHVal(master)

            if fit:
                srcSig = fitTo(srcSig, dstSig)

            yield dstSig(srcSig)

    @internal
    def _signalsForInterface(self, context, prefix='', typeTransform=None):
        """
        generate _sig for each interface which has no subinterface
        if already has _sig return it instead

        :param context: instance of RtlNetlist where signals should be created
        :param prefix: name prefix for created signals
        :param typeTransform: optional function (type) returns modified type
            for signal
        """
        sigs = []
        if self._interfaces:
            for intf in self._interfaces:
                sigs.extend(
                    intf._signalsForInterface(context, prefix,
                                              typeTransform=typeTransform))
        else:
            if hasattr(self, '_sig'):
                sigs = [self._sig]
            else:
                t = self._dtype
                if typeTransform is not None:
                    t = typeTransform(t)

                s = context.sig(prefix + self._getPhysicalName(), t)
                s._interface = self
                self._sig = s

                if hasattr(self, '_boundedEntityPort'):
                    self._boundedEntityPort.connectSig(self._sig)
                sigs = [s]

        return sigs

    def _getPhysicalName(self):
        """Get name in HDL """
        if hasattr(self, "_boundedEntityPort"):
            return self._boundedEntityPort.name
        else:
            return self._getFullName().replace('.', self._NAME_SEPARATOR)

    def _getFullName(self):
        """get all name hierarchy separated by '.' """
        return HObjList._getFullName(self)

    def _replaceParam(self, p, newP):
        """
        Replace parameter on this interface (in configuration stage)

        :ivar pName: actual name of param on me
        :ivar newP: new Param instance by which should be old replaced
        """
        i = self._params.index(p)
        pName = p._scopes[self][1]
        assert i > -1
        self._params[i] = newP
        del p._scopes[self]  # remove reference from old param
        newP._registerScope(pName, self)
        object.__setattr__(self, pName, newP)

    def _updateParamsFrom(self, otherObj, updater=_default_param_updater,
                          exclude=None, prefix=""):
        """
        :note: doc in :func:`~hwt.synthesizer.interfaceLevel.propDeclCollector._updateParamsFrom`
        """
        PropDeclrCollector._updateParamsFrom(self, otherObj, updater, exclude, prefix)

    def _bit_length(self):
        """Sum of all width of interfaces in this interface"""
        try:
            interfaces = self._interfaces
        except AttributeError:
            interfaces = None

        if interfaces is None:
            # not loaded interface
            _intf = self._clone()
            _intf._loadDeclarations()
            interfaces = _intf._interfaces

        if interfaces:
            w = 0
            for i in interfaces:
                w += i._bit_length()
            return w
        else:
            return self._dtype.bit_length()

    def _connectTo(self, master, exclude=None, fit=False):
        """
        connect to another interface interface (on rtl level)
        works like self <= master in VHDL
        """
        return list(self._connectToIter(master, exclude, fit))

    def __repr__(self):
        s = [self.__class__.__name__]
        s.append("name=%s" % self._getFullName())
        if hasattr(self, '_width'):
            s.append("_width=%s" % str(self._width))
        if hasattr(self, '_masterDir'):
            s.append("_masterDir=%s" % str(self._masterDir))
        return "<%s>" % (', '.join(s))


from operator import floordiv, add, sub, inv, mod, mul, ne, and_, or_, \
    xor, gt, ge, lt, le, getitem, neg

from hwt.hdl.constants import SENSITIVITY
from hwt.hdl.types.defs import INT
from hwt.hdl.value import Value
from hwt.doc_markers import internal

class OpDefinition():
    """
    Operator definition

    :ivar id: name of operator
    :ivar _evalFn: function which evaluates operands
    """

    def __init__(self, evalFn):
        self.id = None  # assigned automatically in AllOps
        self._evalFn = evalFn

    def __eq__(self, other):
        return type(self) == type(other) and self.id == other.id

    @internal
    def __hash__(self):
        return hash(self.id)

    def eval(self, operator, simulator=None):
        """Load all operands and process them by self._evalFn"""
        def getVal(v):
            while not isinstance(v, Value):
                v = v._val

            return v

        operands = list(map(getVal, operator.operands))

        if isEventDependentOp(operator.operator):
            operands.append(simulator.now)
        elif operator.operator == AllOps.IntToBits:
            operands.append(operator.result._dtype)

        return self._evalFn(*operands)

    def __repr__(self):
        return "<OpDefinition %s>" % (self.id)

def isEventDependentOp(operator):
    return operator in (AllOps.RISING_EDGE, AllOps.FALLING_EDGE)

def onRisingEdgeFn(a, now):
    return a._onRisingEdge(now)

def onFallingEdgeFn(a, now):
    return a._onFallingEdge(now)

def dotOpFn(a, name):
    return getattr(a, name)

# [TODO] downto / to are relict of vhdl and should be replaced with slice
def downtoFn(a, b):
    return a._downto(b)

def toFn(a, b):
    return a._to(b)

def concatFn(a, b):
    return a._concat(b)

def power(base, exp):
    return base._pow(exp)

def eqFn(a, b):
    return a._eq(b)

def ternaryFn(a, b, c):
    return a._ternary(b, c)

def callFn(fn, *operands, **kwargs):
    return fn(*operands, **kwargs)

def bitsToIntFn(a):
    return a._auto_cast(INT)

def intToBitsFn(a, t):
    return a._auto_cast(t)

def bitsAsSignedFn(a):
    return a._signed()

def bitsAsUnsignedFn(a):
    return a._unsigned()

def bitsAsVec(a):
    return a._vec()

class AllOps():
    """
    :attention: Remember that and operator "and" is & and "or" is \|, "and"
        and "or" can not be used because they can not be overloaded
    :attention: These are operators of internal AST,
        they are not equal to verilog or vhdl operators
    """
    RISING_EDGE = OpDefinition(onRisingEdgeFn)  # unnecessary
    FALLING_EDGE = OpDefinition(onFallingEdgeFn)  # unnecessary

    NEG = OpDefinition(neg)
    DIV = OpDefinition(floordiv)
    ADD = OpDefinition(add)
    SUB = OpDefinition(sub)
    POW = OpDefinition(power)
    UN_MINUS = OpDefinition(inv)
    MOD = OpDefinition(mod)
    MUL = OpDefinition(mul)

    NOT = OpDefinition(inv)
    XOR = OpDefinition(xor)
    AND = OpDefinition(and_)
    OR = OpDefinition(or_)

    DOT = OpDefinition(dotOpFn)
    DOWNTO = OpDefinition(downtoFn)
    TO = OpDefinition(toFn)
    CONCAT = OpDefinition(concatFn)

    EQ = OpDefinition(eqFn)
    NEQ = OpDefinition(ne)
    GT = OpDefinition(gt)
    GE = OpDefinition(ge)
    LT = OpDefinition(lt)
    LE = OpDefinition(le)

    INDEX = OpDefinition(getitem)
    TERNARY = OpDefinition(ternaryFn)
    CALL = OpDefinition(callFn)

    BitsToInt = OpDefinition(bitsToIntFn)
    IntToBits = OpDefinition(intToBitsFn)

    BitsAsSigned = OpDefinition(bitsAsSignedFn)
    BitsAsUnsigned = OpDefinition(bitsAsUnsignedFn)
    BitsAsVec = OpDefinition(bitsAsVec)

for a in dir(AllOps):
    o = getattr(AllOps, a)
    if isinstance(o, OpDefinition):
        o.id = a

def sensitivityByOp(op):
    """
    get sensitivity type for operator
    """
    if op == AllOps.RISING_EDGE:
        return SENSITIVITY.RISING
    elif op == AllOps.FALLING_EDGE:
        return SENSITIVITY.FALLING
    else:
        raise TypeError()


from hwt.hdl.operator import Operator
from hwt.hdl.operatorDefs import AllOps
from hwt.hdl.types.array import HArray
from hwt.hdl.types.bits import Bits
from hwt.hdl.types.bool import HBool
from hwt.hdl.types.defs import INT
from hwt.hdl.types.hdlType import default_auto_cast_fn
from hwt.hdl.types.struct import HStruct
from hwt.hdl.types.union import HUnion
from hwt.hdl.value import Value
from hwt.synthesizer.vectorUtils import iterBits, fitTo_t
from hwt.doc_markers import internal

@internal
def convertBits__val(self, val, toType):
    if isinstance(toType, HBool):
        return val._eq(self.getValueCls().fromPy(1, self))
    elif isinstance(toType, Bits):
        return val._convSign__val(toType.signed)
    elif toType == INT:
        return INT.getValueCls()(val.val,
                                 INT,
                                 int(val._isFullVld()),
                                 val.updateTime)

    return default_auto_cast_fn(self, val, toType)

@internal
def convertBits(self, sigOrVal, toType):
    """
    Cast signed-unsigned, to int or bool
    """
    if isinstance(sigOrVal, Value):
        return convertBits__val(self, sigOrVal, toType)
    elif isinstance(toType, HBool):
        if self.bit_length() == 1:
            v = 0 if sigOrVal._dtype.negated else 1
            return sigOrVal._eq(self.getValueCls().fromPy(v, self))
    elif isinstance(toType, Bits):
        if self.bit_length() == toType.bit_length():
            return sigOrVal._convSign(toType.signed)
    elif toType == INT:
        return Operator.withRes(AllOps.BitsToInt, [sigOrVal], toType)

    return default_auto_cast_fn(self, sigOrVal, toType)

@internal
def reinterpret_bits_to_hstruct(sigOrVal, hStructT):
    """
    Reinterpret signal of type Bits to signal of type HStruct
    """
    container = hStructT.fromPy(None)
    offset = 0
    for f in hStructT.fields:
        t = f.dtype
        width = t.bit_length()
        if f.name is not None:
            s = sigOrVal[(width + offset):offset]
            s = s._reinterpret_cast(t)
            setattr(container, f.name, s)

        offset += width

    return container

@internal
def reinterpret_bits_to_harray(sigOrVal, hArrayT):
    elmT = hArrayT.elmType
    elmWidth = elmT.bit_length()
    a = hArrayT.fromPy(None)
    for i, item in enumerate(iterBits(sigOrVal,
                                      bitsInOne=elmWidth,
                                      skipPadding=False)):
        item = item._reinterpret_cast(elmT)
        a[i] = item

    return a

@internal
def reinterpretBits__val(self, val, toType):
    if isinstance(toType, HStruct):
        return reinterpret_bits_to_hstruct(val, toType)
    elif isinstance(toType, HUnion):
        raise NotImplementedError()
    elif isinstance(toType, HArray):
        return reinterpret_bits_to_harray(val, toType)

    return default_auto_cast_fn(self, val, toType)

@internal
def reinterpretBits(self, sigOrVal, toType):
    """
    Cast object of same bit size between to other type
    (f.e. bits to struct, union or array)
    """
    if isinstance(sigOrVal, Value):
        return reinterpretBits__val(self, sigOrVal, toType)
    elif isinstance(toType, Bits):
        return fitTo_t(sigOrVal, toType)
    elif sigOrVal._dtype.bit_length() == toType.bit_length():
        if isinstance(toType, HStruct):
            raise reinterpret_bits_to_hstruct(sigOrVal, toType)
        elif isinstance(toType, HUnion):
            raise NotImplementedError()
        elif isinstance(toType, HArray):
            reinterpret_bits_to_harray(sigOrVal, toType)

    return default_auto_cast_fn(self, sigOrVal, toType)


from typing import Tuple

from hwt.hdl.transPart import TransPart
from hwt.hdl.transTmpl import OneOfTransaction, StreamTransaction
from hwt.doc_markers import internal

@internal
def iterSort(iterators, cmpFn):
    """
    Sort items from iterators(generators) by alwas selecting item
    with lowest value (min first)

    :return: generator of tuples (origin index, item) where origin index
        is index of iterator in "iterators" from where item commes from
    """
    actual = []
    _iterators = []
    for i, it in enumerate(iterators):
        try:
            a = next(it)
            _iterators.append((i, it))
            actual.append(a)
        except StopIteration:
            continue

    while True:
        if not _iterators:
            return
        elif len(_iterators) == 1:
            originIndex, it = _iterators[0]
            yield originIndex, actual[0]
            for item in it:
                yield originIndex, item
            return

        # select minimum and iterator from where it comes from
        minimum = None
        minimumIndex = None
        secondMin = None
        for i, val in enumerate(actual):
            skipSecMinCheck = False
            if minimum is None:
                minimum = val
                minimumIndex = i
            elif cmpFn(val, minimum):
                secondMin = minimum
                minimum = val
                minimumIndex = i
                skipSecMinCheck = True
            elif not skipSecMinCheck and (
                    secondMin is None or cmpFn(val, secondMin)):
                secondMin = val

        actualI, actualIt = _iterators[minimumIndex]
        while not cmpFn(secondMin, minimum):
            yield (actualI, minimum)
            try:
                minimum = next(actualIt)
            except StopIteration:
                minimum = None
                break

        # consume from actual iterator while
        if minimum is None:
            del _iterators[minimumIndex]
            del actual[minimumIndex]
        else:
            # minimum is not minimum anymore
            actual[minimumIndex] = minimum

class TransPartGroup(list):
    """
    Abstract parent class for groups of TransParts
    """
    def setIsLast(self, val: bool) -> None:
        self._isLast = val

    def isLastPart(self) -> bool:
        """
        :return: True if this part is last in parts derived from original field
            else False
        """
        return self._isLast

    def getBusWordBitRange(self) -> Tuple[int, int]:
        """
        :return: bit range which contains data of this part on bus data signal
        """
        offset = self.startOfPart % self.parent.wordWidth
        return (offset + self.bit_length(), offset)

    def bit_length(self):
        return self.endOfPart - self.startOfPart

    def __repr__(self):
        return "<%s %s>" % (self.__class__.__name__, list.__repr__(self))

class ChoicesOfFrameParts(TransPartGroup):
    """
    List of ChoiceOfFrameParts
    One of ChoiceOfFrameParts is used to represent the word, item depends
    on context

    :ivar origin: OneOfTransaction instance
    :ivar startOfPart: bit addr of start of this group of frame parts
    :ivar endOfPart: bit addr of end of this group of frame parts
    :ivar _isLast: flag which means this is the last part of original union
    """

    def __init__(self, startOfPart: int, origin: OneOfTransaction):
        self.origin = origin
        self.startOfPart = startOfPart
        self.endOfPart = None
        self._isLast = False
        super(ChoicesOfFrameParts, self).__init__()

    def resolveEnd(self):
        end = self.startOfPart
        for items in self:
            if items:
                end = max(end, max(itm.endOfPart for itm in items))
        self.endOfPart = end

class ChoiceOfFrameParts(list):
    """
    :ivar origin: ChoicesOfFrameParts instance
    :ivar tmpl: TransTmpl which was item generated from
    """

    def __init__(self, origin, tmpl):
        self.origin = origin
        self.tmpl = tmpl

    def __repr__(self):
        return "<ChoiceOfFrameParts %s>" % list.__repr__(self)

class StreamOfFramePars(TransPartGroup):
    """
    List of TransPart instances
    One of TransPart is used to represent the word, on target bus,
    All TransPart instances represents one logical word in stream

    :ivar origin: StreamTransaction instance
    :ivar startOfPart: bit addr of start of this group of frame parts
    :ivar endOfPart: bit addr of end of this group of frame parts
    :ivar _isLast: flag which means this is the last part of original union
    """

    def __init__(self, startOfPart: int, origin: StreamTransaction):
        self.origin = origin
        self.startOfPart = startOfPart
        self.endOfPart = None
        self._isLast = False
        super(StreamOfFramePars, self).__init__()

    def resolveEnd(self):
        assert self.startOfPart == self[0].startOfPart
        end = self[-1].endOfPart
        self.endOfPart = end
        return end

def groupIntoChoices(splitsOnWord, wordWidth: int, origin: OneOfTransaction):
    """
    :param splitsOnWord: list of lists of parts (fields splited on word
        boundaries)
    :return: generators of ChoicesOfFrameParts for each word
        which are not crossing word boundaries
    """
    def cmpWordIndex(a, b):
        return a.startOfPart // wordWidth < b.startOfPart // wordWidth

    actual = None
    itCnt = len(splitsOnWord)
    for i, item in iterSort(splitsOnWord, cmpWordIndex):
        _actualW = item.startOfPart // wordWidth
        if actual is None:
            # first pass
            actual = ChoicesOfFrameParts(item.startOfPart, origin)
            actual.extend(
                ChoiceOfFrameParts(actual,
                                   origin.possibleTransactions[_i])
                for _i in range(itCnt))
            actualW = _actualW
        elif _actualW > actualW:
            actual.resolveEnd()
            yield actual
            actual = ChoicesOfFrameParts(item.startOfPart, origin)
            actual.extend(
                ChoiceOfFrameParts(actual,
                                   origin.possibleTransactions[_i])
                for _i in range(itCnt))
            actualW = _actualW
        actual[i].append(item)

    if actual is not None:
        actual.setIsLast(True)
        actual.resolveEnd()
        yield actual

class TransTmplWordIterator():
    """
    Iterator which reinterprets any structure
    as generator of bits of specified width
    """
    def __init__(self, wordWidth):
        self.wordWidth = wordWidth

    def fullWordCnt(self, start: int, end: int):
        """Count of complete words between two addresses
        """
        assert end >= start, (start, end)
        gap = max(0, (end - start) - (start % self.wordWidth))
        return gap // self.wordWidth

    def groupByWordIndex(self, transaction: 'TransTmpl', offset: int):
        """
        Group transaction parts splited on words to words

        :param transaction: TransTmpl instance which parts
            should be grupped into words
        :return: generator of tuples (wordIndex, list of transaction parts
            in this word)
        """
        actualW = None
        partsInWord = []
        wordWidth = self.wordWidth
        for item in self.splitOnWords(transaction, offset):
            _actualW = item.startOfPart // wordWidth
            if actualW is None:
                actualW = _actualW
                partsInWord.append(item)
            elif _actualW > actualW:
                yield (actualW, partsInWord)
                actualW = _actualW
                partsInWord = [item, ]
            else:
                partsInWord.append(item)

        if partsInWord:
            yield (actualW, partsInWord)

    @internal
    def splitOnWords(self, transaction, addrOffset=0):
        """
        :return: generator of TransPart instance
        """
        wordWidth = self.wordWidth
        end = addrOffset
        for tmp in transaction.walkFlatten(offset=addrOffset):
            if isinstance(tmp, OneOfTransaction):
                split = [self.splitOnWords(ch, end)
                         for ch in tmp.possibleTransactions]
                yield from groupIntoChoices(split, wordWidth, tmp)
                end = addrOffset + tmp.possibleTransactions[0].bitAddrEnd
            elif isinstance(tmp, StreamTransaction):
                ch_len = tmp.child.bit_length()
                if end % self.wordWidth != 0 or ch_len != self.wordWidth:
                    # assert start, end is aligned
                    raise NotImplementedError(tmp)
                else:
                    s = StreamOfFramePars(end, tmp)
                    s.extend(self.splitOnWords(tmp.child, end))
                    s.setIsLast(True)
                    s.resolveEnd()
                    yield s
                    end = addrOffset + tmp.child.bitAddrEnd
            else:
                (base, end), tmpl = tmp
                startOfPart = base
                while startOfPart != end:
                    wordIndex = startOfPart // wordWidth
                    endOfWord = (wordIndex + 1) * wordWidth
                    endOfPart = min(endOfWord, end)
                    inFieldOffset = startOfPart - base
                    yield TransPart(self, tmpl, startOfPart, endOfPart,
                                    inFieldOffset)
                    startOfPart = endOfPart


from operator import eq, ne

from hwt.hdl.operator import Operator
from hwt.hdl.operatorDefs import AllOps
from hwt.hdl.types.bitVal_bitOpsVldMask import vldMaskForOr, \
    vldMaskForAnd, vldMaskForXor
from hwt.hdl.types.bitVal_opReduce import tryReduceOr, tryReduceAnd, \
    tryReduceXor
from hwt.hdl.types.defs import BOOL
from hwt.hdl.types.typeCast import toHVal
from hwt.hdl.value import Value, areValues
from hwt.synthesizer.rtlLevel.signalUtils.exceptions import MultipleDriversErr,\
    NoDriverErr

def boolLogOp__val(self, other, op, getVldFn):
    v = bool(op._evalFn(bool(self.val), (other.val)))
    return HBoolVal(v, BOOL,
                    getVldFn(self, other),
                    max(self.updateTime, other.updateTime))

def boolLogOp(self, other, op, getVldFn, reduceCheckFn):
    other = toHVal(other)

    iamVal = isinstance(self, Value)
    otherIsVal = isinstance(other, Value)

    if iamVal and otherIsVal:
        return boolLogOp__val(self, other, op, getVldFn)
    else:
        if otherIsVal:
            r = reduceCheckFn(self, other)
            if r is not None:
                return r

        elif iamVal:
            r = reduceCheckFn(other, self)
            if r is not None:
                return r

        return Operator.withRes(op, [self, other._auto_cast(BOOL)], BOOL)

def boolCmpOp__val(self, other, op, evalFn):
    v = evalFn(bool(self.val), bool(other.val)) and (
        self.vldMask == other.vldMask == 1)
    return HBoolVal(v, BOOL,
                    self.vldMask & other.vldMask,
                    max(self.updateTime, other.updateTime))

def boolCmpOp(self, other, op, evalFn=None):
    other = toHVal(other)
    if evalFn is None:
        evalFn = op._evalFn

    if areValues(self, other):
        return boolCmpOp__val(self, other, op, evalFn)
    else:
        return Operator.withRes(op, [self, other._auto_cast(BOOL)], BOOL)

class HBoolVal(Value):

    def _isFullVld(self):
        return bool(self.vldMask)

    @classmethod
    def fromPy(cls, val, typeObj, vldMask=None):
        """
        :param val: value of python type bool or None
        :param typeObj: instance of HdlType
        :param vldMask: None vldMask is resolved from val,
            if is 0 value is invalidated
            if is 1 value has to be valid
        """
        vld = int(val is not None)
        if not vld:
            assert vldMask is None or vldMask == 0
            val = False
        else:
            if vldMask == 0:
                val = False
                vld = 0
            else:
                val = bool(val)

        return cls(val, typeObj, vld)

    def toPy(self):
        return bool(self)

    def _eq__val(self, other):
        return boolCmpOp__val(self, other, AllOps.EQ, eq)

    def _eq(self, other):
        return boolCmpOp(self, other, AllOps.EQ, evalFn=eq)

    def _ne__val(self, other):
        return boolCmpOp__val(self, other, AllOps.NEQ, ne)

    def __ne__(self, other):
        return boolCmpOp(self, other, AllOps.NEQ)

    def _invert__val(self):
        v = self.clone()
        v.val = not v.val
        return v

    def __invert__(self):
        if isinstance(self, Value):
            return self._invert__val()
        else:
            try:
                # double negation
                d = self.singleDriver()
                if isinstance(d, Operator) and d.operator == AllOps.NOT:
                    return d.operands[0]
            except (MultipleDriversErr, NoDriverErr):
                pass
            return Operator.withRes(AllOps.NOT, [self], BOOL)

    def _ternary__val(self, ifTrue, ifFalse):
        if self.val:
            res = ifTrue.clone()
        else:
            res = ifFalse.clone()

        if not self.vldMask:
            res.vldMask = 0

        return res

    def _ternary(self, ifTrue, ifFalse):
        ifTrue = toHVal(ifTrue)
        ifFalse = toHVal(ifFalse)

        if isinstance(self, Value):
            return self._ternary__val(ifTrue, ifFalse)
        else:
            return Operator.withRes(AllOps.TERNARY, [self, ifTrue, ifFalse], ifTrue._dtype)

    # logic
    def _and__val(self, other):
        return boolLogOp__val(self, other, AllOps.AND, vldMaskForAnd)

    def __and__(self, other):
        return boolLogOp(self, other, AllOps.AND, vldMaskForAnd, tryReduceAnd)

    def _or__val(self, other):
        return boolLogOp__val(self, other, AllOps.OR, vldMaskForOr)

    def __or__(self, other):
        return boolLogOp(self, other, AllOps.OR, vldMaskForOr, tryReduceOr)

    def _xor__val(self, other):
        return boolLogOp__val(self, other, AllOps.OR, vldMaskForOr)

    def __xor__(self, other):
        return boolLogOp(self, other, AllOps.XOR, vldMaskForXor, tryReduceXor)


import sys

from hwt.doc_markers import internal
from hwt.serializer.generic.indent import getIndent
from hwt.synthesizer.hObjList import HObjList
from hwt.synthesizer.interfaceLevel.mainBases import InterfaceBase

@internal
def valueHasChanged(valA, valB):
    return valA.val is not valB.val or valA.vldMask != valB.vldMask

def pprintInterface(intf, prefix="", indent=0, file=sys.stdout):
    """
    Pretty print interface
    """
    try:
        s = intf._sig
    except AttributeError:
        s = ""
    if s is not "":
        s = " " + repr(s)

    file.write("".join([getIndent(indent), prefix, repr(intf._getFullName()),
                        s]))
    file.write("\n")

    if isinstance(intf, HObjList):
        for i, p in enumerate(intf):
            # interfaces have already name of this array and index in it's name
            pprintInterface(p, prefix=prefix, indent=indent + 1, file=file)
    else:
        for i in intf._interfaces:
            pprintInterface(i, indent=indent + 1, file=file)

def pprintAgents(unitOrIntf, indent=0, prefix="", file=sys.stdout):
    if isinstance(unitOrIntf, InterfaceBase):
        ag = unitOrIntf._ag
    elif isinstance(unitOrIntf, HObjList):
        for i, item in enumerate(unitOrIntf):
            item_prefix = "%s_%d" % (prefix, i)
            pprintAgents(item, indent=indent+1, prefix=item_prefix, file=file)
        return
    else:
        ag = None

    if ag is not None:
        file.write("%s%s%r\n" % (getIndent(indent), prefix, ag))

    for i in unitOrIntf._interfaces:
        pprintAgents(i, indent + 1, file=file)


from itertools import zip_longest
from math import ceil, floor, inf
import re
from typing import Union, Generator

from hwt.bitmask import mask, selectBitRange, setBitRange
from hwt.hdl.frameTmplUtils import TransTmplWordIterator, \
    ChoicesOfFrameParts
from hwt.hdl.transPart import TransPart
from hwt.hdl.types.array import HArray
from hwt.hdl.types.bits import Bits
from hwt.hdl.types.struct import HStruct
from hwt.pyUtils.arrayQuery import flatten
from hwt.simulator.types.simBits import simBitsT
from hwt.doc_markers import internal

class FrameTmpl(object):
    """
    Frame template is metainfomation about data structure, it's the template
    of transaction for specific interface

    Usuall flow of frame generatig consists of these steps:

    1. Describe format of data by HDL type (HStruct, HUnion ...)

    2. Convert it to TransTmpl to resolve addresses of each field in structure

    3. Split parts of TransTmpl into words

    4. Use parts in words to assembly frames

    :ivar _fieldToTPart: dictionary {HStructField: TransPart} to resolve
        this association, None by default, builded when packData
        is called and is not builded
    :note: others ivars described in __init__
    """
    __RE_RM_ARRAY_DOTS = re.compile("(\.\[)")

    def __init__(self, origin, wordWidth, startBitAddr, endBitAddr,
                 transParts):
        """
        :param origin: instance of HType (usually HStruct)
            from which this FrameTmpl was generated from
        :param wordWidth: width of word on interface
            where this template should be used
        :param startBitAddr: bit offset where this frame starts
        :param endBitAddr: bit offset where this frame ends
            (bit index of first bit behind this frame)
        :param transParts: instances of TransPart which are parts of this frame
        """
        self.origin = origin
        self.wordWidth = wordWidth
        assert startBitAddr <= endBitAddr
        self.startBitAddr = startBitAddr
        self.endBitAddr = endBitAddr
        self.parts = transParts

        self._fieldToTPart = None

        for p in self.parts:
            p.parent = self
            assert p.startOfPart >= startBitAddr, (p, startBitAddr)
            assert p.endOfPart <= endBitAddr, (p, endBitAddr)

    @staticmethod
    def framesFromTransTmpl(transaction: 'TransTmpl',
                            wordWidth: int,
                            maxFrameLen: Union[int, float]=inf,
                            maxPaddingWords: Union[int, float]=inf,
                            trimPaddingWordsOnStart: bool=False,
                            trimPaddingWordsOnEnd: bool=False) -> Generator[
                                'FrameTmpl', None, None]:
        """
        Convert transaction template into FrameTmpls

        :param transaction: transaction template used which are FrameTmpls
            created from
        :param wordWidth: width of data signal in target interface
            where frames will be used
        :param maxFrameLen: maximum length of frame in bits,
            if exceeded another frame will be created
        :param maxPaddingWords: maximum of continual padding words in frame,
            if exceed frame is split and words are cut of
        :attention: if maxPaddingWords<inf trimPaddingWordsOnEnd
            or trimPaddingWordsOnStart has to be True
            to decide where padding should be trimmed
        :param trimPaddingWordsOnStart: trim padding from start of frame
            at word granularity
        :param trimPaddingWordsOnEnd: trim padding from end of frame
            at word granularity
        """
        isFirstInFrame = True
        partsPending = False

        startOfThisFrame = 0
        assert maxFrameLen > 0
        assert maxPaddingWords >= 0
        if maxPaddingWords < inf:
            assert trimPaddingWordsOnStart or trimPaddingWordsOnEnd, \
                "Padding has to be cut off somewhere"

        it = TransTmplWordIterator(wordWidth)
        lastWordI = 0
        endOfThisFrame = maxFrameLen
        parts = []
        for wordI, word in it.groupByWordIndex(transaction, 0):
            if wordI * wordWidth >= endOfThisFrame:
                # now in first+ word behind the frame
                # cut off padding at end of frame
                paddingWords = wordI - lastWordI
                if trimPaddingWordsOnEnd and paddingWords > maxPaddingWords:
                    # cut off padding and align end of frame to word
                    _endOfThisFrame = (lastWordI + 1) * wordWidth
                else:
                    _endOfThisFrame = wordI * wordWidth

                yield FrameTmpl(transaction,
                                wordWidth,
                                startOfThisFrame,
                                _endOfThisFrame,
                                parts)

                # prepare for start of new frame
                parts = []
                isFirstInFrame = True
                partsPending = False
                # start on new word
                startOfThisFrame = _endOfThisFrame
                endOfThisFrame = startOfThisFrame + maxFrameLen
                lastWordI = wordI

            # check if padding at potential end of frame can be cut off
            if (not isFirstInFrame
                    and trimPaddingWordsOnEnd
                    and wordI - lastWordI > 1):
                # there is too much continual padding,
                # cut it out and start new frame
                _endOfThisFrame = (lastWordI + 1) * wordWidth
                yield FrameTmpl(transaction,
                                wordWidth,
                                startOfThisFrame,
                                _endOfThisFrame,
                                parts)

                # prepare for start of new frame
                parts = []
                isFirstInFrame = True
                partsPending = False
                # start on new word
                startOfThisFrame = _endOfThisFrame
                endOfThisFrame = startOfThisFrame + maxFrameLen
                lastWordI = wordI - 1

            if isFirstInFrame:
                partsPending = True
                isFirstInFrame = False
                # cut off padding at start of frame
                paddingWords = wordI - lastWordI
                if trimPaddingWordsOnStart and paddingWords > maxPaddingWords:
                    startOfThisFrame += paddingWords * wordWidth

                endOfThisFrame = startOfThisFrame + maxFrameLen

            # resolve end of this part
            parts.extend(word)
            lastWordI = wordI

        # reminder in "parts" after last iteration
        endOfThisFrame = transaction.bitAddrEnd
        withPadding = not (trimPaddingWordsOnEnd or trimPaddingWordsOnStart)
        if partsPending or (withPadding
                            and endOfThisFrame != startOfThisFrame):
            # cut off padding at end of frame
            endOfLastWord = (lastWordI + 1) * wordWidth
            if endOfThisFrame < endOfLastWord:
                endOfThisFrame = endOfLastWord
            else:
                paddingWords = it.fullWordCnt(endOfLastWord, endOfThisFrame)
                if trimPaddingWordsOnEnd and paddingWords > maxPaddingWords:
                    endOfThisFrame -= paddingWords * wordWidth
                    # align end of frame to word
            endOfThisFrame = min(startOfThisFrame +
                                 maxFrameLen, endOfThisFrame)

            yield FrameTmpl(transaction,
                            wordWidth,
                            startOfThisFrame,
                            endOfThisFrame,
                            parts)
            parts = []
            startOfThisFrame = endOfThisFrame

        # final padding on the end
        while withPadding and startOfThisFrame < transaction.bitAddrEnd:
            endOfThisFrame = min(startOfThisFrame +
                                 maxFrameLen, transaction.bitAddrEnd)

            yield FrameTmpl(transaction,
                            wordWidth,
                            startOfThisFrame,
                            endOfThisFrame,
                            [])

            startOfThisFrame = endOfThisFrame

    @internal
    def _wordIndx(self, addr: int):
        """
        convert bit address to index of word where this address is
        """
        return floor(addr / self.wordWidth)

    def getWordCnt(self):
        """
        Get count of words in this frame
        """
        return ceil((self.endBitAddr - self.startBitAddr) / self.wordWidth)

    def walkWords(self, showPadding: bool=False):
        """
        Walk enumerated words in this frame

        :attention: not all indexes has to be present, only words
            with items will be generated when not showPadding
        :param showPadding: padding TransParts are also present
        :return: generator of tuples (wordIndex, list of TransParts
            in this word)
        """
        wIndex = 0
        lastEnd = self.startBitAddr
        parts = []
        for p in self.parts:
            end = p.startOfPart
            if showPadding and end != lastEnd:
                # insert padding
                while end != lastEnd:
                    assert end >= lastEnd, (end, lastEnd)
                    endOfWord = ceil(
                        (lastEnd + 1) / self.wordWidth) * self.wordWidth
                    endOfPadding = min(endOfWord, end)
                    _p = TransPart(self, None, lastEnd, endOfPadding, 0)
                    parts.append(_p)

                    if endOfPadding >= endOfWord:
                        yield (wIndex, parts)
                        wIndex += 1
                        parts = []

                    lastEnd = endOfPadding

            if self._wordIndx(lastEnd) != self._wordIndx(p.startOfPart):
                yield (wIndex, parts)

                wIndex += 1
                parts = []
                lastEnd = p.endOfPart

            parts.append(p)
            lastEnd = p.endOfPart
            if lastEnd % self.wordWidth == 0:
                yield (wIndex, parts)

                wIndex += 1
                parts = []

        if showPadding and (parts
                            or lastEnd != self.endBitAddr
                            or lastEnd % self.wordWidth != 0):
            # align end to end of last word
            end = ceil(self.endBitAddr / self.wordWidth) * self.wordWidth
            while end != lastEnd:
                assert end >= lastEnd, (end, lastEnd)
                endOfWord = ((lastEnd // self.wordWidth) + 1) * self.wordWidth
                endOfPadding = min(endOfWord, end)
                _p = TransPart(self, None, lastEnd, endOfPadding, 0)
                _p.parent = self
                parts.append(_p)

                if endOfPadding >= endOfWord:
                    yield (wIndex, parts)
                    wIndex += 1
                    parts = []

                lastEnd = endOfPadding

            if parts:
                # in the case end of frame is not aligned to end of word
                yield (wIndex, parts)

    @staticmethod
    def fieldToDataDict(dtype, data, res):
        """
        Construct dictionary {StructField:value} for faster lookup of values
        for fields
        """
        # assert data is None or isinstance(data, dict)
        for f in dtype.fields:
            try:
                fVal = data[f.name]
            except KeyError:
                fVal = None

            if isinstance(f.dtype, Bits):
                if fVal is not None:
                    assert isinstance(fVal, int)
                    res[f] = fVal
            elif isinstance(f.dtype, HStruct):
                if fVal:
                    FrameTmpl.fieldToDataDict(f.dtype, fVal, res)
            elif isinstance(f.dtype, HArray):
                if fVal:
                    # assert isinstance(fVal, class_or_tuple)
                    res[f] = fVal

        return res

    def packData(self, data):
        """
        Pack data into list of BitsVal of specified dataWidth

        :param data: dict of values for struct fields {fieldName: value}

        :return: list of BitsVal which are representing values of words
        """
        typeOfWord = simBitsT(self.wordWidth, None)
        fieldToVal = self._fieldToTPart
        if fieldToVal is None:
            fieldToVal = self._fieldToTPart = self.fieldToDataDict(
                self.origin.dtype,
                data,
                {})

        for _, transParts in self.walkWords(showPadding=True):
            actualVldMask = 0
            actualVal = 0
            for tPart in transParts:
                high, low = tPart.getBusWordBitRange()
                fhigh, flow = tPart.getFieldBitRange()
                if not tPart.isPadding:
                    val = fieldToVal.get(tPart.tmpl.origin, None)
                else:
                    val = None

                if val is None:
                    newBits = 0
                    vld = 0
                else:
                    newBits = selectBitRange(val, flow, fhigh - flow)
                    vld = mask(high - low) << low

                actualVal = setBitRange(actualVal, low, high - low, newBits)
                actualVldMask = setBitRange(actualVal, low, high - low, vld)

            yield typeOfWord.getValueCls()(actualVal, typeOfWord,
                                           actualVldMask, -1)

    @internal
    def __repr__getName(self, transPart, fieldWidth):
        if transPart.isPadding:
            return "X" * fieldWidth
        else:
            names = []
            tp = transPart.tmpl
            while tp is not None:
                try:
                    isArrayElm = isinstance(tp.parent.dtype, HArray)
                except AttributeError:
                    isArrayElm = False

                if isArrayElm:
                    arr = transPart.tmpl.parent
                    arrS = arr.bitAddr
                    itemW = (arr.bitAddrEnd - arrS) // arr.itemCnt
                    s = transPart.startOfPart
                    indx = (s - arrS) // itemW
                    names.append("[%d]" % indx)
                else:
                    o = tp.origin
                    if o is None:
                        break
                    if o.name is not None:
                        names.append(o.name)
                tp = tp.parent

            # [HOTFIX] rm dots when indexing on array
            return self.__RE_RM_ARRAY_DOTS.sub("[", ".".join(reversed(names)))

    @internal
    def __repr__word(self, index, width, padding, transParts):
        buff = ["{0: <{padding}}|".format(index, padding=padding)]
        DW = self.wordWidth
        partsWithChoice = []

        endAlignment = transParts[-1].endOfPart % DW
        if endAlignment:
            # -1 for ending |
            percentOfWidth = (DW - endAlignment) / DW
            # -1 for ending |
            fieldWidth = max(0, int(percentOfWidth * width) - 1)
            assert fieldWidth >= 0
            s = '%s|' % ("^" * (fieldWidth))
            buff.append(s)

        for tp in reversed(transParts):
            percentOfWidth = tp.bit_length() / DW
            # -1 for ending |
            fieldWidth = max(0, int(percentOfWidth * width) - 1)
            assert fieldWidth >= 0

            # percentOffset = (tp.inFrameBitAddr % DW) / DW
            # offset = int(percentOffset * width)
            if isinstance(tp, ChoicesOfFrameParts):
                name = "<union>"
                partsWithChoice.append(tp)
            else:
                name = self.__repr__getName(tp, fieldWidth)
            buff.append('{0: ^{fieldWidth}}|'.format(
                name, fieldWidth=fieldWidth))
        return ("".join(buff), partsWithChoice)

    def __repr__(self, scale=1):
        buff = []
        s = "<%s start:%d, end:%d" % (
            self.__class__.__name__, self.startBitAddr, self.endBitAddr)
        buff.append(s)

        padding = 5
        DW = self.wordWidth
        width = int(DW * scale)

        buff.append(
            '{0: <{padding}}{1: <{halfLineWidth}}{2: >{halfLineWidth}}'.format(
                "", DW - 1, 0, padding=padding, halfLineWidth=width // 2))
        line = '{0: <{padding}}{1:-<{lineWidth}}'.format(
            "", "", padding=padding, lineWidth=width + 1)
        buff.append(line)

        for w, transParts in self.walkWords(showPadding=True):
            wStr, partsWithChoice = self.__repr__word(
                w, width, padding, transParts)
            buff.append(wStr)
            while partsWithChoice:
                for parts in zip_longest(*partsWithChoice):
                    parts = list(flatten(parts, level=1))
                    wStr, _partsWithChoice = self.__repr__word(
                        w, width, padding, parts)
                    buff.append(wStr)
                partsWithChoice = _partsWithChoice

        buff.append(line)
        buff.append(">")
        return "\n".join(buff)


from itertools import chain, islice
from typing import List, Tuple, Union

from hwt.hdl.hdlObject import HdlObject
from hwt.hdl.sensitivityCtx import SensitivityCtx
from hwt.hdl.value import Value
from hwt.pyUtils.arrayQuery import flatten, groupedby
from hwt.pyUtils.uniqList import UniqList
from hwt.synthesizer.rtlLevel.mainBases import RtlSignalBase
from hwt.doc_markers import internal

class HwtSyntaxError(Exception):
    pass

class IncompatibleStructure(Exception):
    """
    Statements are not comparable due incompatible structure
    """

class HdlStatement(HdlObject):
    """
    :ivar _is_completly_event_dependent: statement does not have
         any cobinational statement
    :ivar _now_is_event_dependent: statement is event (clk) dependent
    :ivar parentStm: parent isnstance of HdlStatement or None
    :ivar _inputs: UniqList of input signals for this statement
    :ivar _outputs: UniqList of output signals for this statement
    :ivar _sensitivity: UniqList of input signals
        or (rising/falling) operator
    :ivar _enclosed_for: set of outputs for which this statement is enclosed
        (for which there is not any unused branch)
    :ivar rank: number of used branches in statement, used as prefilter
        for statement comparing
    """

    def __init__(self, parentStm=None, sensitivity=None,
                 is_completly_event_dependent=False):
        self._is_completly_event_dependent = is_completly_event_dependent
        self._now_is_event_dependent = is_completly_event_dependent
        self.parentStm = parentStm
        self._inputs = UniqList()
        self._outputs = UniqList()
        self._enclosed_for = None

        self._sensitivity = sensitivity
        self.rank = 0

    @internal
    def _clean_signal_meta(self):
        """
        Clean informations about enclosure for outputs and sensitivity
        of this statement
        """
        self._enclosed_for = None
        self._sensitivity = None
        for stm in self._iter_stms():
            stm._clean_signal_meta()

    @internal
    def _collect_io(self) -> None:
        """
        Collect inputs/outputs from all child statements
        to :py:attr:`~_input` / :py:attr:`_output` attribure on this object
        """
        in_add = self._inputs.extend
        out_add = self._outputs.extend

        for stm in self._iter_stms():
            in_add(stm._inputs)
            out_add(stm._outputs)

    @internal
    @staticmethod
    def _cut_off_drivers_of_list(sig: RtlSignalBase,
                                 statements: List["HdlStatement"],
                                 keep_mask: List["HdlStatement"],
                                 new_statements: List["HdlStatement"],):
        all_cut_off = True
        for stm in statements:
            newStm = stm._cut_off_drivers_of(sig)
            keep = True
            if newStm is None:
                # statement is des not have drivers of sig
                all_cut_off = False
            elif newStm is stm:
                # statement drives only sig
                keep = False
                new_statements.append(newStm)
            else:
                # statement was splited on multiple statements
                all_cut_off = False
                new_statements.append(newStm)

            keep_mask.append(keep)

        return all_cut_off

    @internal
    def _discover_enclosure(self) -> None:
        """
        Discover all outputs for which is this steement enclosed _enclosed_for property
        (has driver in all code branches)
        """
        raise NotImplementedError("This menthod shoud be implemented"
                                  " on class of statement", self.__class__, self)

    @internal
    @staticmethod
    def _discover_enclosure_for_statements(statements: List['HdlStatement'],
                                           outputs: List['HdlStatement']):
        """
        Discover enclosure for list of statements

        :param statements: list of statements in one code branch
        :param outputs: list of outputs which should be driven from this statement list
        :return: set of signals for which this statement list have always some driver
            (is enclosed)
        """
        result = set()
        if not statements:
            return result

        for stm in statements:
            stm._discover_enclosure()

        for o in outputs:
            has_driver = False

            for stm in statements:
                if o in stm._outputs:
                    assert not has_driver
                    has_driver = False
                    if o in stm._enclosed_for:
                        result.add(o)
                else:
                    pass

        return result

    @internal
    def _discover_sensitivity(self, seen: set) -> None:
        """
        discover all sensitivity signals and store them to _sensitivity property
        """
        raise NotImplementedError("This menthod shoud be implemented"
                                  " on class of statement", self.__class__, self)

    @internal
    def _discover_sensitivity_sig(self, signal: RtlSignalBase,
                                  seen: set, ctx: SensitivityCtx):
        casualSensitivity = set()
        signal._walk_sensitivity(casualSensitivity, seen, ctx)
        if not ctx.contains_ev_dependency:
            # if event dependent sensitivity found do not add other sensitivity
            ctx.extend(casualSensitivity)

    @internal
    def _discover_sensitivity_seq(self,
                                  signals: List[RtlSignalBase],
                                  seen: set, ctx: SensitivityCtx)\
            -> None:
        """
        Discover sensitivity for list of signals

        """
        casualSensitivity = set()
        for s in signals:
            s._walk_sensitivity(casualSensitivity, seen, ctx)
            if ctx.contains_ev_dependency:
                break

        # if event dependent sensitivity found do not add other sensitivity
        if not ctx.contains_ev_dependency:
            ctx.extend(casualSensitivity)

    @internal
    def _get_rtl_context(self):
        """
        get RtlNetlist context from signals
        """
        for sig in chain(self._inputs, self._outputs):
            if sig.ctx:
                return sig.ctx
            else:
                # Param instances does not have context
                continue
        raise HwtSyntaxError(
            "Statement does not have any signal in any context", self)

    def _iter_stms(self):
        """
        :return: iterator over all children statements
        """
        raise NotImplementedError("This menthod shoud be implemented"
                                  " on class of statement", self.__class__,
                                  self)

    @internal
    def _on_reduce(self, self_reduced: bool, io_changed: bool,
                   result_statements: List["HdlStatement"]) -> None:
        """
        Update signal IO after reuce atempt

        :param self_reduced: if True this object was reduced
        :param io_changed: if True IO of this object may changed
            and has to be updated
        :param result_statements: list of statements which are result
            of reduce operation on this statement
        """

        parentStm = self.parentStm
        if self_reduced:
            was_top = parentStm is None
            # update signal drivers/endpoints
            if was_top:
                # disconnect self from signals
                ctx = self._get_rtl_context()
                ctx.statements.remove(self)
                ctx.statements.update(result_statements)

                for i in self._inputs:
                    i.endpoints.discard(self)
                for o in self._outputs:
                    o.drivers.remove(self)

            for stm in result_statements:
                stm.parentStm = parentStm
                if parentStm is None:
                    # conect signals to child statements
                    for inp in stm._inputs:
                        inp.endpoints.append(stm)
                    for outp in stm._outputs:
                        outp.drivers.append(stm)
        else:
            # parent has to update it's inputs/outputs
            if io_changed:
                self._inputs = UniqList()
                self._outputs = UniqList()
                self._collect_io()

    @internal
    def _on_merge(self, other):
        """
        After merging statements update IO, sensitivity and context

        :attention: rank is not updated
        """
        self._inputs.extend(other._inputs)
        self._outputs.extend(other._outputs)

        if self._sensitivity is not None:
            self._sensitivity.extend(other._sensitivity)
        else:
            assert other._sensitivity is None

        if self._enclosed_for is not None:
            self._enclosed_for.update(other._enclosed_for)
        else:
            assert other._enclosed_for is None

        other_was_top = other.parentStm is None
        if other_was_top:
            other._get_rtl_context().statements.remove(other)
            for s in other._inputs:
                s.endpoints.discard(other)
                s.endpoints.append(self)

            for s in other._outputs:
                s.drivers.discard(other)
                s.drivers.append(self)

    @internal
    def _try_reduce(self) -> Tuple[List["HdlStatement"], bool]:
        raise NotImplementedError("This menthod shoud be implemented"
                                  " on class of statement", self.__class__,
                                  self)

    def _is_enclosed(self) -> bool:
        """
        :return: True if every branch in statement is covered for all signals else False
        """
        return len(self._outputs) == len(self._enclosed_for)

    @internal
    def _is_mergable(self, other: "HdlStatement") -> bool:
        if self is other:
            raise ValueError("Can not merge statment with itself")
        else:
            raise NotImplementedError("This menthod shoud be implemented"
                                      " on class of statement", self.__class__,
                                      self)

    @internal
    @classmethod
    def _is_mergable_statement_list(cls, stmsA, stmsB):
        """
        Walk statements and compare if they can be merged into one statement list
        """
        if stmsA is None and stmsB is None:
            return True

        elif stmsA is None or stmsB is None:
            return False

        a_it = iter(stmsA)
        b_it = iter(stmsB)

        a = _get_stm_with_branches(a_it)
        b = _get_stm_with_branches(b_it)
        while a is not None or b is not None:
            if a is None or b is None or not a._is_mergable(b):
                return False

            a = _get_stm_with_branches(a_it)
            b = _get_stm_with_branches(b_it)

        # lists are empty
        return True

    @internal
    @staticmethod
    def _merge_statements(statements: List["HdlStatement"])\
            -> Tuple[List["HdlStatement"], int]:
        """
        Merge statements in list to remove duplicated if-then-else trees

        :return: tuple (list of merged statements, rank decrease due merging)
        :note: rank decrease is sum of ranks of reduced statements
        :attention: statement list has to me mergable
        """
        order = {}
        for i, stm in enumerate(statements):
            order[stm] = i

        new_statements = []
        rank_decrease = 0

        for rank, stms in groupedby(statements, lambda s: s.rank):
            if rank == 0:
                new_statements.extend(stms)
            else:
                if len(stms) == 1:
                    new_statements.extend(stms)
                    continue

                # try to merge statements if they are same condition tree
                for iA, stmA in enumerate(stms):
                    if stmA is None:
                        continue

                    for iB, stmB in enumerate(islice(stms, iA + 1, None)):
                        if stmB is None:
                            continue

                        if stmA._is_mergable(stmB):
                            rank_decrease += stmB.rank
                            stmA._merge_with_other_stm(stmB)
                            stms[iA + 1 + iB] = None
                            new_statements.append(stmA)
                        else:
                            new_statements.append(stmA)
                            new_statements.append(stmB)

        new_statements.sort(key=lambda stm: order[stm])
        return new_statements, rank_decrease

    @internal
    @staticmethod
    def _merge_statement_lists(stmsA: List["HdlStatement"], stmsB: List["HdlStatement"])\
            -> List["HdlStatement"]:
        """
        Merge two lists of statements into one

        :return: list of merged statements
        """
        if stmsA is None and stmsB is None:
            return None

        tmp = []

        a_it = iter(stmsA)
        b_it = iter(stmsB)

        a = None
        b = None
        a_empty = False
        b_empty = False

        while not a_empty and not b_empty:
            while not a_empty:
                a = next(a_it, None)
                if a is None:
                    a_empty = True
                    break
                elif a.rank == 0:
                    # simple statement does not require merging
                    tmp.append(a)
                    a = None
                else:
                    break

            while not b_empty:
                b = next(b_it, None)
                if b is None:
                    b_empty = True
                    break
                elif b.rank == 0:
                    # simple statement does not require merging
                    tmp.append(b)
                    b = None
                else:
                    break

            if a is not None or b is not None:
                a._merge_with_other_stm(b)
                tmp.append(a)
                a = None
                b = None

        return tmp

    @internal
    @staticmethod
    def _try_reduce_list(statements: List["HdlStatement"]):
        """
        Simplify statements in the list
        """
        io_change = False
        new_statements = []

        for stm in statements:
            reduced, _io_change = stm._try_reduce()
            new_statements.extend(reduced)
            io_change |= _io_change

        new_statements, rank_decrease = HdlStatement._merge_statements(
            new_statements)

        return new_statements, rank_decrease, io_change

    @internal
    def _on_parent_event_dependent(self):
        """
        After parrent statement become event dependent
        propagate event dependency flag to child statements
        """
        if not self._is_completly_event_dependent:
            self._is_completly_event_dependent = True
            for stm in self._iter_stms():
                stm._on_parent_event_dependent()

    @internal
    def _set_parent_stm(self, parentStm: "HdlStatement"):
        """
        Assign parent statement and propagate dependency flags if necessary
        """
        was_top = self.parentStm is None
        self.parentStm = parentStm
        if not self._now_is_event_dependent\
                and parentStm._now_is_event_dependent:
            self._on_parent_event_dependent()

        topStatement = parentStm
        while topStatement.parentStm is not None:
            topStatement = topStatement.parentStm

        parent_out_add = topStatement._outputs.append
        parent_in_add = topStatement._inputs.append

        if was_top:
            for inp in self._inputs:
                inp.endpoints.discard(self)
                inp.endpoints.append(topStatement)
                parent_in_add(inp)

            for outp in self._outputs:
                outp.drivers.discard(self)
                outp.drivers.append(topStatement)
                parent_out_add(outp)

            ctx = self._get_rtl_context()
            ctx.statements.discard(self)

        parentStm.rank += self.rank

    @internal
    def _register_stements(self, statements: List["HdlStatement"],
                           target: List["HdlStatement"]):
        """
        Append statements to this container under conditions specified
        by condSet
        """
        for stm in flatten(statements):
            assert stm.parentStm is None, stm
            stm._set_parent_stm(self)
            target.append(stm)

    def isSame(self, other: "HdlStatement") -> bool:
        """
        :return: True if other has same meaning as self
        """
        raise NotImplementedError("This menthod shoud be implemented"
                                  " on class of statement", self.__class__, self)

    @internal
    def _destroy(self):
        """
        Disconnect this statement from signals and delete it from RtlNetlist context

        :attention: signal endpoints/drivers will be altered
            that means they can not be used for iteration
        """
        ctx = self._get_rtl_context()
        for i in self._inputs:
            i.endpoints.discard(self)

        for o in self._outputs:
            o.drivers.remove(self)

        ctx.statements.remove(self)

@internal
def seqEvalCond(cond) -> bool:
    """
    Evaluate condition signal in sequential context
    """
    return bool(cond.staticEval().val)

def isSameHVal(a: Value, b: Value) -> bool:
    """
    :return: True if two Value instances are same
    :note: not just equal
    """
    return a is b or (isinstance(a, Value)
                      and isinstance(b, Value)
                      and a.val == b.val
                      and a.vldMask == b.vldMask)

def areSameHVals(a: Union[None, List[Value]],
                 b: Union[None, List[Value]]) -> bool:
    """
    :return: True if two vectors of Value instances are same
    :note: not just equal
    """
    if a is b:
        return True
    if a is None or b is None:
        return False
    if len(a) == len(b):
        for a_, b_ in zip(a, b):
            if not isSameHVal(a_, b_):
                return False
        return True
    else:
        return False

def isSameStatementList(stmListA: List[HdlStatement],
                        stmListB: List[HdlStatement]) -> bool:
    """
    :return: True if two lists of HdlStatement instances are same
    """
    if stmListA is stmListB:
        return True
    if stmListA is None or stmListB is None:
        return False

    for a, b in zip(stmListA, stmListB):
        if not a.isSame(b):
            return False

    return True

def statementsAreSame(statements: List[HdlStatement]) -> bool:
    """
    :return: True if all statements are same
    """
    iterator = iter(statements)
    try:
        first = next(iterator)
    except StopIteration:
        return True

    return all(first.isSame(rest) for rest in iterator)

@internal
def _get_stm_with_branches(stm_it):
    """
    :return: first statement with rank > 0 or None if iterator empty
    """
    last = None
    while last is None or last.rank == 0:
        try:
            last = next(stm_it)
        except StopIteration:
            last = None
            break

    return last


from hwt.hdl.value import Value
from hwt.hdl.types.defs import BOOL
from hwt.hdl.types.typeCast import toHVal
from hwt.doc_markers import internal

class StringVal(Value):
    """
    Value class for hdl String type
    """

    @classmethod
    def fromPy(cls, val, typeObj, vldMask=None):
        """
        :param val: python string or None
        :param typeObj: instance of String HdlType
        :param vldMask: if is None validity is resolved from val
            if is 0 value is invalidated
            if is 1 value has to be valid
        """
        assert isinstance(val, str) or val is None
        vld = 0 if val is None else 1
        if not vld:
            assert vldMask is None or vldMask == 0
            val = ""
        else:
            if vldMask == 0:
                val = ""
                vld = 0

        return cls(val, typeObj, vld)

    def toPy(self):
        if not self._isFullVld():
            raise ValueError("Value of %r is not fully defined" % self)
        return self.val

    @internal
    def _eq__val(self, other):
        eq = self.val == other.val
        vld = int(self.vldMask and other.vldMask)
        updateTime = max(self.updateTime, other.updateTime)

        return BOOL.getValueCls()(eq, BOOL, vld, updateTime)

    def _eq(self, other):
        other = toHVal(other)
        if isinstance(other, Value):
            return self._eq__val(other)
        else:
            raise NotImplementedError()