from functools import reduce
from math import ceil
from typing import Union

from hwt.hdl.typeShortcuts import vec
from hwt.hdl.types.bits import Bits
from hwt.hdl.types.structUtils import walkFlattenFields
from hwt.hdl.value import Value
from hwt.synthesizer.rtlLevel.rtlSignal import RtlSignal
from hwt.hdl.types.hdlType import HdlType
from hwt.doc_markers import internal

class BitWidthErr(Exception):
    """
    Wrong bit width of signal/value
    """

def fitTo_t(what: Union[RtlSignal, Value], where_t: HdlType,
            extend: bool=True, shrink: bool=True):
    """
    Slice signal "what" to fit in "where"
    or
    arithmetically (for signed by MSB / unsigned, vector with 0) extend
    "what" to same width as "where"

    little-endian impl.
    """

    whatWidth = what._dtype.bit_length()
    toWidth = where_t.bit_length()
    if toWidth == whatWidth:
        return what
    elif toWidth < whatWidth:
        # slice
        if not shrink:
            raise BitWidthErr()

        return what[toWidth:]
    else:
        if not extend:
            raise BitWidthErr()

        w = toWidth - whatWidth

        if what._dtype.signed:
            # signed extension
            msb = what[whatWidth - 1]
            ext = reduce(lambda a, b: a._concat(b), [msb for _ in range(w)])
        else:
            # 0 extend
            ext = vec(0, w)

        return ext._concat(what)

def fitTo(what: Union[RtlSignal, Value], where: Union[RtlSignal, Value],
          extend: bool=True, shrink: bool=True):
    return fitTo_t(what, where._dtype, extend, shrink)

class NotEnoughtBitsErr(Exception):
    """
    More bits is required for such an operation
    """

class BitWalker():
    """
    Walker which can walk chunks of bits on signals/values of all types

    :ivar sigOrVal: signal or value to iterate over
    :ivar fillup: flag that means that if there is not enought bits
        for last iterm fill it up with invalid bits (otherwise raise)
    """

    def __init__(self, sigOrVal: Union[RtlSignal, Value],
                 skipPadding: bool=True,
                 fillup: bool=False):
        """
        :param skipPadding: if true padding is skipped in dense types
        """
        self.it = walkFlattenFields(sigOrVal, skipPadding=skipPadding)
        self.fillup = fillup
        self.actuallyHave = 0
        self.actual = None
        self.actualOffset = 0

    @internal
    def _get(self, numberOfBits: int, doCollect: bool):
        """
        :param numberOfBits: number of bits to get from actual possition
        :param doCollect: if False output is not collected just iterator moves
            in structure
        """
        if not isinstance(numberOfBits, int):
            numberOfBits = int(numberOfBits)

        while self.actuallyHave < numberOfBits:
            # accumulate while not has enought
            try:
                f = next(self.it)
            except StopIteration:
                if self.fillup and self.actual is not None:
                    break
                else:
                    raise NotEnoughtBitsErr()

            thisFieldLen = f._dtype.bit_length()
            if self.actual is None:
                if not doCollect and thisFieldLen <= numberOfBits:
                    numberOfBits -= thisFieldLen
                else:
                    self.actual = f
                    self.actuallyHave = thisFieldLen
            else:
                if not doCollect and self.actuallyHave < numberOfBits:
                    self.actuallyHave = thisFieldLen
                    self.actual = f
                else:
                    self.actuallyHave += thisFieldLen
                    self.actual = f._concat(self.actual)

        # slice out from actual
        actual = self.actual
        actualOffset = self.actualOffset

        if self.actuallyHave < numberOfBits:
            assert self.fillup
            if doCollect:
                t = self.actual._dtype
                fillupW = numberOfBits - self.actuallyHave
                padding_t = Bits(fillupW, signed=t.signed, negated=t.negated)
                padding = padding_t.fromPy(None)
                actual = padding._concat(actual)
            self.actuallyHave = 0

        # update about what was taken
        self.actuallyHave -= numberOfBits
        self.actualOffset += numberOfBits
        if self.actuallyHave == 0:
            self.actual = None
            self.actualOffset = 0

        if doCollect:
            if numberOfBits == 1:
                return actual[actualOffset]
            else:
                return actual[(actualOffset + numberOfBits):actualOffset]

    def get(self, numberOfBits: int) -> Union[RtlSignal, Value]:
        """
        :param numberOfBits: number of bits to get from actual possition
        :return: chunk of bits of specified size (instance of Value or RtlSignal)
        """
        return self._get(numberOfBits, True)

    def skip(self, numberOfBits: int) -> None:
        """
        Move this iterator without care about item

        :param numberOfBits: number of bits to get from actual possition
        """
        self._get(numberOfBits, False)

    def assertIsOnEnd(self):
        """
        Assert there is nothing left in this iterator
        """
        try:
            next(self.it)
        except StopIteration:
            return

        raise AssertionError("BitWalker there stil were some items")

def iterBits(sigOrVal: Union[RtlSignal, Value], bitsInOne: int=1,
             skipPadding: bool=True, fillup: bool=False):
    """
    Iterate over bits in vector

    :param sigOrVal: signal or value to iterate over
    :param bitsInOne: number of bits in one part
    :param skipPadding: if true padding is skipped in dense types
    """
    bw = BitWalker(sigOrVal, skipPadding, fillup)
    for _ in range(ceil(sigOrVal._dtype.bit_length() / bitsInOne)):
        yield bw.get(bitsInOne)

    bw.assertIsOnEnd()


from collections import namedtuple

from hwt.synthesizer.param import evalParam
from hwt.doc_markers import internal

"""
Serializer mode specifies if hdl objects derived from parent unit
should be serialized to target HDL or not

use serialize* methods to specify serialization mode for unit class

.. code-block:: python

    @serializeExclude
    class MyUnit(Unit):
        # ...
        pass

"""

@internal
def freeze_dict(data):
    keys = sorted(data.keys())
    frozen_type = namedtuple(''.join(keys), keys)
    return frozen_type(**data)

@internal
def paramsToValTuple(unit):
    d = {}
    for p in unit._params:
        name = p.getName(unit)
        v = evalParam(p)
        d[name] = v
    return freeze_dict(d)

@internal
def prepareEntity(ent, name, templateUnit):
    ent.name = name
    ent.generics.sort(key=lambda x: x.name)
    ent.ports.sort(key=lambda x: x.name)
    # copy names
    if templateUnit is not None:
        # sort in python is stable, ports and generic were added in same order
        # templateUnit should have generic and ports sorted
        for gp, gch in zip(templateUnit._entity.generics, ent.generics):
            gch.name = gp.name
        for pp, pch in zip(templateUnit._entity.ports, ent.ports):
            pch.name = pp.name

def serializeExclude(cls):
    """
    Never serialize HDL objects from this class
    """
    cls._serializeDecision = staticmethod(_serializeExclude_eval)
    return cls

def serializeOnce(cls):
    """
    Serialize HDL objects only once per class
    """
    cls._serializeDecision = staticmethod(_serializeOnce_eval)
    return cls

def serializeParamsUniq(cls):
    """
    Decide to serialize only when parameters are unique
    """
    cls._serializeDecision = staticmethod(_serializeParamsUniq_eval)
    return cls

@internal
def _serializeExclude_eval(parentUnit, obj, isDeclaration, priv):
    """
    Always decide not to serialize obj

    :param priv: private data for this function first unit of this class
    :return: tuple (do serialize this object, next priv)
    """
    if isDeclaration:
        # prepare entity which will not be serialized
        prepareEntity(obj, parentUnit.__class__.__name__, priv)

    if priv is None:
        priv = parentUnit

    return False, priv

@internal
def _serializeOnce_eval(parentUnit, obj, isDeclaration, priv):
    """
    Decide to serialize only first obj of it's class

    :param priv: private data for this function
        (first object with class == obj.__class__)

    :return: tuple (do serialize this object, next priv)
        where priv is private data for this function
        (first object with class == obj.__class__)
    """
    clsName = parentUnit.__class__.__name__

    if isDeclaration:
        obj.name = clsName

    if priv is None:
        priv = parentUnit
    elif isDeclaration:
        # prepare entity which will not be serialized
        prepareEntity(obj, clsName, parentUnit)

    serialize = priv is parentUnit

    return serialize, priv

@internal
def _serializeParamsUniq_eval(parentUnit, obj, isDeclaration, priv):
    """
    Decide to serialize only objs with uniq parameters and class

    :param priv: private data for this function
        ({frozen_params: obj})

    :return: tuple (do serialize this object, next priv)
    """

    params = paramsToValTuple(parentUnit)

    if priv is None:
        priv = {}

    if isDeclaration:
        try:
            prevUnit = priv[params]
        except KeyError:
            priv[params] = parentUnit
            return True, priv

        prepareEntity(obj, prevUnit._entity.name, prevUnit)
        return False, priv

    return priv[params] is parentUnit, priv


from hwt.synthesizer.interfaceLevel.mainBases import InterfaceBase

class HObjList(list):
    """
    Regular list with some interface/unit methods delegated on items.

    Main purpose of this class it let :class:`hwt.synthesizer.PropDeclrCollector.PropDeclrCollector`
    know that this is not an regular python array and that items should be registered as HW objects.

    :note: :class:`hwt.synthesizer.PropDeclrCollector.PropDeclrCollector` is used by
        :class:`hwt.synthesizer.interface.Interface` and :class:`hwt.synthesizer.unit.Unit`
    """

    def __init__(self, *args, **kwargs):
        list.__init__(self, *args, **kwargs)
        self._name = None
        self._parent = None

    def _m(self):
        for item in self:
            item._m()
        return self

    def _getFullName(self):
        """get all name hierarchy separated by '.' """
        name = ""
        tmp = self
        while isinstance(tmp, (InterfaceBase, HObjList)):
            if hasattr(tmp, "_name"):
                n = tmp._name
            else:
                n = ''
            if name == '':
                name = n
            else:
                name = n + '.' + name
            if hasattr(tmp, "_parent"):
                tmp = tmp._parent
            else:
                tmp = None
        return name

    def _make_association(self, *args, **kwargs):
        """
        Delegate _make_association on items

        :note: doc in :func:`~hwt.synthesizer.interfaceLevel.propDeclCollector._make_association`
        """
        for o in self:
            o._make_association(*args, **kwargs)

    def _updateParamsFrom(self, *args, **kwargs):
        """
        :note: doc in :func:`~hwt.synthesizer.interfaceLevel.propDeclCollector._updateParamsFrom`
        """
        for o in self:
            o._updateParamsFrom(*args, **kwargs)

    def __call__(self, other):
        if not isinstance(other, list):
            raise TypeError(other)
        if len(self) != len(other):
            raise ValueError("Different number of interfaces in list",
                             len(self), len(other))

        statements = []
        for a, b in zip(self, other):
            statements += a(b)

        return statements


import importlib
import inspect
import os
import sys
from types import ModuleType
from typing import Optional

from hwt.doc_markers import internal
from hwt.hdl.constants import Time
from hwt.serializer.simModel.serializer import SimModelSerializer
from hwt.simulator.agentConnector import autoAddAgents
from hwt.simulator.hdlSimulator import HdlSimulator
from hwt.simulator.simModel import SimModel
from hwt.simulator.simSignal import SimSignal
from hwt.simulator.vcdHdlSimConfig import VcdHdlSimConfig
from hwt.synthesizer.dummyPlatform import DummyPlatform
from hwt.synthesizer.unit import Unit
from hwt.synthesizer.utils import toRtl

def simPrepare(unit: Unit, modelCls: Optional[SimModel]=None,
               targetPlatform=DummyPlatform(),
               dumpModelIn: str=None, onAfterToRtl=None):
    """
    Create simulation model and connect it with interfaces of original unit
    and decorate it with agents

    :param unit: interface level unit which you wont prepare for simulation
    :param modelCls: class of rtl simulation model to run simulation on,
        if is None rtl sim model will be generated from unit
    :param targetPlatform: target platform for this synthes
    :param dumpModelIn: folder to where put sim model files
        (if is None sim model will be constructed only in memory)
    :param onAfterToRtl: callback fn(unit, modelCls) which will be called
        after unit will be synthesised to rtl

    :return: tuple (fully loaded unit with connected sim model,
        connected simulation model,
        simulation processes of agents
        )
    """
    if modelCls is None:
        modelCls = toSimModel(
            unit, targetPlatform=targetPlatform, dumpModelIn=dumpModelIn)
    else:
        # to instantiate hierarchy of unit
        toSimModel(unit)

    if onAfterToRtl:
        onAfterToRtl(unit, modelCls)

    reconnectUnitSignalsToModel(unit, modelCls)
    model = modelCls()
    procs = autoAddAgents(unit)
    return unit, model, procs

def toSimModel(unit, targetPlatform=DummyPlatform(), dumpModelIn=None):
    """
    Create a simulation model for unit

    :param unit: interface level unit which you wont prepare for simulation
    :param targetPlatform: target platform for this synthes
    :param dumpModelIn: folder to where put sim model files
        (otherwise sim model will be constructed only in memory)
    """
    sim_code = toRtl(unit,
                     targetPlatform=targetPlatform,
                     saveTo=dumpModelIn,
                     serializer=SimModelSerializer)
    if dumpModelIn is not None:
        d = os.path.join(os.getcwd(), dumpModelIn)
        dInPath = d in sys.path
        if not dInPath:
            sys.path.insert(0, d)
        if unit._name in sys.modules:
            del sys.modules[unit._name]
        simModule = importlib.import_module(unit._name)

        if not dInPath:
            sys.path.remove(d)
    else:
        simModule = ModuleType('simModule')
        # python supports only ~100 opened brackets
        # it exceded it throws MemoryError: s_push: parser stack overflow
        exec(sim_code, simModule.__dict__)

    return simModule.__dict__[unit._name]

@internal
def reconnectUnitSignalsToModel(synthesisedUnitOrIntf, modelCls):
    """
    Reconnect model signals to unit to run simulation with simulation model
    but use original unit interfaces for communication

    :param synthesisedUnitOrIntf: interface where should be signals
        replaced from signals from modelCls
    :param modelCls: simulation model form where signals
        for synthesisedUnitOrIntf should be taken
    """
    obj = synthesisedUnitOrIntf
    subInterfaces = obj._interfaces

    if subInterfaces:
        for intf in subInterfaces:
            # proxies are destroyed on original interfaces and only proxies on
            # array items will remain
            reconnectUnitSignalsToModel(intf, modelCls)
    else:
        # reconnect signal from model
        s = synthesisedUnitOrIntf
        s._sigInside = getattr(modelCls, s._sigInside.name)

def simUnitVcd(simModel, stimulFunctions, outputFile=sys.stdout,
               until=100 * Time.ns):
    """
    Syntax sugar
    If outputFile is string try to open it as file

    :return: hdl simulator object
    """
    assert isinstance(simModel, SimModel), \
        "Class of SimModel is required (got %r)" % (simModel)
    if isinstance(outputFile, str):
        d = os.path.dirname(outputFile)
        if d:
            os.makedirs(d, exist_ok=True)
        with open(outputFile, 'w') as f:
            return _simUnitVcd(simModel, stimulFunctions,
                               f, until)
    else:
        return _simUnitVcd(simModel, stimulFunctions,
                           outputFile, until)

@internal
def _simUnitVcd(simModel, stimulFunctions, outputFile, until):
    """
    :param unit: interface level unit to simulate
    :param stimulFunctions: iterable of function(env)
        (simpy environment) which are driving the simulation
    :param outputFile: file where vcd will be dumped
    :param time: endtime of simulation, time units are defined in HdlSimulator
    :return: hdl simulator object
    """
    sim = HdlSimulator()

    # configure simulator to log in vcd
    sim.config = VcdHdlSimConfig(outputFile)

    # run simulation, stimul processes are register after initial
    # initialization
    sim.simUnit(simModel, until=until, extraProcesses=stimulFunctions)
    return sim

class CallbackLoop(object):
    def __init__(self, sig: SimSignal, fn, shouldBeEnabledFn):
        """
        :param sig: signal on which write callback should be used
        :attention: if condFn is None callback function is always executed

        :ivra fn: function/generator which is callback which should be executed
        :ivar isGenerator: flag if callback function is generator
            or normal function
        :ivar _callbackIndex: index of callback in write callbacks on sig,
            if is None callback was not registered yet
        :ivar shouldBeEnabledFn: function() -> bool, which returns True if this
            callback loop should be enabled
        """
        assert not isinstance(fn, CallbackLoop)
        self.fn = fn
        self.isGenerator = inspect.isgeneratorfunction(fn)
        self.shouldBeEnabledFn = shouldBeEnabledFn
        self._callbackIndex = None

        try:
            # if sig is interface we need internal signal
            self.sig = sig._sigInside
        except AttributeError:
            self.sig = sig

    def setEnable(self, en, sim):
        if self._callbackIndex is None:
            return

        if en:
            c = self.onWriteCallback
        else:
            c = None
        self.sig._writeCallbacks[self._callbackIndex] = c

    def onWriteCallback(self, sim):
        if self.isGenerator:
            yield from self.fn(sim)
        else:
            self.fn(sim)

    def __call__(self, sim):
        """
        Process for injecting of this callback loop into simulator
        """
        self._callbackIndex = self.sig.registerWriteCallback(
            self.onWriteCallback,
            self.shouldBeEnabledFn)
        return
        yield

class OnRisingCallbackLoop(CallbackLoop):

    def onWriteCallback(self, sim):
        if bool(sim.read(self.sig)._onRisingEdge__val(sim.now)):
            if self.isGenerator:
                yield from self.fn(sim)
            else:
                self.fn(sim)

class OnFallingCallbackLoop(CallbackLoop):

    def onWriteCallback(self, sim):
        if bool(sim.read(self.sig)._onFallingEdge__val(sim.now)):
            if self.isGenerator:
                yield from self.fn(sim)
            else:
                self.fn(sim)

def oscilate(sig, period=10 * Time.ns, initWait=0):
    """
    Oscillative simulation driver for your signal
    (usually used as clk generator)
    """
    def oscillateStimul(s):
        s.write(False, sig)
        halfPeriod = period / 2
        yield s.wait(initWait)

        while True:
            yield s.wait(halfPeriod)
            s.write(True, sig)
            yield s.wait(halfPeriod)
            s.write(False, sig)

    return oscillateStimul


from hwt.simulator.agentBase import AgentWitReset
from hwt.interfaces.agents.signal import DEFAULT_CLOCK
from hwt.hdl.constants import NOP
from collections import deque

def toGenerator(fn):
    def asGen(sim):
        fn(sim)
        return
        yield

    return asGen

class TristateAgent(AgentWitReset):
    def __init__(self, intf, allowNoReset=True):
        super(TristateAgent, self).__init__(intf, allowNoReset=allowNoReset)
        self.data = deque()
        # can be (1: pull-up, 0: pull-down, None: disconnected)
        self.pullMode = 1
        self.selfSynchronization = True
        self.collectData = True

    def monitor(self, sim):
        intf = self.intf
        # read in pre-clock-edge
        t = sim.read(intf.t)
        o = sim.read(intf.o)

        if self.pullMode is not None and sim.now > 0:
            assert t.vldMask, (
                sim.now, intf, "This mode, this value => ioblock would burn")
            assert o.vldMask, (
                sim.now, intf, "This mode, this value => ioblock would burn")
            assert self.pullMode != o.val, (
                sim.now, intf, "This mode, this value => ioblock would burn")

        if t.val:
            v = o
        else:
            v = self.pullMode

        sim.write(v, intf.i)
        if self.collectData and sim.now > 0 and self.notReset(sim):
            self.data.append(v)

    def getMonitors(self):
        return [self.onTWriteCallback__init]

    def onTWriteCallback(self, sim):
        self.monitor(sim)
        return
        yield

    def _write(self, val, sim):
        if val is NOP:
            # controll now has slave
            t = 0
            o = self.pullMode
        else:
            # controll now has this agent
            t = 1
            o = val

        intf = self.intf
        w = sim.write
        w(t, intf.t)
        w(o, intf.o)

    def onTWriteCallback__init(self, sim):
        """
        Process for injecting of this callback loop into simulator
        """
        yield from self.onTWriteCallback(sim)
        self.intf.t._sigInside.registerWriteCallback(
            self.onTWriteCallback,
            self.getEnable)
        self.intf.o._sigInside.registerWriteCallback(
            self.onTWriteCallback,
            self.getEnable)

    def driver(self, sim):
        while True:
            if self.data:
                b = self.data.popleft()
                if b == self.START:
                    return
                self.sda._write(b, sim)
            if self.selfSynchronization:
                yield sim.wait(DEFAULT_CLOCK)
            else:
                break

class TristateClkAgent(TristateAgent):
    def __init__(self, intf, onRisingCallback=None, onFallingCallback=None):
        super(TristateClkAgent, self).__init__(intf)
        self.onRisingCallback = onRisingCallback
        self.onFallingCallback = onFallingCallback
        self.period = DEFAULT_CLOCK

    def driver(self, sim):
        o = self.intf.o
        high = self.pullMode
        low = not self.pullMode
        halfPeriod = self.period / 2

        sim.write(low, o)
        sim.write(1, self.intf.t)
        if high:
            onHigh = self.onRisingCallback
            onLow = self.onFallingCallback
        else:
            onHigh = self.onFallingCallback
            onLow = self.onRisingCallback

        while True:
            yield sim.wait(halfPeriod)
            sim.write(high, o)

            if onHigh:
                sim.add_process(onHigh(sim))

            yield sim.wait(halfPeriod)
            sim.write(low, o)

            if onLow:
                sim.add_process(onLow(sim))

    def monitor(self, sim):
        intf = self.intf
        yield sim.waitOnCombUpdate()
        # read in pre-clock-edge
        t = sim.read(intf.t)
        o = sim.read(intf.o)

        if sim.now > 0 and self.pullMode is not None:
            assert t.vldMask, (
                sim.now, intf, "This mode, this value => ioblock would burn")
            assert o.vldMask, (
                sim.now, intf, "This mode, this value => ioblock would burn")
            assert self.pullMode != o.val, (
                sim.now, intf, "This mode, this value => ioblock would burn")

        if t.val:
            v = o
        else:
            v = self.pullMode

        last = sim.read(intf.i)
        sim.write(v, intf.i)

        if self.onRisingCallback and (not last.val or not last.vldMask) and v:
            sim.add_process(self.onRisingCallback(sim))

        if self.onFallingCallback and not v and (last.val or not last.vldMask):
            sim.add_process(self.onFallingCallback(sim))

    def getMonitors(self):
        return [self.onTWriteCallback__init]

    def onTWriteCallback(self, sim):
        yield from self.monitor(sim)
        return
        yield

    def onTWriteCallback__init(self, sim):
        """
        Process for injecting of this callback loop into simulator
        """
        yield from self.onTWriteCallback(sim)
        self.intf.t._sigInside.registerWriteCallback(
            self.onTWriteCallback,
            self.getEnable)
        self.intf.o._sigInside.registerWriteCallback(
            self.onTWriteCallback,
            self.getEnable)


from hwt.hdl.constants import DIRECTION
from hwt.hdl.sensitivityCtx import SensitivityCtx
from hwt.hdl.statements import HwtSyntaxError
from hwt.hdl.variables import SignalItem
from hwt.doc_markers import internal

class PortItem(SignalItem):
    """basic hdl entity port item"""

    def __init__(self, name, direction, dtype, unit):
        self.name = name
        self.unit = unit
        self.direction = direction
        self._dtype = dtype
        self.src = None
        self.dst = None

    @internal
    def connectSig(self, signal):
        """
        Connect to port item on subunit
        """
        if self.direction == DIRECTION.IN:
            if self.src is not None:
                raise HwtSyntaxError(
                    "Port %s is already associated with %r"
                    % (self.name, self.src))
            self.src = signal
            signal.endpoints.append(self)

        elif self.direction == DIRECTION.OUT:
            if self.dst is not None:
                raise HwtSyntaxError(
                    "Port %s is already associated with %r"
                    % (self.name, self.dst))
            self.dst = signal
            signal.drivers.append(self)

        else:
            raise NotImplementedError(self)

        signal.hidden = False
        signal.ctx.subUnits.add(self.unit)

    @internal
    def registerInternSig(self, signal):
        """
        Connect internal signal to port item,
        this connection is used by simulator and only output port items
        will be connected
        """
        if self.direction == DIRECTION.OUT:
            if self.src is not None:
                raise HwtSyntaxError(
                    "Port %s is already associated with %s"
                    % (self.name, str(self.src)))
            self.src = signal

        elif self.direction == DIRECTION.IN:
            if self.dst is not None:
                raise HwtSyntaxError(
                    "Port %s is already associated with %s"
                    % (self.name, str(self.dst)))
            self.dst = signal

        else:
            raise NotImplementedError(self.direction)

    @internal
    def connectInternSig(self):
        """
        connet signal from internal side of of this component to this port
        """
        d = self.direction
        if d == DIRECTION.OUT:
            self.src.endpoints.append(self)
        elif d == DIRECTION.IN or d == DIRECTION.INOUT:
            self.dst.drivers.append(self)
        else:
            raise NotImplementedError(d)

    @internal
    def getInternSig(self):
        """
        return signal inside unit which has this port
        """
        d = self.direction
        if d == DIRECTION.IN:
            return self.dst
        elif d == DIRECTION.OUT:
            return self.src
        else:
            raise NotImplementedError(d)

    @internal
    def _walk_sensitivity(self, casualSensitivity: set, seen: set, ctx: SensitivityCtx):
        yield from []

    def __repr__(self):
        return "<PortItem src:%r, dst:%r>" % (self.src, self.dst)


from heapq import heappush, heappop
from typing import Tuple, Generator, Callable

from hwt.doc_markers import internal
from hwt.hdl.value import Value
from hwt.pyUtils.uniqList import UniqList
from hwt.simulator.hdlSimConfig import HdlSimConfig
from hwt.simulator.simModel import mkUpdater, mkArrayUpdater
from hwt.simulator.simSignal import SimSignal
from hwt.simulator.utils import valueHasChanged
from hwt.synthesizer.unit import Unit

@internal
def isEvDependentOn(sig, process) -> bool:
    """
    Check if hdl process has event depenency on signal
    """
    if sig is None:
        return False

    return process in sig.simFallingSensProcs\
        or process in sig.simRisingSensProcs

@internal
class IoContainer():
    """
    Container for outputs of process
    """
    __slots__ = ["_all_signals"]

    def __init__(self, dstSignalsTuples):
        """
        :param dstSignalsTuples: tuples (name, signal)
        """
        self._all_signals = []
        for name, s in dstSignalsTuples:
            setattr(self, name, None)
            self._all_signals.append([name, s])

@internal
class Wait(BaseException):
    """
    Container for wait time of processes

    next activation of process will be now + time
    """

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

    def __repr__(self):
        return "<Wait %r>" % (self.time)

class StopSimumulation(BaseException):
    """
    Exception raised from handle in simulation to stop simulation
    """
    pass

@internal
class Event():
    """
    Simulation event

    Container of processes to wake
    """

    def __init__(self, sim):
        self.sim = sim
        self.process_to_wake = []

    def __iter__(self):
        procs = iter(self.process_to_wake)
        self.process_to_wake = None
        return procs

@internal
def raise_StopSimulation(sim):
    """
    Simulation process used to stop simulation
    """
    raise StopSimumulation()
    return
    yield

@internal
class CalendarItem():
    def __init__(self, time, priority, value):
        self.key = (time, priority)
        self.value = value

    def __lt__(self, other):
        return self.key < other.key

@internal
class SimCalendar():
    """
    Priority queue where key is time and priority
    """

    def __init__(self):
        self._q = []

    def push(self, time: float, priority: int, value):
        item = CalendarItem(time, priority, value)
        heappush(self._q, item)

    def pop(self) -> Tuple[float, int, object]:
        item = heappop(self._q)
        return (*item.key, item.value)

PRIORITY_URGENT = 0
PRIORITY_NORMAL = PRIORITY_URGENT + 1
# updating of combinational signals (wire updates)
PRIORITY_APPLY_COMB = PRIORITY_NORMAL + 1
# simulation agents waiting for combUpdate event
PRIORITY_AGENTS_UPDATE_DONE = PRIORITY_APPLY_COMB + 1
# updateing of event dependent signals (writing in gegisters,rams etc)
PRIORITY_APPLY_SEQ = PRIORITY_AGENTS_UPDATE_DONE + 1

class HdlSimulator():
    """
    Circuit simulator with support for external agents

    .. note:: *Signals without driver, constant driver, initial value*
        Every signal is initialized at start with its default value

    .. note:: *Communication between processes*
        For each output for every process there is an IoContainer instance
        which is container container of updates to signals.
        Every process has (generated) sensitivity-list.
        Process is reevaluated when there is a new value on any signal
        from sensitivity list.

    .. note: *Delta steps*
        Delta step is minimum quantum of changes in simulation, on the begining
        of delta step all reads are performed and on the end all writes
        are performed. Writes are causing revalution of HWprocesses
        which are schedueled into next delta step.
        Delta steps does not update time.
        When there is no process to reevaluate that means thereis nothing to do
        in delta step this delta step is considered as last in this time
        and time is shifted on begining of next event by simulator.

    .. note:: *Simulation inputs*
        HWprocess can not contain any blocking statement
        Simulation processes are written in python and can contain anything
        including blocking statements realized by yield sim.wait(time).
        (Using hdl as main simulator driver is not efficient.)

    .. note::
        HWprocesses have lower priority than simulation processes
        this allows simplify logic of agents.
        When simulation process is executed, HW part did not anything
        in this time, Simulation process can prepare anything for HW part
        (= can write) if simulation process need to read, it has to yield
        simulator.updateComplete event first, process then will be wakened
        after reaction of HW in this time:
        agents are greatly simplified, they just need to yield
        simulator.waitOnCombUpdate() before first read
        and then can not write in this time. (Agent can be realized by multiple
        simulation processes.)

    :ivar now: actual simulation time
    :ivar _combUpdateDonePlaned: flag, True if event for combinational
        update is planed, this event is triggered
        when there are not any combinational signal updates in this time
    :ivar _applyValPlaned: flag, True if there is planed event for write
        stashed signal updates to signals
    :ivar _runSeqProcessesPlaned: flag, True if there is planed event for
        running sequential (rising/falling event) dependent processes to reevaluate
    :ivar _valuesToApply: is container of values
        which should be applied in this delta step
    :ivar _combProcsToRun: list of hdl processes to run
    :ivar _seqProcsToRun: list of rising/falling event dependent processes
        which should be evaluated after all combinational changes are applied
    :ivar _outputContainers: dictionary {SimSignal:IoContainer} for each hdl process
    :ivar _events: heap of simulation events and processes
    """

    wait = Wait

    def __init__(self, config=None):
        super(HdlSimulator, self).__init__()
        if config is None:
            # default config
            config = HdlSimConfig()

        self.config = config
        self.now = 0.0

        self._combUpdateDonePlaned = False
        self._applyValPlaned = False
        self._runSeqProcessesPlaned = False

        # (signal, value) tuples which should be applied before
        # new round of processes
        #  will be executed
        self._valuesToApply = []
        self._seqProcsToRun = UniqList()
        self._combProcsToRun = UniqList()
        # container of outputs for every process
        self._outputContainers = {}
        self._events = SimCalendar()

    @internal
    def _add_process(self, proc, priority) -> None:
        """
        Schedule process on actual time with specified priority
        """
        self._events.push(self.now, priority, proc)

    def waitOnCombUpdate(self) -> Event:
        """
        Sim processes can wait on combUpdateDone by:
        yield sim.waitOnCombUpdate()

        Sim process is then woken up when all combinational updates
        are done in this delta step
        """
        if not self._combUpdateDonePlaned:
            return self._scheduleCombUpdateDoneEv()
        else:
            return self.combUpdateDoneEv

    @internal
    def _addHdlProcToRun(self, trigger: SimSignal, proc) -> None:
        """
        Add hdl process to execution queue

        :param trigger: instance of SimSignal
        :param proc: python generator function representing HDL process
        """
        # first process in time has to plan executing of apply values on the
        # end of this time
        if not self._applyValPlaned:
            # (apply on end of this time to minimalize process reevaluation)
            self._scheduleApplyValues()

        if isEvDependentOn(trigger, proc):
            if self.now == 0:
                return  # pass event dependent on startup
            self._seqProcsToRun.append(proc)
        else:
            self._combProcsToRun.append(proc)

    @internal
    def _initUnitSignals(self, unit: Unit) -> None:
        """
        * Inject default values to simulation

        * Instantiate IOs for every process
        """
        # set initial value to all signals and propagate it
        for s in unit._ctx.signals:
            if s.defVal.vldMask:
                v = s.defVal.clone()
                s.simUpdateVal(self, mkUpdater(v, False))

        for u in unit._units:
            self._initUnitSignals(u)

        for p in unit._processes:
            self._addHdlProcToRun(None, p)

        for p, outputs in unit._outputs.items():
            # name has to be explicit because it may be possible that signal
            # with has this name was replaced by signal from parent/child
            containerNames = list(map(lambda x: x[0], outputs))

            class SpecificIoContainer(IoContainer):
                __slots__ = containerNames

            self._outputContainers[p] = SpecificIoContainer(outputs)

    @internal
    def __deleteCombUpdateDoneEv(self) -> Generator[None, None, None]:
        """
        Callback called on combUpdateDoneEv finished
        """
        self._combUpdateDonePlaned = False
        return
        yield

    @internal
    def _scheduleCombUpdateDoneEv(self) -> Event:
        """
        Schedule combUpdateDoneEv event to let agents know that current
        delta step is ending and values from combinational logic are stable
        """
        assert not self._combUpdateDonePlaned, self.now
        cud = Event(self)
        cud.process_to_wake.append(self.__deleteCombUpdateDoneEv())
        self._add_process(cud, PRIORITY_AGENTS_UPDATE_DONE)
        self._combUpdateDonePlaned = True
        self.combUpdateDoneEv = cud
        return cud

    @internal
    def _scheduleApplyValues(self) -> None:
        """
        Apply stashed values to signals
        """
        assert not self._applyValPlaned, self.now
        self._add_process(self._applyValues(), PRIORITY_APPLY_COMB)
        self._applyValPlaned = True

        if self._runSeqProcessesPlaned:
            # if runSeqProcesses is already scheduled
            return

        assert not self._seqProcsToRun and not self._runSeqProcessesPlaned, self.now
        self._add_process(self._runSeqProcesses(), PRIORITY_APPLY_SEQ)
        self._runSeqProcessesPlaned = True

    @internal
    def _conflictResolveStrategy(self, newValue: set)\
            -> Tuple[Callable[[Value], bool], bool]:
        """
        This functions resolves write conflicts for signal

        :param actionSet: set of actions made by process
        """

        invalidate = False
        resLen = len(newValue)
        if resLen == 3:
            # update for item in array
            val, indexes, isEvDependent = newValue
            return (mkArrayUpdater(val, indexes, invalidate), isEvDependent)
        else:
            # update for simple signal
            val, isEvDependent = newValue
            return (mkUpdater(val, invalidate), isEvDependent)

    @internal
    def _runCombProcesses(self) -> None:
        """
        Delta step for combinational processes
        """
        for proc in self._combProcsToRun:
            cont = self._outputContainers[proc]
            proc(self, cont)
            for sigName, sig in cont._all_signals:
                newVal = getattr(cont, sigName)
                if newVal is not None:
                    res = self._conflictResolveStrategy(newVal)
                    # prepare update
                    updater, isEvDependent = res
                    self._valuesToApply.append(
                        (sig, updater, isEvDependent, proc))
                    setattr(cont, sigName, None)
                    # else value is latched

        self._combProcsToRun = UniqList()

    @internal
    def _runSeqProcesses(self) -> Generator[None, None, None]:
        """
        Delta step for event dependent processes
        """
        updates = []
        for proc in self._seqProcsToRun:
            try:
                outContainer = self._outputContainers[proc]
            except KeyError:
                # processes does not have to have outputs
                outContainer = None

            proc(self, outContainer)

            if outContainer is not None:
                updates.append(outContainer)

        self._seqProcsToRun = UniqList()
        self._runSeqProcessesPlaned = False

        for cont in updates:
            for sigName, sig in cont._all_signals:
                newVal = getattr(cont, sigName)
                if newVal is not None:
                    v = self._conflictResolveStrategy(newVal)
                    updater, _ = v
                    sig.simUpdateVal(self, updater)
                    setattr(cont, sigName, None)
        return
        yield

    @internal
    def _applyValues(self) -> Generator[None, None, None]:
        """
        Perform delta step by writing stacked values to signals
        """
        va = self._valuesToApply
        self._applyValPlaned = False

        # log if there are items to log
        lav = self.config.logApplyingValues
        if va and lav:
            lav(self, va)
        self._valuesToApply = []

        # apply values to signals, values can overwrite each other
        # but each signal should be driven by only one process and
        # it should resolve value collision
        addSp = self._seqProcsToRun.append
        for s, vUpdater, isEventDependent, comesFrom in va:
            if isEventDependent:
                # now=0 and this was process initialization or async reg
                addSp(comesFrom)
            else:
                # regular combinational process
                s.simUpdateVal(self, vUpdater)

        self._runCombProcesses()

        # processes triggered from simUpdateVal can add new values
        if self._valuesToApply and not self._applyValPlaned:
            self._scheduleApplyValues()

        return
        yield

    def read(self, sig) -> Value:
        """
        Read value from signal or interface
        """
        try:
            v = sig._val
        except AttributeError:
            v = sig._sigInside._val

        return v.clone()

    def write(self, val, sig: SimSignal)-> None:
        """
        Write value to signal or interface.
        """
        # get target RtlSignal
        try:
            simSensProcs = sig.simSensProcs
        except AttributeError:
            sig = sig._sigInside
            simSensProcs = sig.simSensProcs

        # type cast of input value
        t = sig._dtype

        if isinstance(val, Value):
            v = val.clone()
            v = v._auto_cast(t)
        else:
            v = t.fromPy(val)

        # can not update value in signal directly due singnal proxies
        sig.simUpdateVal(self, lambda curentV: (
            valueHasChanged(curentV, v), v))

        if not self._applyValPlaned:
            if not (simSensProcs or
                    sig.simRisingSensProcs or
                    sig.simFallingSensProcs):
                # signal value was changed but there are no sensitive processes
                # to it because of this _applyValues is never planed
                # and should be
                self._scheduleApplyValues()
            elif (sig._writeCallbacks or
                  sig._writeCallbacksToEn):
                # signal write did not caused any change on any other signal
                # but there are still simulation agets waiting on
                # updateComplete event
                self._scheduleApplyValues()

    def run(self, until: float) -> None:
        """
        Run simulation until specified time
        :note: can be used to run simulation again after it ends from time when it ends
        """
        assert until > self.now
        events = self._events
        schedule = events.push
        next_event = events.pop

        # add handle to stop simulation
        schedule(until, PRIORITY_URGENT, raise_StopSimulation(self))

        try:
            # for all events
            while True:
                nextTime, priority, process = next_event()
                self.now = nextTime
                # process is python generator or Event
                if isinstance(process, Event):
                    process = iter(process)

                # run process or activate processes dependent on Event
                while True:
                    try:
                        ev = next(process)
                    except StopIteration:
                        break

                    # if process requires waiting put it back in queue
                    if isinstance(ev, Wait):
                        # nextTime is self.now
                        schedule(nextTime + ev.time, priority, process)
                        break
                    elif isinstance(ev, Event):
                        # process going to wait for event
                        # if ev.process_to_wake is None event was already
                        # destroyed
                        ev.process_to_wake.append(process)
                        break
                    else:
                        # else this process spoted new process
                        # and it has to be put in queue
                        schedule(nextTime, priority, ev)

        except StopSimumulation:
            return

    def add_process(self, proc) -> None:
        """
        Add process to events with default priority on current time
        """
        self._events.push(self.now, PRIORITY_NORMAL, proc)

    def simUnit(self, synthesisedUnit: Unit, until: float, extraProcesses=[]):
        """
        Run simulation for Unit instance
        """
        beforeSim = self.config.beforeSim
        if beforeSim is not None:
            beforeSim(self, synthesisedUnit)

        add_proc = self.add_process
        for p in extraProcesses:
            add_proc(p(self))

        self._initUnitSignals(synthesisedUnit)
        self.run(until)


from hwt.hdl.types.typeCast import toHVal
from hwt.synthesizer.interfaceLevel.mainBases import InterfaceBase
from hwt.synthesizer.rtlLevel.mainBases import RtlSignalBase
from hwt.synthesizer.vectorUtils import fitTo
from hwt.doc_markers import internal

@internal
def _intfToSig(obj):
    if isinstance(obj, InterfaceBase):
        return obj._sig
    else:
        return obj

@internal
def _connect(src, dst, exclude, fit):
    if isinstance(src, InterfaceBase):
        if isinstance(dst, InterfaceBase):
            return dst._connectTo(src, exclude=exclude, fit=fit)
        src = src._sig

    assert not exclude, "this intf. is just a signal"
    if src is None:
        src = dst._dtype.fromPy(None)
    else:
        src = toHVal(src)

    if fit:
        src = fitTo(src, dst)

    src = src._auto_cast(dst._dtype)

    return dst(src)

@internal
def _mkOp(fn):
    """
    Function to create variadic operator function

    :param fn: function to perform binary operation
    """
    def op(*operands, key=None) -> RtlSignalBase:
        """
        :param operands: variadic parameter of input uperands
        :param key: optional function applied on every operand
            before processing
        """
        assert operands, operands
        top = None
        if key is not None:
            operands = map(key, operands)

        for s in operands:
            if top is None:
                top = s
            else:
                top = fn(top, s)
        return top

    return op


from hwt.hdl.statements import HdlStatement
from hwt.pyUtils.arrayQuery import arr_any
from hwt.serializer.generic.constants import SIGNAL_TYPE

def systemCTypeOfSig(signalItem):
    """
    Check if is register or wire
    """
    if signalItem._const or\
       arr_any(signalItem.drivers,
               lambda d: isinstance(d, HdlStatement)
               and d._now_is_event_dependent):

        return SIGNAL_TYPE.REG
    else:
        return SIGNAL_TYPE.WIRE


from copy import copy

from hwt.hdl.assignment import Assignment
from hwt.hdl.ifContainter import IfContainer
from hwt.hdl.operator import Operator
from hwt.hdl.operatorDefs import AllOps
from hwt.hdl.switchContainer import SwitchContainer
from hwt.hdl.types.bits import Bits
from hwt.hdl.types.sliceVal import SliceVal
from hwt.hdl.variables import SignalItem
from hwt.hdl.waitStm import WaitStm
from hwt.hdl.whileContainer import WhileContainer
from hwt.pyUtils.arrayQuery import arr_any
from hwt.serializer.exceptions import SerializerException
from hwt.serializer.generic.indent import getIndent
from hwt.serializer.vhdl.utils import VhdlVersion
from hwt.synthesizer.rtlLevel.mainBases import RtlSignalBase
from hwt.synthesizer.rtlLevel.signalUtils.exceptions import MultipleDriversErr,\
    NoDriverErr
from hwt.synthesizer.rtlLevel.rtlSignal import RtlSignal
from hwt.doc_markers import internal

@internal
class DoesNotContainsTernary(Exception):
    pass

@internal
def ternaryOpsToIf(statements):
    """Convert all ternary operators to IfContainers"""
    stms = []

    for st in statements:
        if isinstance(st, Assignment):
            try:
                if not isinstance(st.src, RtlSignalBase):
                    raise DoesNotContainsTernary()
                d = st.src.singleDriver()
                if not isinstance(d, Operator) or d.operator != AllOps.TERNARY:
                    raise DoesNotContainsTernary()
                else:
                    ops = d.operands
                    ifc = IfContainer(ops[0],
                                      [Assignment(ops[1], st.dst)],
                                      [Assignment(ops[2], st.dst)]
                                      )
                    stms.append(ifc)
                    continue

            except (MultipleDriversErr, DoesNotContainsTernary):
                pass
            except NoDriverErr:
                assert (hasattr(st.src, "_interface")
                        and st.src._interface is not None)\
                    or st.src.defVal.vldMask, st.src

        stms.append(st)
    return stms

class VhdlSerializer_statements():

    @classmethod
    def Assignment(cls, a, ctx):
        dst = a.dst
        assert isinstance(dst, SignalItem)

        def valAsHdl(v):
            return cls.Value(v, ctx)

        if dst.virtualOnly:
            symbol = ":="
        else:
            symbol = "<="

        if a.indexes is not None:
            for i in a.indexes:
                if isinstance(i, SliceVal):
                    i = i.clone()
                    i.val = (i.val[0], i.val[1])
                dst = dst[i]

        indent_str = getIndent(ctx.indent)
        dstStr = cls.asHdl(dst, ctx)
        src_t = a.src._dtype
        dst_t = dst._dtype

        if dst_t == src_t:
            return "%s%s %s %s" % (indent_str, dstStr, symbol, valAsHdl(a.src))
        else:
            if (isinstance(dst_t, Bits)
                    and isinstance(src_t, Bits)
                    and dst_t.bit_length() == src_t.bit_length() == 1):
                if dst_t.forceVector and not src_t.forceVector:
                    return "%s%s(0) %s %s" % (indent_str, dstStr, symbol,
                                              valAsHdl(a.src))
                if not dst_t.forceVector and src_t.forceVector:
                    return "%s%s %s %s(0)" % (indent_str, dstStr, symbol,
                                              valAsHdl(a.src))

            raise SerializerException(
                "%s%s %s %s  is not valid assignment\n"
                " because types are different (%r; %r) " %
                (indent_str, dstStr, symbol, valAsHdl(a.src),
                 dst._dtype, a.src._dtype))

    @classmethod
    def HWProcess(cls, proc, ctx):
        """
        Serialize HWProcess objects as VHDL

        :param scope: name scope to prevent name collisions
        """
        body = proc.statements
        extraVars = []
        extraVarsSerialized = []

        hasToBeVhdlProcess = arr_any(body,
                                     lambda x: isinstance(x,
                                                          (IfContainer,
                                                           SwitchContainer,
                                                           WhileContainer,
                                                           WaitStm)))

        sensitivityList = sorted(
            map(lambda s: cls.sensitivityListItem(s, ctx),
                proc.sensitivityList))

        if hasToBeVhdlProcess:
            childCtx = ctx.withIndent()
        else:
            childCtx = copy(ctx)

        def createTmpVarFn(suggestedName, dtype):
            s = RtlSignal(None, None, dtype, virtualOnly=True)
            s.name = ctx.scope.checkedName(suggestedName, s)
            s.hidden = False
            serializedS = cls.SignalItem(s, childCtx, declaration=True)
            extraVars.append(s)
            extraVarsSerialized.append(serializedS)
            return s

        childCtx.createTmpVarFn = createTmpVarFn

        statemets = [cls.asHdl(s, childCtx) for s in body]
        proc.name = ctx.scope.checkedName(proc.name, proc)

        extraVarsInit = []
        for s in extraVars:
            if isinstance(s.defVal, RtlSignalBase) or s.defVal.vldMask:
                a = Assignment(s.defVal, s, virtualOnly=True)
                extraVarsInit.append(cls.Assignment(a, childCtx))
            else:
                assert s.drivers, s
            for d in s.drivers:
                extraVarsInit.append(cls.asHdl(d, childCtx))

        _hasToBeVhdlProcess = hasToBeVhdlProcess
        hasToBeVhdlProcess = extraVars or hasToBeVhdlProcess

        if hasToBeVhdlProcess and not _hasToBeVhdlProcess:
            # add indent because we did not added it before because we did not
            # know t
            oneIndent = getIndent(1)
            statemets = list(map(lambda x: oneIndent + x, statemets))

        return cls.processTmpl.render(
            indent=getIndent(ctx.indent),
            name=proc.name,
            hasToBeVhdlProcess=hasToBeVhdlProcess,
            extraVars=extraVarsSerialized,
            sensitivityList=", ".join(sensitivityList),
            statements=extraVarsInit + statemets
        )

    @classmethod
    def IfContainer(cls, ifc, ctx):
        childCtx = ctx.withIndent()

        def asHdl(statements):
            return [cls.asHdl(s, childCtx) for s in statements]

        cond = cls.condAsHdl(ifc.cond, True, childCtx)
        elIfs = []
        if ifc.ifFalse is not None:
            ifFalse = ifc.ifFalse
        else:
            ifFalse = []
        ifTrue = ifc.ifTrue

        if cls.VHDL_VER < VhdlVersion.v2008:
            ifTrue = ternaryOpsToIf(ifTrue)
            ifFalse = ternaryOpsToIf(ifFalse)

        for c, statements in ifc.elIfs:
            if cls.VHDL_VER < VhdlVersion.v2008:
                statements = ternaryOpsToIf(statements)

            elIfs.append((cls.condAsHdl(c, True, childCtx), asHdl(statements)))

        return cls.ifTmpl.render(
            indent=getIndent(ctx.indent),
            cond=cond,
            ifTrue=asHdl(ifTrue),
            elIfs=elIfs,
            ifFalse=asHdl(ifFalse))

    @classmethod
    def SwitchContainer(cls, sw, ctx):
        childCtx = ctx.withIndent()

        def asHdl(statements):
            return [cls.asHdl(s, childCtx) for s in statements]

        switchOn = cls.condAsHdl(sw.switchOn, False, ctx)

        cases = []
        for key, statements in sw.cases:
            key = cls.asHdl(key, ctx)

            if cls.VHDL_VER < VhdlVersion.v2008:
                statements = ternaryOpsToIf(statements)

            cases.append((key, asHdl(statements)))

        if sw.default:
            cases.append((None, asHdl(sw.default)))

        return cls.switchTmpl.render(
            indent=getIndent(ctx.indent),
            switchOn=switchOn,
            cases=cases)

    @classmethod
    def WaitStm(cls, w, ctx):
        indent_str = getIndent(ctx.indent)

        if w.isTimeWait:
            return "%swait for %d ns" % (indent_str, w.waitForWhat)
        elif w.waitForWhat is None:
            return "%swait" % indent_str
        else:
            raise NotImplementedError()


from __future__ import division

from PIL import Image

def hash_distance(left_hash, right_hash):
    """Compute the hamming distance between two hashes"""
    if len(left_hash) != len(right_hash):
        raise ValueError('Hamming distance requires two strings of equal length')

    return sum(map(lambda x: 0 if x[0] == x[1] else 1, zip(left_hash, right_hash)))

def hashes_are_similar(left_hash, right_hash, tolerance=6):
    """
    Return True if the hamming distance between
    the image hashes are less than the given tolerance.
    """
    return hash_distance(left_hash, right_hash) <= tolerance

def average_hash(image_path, hash_size=8):
    """ Compute the average hash of the given image. """
    with open(image_path, 'rb') as f:
        # Open the image, resize it and convert it to black & white.
        image = Image.open(f).resize((hash_size, hash_size), Image.ANTIALIAS).convert('L')
        pixels = list(image.getdata())

    avg = sum(pixels) / len(pixels)

    # Compute the hash based on each pixels value compared to the average.
    bits = "".join(map(lambda pixel: '1' if pixel > avg else '0', pixels))
    hashformat = "0{hashlength}x".format(hashlength=hash_size ** 2 // 4)
    return int(bits, 2).__format__(hashformat)

def distance(image_path, other_image_path):
    """ Compute the hamming distance between two images"""
    image_hash = average_hash(image_path)
    other_image_hash = average_hash(other_image_path)

    return hash_distance(image_hash, other_image_hash)

def is_look_alike(image_path, other_image_path, tolerance=6):
    image_hash = average_hash(image_path)
    other_image_hash = average_hash(other_image_path)

    return hashes_are_similar(image_hash, other_image_hash, tolerance)


"""
Vizio SmartCast TV + Sound Bar support.

For more details about this platform, please refer to the documentation at
https://home-assistant.io/components/media_player.vizio/
"""
from datetime import timedelta
import logging
import voluptuous as vol
from homeassistant import util
from homeassistant.components.media_player import (
    MediaPlayerDevice,
    PLATFORM_SCHEMA
)
from homeassistant.components.media_player.const import (
    SUPPORT_NEXT_TRACK,
    SUPPORT_PAUSE,
    SUPPORT_PLAY,
    SUPPORT_PREVIOUS_TRACK,
    SUPPORT_SELECT_SOURCE,
    SUPPORT_TURN_OFF,
    SUPPORT_TURN_ON,
    SUPPORT_VOLUME_MUTE,
    SUPPORT_VOLUME_SET,
    SUPPORT_VOLUME_STEP
)
from homeassistant.const import (
    CONF_ACCESS_TOKEN,
    CONF_DEVICE_CLASS,
    CONF_HOST,
    CONF_NAME,
    STATE_OFF,
    STATE_ON
)
from homeassistant.helpers import config_validation as cv

REQUIREMENTS = ['pyvizio==0.0.7']

_LOGGER = logging.getLogger(__name__)

CONF_SUPPRESS_WARNING = 'suppress_warning'
CONF_VOLUME_STEP = 'volume_step'

DEFAULT_NAME = 'Vizio SmartCast'
DEFAULT_VOLUME_STEP = 1
DEFAULT_DEVICE_CLASS = 'tv'
DEVICE_ID = 'pyvizio'
DEVICE_NAME = 'Python Vizio'

ICON = 'mdi:television'

MIN_TIME_BETWEEN_FORCED_SCANS = timedelta(seconds=1)
MIN_TIME_BETWEEN_SCANS = timedelta(seconds=10)

COMMON_SUPPORTED_COMMANDS = (
    SUPPORT_PLAY
    | SUPPORT_PAUSE
    | SUPPORT_SELECT_SOURCE
    | SUPPORT_TURN_ON
    | SUPPORT_TURN_OFF
    | SUPPORT_VOLUME_MUTE
    | SUPPORT_VOLUME_SET
    | SUPPORT_VOLUME_STEP
)

SUPPORTED_COMMANDS = {
    'soundbar': COMMON_SUPPORTED_COMMANDS,
    'tv': (
        COMMON_SUPPORTED_COMMANDS
        | SUPPORT_VOLUME_MUTE
        | SUPPORT_VOLUME_STEP
    )
}

PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
    vol.Required(CONF_HOST): cv.string,
    vol.Optional(CONF_ACCESS_TOKEN): cv.string,
    vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string,
    vol.Optional(CONF_SUPPRESS_WARNING, default=False): cv.boolean,
    vol.Optional(CONF_DEVICE_CLASS, default=DEFAULT_DEVICE_CLASS):
        vol.All(cv.string, vol.Lower, vol.In(['tv', 'soundbar'])),
    vol.Optional(CONF_VOLUME_STEP, default=DEFAULT_VOLUME_STEP):
        vol.All(vol.Coerce(int), vol.Range(min=1, max=10)),
})

def setup_platform(hass, config, add_entities, discovery_info=None):
    """Set up the Vizio media player platform."""
    host = config.get(CONF_HOST)
    token = config.get(CONF_ACCESS_TOKEN)
    name = config.get(CONF_NAME)
    volume_step = config.get(CONF_VOLUME_STEP)
    device_type = config.get(CONF_DEVICE_CLASS)
    device = VizioDevice(host, token, name, volume_step, device_type)
    if device.validate_setup() is False:
        _LOGGER.error("Failed to set up Vizio platform, "
                      "please check if host and API key are correct")
        return
    elif (token is None or token == "") and device_type == "tv":
        _LOGGER.error("Failed to set up Vizio platform, "
                      "if device_class is 'tv' then an auth_token needs "
                      "to be provided, otherwise if device_class is "
                      "'soundbar' then add the right device_class to config")
        return

    if config.get(CONF_SUPPRESS_WARNING):
        from requests.packages import urllib3
        _LOGGER.warning("InsecureRequestWarning is disabled "
                        "because of Vizio platform configuration")
        urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)
    add_entities([device], True)

class VizioDevice(MediaPlayerDevice):
    """Media Player implementation which performs REST requests to device."""

    def __init__(self, host, token, name, volume_step, device_type):
        """Initialize Vizio device."""
        import pyvizio

        self._name = name
        self._state = None
        self._volume_level = None
        self._volume_step = volume_step
        self._current_input = None
        self._available_inputs = None
        self._device_type = device_type
        self._supported_commands = SUPPORTED_COMMANDS[device_type]
        self._device = pyvizio.Vizio(DEVICE_ID, host, DEFAULT_NAME, token,
                                     device_type)
        self._max_volume = float(self._device.get_max_volume())

    @util.Throttle(MIN_TIME_BETWEEN_SCANS, MIN_TIME_BETWEEN_FORCED_SCANS)
    def update(self):
        """Retrieve latest state of the device."""
        is_on = self._device.get_power_state()

        if is_on:
            self._state = STATE_ON

            volume = self._device.get_current_volume()
            if volume is not None:
                self._volume_level = float(volume) / self._max_volume

            input_ = self._device.get_current_input()
            if input_ is not None:
                self._current_input = input_.meta_name

            inputs = self._device.get_inputs()
            if inputs is not None:
                self._available_inputs = [input_.name for input_ in inputs]

        else:
            if is_on is None:
                self._state = None
            else:
                self._state = STATE_OFF

            self._volume_level = None
            self._current_input = None
            self._available_inputs = None

    @property
    def state(self):
        """Return the state of the device."""
        return self._state

    @property
    def name(self):
        """Return the name of the device."""
        return self._name

    @property
    def volume_level(self):
        """Return the volume level of the device."""
        return self._volume_level

    @property
    def source(self):
        """Return current input of the device."""
        return self._current_input

    @property
    def source_list(self):
        """Return list of available inputs of the device."""
        return self._available_inputs

    @property
    def supported_features(self):
        """Flag device features that are supported."""
        return self._supported_commands

    def turn_on(self):
        """Turn the device on."""
        self._device.pow_on()

    def turn_off(self):
        """Turn the device off."""
        self._device.pow_off()

    def mute_volume(self, mute):
        """Mute the volume."""
        if mute:
            self._device.mute_on()
        else:
            self._device.mute_off()

    def media_play(self):
        """Send play command."""
        self._device.play()

    def media_pause(self):
        """Send pause command."""
        self._device.pause()

    def media_previous_track(self):
        """Send previous channel command."""
        self._device.ch_down()

    def media_next_track(self):
        """Send next channel command."""
        self._device.ch_up()

    def select_source(self, source):
        """Select input source."""
        self._device.input_switch(source)

    def volume_up(self):
        """Increasing volume of the device."""
        self._volume_level += self._volume_step / self._max_volume
        self._device.vol_up(num=self._volume_step)

    def volume_down(self):
        """Decreasing volume of the device."""
        self._volume_level -= self._volume_step / self._max_volume
        self._device.vol_down(num=self._volume_step)

    def validate_setup(self):
        """Validate if host is available and key is correct."""
        return self._device.get_current_volume() is not None

    def set_volume_level(self, volume):
        """Set volume level."""
        if self._volume_level is not None:
            if volume > self._volume_level:
                num = int(self._max_volume * (volume - self._volume_level))
                self._volume_level = volume
                self._device.vol_up(num=num)
            elif volume < self._volume_level:
                num = int(self._max_volume * (self._volume_level - volume))
                self._volume_level = volume
                self._device.vol_down(num=num)