'use strict'

const MATERIALS = ['Aggregate', 'Cement', 'Admixture', 'Water']

const DELTA = '\u0394'
const DEG_C = '\u2103'
const DEG = '\u00B0' // without celsius

// this is for lift/liftedmixer
// TODO: duplication of constants from control - unite!
const POS_DOWN = 0
const POS_WAITING = 1
const POS_EMPTY_SEMI = 2
const POS_EMPTY = 3
const POS_TOTAL = 4

const rce = React.createElement

const urlParams = new URLSearchParams(window.location.search)
const MANAGER_QUEUE_ENABLED = (urlParams.get('queue') || (window.location.pathname.indexOf('/control') !== -1))
const DISPATCH_TITLE_ENABLED = urlParams.get('dispatchTitle')
const CTRL_ENABLED = 1
let gloDebugReact = urlParams.get('debug')
const gloDebugStuff = {}
let gloCountOfMessages = 0

function incDebugRenderCount (k, props) {
  console.log('render ' + k, props)
  _.updateWith(gloDebugStuff, ['render_count', k], x => x ? x + 1 : 1)
}

// https://gist.github.com/Yimiprod/7ee176597fef230d1451
function difference (object, base) {
  return _.transform(object, (result, value, key) => {
    if (!_.isEqual(value, base[key])) {
      result[key] = _.isObject(value) && _.isObject(base[key]) ? difference(value, base[key]) : value
    }
  })
}

// https://stackoverflow.com/questions/8362952/javascript-output-current-datetime-in-yyyy-mm-dd-hhmsec-format
// TODO: ugly but working - find something more efficient
function dateTimeString () {
  const now = new Date()
  const offsetMs = now.getTimezoneOffset() * 60 * 1000
  const dateLocal = new Date(now.getTime() - offsetMs)
  return dateLocal.toISOString().slice(0, 19).replace('T', ' ')
}

// TODO: the whole audio/beep stuff is cut-n-pasted from pymanager - unite!
// taken from https://stackoverflow.com/questions/879152/how-do-i-make-javascript-beep
// if you have another AudioContext class use that one, as some browsers have a limit
const audioCtx = new (window.AudioContext || window.webkitAudioContext || window.audioContext)()

// All arguments are optional:

// duration of the tone in milliseconds. Default is 500
// frequency of the tone in hertz. default is 440
// volume of the tone. Default is 1, off is 0.
// type of tone. Possible values are sine, square, sawtooth, triangle, and custom. Default is sine.
// callback to use on end of tone
function beep (duration, frequency, volume, type, callback) {
  const oscillator = audioCtx.createOscillator()
  const gainNode = audioCtx.createGain()

  oscillator.connect(gainNode)
  gainNode.connect(audioCtx.destination)

  if (volume) { gainNode.gain.value = volume }
  if (frequency) { oscillator.frequency.value = frequency }
  if (type) { oscillator.type = type }
  if (callback) { oscillator.onended = callback }

  oscillator.start(audioCtx.currentTime)
  oscillator.stop(audioCtx.currentTime + ((duration || 500) / 1000))
};

function soundAlert () {
  beep(70, 500)
  console.log('play tone')
}
// TODO: end of cut-n-pasted audio/beep shit

function callApi (msg) {
  fetch('./' + msg.join('/')) // TODO: what about the timeout?
}

const by_key_to_by_lang = d => {
  const ret = {}
  for (const [k, v] of Object.entries(d)) {
    for (const [kk, vv] of Object.entries(v)) {
      if (!(kk in ret)) {
        ret[kk] = {}
      }
      ret[kk][k] = vv
    }
  }
  return ret
}

// TODO: it would be better to use set type for recursion protection but it only provides in-place operations afaik
const _fmt = (s, vals, recurProtection = []) => {
  if (typeof s !== "string") {
    return s
  }
  return s.replace(/{(.*?)}/g, (_, k) => ((recurProtection.indexOf(k) === -1) && vals[k] !== undefined && vals[k] !== null ? _fmt(vals[k], vals, recurProtection.concat(k)) : '!' + k + '!'))
}

// hyperscript helper - stupid version
// TODO: add support for id (div.class#id)
// TODO: add support for component (non-string) as first arg
const h = (tag, props, children) => {
  if (tag.indexOf('.') < 0) { // TODO: does this really make it faster?
    return rce(tag || 'div', props, React.Children.toArray(children))
  }
  const parts = tag.split('.')
  const tag_ = parts[0]
  const classes = parts.slice(1)
  const props_ = props || {}
  if (classes) {
    props_.className = (props_.className ? props_.className + ' ' : '') + classes.join(' ')
  }
  return rce(tag_ || 'div', props_, React.Children.toArray(children))
}

const reasonToClassSpace = s => s === 'manual' ? ' reasonManual' : (s === 'automat' ? ' reasonAutomat' : '')

const reasonToClassDot = s => s === 'manual' ? '.reasonManual' : (s === 'automat' ? '.reasonAutomat' : '')

function myShouldComponentUpdate (name, props, nextProps, state, nextState) {
  const x = !_.isEqual(props, nextProps) // deep compare
  if (gloDebugReact && x) {
    console.log('shouldComponentUpdate', name, difference(nextProps, props))
  }
  return x
}

class Main extends React.Component {
  shouldComponentUpdate (nextProps, nextState) {
    return myShouldComponentUpdate('Main', this.props, nextProps, this.state, nextState)
  }

  render () { return renderMain(this.props) }
}

class Queue extends React.Component {
  shouldComponentUpdate (nextProps, nextState) {
    return myShouldComponentUpdate('Queue', this.props, nextProps, this.state, nextState)
  }

  componentDidMount () {
    const props = this.props
    const el = window.document.getElementById('managerQueueIframe')
    el.onload = () => {
      console.debug('queue.onload')
      const el2 = el.contentWindow.document.getElementById('orderEventContent')
      if (el2) {
        console.debug('queue load success')
        el2.addEventListener('input', e => {
          const orderId = e.target.value
          props.managerOpenOrderDialog(orderId)
        })
        el.contentWindow.document.body.addEventListener('keydown', cloneAndDispatchEvent)
        el.contentWindow.document.body.addEventListener('keyup', cloneAndDispatchEvent)
      } else {
        console.debug('queue load failure, retrying in 1 second')
        setTimeout(() => {
          el.src = '../manager/400?lang=' + props.lang_cur
        }, 1000)
      }
    }
  }

  render () {
    const props = this.props
    gloDebugReact && incDebugRenderCount('Queue', props)
    return h('iframe.managerQueueIframe', {
      id: 'managerQueueIframe',
      src: '../manager/400?lang=' + props.lang_cur,
      width: '100%',
      height: '100%'
    })
  }
}

class ContinuousConcreteSettings extends React.Component {
  shouldComponentUpdate (nextProps, nextState) {
    return myShouldComponentUpdate('NodeContinuousConcreteSettings', this.props, nextProps, this.state, nextState)
  }

  render () { return renderNodeContinuousConcreteSettings(this.props) }
}

class OrderOnHold extends React.Component {
  shouldComponentUpdate (nextProps, nextState) {
    return myShouldComponentUpdate('NodeOrderOnHold', this.props, nextProps, this.state, nextState)
  }

  render () { return renderNodeOrderOnHold(this.props) }
}

class NodeBinsAndFlowmeters extends React.Component {
  shouldComponentUpdate (nextProps, nextState) {
    return myShouldComponentUpdate('NodeBinsAndFlowmeters', this.props, nextProps, this.state, nextState)
  }

  render () { return renderNodeBinsAndFlowmeters(this.props) }
}

class NodeBins extends React.Component {
  shouldComponentUpdate (nextProps, nextState) {
    return myShouldComponentUpdate('NodeBins', this.props, nextProps, this.state, nextState)
  }

  render () { return renderNodeBins(this.props) }
}

class NodeFlowmeters extends React.Component {
  shouldComponentUpdate (nextProps, nextState) {
    return myShouldComponentUpdate('NodeFlowmeters', this.props, nextProps, this.state, nextState)
  }

  render () { return renderNodeFlowmeters(this.props) }
}

class NodeConveyers extends React.Component {
  shouldComponentUpdate (nextProps, nextState) {
    return myShouldComponentUpdate('NodeConveyers', this.props, nextProps, this.state, nextState)
  }

  render () { return renderNodeConveyers(this.props) }
}

class NodeSilos extends React.Component {
  shouldComponentUpdate (nextProps, nextState) {
    return myShouldComponentUpdate('NodeSilos', this.props, nextProps, this.state, nextState)
  }

  render () { return renderNodeSilos(this.props) }
}

class NodeLifts extends React.Component {
  shouldComponentUpdate (nextProps, nextState) {
    return myShouldComponentUpdate('NodeLifts', this.props, nextProps, this.state, nextState)
  }

  render () { return renderNodeLifts(this.props) }
}

class NodeFunctionsKey extends React.Component {
  shouldComponentUpdate (nextProps, nextState) {
    return myShouldComponentUpdate('NodeFunctionsKey', this.props, nextProps, this.state, nextState)
  }

  render () { return renderNodeFunctionsKey(this.props) }
}

class NodeMessage extends React.Component {
  shouldComponentUpdate (nextProps, nextState) {
    return myShouldComponentUpdate('NodeMessage', this.props, nextProps, this.state, nextState)
  }

  render () { return renderNodeMessage(this.props) }
}

class NodeMixerBay extends React.Component {
  shouldComponentUpdate (nextProps, nextState) {
    return myShouldComponentUpdate('NodeMixerBay', this.props, nextProps, this.state, nextState)
  }

  render () { return renderNodeMixerBay(this.props) }
}

class NodeMixers extends React.Component {
  shouldComponentUpdate (nextProps, nextState) {
    return myShouldComponentUpdate('NodeMixers', this.props, nextProps, this.state, nextState)
  }

  render () { return renderNodeMixers(this.props) }
}

class NodeMixerLift extends React.Component {
  shouldComponentUpdate (nextProps, nextState) {
    return myShouldComponentUpdate('NodeMixerLift', this.props, nextProps, this.state, nextState)
  }

  render () { return renderNodeMixerLift(this.props) }
}

class NodeFunnels extends React.Component {
  shouldComponentUpdate (nextProps, nextState) {
    return myShouldComponentUpdate('NodeFunnels', this.props, nextProps, this.state, nextState)
  }

  render () { return renderNodeFunnels(this.props) }
}

class NodeChutes extends React.Component {
  shouldComponentUpdate (nextProps, nextState) {
    return myShouldComponentUpdate('NodeChutes', this.props, nextProps, this.state, nextState)
  }

  render () { return renderNodeChutes(this.props) }
}

class NodeFreePaths extends React.Component {
  shouldComponentUpdate (nextProps, nextState) {
    return myShouldComponentUpdate('NodeFreePaths', this.props, nextProps, this.state, nextState)
  }

  render () { return renderNodeFreePaths(this.props) }
}

class NodeCart extends React.Component {
  shouldComponentUpdate (nextProps, nextState) {
    return myShouldComponentUpdate('NodeCart', this.props, nextProps, this.state, nextState)
  }

  render () { return renderNodeCart(this.props) }
}

class NodeLeftMenuCompressor extends React.Component {
  shouldComponentUpdate (nextProps, nextState) {
    return myShouldComponentUpdate('NodeLeftMenuCompressor', this.props, nextProps, this.state, nextState)
  }

  render () { return renderNodeLeftMenuCompressor(this.props) }
}

class NodeLeftMenuScalesTare extends React.Component {
  shouldComponentUpdate (nextProps, nextState) {
    return myShouldComponentUpdate('NodeLeftMenuScalesTare', this.props, nextProps, this.state, nextState)
  }

  render () { return renderNodeLeftMenuScalesTare(this.props) }
}

class NodeLeftMenuFlowmetersReset extends React.Component {
  shouldComponentUpdate (nextProps, nextState) {
    return myShouldComponentUpdate('NodeLeftMenuFlowmetersReset', this.props, nextProps, this.state, nextState)
  }

  render () { return renderNodeLeftMenuFlowmetersReset(this.props) }
}

class NodeLeftMenuMixersWashing extends React.Component {
  shouldComponentUpdate (nextProps, nextState) {
    return myShouldComponentUpdate('NodeLeftMenuMixersWashing', this.props, nextProps, this.state, nextState)
  }

  render () { return renderNodeLeftMenuMixersWashing(this.props) }
}

/* class NodeModal extends React.Component {
  shouldComponentUpdate(nextProps, nextState) {
    return myShouldComponentUpdate("NodeModal", this.props, nextProps, this.state, nextState);
  }
  render() {return renderNodeModal(this.props)}
} */

const precalcStuff = (pnodes, xxx_aggregatesToNextLineCutoff) => {
  const ret = {
    horizontalConveyers: [],
    inclinedConveyers: [],
    lifts: [],
    mixers: [],
    mixerIsLifted: pnodes.Mixer1._type === 'LiftedMixer',
    continuousPlant: pnodes.control._is_continuous_plant,
    aboveMixer: [],
    underMixer: [],  // TODO: rename to belowMixer?
    chutes: [],
    freePaths: [],
    carts: [],
    liftIsAggregateScale: false,
    compressors: [],
    scalesTare: [],
    flowmetersReset: [],
    mixersWashing: []
  }
  const silos = {}
  const binsAndFlowmeters = {}
  const scales = {}
  let countOfAllSilos = 0
  let countOfSilosWithoutAggregate = 0
  let countOfAggregateSilos = 0
  const ks = Object.keys(pnodes).sort()
  for (const material of MATERIALS) {
    silos[material] = {}
    binsAndFlowmeters[material] = {}
    scales[material] = {}
    for (const k of ks) {
      if (k.indexOf(material + 'Silo') !== -1) {
        silos[material][k] = {}
        silos[material][k].sink = pnodes[k].sink
        countOfAllSilos += 1
        if (material != 'Aggregate') {
          countOfSilosWithoutAggregate += 1
        } else {
          countOfAggregateSilos += 1
        }
      }
      if (k.indexOf("Reservoir") !== -1) {
        continue
      }
      if ((k.indexOf(material + 'Bin') !== -1) || (k.indexOf(material + 'Flowmeter') !== -1) || (material === 'Aggregate' && (k.indexOf('Lift1') !== -1 || k.indexOf('Conveyer1') !== -1 || k.indexOf('Conveyer2') !== -1 || k.indexOf('Conveyer3') !== -1))) {
        if (k.indexOf('Lift1') !== -1 || k.indexOf('Conveyer1') !== -1) {
          if (k.indexOf('Lift1') !== -1) {
            if (_.get(pnodes, ['Lift1', 'scale']) == null) {
              continue
            }
          }
        }
        binsAndFlowmeters[material][k] = {}
        binsAndFlowmeters[material][k].silos = []
        binsAndFlowmeters[material][k].flowmeters = []
      }
      if (k.indexOf(material + 'Scale') !== -1) {
        scales[material][k] = {}
        scales[material][k].bins = []
      }
    }
  }

  for (const k in silos) {
    console.log(k + ' - Silos: ' + Object.keys(silos[k]).length)
    for (const key in silos[k]) {
      const sink = silos[k][key].sink
      console.log(key + ' sink: ' + sink)
      binsAndFlowmeters[k][sink].silos.push(key)
    }
  }

  for (const k in binsAndFlowmeters) {
    for (const k_ in binsAndFlowmeters[k]) {
      // TODO: this is waaay too verbose - find more elegant solution (sort_by?)
      binsAndFlowmeters[k][k_].silos.sort((k1, k2) => {
        const v1 = pnodes[k1]
        const v2 = pnodes[k2]
        const i1 = v1._visual_index != null ? v1._visual_index : v1._index
        const i2 = v2._visual_index != null ? v2._visual_index : v2._index
        console.debug('SORT', k1, k2, v1, v2)
        if (i1 < i2) {
          return -1
        } else if (i1 > i2) {
          return 1
        } else {
          return 0
        }
      })
    }
  }

  for (const k in binsAndFlowmeters) {
    const count = Object.keys(binsAndFlowmeters[k]).length
    console.log(k + ' - BinsAndFlowmetersCount: ' + count)
    for (const key in binsAndFlowmeters[k]) {
      console.log(k + ' sink: ' + pnodes[key].sink)
      const sink = pnodes[key].sink
      if (_.get(binsAndFlowmeters, [k, sink])) {
        binsAndFlowmeters[k][sink].flowmeters.push(key)
      }
      console.log(JSON.stringify(binsAndFlowmeters[k][sink]))
      console.log(k + ' - ' + [key] + ' - ' + binsAndFlowmeters[k][key].silos)
      for (const key2 in scales[k]) {
        if (pnodes[key].scale === key2) {
          scales[k][key2].bins.push(key)
          console.log(k + ' - ' + key2 + ' bins: ' + scales[k][key2].bins)
        }
      }
    }
  }

  for (const k of ks) {
    const v = pnodes[k]
    if ((v._type === 'Compressor') && v.can_start_stop) {
      ret.compressors.push(k)
    }
    if ((v._type === 'Scale') && v.can_tare) {
      ret.scalesTare.push(k)
    }
    if ((v._type === 'Flowmeter') && v.can_reset) {
      ret.flowmetersReset.push(k)
    }
    if (v._type === 'Mixer') {
      ret.mixers.push(k)
      if (v.has_washing) {
        ret.mixersWashing.push(k)
      }
    }
    if (v._type === 'LiftedMixer') {
      ret.mixers.push(k)
    }
    if (v._type === 'Lift') {
      ret.lifts.push(k)
      if (v.scale === 'AggregateScale1') {
        ret.liftIsAggregateScale = true
      }
    }
    if (v._type === 'Chute') {
      ret.chutes.push(k)
    }
    //if (v._type === 'Reservoir') {
    if (k.indexOf('Reservoir') !== -1) {  // TODO: don't string-match on key
      ret.aboveMixer.push(k)
    }
    if (v._type === 'Funnel') {
      ret.underMixer.push(k)
    }
    if (v._type === 'Cart') {
      ret.carts.push(k)
    }
    if ((v._type === 'Conveyer') || (v._type === 'Screw')) {
      if (('Mixer1' in pnodes && pnodes.Mixer1.sink === k) || ('Chute1' in pnodes && pnodes.Chute1.sink === k)) {
        ret.underMixer.push(k)
      } else if (v._type === 'Conveyer') {
        if (v._is_inclined) {
          ret.inclinedConveyers.push(k)
        } else {
          ret.horizontalConveyers.push(k)
        }
      }
    }
    // TODO: this somehow mixes Other and freepath thing - solve better!
    if (k === 'Other') {
      if (_.get(pnodes, [k, '_freepath']) != null) {
        ret.freePaths.push('Other')
      }
    }
  }

  ret.aggregatesOnBottom = countOfAllSilos >= xxx_aggregatesToNextLineCutoff
  ret.countOfSilosWithoutAggregate = countOfSilosWithoutAggregate
  ret.countOfAggregateSilos = countOfAggregateSilos
  ret.binsAndFlowmeters = binsAndFlowmeters
  ret.scales = scales

  console.log('precalc: ', ret)

  return ret
}

const renderMain = props => {
  gloDebugReact && incDebugRenderCount('Main', props)

  let subTree = null
  let menuTree = null
  if (props.nodes) {
    const wideModeCoef = props.xxx_wide_mode
      ? 0.74 // original was 0.75 but mj83 (zapa kacerov merko) needs 0.74
      : (props.precalc.countOfSilosWithoutAggregate == 16  // ugly hack for mj204
        ? 0.945  // ugly hack for mj204 (they have 16 non-aggregate silos and 16/9 display)
        : 1
      )

    const nodes = {}
    for (const material of MATERIALS) {
      nodes[material + 'Silos'] = []
      nodes[material + 'Bins'] = []
      for (const k in props.precalc.binsAndFlowmeters[material]) {
        // TODO: hacky shit
        //if (k == 'count') {
        //  continue;
        //}
        const v = props.nodes[k]
        const cementScrew = v.screw
        const cementScrewState = _.get(props.nodes, [cementScrew, 'is_running'])
        const cementScrewScaleValueStr = _.get(props.nodes, [cementScrew, 'scale_val_str'])
        const cementScrewFlowStr = _.get(props.nodes, [cementScrew, 'flow_str'])
        const cementScrewSetOpenReason = _.get(props.nodes, [cementScrew, 'set_open_reason'])
        nodes[material + 'Bins'].push(rce(NodeBinsAndFlowmeters, {
          key: k,
          data: v,
          precalc: props.precalc,
          xxx_keys: props.xxx_keys,
          xxx_cementScrewState: cementScrewState,
          xxx_cementScrewScaleValueStr: cementScrewScaleValueStr,
          xxx_cementScrewFlowStr: cementScrewFlowStr,
          xxx_cementScrewSetOpenReason: cementScrewSetOpenReason,
          xxx_state_frozen: props.xxx_state_frozen,
          xxx_wide_mode_coef: wideModeCoef,
          fmt: props.fmt
        }))
        for (const k2 of props.precalc.binsAndFlowmeters[material][k].silos) {
          const v = props.nodes[k2]
          nodes[material + 'Silos'].push(rce(NodeSilos, {
            key: k2,
            data: v,
            xxx_keys: props.xxx_keys,
            xxx_state_frozen: props.xxx_state_frozen,
            xxx_wide_mode_coef: wideModeCoef,
            fmt: props.fmt
          }))
        }
      }
    }

    const nodesMixerLift = []
    for (const k of props.precalc.mixers) {
      const v = props.nodes[k]
      nodes[k] = []
      nodes[k].push(rce(NodeMixers, {
        key: k,
        data: v,
        xxx_state_frozen: props.xxx_state_frozen,
        xxx_keys: props.xxx_keys,
        xxx_wide_mode_coef: wideModeCoef,
        fmt: props.fmt
      }))
      if (props.precalc.mixerIsLifted) {
        nodesMixerLift.push(rce(NodeMixerLift, {
          key: k + 'Lift',
          data: v,
          xxx_state_frozen: props.xxx_state_frozen,
          xxx_keys: props.xxx_keys,
          xxx_wide_mode_coef: wideModeCoef,
          fmt: props.fmt
        }))
      }
    }

    const nodesAboveMixer = []
    for (const k of props.precalc.aboveMixer) {
      const v = props.nodes[k]
      nodesAboveMixer.push(rce(NodeBins, {
        key: k,
        data: v,
        precalc: props.precalc,
        xxx_keys: props.xxx_keys,
        xxx_state_frozen: props.xxx_state_frozen,
        xxx_wide_mode_coef: wideModeCoef,
        fmt: props.fmt
      }))
    }

    const nodesUnderMixer = []
    for (const k of props.precalc.underMixer) {
      const v = props.nodes[k]
      const _type = v._type === 'Funnel' ? NodeFunnels : NodeConveyers
      nodesUnderMixer.push(rce(_type, {
        key: k,
        data: v,
        precalc: props.precalc,
        xxx_keys: props.xxx_keys,
        xxx_state_frozen: props.xxx_state_frozen,
        xxx_wide_mode_coef: wideModeCoef,
        fmt: props.fmt
      }))
    }

    const nodesFreePaths = []
    for (const k of props.precalc.freePaths) {
      const v = props.nodes[k]
      nodesFreePaths.push(rce(NodeFreePaths, {
        key: k,
        data: v,
        xxx_wide_mode_coef: wideModeCoef,
      }))
    }

    const nodesChutes = []
    for (const k of props.precalc.chutes) {
      const v = props.nodes[k]
      nodesChutes.push(rce(NodeChutes, {
        key: k,
        data: v,
        xxx_keys: props.xxx_keys,
        xxx_state_frozen: props.xxx_state_frozen,
        fmt: props.fmt
      }))
    }

    const nodesCart = []
    for (const k of props.precalc.carts) {
      const v = props.nodes[k]
      nodesCart.push(rce(NodeCart, {
        key: k,
        data: v,
        xxx_keys: props.xxx_keys,
        xxx_state_frozen: props.xxx_state_frozen,
        fmt: props.fmt
      }))
    }

    const nodesLeftMenu = []
    for (const k of props.precalc.compressors) {
      const v = props.nodes[k]
      nodesLeftMenu.push(rce(NodeLeftMenuCompressor, {
        key: k,
        data: v,
        fmt: props.fmt
      }))
    }
    nodesLeftMenu.push(rce(NodeLeftMenuScalesTare, {
      key: 'nodeLeftMenuScalesTare',
      data: props.precalc.scalesTare,
      fmt: props.fmt
    }))
    nodesLeftMenu.push(rce(NodeLeftMenuFlowmetersReset, {
      key: 'nodeLeftMenuFlowmetersReset',
      data: props.precalc.flowmetersReset,
      fmt: props.fmt
    }))
    nodesLeftMenu.push(rce(NodeLeftMenuMixersWashing, {
      key: 'nodeLeftMenuMixersWashing',
      data: props.precalc.mixersWashing,
      fmt: props.fmt
    }))

    for (const k of props.precalc.lifts) {
      const v = props.nodes[k]
      nodes[k] = []
      nodes[k].push(rce(NodeLifts, {
        key: k,
        data: v,
        xxx_keys: props.xxx_keys,
        xxx_state_frozen: props.xxx_state_frozen,
        xxx_wide_mode_coef: wideModeCoef,
        fmt: props.fmt
      }))
    }

    nodes.horizontalConveyers = []
    for (const k of props.precalc.horizontalConveyers) {
      const v = props.nodes[k]
      const hornId = v.horn
      const horn = hornId ? props.nodes[hornId] : null
      nodes.horizontalConveyers.push(rce(NodeConveyers, {
        key: k,
        data: v,
        precalc: props.precalc,
        xxx_keys: props.xxx_keys,
        xxx_horn: horn,
        xxx_state_frozen: props.xxx_state_frozen,
        xxx_wide_mode_coef: wideModeCoef,
        fmt: props.fmt
      }))
    }

    nodes.inclinedConveyers = []
    for (const k of props.precalc.inclinedConveyers) {
      const v = props.nodes[k]
      const hornId = v.horn
      const horn = hornId ? props.nodes[hornId] : null
      nodes.inclinedConveyers.push(rce(NodeConveyers, {
        key: k,
        data: v,
        precalc: props.precalc,
        xxx_keys: props.xxx_keys,
        xxx_horn: horn,
        xxx_state_frozen: props.xxx_state_frozen,
        xxx_wide_mode_coef: wideModeCoef,
        fmt: props.fmt
      }))
    }

    const nodesFunctionsKey = []
    const mixerLock = _.get(props.nodes, ['Mixer1', 'is_locked'])
    nodesFunctionsKey.push(rce(NodeFunctionsKey, {
      key: 'controlKeys',
      xxx_mixer_is_locked: mixerLock,
      xxx_keys: props.xxx_keys,
      xxx_state_frozen: props.xxx_state_frozen,
      xxx_additionalWaterIsPresent: props.nodes.control.water_additional_is_present,
      xxx_additionalWaterSiloOpen: props.nodes.control.water_additional_open_percent,
      xxx_additionalWaterValueStr: props.nodes.control.water_additional_val_str,
      xxx_semaphore_id: props.nodes.control.semaphore_id,
      xxx_semaphore_color: props.nodes.control.semaphore_color,
      xxx_horn_id: props.nodes.control.horn_id,
      xxx_horn_is_enabled: props.nodes.control.horn_is_enabled,
      openHelpWindow: props.openHelpWindow,
      fmt: props.fmt
    }))

    const messages = []
    const ks = Object.keys(props.nodes).sort()
    for (const k of ks) {
      for (const k_ in props.nodes[k]._messages) {
        messages.push({ ...props.nodes[k]._messages[k_], node_id: k })
      }
    }
    messages.sort((x, y) => x.t - y.t)
    let someoneHasOptions = 0
    const nodesMessages = []
    for (const i of messages) {
      if (i.level === 'beep') {
        continue
      }
      const hasOptions = _.get(i, ['options'], []).length ? 1 : 0
      nodesMessages.push(rce(NodeMessage, {
        key: 'controlMessages' + i.node_id + '_' + i.k,
        data: i,
        xxx_keys: props.xxx_keys,
        xxx_enterOrEscOrNumUsed: props.xxx_enterOrEscOrNumUsed,
        setEnterOrEscUsed: props.setEnterOrEscUsed,
        xxx_keyboard_focus: hasOptions && !someoneHasOptions,
        fmt: props.fmt
      }))
      someoneHasOptions |= hasOptions
    }
    // TODO: ugly shit - don't just count the messages, compare their identity!
    if (gloCountOfMessages != messages.length) {
      if (messages.length > gloCountOfMessages) {
        soundAlert()
      }
      gloCountOfMessages = messages.length
    }

    const subTreeOpacity = props.xxx_connected ? 1 : 0.2
    // the hard-coded shit "12" was added because of mj83 (zapa kacerov merko) because the aggregates were pushing the mixer to the messages section - TODO: solve better
    const widerSpacing = (props.precalc.countOfSilosWithoutAggregate < props.xxx_aggregatesToNextLineCutoff) && (props.precalc.countOfAggregateSilos < 12)  // TODO: this is wrong - wider spacing has nothing to do with cutoff, TODO: hard-coded shit

    // left menu
    const menuIsOpened = props.xxx_menuIsOpened
    const menuClass = '.leftMenu' + (menuIsOpened ? 'Opened' : 'Closed')
    const menuButtonClass = menuIsOpened ? '.left-menu-button' : '.left-menuClosed-button'
    const essentialsOk = props.nodes.control._essentials_ok
    menuTree = h(menuClass, { key: 'menuTree' }, [
      h(menuButtonClass, { title: 'MENU', onMouseDown: _e => props.menuToggle() }, [
        h('i.fa.fa-ellipsis-v'),
        //h(essentialsOk ? 'i.fa.fa-thumbs-up.green' : 'i.fa.fa-exclamation.faa-flash.animated.red')
        h(essentialsOk ? 'i.fa.fa-thumbs-up.green' : 'i.fa.fa-exclamation.red')
      ]),
      menuIsOpened ? nodesLeftMenu : null
    ])

    const aggSiloWidth = (props.precalc.countOfAggregateSilos * 5 * props.xxx_wide_mode_coef) + (props.precalc.countOfAggregateSilos * 0.05) + 1 + 'vw'

    subTree = h('', { style: { opacity: subTreeOpacity } }, [
      h('.plant', {}, [
        h('.plantLeft', {}, [
          h('.topSilos', { style: { justifyContent: !props.precalc.aggregatesOnBottom ? 'center' : 'flex-start' } }, [
            props.precalc.aggregatesOnBottom
              ? null
              : h('.content-aggregate', {}, [
                h('.content-aggregates', { style: { width: aggSiloWidth } }, nodes.AggregateSilos),
                h('.content-flowmetersAndBins', { style: { width: aggSiloWidth } }, nodes.AggregateBins),
                h('.content-conveyers', { style: { width: aggSiloWidth } }, nodes.horizontalConveyers)
              ]),
            h('.content-cement', { style: { marginLeft: widerSpacing ? '2vw' : '0.5vw' } }, [
              nodes.CementSilos,
              nodes.CementBins
            ]),
            h('.content-spacer', { style: { width: widerSpacing ? '2vw' : '0.5vw' } }),
            h('.content-admixture', {}, [
              nodes.AdmixtureSilos,
              nodes.AdmixtureBins
            ]),
            h('.content-spacer', { style: { width: widerSpacing ? '2vw' : '0.5vw' } }),
            h('.content-water', {}, [
              nodes.WaterSilos,
              nodes.WaterBins
            ]),
            h('.recycledWaterDisabled', {}, [
              props.nodes.control.recycled_water_disabled ? props.fmt('{RecycledWaterDisabled}') : ''
            ])
          ]),
          h('.middle', {}, [
            h('.middleLeft', {}, [
              h('.middleLeftSilos', {}, [
                props.precalc.aggregatesOnBottom
                  ? h('.content-aggregate', { style: { marginLeft: widerSpacing ? '2vw' : '0.5vw' } }, [
                    nodes.AggregateSilos,
                    nodes.AggregateBins
                  ])
                  : null
              ]),
              h('.middleLeftConveyers', { style: { marginLeft: widerSpacing ? '2vw' : '0.5vw' } }, [
                (props.precalc.horizontalConveyers.length > 0 && props.precalc.aggregatesOnBottom) ? nodes.horizontalConveyers : null
              ])
            ]),
            h('.middleMiddle', {}, [
              h('.middleMiddleTop', {}, [
                rce(OrderOnHold, {
                  order_on_hold: props.nodes.control.order_on_hold,
                  stop_after_batch: props.nodes.control.stop_after_batch,
                  fmt: props.fmt
                })
              ]),
              h('.middleMiddleCenter', {}, [
                props.precalc.lifts.length > 0 || props.precalc.inclinedConveyers.length > 0
                  ? h('.middleMiddleCenterLeft', {}, [
                    props.precalc.lifts.length > 0 ? nodes.Lift1 : null,
                    props.precalc.inclinedConveyers.length > 0 ? nodes.inclinedConveyers : null
                  ])
                  : h('.middleMiddleCenterLeftHalf'),
                h('.middleMiddleCenterCenter', {}, [
                  h('.reservoir', {}, [
                    nodesAboveMixer.length > 0 ? nodesAboveMixer : null,
                  ]),
                  h('.content-mixer-global', {}, [
                    nodes.Mixer1,
                    nodesChutes.length > 0 ? nodesChutes : h('.chutes-content'),
                    nodesUnderMixer.length > 0
                      ? nodesUnderMixer
                      : (nodesChutes.length === 0 ? h('.content-funnel') : null),  // TODO: funnel is also being used as bottom padding for mixer. we don't want that when chute (with extend) is present - still, this is a hack -> implement proper padding and make funnel's height not fixed
                    nodesFreePaths.length > 0 ? nodesFreePaths : h('.content-freePath'),
                    nodesCart.length > 0 ? nodesCart : h('.carts-content'),
                  ])
                ]),
                h('.middleMiddleCenterRight', {}, [
                  props.precalc.mixerIsLifted ? nodesMixerLift : null
                ])
              ])
            ]),
            h('') // TODO: fake div !!!!! why??
          ])
        ]),
        h('.plantRight', {}, [
          // TODO: rework the following - there's way too much repetition
          props.precalc.continuousPlant
            ? h('.topQueue', {}, [
              h('.topContinuousSettings', {}, [
                rce(ContinuousConcreteSettings, {
                  hour_output: props.nodes.control.hour_output,
                  truck_capacity: props.nodes.control.truck_capacity,
                  fmt: props.fmt
                })
              ]),
              h('.topQueueContinuous', {}, [
                MANAGER_QUEUE_ENABLED
                  ? rce(Queue, {
                    lang_cur: props.lang_cur,
                    managerOpenOrderDialog: props.managerOpenOrderDialog
                  })
                  : null
              ])
            ])
            : h('.topQueue', {}, [
              h('.topQueueBatchMode', {}, [
                MANAGER_QUEUE_ENABLED
                  ? rce(Queue, {
                    lang_cur: props.lang_cur,
                    managerOpenOrderDialog: props.managerOpenOrderDialog
                  })
                  : null
              ])
            ]),
          h('.middleMessages.scroller', {}, nodesMessages)
        ])
      ]),
      h('.footer', {}, [
        h('.plantId', {}, props.nodes.control.plant_id),
        nodesFunctionsKey
      ]),
      rce(Clock),
      props.xxx_is_help ? rce(NodeHelp, props) : null,
      props.xxx_is_language ? rce(LanguageDialog, props) : null,
      props.xxx_is_exit ? rce(ExitDialog, props) : null,
      props.xxx_is_dispatch
        ? rce(IframeModal, {
          title: props.fmt('{dispatcher}'),
          url: '../dispatcher/?lang=' + props.lang_cur,
          noResize: true,
          onClose: _e => { props.dispatchCloseDialog() },
          extraClass: '.modalDispatch'
        })
        : null,
      props.xxx_is_param_quick
        ? rce(IframeModal, {
          title: props.fmt('{moistures}'),
          url: '../parameters/?quick=1&lang=' + props.lang_cur,
          onClose: _e => { props.paramQuickCloseDialog() },
          extraClass: '.modalParamQuick'
        })
        : null,
      props.xxx_is_param
        ? rce(IframeModal, {
          title: props.fmt('{parameters}'),
          url: '../parameters/?lang=' + props.lang_cur,
          onClose: _e => { props.paramCloseDialog() },
          extraClass: '.modalParam'
        })
        : null,
      props.xxx_is_place
        ? rce(IframeModal, {
          title: props.fmt('{place}'),
          url: '../place/?lang=' + props.lang_cur,
          onClose: _e => { props.placeCloseDialog() },
          extraClass: '.modalPlace'
        })
        : null,
      props.xxx_modal_order_id
        ? rce(Modal, {
          xxx_order_id: props.xxx_modal_order_id,
          managerCloseOrderDialog: props.managerCloseOrderDialog,
          fmt: props.fmt,
          lang_cur: props.lang_cur
        })
        : null,
      gloDebugReact
        ? h('.debug', {}, [
          h('.top-menu-button', { onMouseDown: _e => callApi(['control', 'reset_measured_steps']) }, 'reset measured steps'),
          h('.top-menu-button', { onMouseDown: _e => callApi(['control', 'reset_measured_overshoots']) }, 'reset measured overshoots'),
          h('.top-menu-button', { onMouseDown: _e => callApi(['control', 'reset_measured_inflights']) }, 'reset measured inflights'),
          h('.top-menu-button', { onMouseDown: _e => callApi(['control', 'reset_measured_lift_durations']) }, 'reset measured lift durations'),
          h('.top-menu-button', { onMouseDown: _e => callApi(['control', 'reset_measured_mixer_durations']) }, 'reset measured mixer durations'),
          h('.top-menu-button', { onMouseDown: _e => props.doCrash() }, 'crash frontend'),
          h('pre', {}, JSON.stringify(gloDebugStuff, null, 2))
        ])
        : null
    ])
  }

  const contentClass = 'content' + (!props.xxx_connected ? ' disconnected' : (props.xxx_state_is_pc != 0 ? (props.xxx_state_frozen ? ' f10enabled' : ' f10disabled') : ' pcNotActive')) + (' ' + props.xxx_theme)

  const themeIcon = 'i.fa' + (props.xxx_theme === 'day' ? '.fa-moon' : '.fa-sun')

  return h('', { id: "main", className: contentClass }, [
    props.precalc ? menuTree : null,
    h('.header', {}, [
      h('.top-menu', {}, [
        h('.top-menu-button', {
          onMouseDown: _e => props.openExitDialog(),
          title: props.fmt('{exit_control_program}'),
        }, h('i.fa.fa-power-off')),
        DISPATCH_TITLE_ENABLED
          ? h('.top-menu-button', { onMouseDown: _e => sendTitle('control:dispatcher') }, props.fmt('{dispatcher}'))
          : h('.top-menu-button', { onMouseDown: _e => props.dispatchOpenDialog() }, props.fmt('{dispatcher}')),
        h('.top-menu-button', { onMouseDown: _e => props.paramQuickOpenDialog() }, props.fmt('{moistures}')),
        h('.top-menu-button', { onMouseDown: _e => props.paramOpenDialog() }, props.fmt('{parameters}')),
        h('.top-menu-button', { onMouseDown: _e => props.placeOpenDialog() }, props.fmt('{place}')),
        h('.top-menu-button', {
          onMouseDown: _e => props.openLanguageDialog(),
          title: props.fmt('{language}'),
        }, h('i.fa.fa-globe')),
        h('.top-menu-button', {
          onMouseDown: _e => props.themeToggle(),
          title: props.fmt('{theme}'),
        }, h(themeIcon))
      ])
    ]),
    props.precalc ? subTree : null
  ])
}

/* TODO: finish this
const arrowCombo = (open_percent, opening_state, closing_state) => {
  const openingArrowAvailable = opening_state != null;
  const openingArrowState = opening_state;
  const openingArrowIcon = "fad fa-arrow-alt-up" + (openingArrowState ? " arrow-signal-open-100" : "");
  const closingArrowAvailable = closing_state != null;
  const closingArrowState = closing_state;
  const closingArrowIcon = "fad fa-arrow-alt-down" + (closingArrowState ? " arrow-signal-open-100" : "");
  const arrowIcon = "fad fa-arrow-alt-down" + (open_percent ? " arrow-signal-open-" + (isWarning ? "warning-" : "") + setOpenFinalPercent : "") + " isArrow isControl needsCTRL" + (props.xxx_keys['CTRL'] ? " hasCTRL" : "") + (props.xxx_state_frozen ? " isFrozen" : "");
  return h("", {}, [
    closingArrowAvailable
      ? h(".bin-closing-arrow-content", {},
        h("i", {className: closingArrowIcon}))
      : null,
    h("i", {
      className: arrowIcon,
      onMouseDown: _e => (props.xxx_keys.CTRL || props.xxx_state_frozen) && callApi([id, 'set_open_invert']),
      onMouseUp: _e => callApi([id, 'unset_open_invert']),
      onTouchStart: _e => callApi([id, 'set_open_invert']),
      onTouchEnd: _e => callApi([id, 'unset_open_invert']),
      title: props.fmt("{filling}") }
    ),
    openingArrowAvailable
      ? h(".bin-opening-arrow-content", {},
        h("i", {className: openingArrowIcon}))
      : null,
  ])
}
*/

const renderNodeSilos = props => {
  gloDebugReact && incDebugRenderCount('NodeSilos', props)
  const dfs = props.data
  const id = dfs.id
  const ctrlPressed = props.xxx_keys.CTRL ? ' hasCTRL' : ''
  const number = dfs._index
  const nameShort = dfs.material_name_short
  const nameLong = dfs.material_name_long
  const materialType = dfs._material_type
  const hasTemperature = dfs.temperature_str != null
  const temperatureStr = hasTemperature ? (dfs.temperature_str + DEG) : ''
  const hasTemperatureProbe = dfs.temperature_from_probe_str != null
  const temperatureFromProbeStr = hasTemperatureProbe ? (dfs.temperature_from_probe_str + DEG) : ''
  const temperatureIsLinkedSymbolClass = 'fa fa-' + (dfs.temperature_is_linked ? 'link' : 'unlink')
  const temperatureLink = '.silo-link' + (dfs.temperature_is_linked ? '.link' : '.unlink')
  const hasMoistureProbe = dfs.moisture_from_probe_str != null
  const moistureFromProbeStr = hasMoistureProbe ? dfs.moisture_from_probe_str + '%' : ''
  const moistureIsLinkedSymbolClass = 'fa fa-' + (dfs.moisture_is_linked ? 'link' : 'unlink')
  const moistureLink = '.silo-link' + (dfs.moisture_is_linked ? '.link' : '.unlink')
  const hasMoisture = dfs.moisture_str != null
  const moistureStr = hasMoisture ? dfs.moisture_str + '%' : ''
  const hasDensityProbe = dfs.density_from_probe_str != null
  const densityFromProbeStr = hasDensityProbe ? dfs.density_from_probe_str : ''
  const densityIsLinkedSymbolClass = 'fa fa-' + (dfs.density_is_linked ? 'link' : 'unlink')
  const densityLink = '.silo-link' + (dfs.density_is_linked ? '.link' : '.unlink')
  const hasDensity = dfs.density_str != null
  const densityStr = hasDensity ? dfs.density_str : ''
  const isDimmed = dfs._is_dimmed
  const isFast = dfs.is_fast
  const isFlow = dfs._is_flow
  const isWarning = dfs._is_warning
  const isValWarning = dfs._is_val_warning
  const valDisplay = '.silo-display' + (isValWarning ? '.red' : '')

  const requestValueStr = dfs.req_str
  const recipePosition = dfs.recipe_pos
  const requestValueOrigStr = dfs.req_orig_str
  const valueStr = dfs.val_str

  const reqDisplay = '.silo-requestDisplay' + (requestValueOrigStr ? '.siloRequestAltered' : '')

  const levelStr = dfs.level_str
  const levelPercent = dfs.level_percent
  const levelPercentInvertStyle = (100 - (levelPercent < 1 ? 1 : levelPercent > 99 ? 99 : levelPercent)) + '%' // correction of overflow percent value between 1%-99% - TODO: do the correction on back-end
  const levelClass = '.silo-level-box' + ((levelPercent > 20) ? '.green' : '.red')
  const levelTooltip = levelStr + " (" + levelPercent + "%)"

  const canVibrate = dfs.can_vibrate
  const isVibrating = dfs._is_vibrating
  const vibrationButton = 'siloControl isButton isControl'
    + ctrlPressed
    + (isVibrating ? ' signalActive' : '')
    + reasonToClassSpace(dfs.set_vibrate_reason)
  const canVibrateB = dfs.can_vibrate_b
  const isVibratingB = dfs._is_vibrating_b
  const vibrationBButton = 'siloControl isButton isControl'
    + ctrlPressed
    + (isVibratingB ? ' signalActive' : '')
    + reasonToClassSpace(dfs.set_vibrate_b_reason)
  const canFast = dfs._has_sig_fast
  const canAerate = dfs.can_aerate
  const isAerating = dfs._is_aerating
  const aerationButton = 'siloControl isButton isControl'
    + ctrlPressed
    + (isAerating ? ' signalActive' : '')
    + reasonToClassSpace(dfs.set_aerate_reason)
  const canAerateB = dfs.can_aerate_b
  const isAeratingB = dfs._is_aerating_b
  const aerationBButton = 'siloControl isButton isControl'
    + ctrlPressed
    + (isAeratingB ? ' signalActive' : '')
    + reasonToClassSpace(dfs.set_aerate_b_reason)
  // TODO: hacky shit - mj191
  const additionalHackedValve = dfs.additional_hacked_valve
  const additionalHackedValveClass = 'siloControl isButton isControl'
    + ctrlPressed
    + (additionalHackedValve ? ' signalActive' : '')
    + reasonToClassSpace(dfs.additional_hacked_valve_reason)

  const sensorState = dfs.sig_is_closed_state
  const sensorBState = dfs.sig_is_closed_b_state
  const sensorClass = sensorState == null
    ? null
    : ('silo-sensor' + (sensorState === 0 ? ' silo-sensor-opened' : ''))
  const sensorBClass = sensorBState == null
    ? null
    : ('silo-sensor' + (sensorBState === 0 ? ' silo-sensor-opened' : ''))

  const closingArrowAvailable = dfs.sig_close_state != null
  const closingArrowState = dfs.sig_close_state
  const closingArrowIcon = 'fad fa-arrow-alt-up' + (closingArrowState ? ' arrow-signal-open-100' : '')
  const openingArrowAvailable = dfs.sig_open_state != null
  const openingArrowState = dfs.sig_open_state
  const openingArrowIcon = 'fad fa-arrow-alt-down' + (openingArrowState ? ' arrow-signal-open-100' : '')
  const closingBArrowAvailable = dfs.sig_close_b_state != null
  const closingBArrowState = dfs.sig_close_b_state
  const closingBArrowIcon = 'fad fa-arrow-alt-up' + (closingBArrowState ? ' arrow-signal-open-100' : '')
  const openingBArrowAvailable = dfs.sig_open_b_state != null
  const openingBArrowState = dfs.sig_open_b_state
  const openingBArrowIcon = 'fad fa-arrow-alt-down' + (openingBArrowState ? ' arrow-signal-open-100' : '')

  const sizeCoeficientWidth = 5 * props.xxx_wide_mode_coef
  const sizeCoeficientHeight = 23
  const siloSizeWidth = sizeCoeficientWidth + 'vw'
  const siloSizeHeight = sizeCoeficientHeight + 'vh'
  const siloSizeHeightTop = (sizeCoeficientHeight * 0.1) + 'vh'
  const siloSizeHeightBody = (sizeCoeficientHeight * 0.41) + 'vh'
  const siloSizeHeightBottom = (sizeCoeficientHeight * 0.10) + 'vh'
  const siloContentMarginLeft = (sizeCoeficientWidth * 0.05) + 'vw'

  const sensorHeight = (sizeCoeficientHeight * 0.05) + 'vh'

  const siloDisplayWidth = '100%'
  const siloDisplayHeight = (sizeCoeficientHeight * 0.1) + 'vh'
  const siloBottom = 'silo-bottom ' + materialType + (isDimmed ? 'Dimmed' : '')
  const setOpenFinalPercent = dfs.set_open_final_percent
  const arrowIcon = 'fad fa-arrow-alt-down'
    + (setOpenFinalPercent ? ' arrow-signal-open-' + (isWarning ? 'warning-' : '') + setOpenFinalPercent : '')
    + ' isArrow isControl needsCTRL'
    + (props.xxx_keys.CTRL ? ' hasCTRL' : '')
    + (props.xxx_state_frozen ? ' isFrozen' : '')

  const setOpenBFinalPercent = dfs.set_open_b_final_percent
  const arrowBIcon = setOpenBFinalPercent == null
    ? null
    : 'fad fa-arrow-alt-down'
      + (setOpenBFinalPercent ? ' arrow-signal-open-' + (isWarning ? 'warning-' : '') + setOpenBFinalPercent : '')
      + ' isArrow isControl needsCTRL'
      + (props.xxx_keys.CTRL ? ' hasCTRL' : '')
      + (props.xxx_state_frozen ? ' isFrozen' : '')
  const arrowIconFast = 'fad fa-arrow-alt-down'
    + (isFast ? ' arrow-signal-open-' + (isWarning ? 'warning-' : '') + setOpenFinalPercent : '')
    + ' isArrow isControl needsCTRL'
    + (props.xxx_keys.CTRL ? ' hasCTRL' : '')
    + (props.xxx_state_frozen ? ' isFrozen' : '')

  const style = { width: siloDisplayWidth, height: siloDisplayHeight }
  const linkStyle = { height: (sizeCoeficientHeight * 0.2) + 'vh' }
  const canCallApi = (CTRL_ENABLED && props.xxx_keys.CTRL) || props.xxx_state_frozen

  const arrowSubA = h('.silo-arrows-sub', {}, [
    h('', { style: { height: sensorHeight } }, [
      sensorClass
        ? h('', { className: sensorClass })
        : null,
    ]),
    h('.silo-arrow-triad', {}, [
      closingArrowAvailable
        ? h('.silo-closing-arrow-content', {},
          h('i', { className: closingArrowIcon }))
        : null,
      h('i', {
        className: arrowIcon,
        onMouseDown: _e => canCallApi && callApi([id, 'set_open_invert']),
        onMouseUp: _e => callApi([id, 'unset_open_invert']),
        onTouchStart: _e => callApi([id, 'set_open_invert']),
        onTouchEnd: _e => callApi([id, 'unset_open_invert']),
        title: props.fmt('{filling}'),
      }),
      openingArrowAvailable
        ? h('.silo-opening-arrow-content', {},
          h('i', { className: openingArrowIcon }))
        : null
    ]),
    h('.silo-control-content', {}, [
      canVibrate === 1
        ? h('.buttonFontSize', {
          className: vibrationButton,
          onMouseDown: _e => callApi([id, 'set_override', 'set_vibrate', 1]),
          onMouseUp: _e => callApi([id, 'set_override', 'set_vibrate']),
          title: props.fmt('{siloVibration}')
        },
        h('i.icon-vibration'))
        : null,
      canAerate === 1
        ? h('.buttonFontSize', {
          className: aerationButton,
          onMouseDown: _e => callApi([id, 'set_override', 'set_aerate', 1]),
          onMouseUp: _e => callApi([id, 'set_override', 'set_aerate']),
          title: props.fmt('{siloAeration}')
        },
        h('i.fa.fa-wind'))
        : null,
      additionalHackedValve != null  // this can also be undefined - hence the != comparison - TODO: find more elegant way?
        ? h('.buttonFontSize', {
          className: additionalHackedValveClass,
          onMouseDown: _e => callApi([id, 'set_override', 'additional_hacked_valve', 1]),
          onMouseUp: _e => callApi([id, 'set_override', 'additional_hacked_valve']),
          title: props.fmt('{additional_hacked_valve}'),
        }, h('i.fa.fa-arrow-down'))
          //h('i.fa.fa-chevron-down'))
          //h('i.fa.fa-caret-down'))
          //h('i.fa.fa-angle-down'))
          //h('i.fa.fa-poo'))
        : null,
    ])
  ])

  const arrowSubB = h('.silo-arrows-sub', {}, [
    h('', { style: { height: sensorHeight } }, [
      sensorBClass
        ? h('', { className: sensorBClass })
        : null
    ]),
    h('.silo-arrow-triad', {}, [
      closingBArrowAvailable
        ? h('.silo-closing-arrow-content', {},
          h('i', { className: closingBArrowIcon }))
        : null,
      arrowBIcon
        ? h('i', {
          className: arrowBIcon,
          onMouseDown: _e => canCallApi && callApi([id, 'set_open_b_invert']),
          onMouseUp: _e => callApi([id, 'unset_open_b_invert']),
          onTouchStart: _e => callApi([id, 'set_open_b_invert']),
          onTouchEnd: _e => callApi([id, 'unset_open_b_invert']),
          title: props.fmt('{filling}')
        })
        : null,
      openingBArrowAvailable
        ? h('.silo-opening-arrow-content', {},
          h('i', { className: openingBArrowIcon }))
        : null
    ]),
    h('.silo-control-content', {}, [
      canVibrateB === 1
        ? h('.buttonFontSize', {
          className: vibrationBButton,
          onMouseDown: _e => callApi([id, 'set_override', 'set_vibrate_b', 1]),
          onMouseUp: _e => callApi([id, 'set_override', 'set_vibrate_b']),
          title: props.fmt('{siloVibration}')
        },
        h('i.icon-vibration'))
        : null,
      canAerateB === 1
        ? h('.buttonFontSize', {
          className: aerationBButton,
          onMouseDown: _e => callApi([id, 'set_override', 'set_aerate_b', 1]),
          onMouseUp: _e => callApi([id, 'set_override', 'set_aerate_b']),
          title: props.fmt('{siloAeration}')
        },
        h('i.fa.fa-wind'))
        : null,
    ])
  ])

  const arrowSubFast = h('.silo-arrows-sub', {}, [
    h('', { style: { height: sensorHeight } }, [
      // no sensor for fast
    ]),
    h('.silo-arrow-triad', {}, [
      canFast
        ? h('i', {
          className: arrowIconFast,
          onMouseDown: _e => canCallApi && callApi([id, 'set_fast_invert']),
          onMouseUp: _e => callApi([id, 'unset_fast_invert']),
          onTouchStart: _e => callApi([id, 'set_fast_invert']),
          onTouchEnd: _e => callApi([id, 'unset_fast_invert']),
          title: props.fmt('{fastFilling}')
        })
        : null
    ]),
    h('.silo-control-content', {}, [
      // no vibration for fast
    ])
  ])

  return h('.silo-content', { style: { width: siloSizeWidth, height: siloSizeHeight, marginLeft: siloContentMarginLeft } }, [
    h('.silo-top', { style: { width: siloSizeWidth, height: siloSizeHeightTop } }, [
      h('.number'),
      h('.numberPosition', {}, number),
      h('.recipePosition', {}, recipePosition)
    ]),
    h('.silo-body', { style: { width: siloSizeWidth, height: siloSizeHeightBody } }, [
      h('.silo-level-content', {}, [
        levelPercent != null
          ? h(levelClass, { title: levelTooltip }, [
            h('.level', { style: { height: levelPercentInvertStyle } })
          ])
          : null,
        hasMoistureProbe
          ? h(moistureLink, {
            style: linkStyle,
            title: props.fmt('{moistureLink}')
          }, h('i', {
            className: moistureIsLinkedSymbolClass,
            onClick: _e => callApi([id, 'moisture_link_toggle'])
          }))
          : null,
        hasTemperatureProbe
          ? h(temperatureLink, {
            style: linkStyle,
            title: props.fmt('{temperatureLink}')
          }, h('i', {
            className: temperatureIsLinkedSymbolClass,
            onClick: _e => callApi([id, 'temperature_link_toggle'])
          }))
          : null,
        hasDensityProbe
          ? h(densityLink, {
            style: linkStyle,
            title: props.fmt('{densityLink}')
          }, h('i', {
            className: densityIsLinkedSymbolClass,
            onClick: _e => callApi([id, 'density_link_toggle'])
          }))
          : null,
      ]),
      h('.silo-display-content', {}, [
        h(valDisplay, { style }, [
          (isFlow && valueStr) ? h('.silo-delta', {}, DELTA) : null,
          valueStr
        ]),
        h(reqDisplay, {
          style,
          title: (requestValueOrigStr || '')
        }, [
          (isFlow && requestValueStr) ? h('.silo-delta', {}, DELTA) : null,
          requestValueStr
        ]),
        hasMoisture
          ? h('.silo-moisture', {
            style,
            title: props.fmt('{moisture}')
          }, moistureStr)
          : null,
        hasMoistureProbe
          ? h('.silo-moisture-probe', {
            style,
            title: props.fmt('{moistureFromProbe}')
          }, moistureFromProbeStr)
          : null,
        hasTemperature
          ? h('.silo-temperature', { style }, temperatureStr)
          : null,
        hasTemperatureProbe
          ? h('.silo-temperature-probe', { style }, temperatureFromProbeStr)
          : null,
        hasDensityProbe // TODO: temporary - solver better. the thing is, most plants don't have the density meter and are not really interested in seeing this value at all times...
          ? h('.silo-density', {
            style,
            title: props.fmt('{density}')
          }, densityStr)
          : null,
        hasDensityProbe
          ? h('.silo-density-probe', {
            style,
            title: props.fmt('{densityFromProbe}')
          }, densityFromProbeStr)
          : null,
      ])
    ]),
    h('', {
      className: siloBottom,
      style: { width: siloSizeWidth, height: siloSizeHeightBottom }
    }, [
      h('.nameShort', { title: nameLong }, nameShort),
    ]),
    h('.silo-arrows', {}, dfs._right_to_left
      ? [arrowSubFast, arrowSubB, arrowSubA]
      : [arrowSubA, arrowSubB, arrowSubFast]
    )
  ])
}

const renderNodeBinsAndFlowmeters = props => {
  gloDebugReact && incDebugRenderCount('NodeBinsAndFlowmeters', props)
  const dfs = props.data
  const type = dfs._type
  return h('', {}, [
    type === 'Bin' || type === 'Lift' || type === 'SBMBin' ? rce(NodeBins, props) : null,
    type === 'Flowmeter' ? rce(NodeFlowmeters, props) : null
  ])
}

const renderNodeBins = props => {
  gloDebugReact && incDebugRenderCount('NodeBins', props)
  const dfs = props.data
  const id = dfs.id
  const cementScrew = dfs.screw
  const cementScrewState = props.xxx_cementScrewState
  const cementScrewStateRotation = cementScrewState ? 'scaleScrew-run' : 'scaleScrew-idle'
  const cementScrewSetOpenReason = props.xxx_cementScrewSetOpenReason
  const cementScrewStateStartButton = 'scaleScrewControl isButton isControl needsCTRL'
    + (props.xxx_keys.CTRL ? ' hasCTRL' : '')
    + (props.xxx_state_frozen ? ' isFrozen' : '')
    + (cementScrewState === 1 ? ' red' : ' green')
    + reasonToClassSpace(cementScrewSetOpenReason)
  const cementScrewScaleValueStr = props.xxx_cementScrewScaleValueStr
  const cementScrewFlowStr = props.xxx_cementScrewFlowStr
  const openingArrowAvailable = dfs.sig_open_state != null && dfs.sig_close_state != null
  const openingArrowState = dfs.sig_open_state
  const openingArrowIcon = 'fad fa-arrow-alt-up' + (openingArrowState ? ' arrow-signal-open-100' : '')
  const closingArrowAvailable = dfs.sig_close_state != null
  const closingArrowState = dfs.sig_close_state
  const closingArrowIcon = 'fad fa-arrow-alt-down' + (closingArrowState ? ' arrow-signal-open-100' : '')
  const exhaustAvailable = dfs.can_exhaust
  const exhaustClass = 'bin-top-flush-button isButton isControl needsCTRL'  // TODO: flush? really?
    + (dfs._is_exhausting === 1 ? ' signalActive' : '')
    + (props.xxx_keys.CTRL ? ' hasCTRL' : '')
    + (props.xxx_state_frozen ? ' isFrozen' : '')
    + reasonToClassSpace(dfs.set_exhaust_reason)
  const filterCleanStates = dfs.sigs_filter_clean_states || []
  const filterClean = [] // TODO: use map
  for (let i = 0; i < filterCleanStates.length; i++) {
    const valveState = filterCleanStates[i]
    const valveTitle = props.fmt('{filterClean}') + ' ' + (i + 1)
    const filterCleanClass = 'mixer-exhaust-button isOnDark isButton isControl needsCTRL'  // TODO: exhaust? really?
      + (props.xxx_keys.CTRL ? ' hasCTRL' : '')
      + (props.xxx_state_frozen ? ' isFrozen' : '')
      + (valveState ? ' signalActive' : '')
      + reasonToClassSpace(dfs.filter_clean_reasons[i])
    filterClean.push(h('', {
      className: filterCleanClass,
      title: valveTitle,
      onMouseDown: _e => canCallApi && callApi([id, 'set_filter_clean_manual', i]),
      onMouseUp: _e => callApi([id, 'unset_filter_clean_manual', i])
    }, h('i.icon-vibration')))
  }
  const setOpenFinalPercent = dfs.set_open_final_percent
  const isWarning = dfs._is_warning
  // const isValWarning = dfs._is_val_warning;
  const scale = dfs.scale
  const type = dfs._type
  // const valueClassName = "display" + (isValWarning ? " red" : "");
  const binCanVibration = dfs.can_vibrate
  const binOutputPump = dfs.has_pump
  const binVibrationButton = 'isButton isControl'
    + (type === 'SBMBin' ? ' scaleScrewControl' : ' binControl')
    + (props.xxx_keys.CTRL ? ' hasCTRL' : '')
    + (dfs._is_vibrating ? ' signalActive' : '')
    + reasonToClassSpace(dfs.set_vibrate_reason)
  const binOutputPumpButton = 'isButton isControl needsCTRL'
    + (type === 'SBMBin' ? ' scaleScrewControl' : ' binControlPump')
    + (props.xxx_keys.CTRL ? ' hasCTRL' : '')
    + (dfs.pump_open ? ' signalActive' : (dfs.pump_is_flooded ? ' signalActiveAnother' : ''))
    + reasonToClassSpace(dfs.set_pump_open_reason)
  const binControlContent = binCanVibration || binOutputPump

  const scaleRequestStr = dfs.scale_req_str
  const scaleValueStr = dfs.scale_val_str

  const fillPercentScale = dfs.scale_fill_within_bin_percent

  const materialType = dfs._material_type

  const hasTemperatureProbe = dfs.temperature_from_probe_str != null
  const temperatureFromProbeStr = (hasTemperatureProbe ? (dfs.temperature_from_probe_str + DEG) : '')
  // const hasDensityProbe = 1;
  // const density_from_probe_str = (hasDensityProbe ? "1.03" : "");

  const requirementState = dfs._set_requirement
  const requirementGranted = dfs.is_granted

  const flushAvailable = dfs.can_flush
  const flushClass = 'bin-top-flush-button isButton isControl needsCTRL'
    + (dfs._is_flushing === 1 ? ' signalActive' : '')
    + (props.xxx_keys.CTRL ? ' hasCTRL' : '')
    + (props.xxx_state_frozen ? ' isFrozen' : '')
    + reasonToClassSpace(dfs.set_flush_reason)

  const sensorState = dfs.sig_is_closed_state
  const sensorAvailable = sensorState != null
  const sensorSectionVisible = sensorAvailable ? '' : 'hidden'
  const sensor = 'bin-sensor' + (sensorState === 0 ? ' bin-sensor-opened' : '')

  let countOfSilos = 1  // TODO: ugly fucking fallback for reservoir
  let countOfBinsInScale = 0
  let binPosition = 0
  if (id.indexOf("Reservoir") === -1) {
    countOfSilos = props.precalc.binsAndFlowmeters[materialType][id].silos.length
    countOfBinsInScale = props.precalc.scales[materialType][scale].bins.length
  /* let countOfAllSilosInScale = 0;
  for (let s in scales[materialType][scale]['bins']) {
    let s2 = scales[materialType][scale]['bins'][s];
    countOfAllSilosInScale += props.precalc.binsAndFlowmeters[materialType][s2]['silos'].length;
  } */
    binPosition = props.precalc.scales[materialType][scale].bins.indexOf(id) + 1
  }

  let shapeOfBinLeft = '0%'
  if (countOfSilos === 1 && countOfBinsInScale > 1) {
    shapeOfBinLeft = binPosition === 1 ? null : '0%'
  } else {
    shapeOfBinLeft = (countOfSilos != 1 && binPosition > 1) ? '0%' : null
  }
  let shapeOfBinRight = '0%'
  if (countOfSilos === 1 && countOfBinsInScale > 1) {
    shapeOfBinRight = countOfBinsInScale === binPosition ? null : '0%'
  } else {
    shapeOfBinRight = countOfSilos != 1 && countOfBinsInScale > binPosition ? '0%' : null
  }
  const reservoir = id == "AggregateReservoir1" // TODO: ugly shit this is for smaller coefficient for reservoir
  const sizeCoeficientWidth = (reservoir ? 3 : 5) * props.xxx_wide_mode_coef
  const sizeCoeficientHeight = reservoir ? 10 : 15
  //const sizeCoeficientWidth = 5 * props.xxx_wide_mode_coef
  //const sizeCoeficientHeight = 15

  // const binDisplayContentWidth = (sizeCoeficientWidth * 1.191) * ((countOfSilos === 1) || (type === 'Flowmeter') ? 1 : countOfSilos / 1.2) + "vw";
  const binDisplayContentHeight = (sizeCoeficientHeight * 0.24) + 'vh'
  const binDisplayWidth = sizeCoeficientWidth + 'vw'
  const binDisplayHeight = (sizeCoeficientHeight * 0.15) + 'vh'
  const sensorHeight = (sizeCoeficientHeight * 0.032) + 'vh'
  const sensorWidth = (sizeCoeficientWidth * 0.5) + 'vw'
  const binMarginLeft = (sizeCoeficientWidth * (binPosition === 1 ? 0.05 : 0.01)) + 'vw'
  const arrowHeight = (sizeCoeficientHeight * 0.27) + 'vh'
  const arrowWidth = (sizeCoeficientWidth * 0.45) + 'vw'
  const binSizeHeight = sizeCoeficientHeight + 'vh'
  const binSizeHeightWithoutBottomContents = (sizeCoeficientHeight * 0.61) + 'vh'
  const binSizeHeightTopIcon = (sizeCoeficientHeight * 0.2) + 'vh'
  const binSizeHeightTop = (sizeCoeficientHeight * 0.06) + 'vh'
  const binSizeHeightBody = (sizeCoeficientHeight * 0.32) + 'vh'
  const spaceBetween = (binPosition === 1) ? 0.05 : 0.09
  const binContentSizeWidth = (sizeCoeficientWidth * countOfSilos + ((sizeCoeficientWidth * 0.05) * countOfSilos)) - (spaceBetween + (sizeCoeficientWidth * (binPosition === 1 ? 0 : 0.01))) + 'vw'
  const binSizeWidth = (sizeCoeficientWidth * countOfSilos + ((sizeCoeficientWidth * 0.05) * countOfSilos)) - (spaceBetween + (sizeCoeficientWidth * (binPosition === 1 ? 0.05 : 0.01))) + 'vw'
  const binStyle = {
    width: binSizeWidth,
    height: binSizeHeightBody,
    borderBottomLeftRadius: shapeOfBinLeft,
    borderBottomRightRadius: shapeOfBinRight
  }
  const binTopScale = 'bin-topScale ' + materialType
  const binTopLevelScale = 'linear-gradient(to right, var(--colorWeightProgress' + materialType + ') 0% ' + fillPercentScale + '%, var(--colorWeight' + materialType + ') ' + fillPercentScale + '% 100%)'
  const binArrow = (reservoir ? '.bin-arrow-reservoir' : '.bin-arrow') + '.isControl.needsCTRL.isArrow'
    + (props.xxx_keys.CTRL ? '.hasCTRL' : '')
    + (props.xxx_state_frozen ? '.isFrozen' : '')

  const arrowAndScrewVisibility = ((type === 'Lift') ? 'hidden' : '')
  const arrowIcon = 'fad fa-arrow-alt-down' + (setOpenFinalPercent ? ' arrow-signal-open-' + (isWarning ? 'warning-' : '') + setOpenFinalPercent : '')

  const canCallApi = (CTRL_ENABLED && props.xxx_keys.CTRL) || props.xxx_state_frozen

  return h('.bins-content', { style: { width: binContentSizeWidth } }, [
    h('.left', { style: { width: binSizeWidth, height: binSizeHeight } }, [
      h('.bin-content', { style: { marginLeft: binMarginLeft, height: binSizeHeightWithoutBottomContents } }, [
        h('.bin-top-flush', { style: { width: binSizeWidth, height: binSizeHeightTopIcon } }, [
          h('.bin-top-countdown', {}, dfs.countdown_str),
          flushAvailable
            ? h('', {
              className: flushClass,
              onMouseDown: _e => canCallApi && callApi([id, 'set_override', 'set_flush', 1]),
              onMouseUp: _e => callApi([id, 'set_override', 'set_flush']),
              onTouchStart: _e => callApi([id, 'set_override', 'set_flush', 1]),
              onTouchEnd: _e => callApi([id, 'set_override', 'set_flush']),
              title: props.fmt('{binFlush}')
            }, [
              h('i.fas.fa-tint')
            ])
            : h(''),
          filterClean.length > 0 ? filterClean : null,
          exhaustAvailable
            ? h('', {
              className: exhaustClass,
              onMouseDown: _e => canCallApi && callApi([id, 'set_override', 'set_exhaust', 1]),
              onMouseUp: _e => callApi([id, 'set_override', 'set_exhaust']),
              onTouchStart: _e => callApi([id, 'set_override', 'set_exhaust', 1]),
              onTouchEnd: _e => callApi([id, 'set_override', 'set_exhaust']),
              title: props.fmt('{exhaust}')
            }, [
              h('i.fa.fa-fan')
            ])
            : h(''),
          requirementState != null
            ? h('.bin-top-request', {}, requirementState ? props.fmt('{requested}') : '')
            : h(''),
          requirementGranted != null
            ? h('.bin-top-request-accepted', {}, requirementGranted ? props.fmt('{switched}') : '')
            : h(''),
          h('.bin-top-batches', {}, dfs.seq_tot)
        ]),
        h('', {
          className: binTopScale,
          style: { width: binSizeWidth, height: binSizeHeightTop, background: binTopLevelScale }
        }),
        h('.bin', { style: binStyle }, [
          h('.bin-displays-content', { style: { height: binDisplayContentHeight } }, [
            (dfs.req_str != null || dfs.val_str != null)
              ? h('.bin-display-content', {}, [
                h('.request', { style: { width: binDisplayWidth, height: binDisplayHeight } }, [
                  dfs._is_flow && dfs.req_str ? h('.bin-delta', {}, DELTA) : null,
                  dfs.req_str
                ]),
                h('.value', { style: { width: binDisplayWidth, height: binDisplayHeight } }, [
                  dfs._is_flow && dfs.val_str ? h('.bin-delta', {}, DELTA) : null,
                  dfs.val_str
                ])
              ])
              : null,
            (binPosition === 1)
              ? h('.bin-display-content', {}, [
                h('.request', { style: { width: binDisplayWidth, height: binDisplayHeight } }, scaleRequestStr),
                h('.value', { style: { width: binDisplayWidth, height: binDisplayHeight } }, scaleValueStr)
              ])
              : null,
            (hasTemperatureProbe /* || hasDensityProbe */)
              ? h('.bin-display-content', {}, [
                hasTemperatureProbe
                  ? h('.bin-temperature-probe', { style: { width: binDisplayWidth, height: binDisplayHeight } }, temperatureFromProbeStr)
                  : null
                /* hasDensityProbe
                  ? h(".bin-density-probe", {style: {width: binDisplayWidth, height: binDisplayHeight}}, density_from_probe_str)
                  : null, */
              ])
              : null
          ])
        ])
      ]),
      h('.bin-sensor-content', { style: { marginLeft: binMarginLeft, width: binSizeWidth, height: sensorHeight, visibility: sensorSectionVisible } }, [
        h('', { className: sensor, style: { height: sensorHeight, width: sensorWidth } })
      ]),
      h('.bin-arrow-and-screw-content', { style: { marginLeft: binMarginLeft, width: binSizeWidth, height: arrowHeight, visibility: arrowAndScrewVisibility } }, [
        dfs.has_opening
          ? h(binArrow, {
            style: { width: arrowWidth },
            onMouseDown: _e => canCallApi && callApi([id, 'set_open_invert']),
            onMouseUp: _e => callApi([id, 'unset_open_invert']),
            onTouchStart: _e => callApi([id, 'set_open_invert']),
            onTouchEnd: _e => callApi([id, 'unset_open_invert']),
            title: props.fmt('{emptying}')
          }, [
            closingArrowAvailable
              ? h('.bin-closing-arrow-content', {}, [
                h('i', { className: closingArrowIcon })
              ])
              : null,
            h('i', { className: arrowIcon }),
            openingArrowAvailable
              ? h('.bin-opening-arrow-content', {}, [
                h('i', { className: openingArrowIcon })
              ])
              : null
          ])
          : null,
        type === 'SBMBin'
          ? h('.scale-screw-content' + (dfs.has_opening ? '' : '.noMargin'), {}, [
            h('.scaleScrew', {}, [
              h('.scaleScrewRotationBackground', {}, [
                h('', { className: cementScrewStateRotation })
              ])
            ]),
            h('.scale-screw-button-content', {}, [
              binCanVibration === 1
                ? h('.buttonFontSize', {
                  className: binVibrationButton,
                  onMouseDown: _e => callApi([id, 'set_override', 'set_vibrate', 1]),
                  onMouseUp: _e => callApi([id, 'set_override', 'set_vibrate']),
                  title: props.fmt('{siloVibration}')
                }, h('i.icon-vibration'))
                : null,
              h('', {
                className: cementScrewStateStartButton,
                onMouseDown: _e => canCallApi && callApi([cementScrew, 'toggle_open_invert']),
                title: props.fmt('{cementScrewStartStop}')
              }, [
                h('i.far.fa-dot-circle')
              ]),
              h('.scale-screw-scale', {}, cementScrewScaleValueStr),
              cementScrewFlowStr != null
                ? h('.scale-screw-scale', {}, [
                  h('.conveyer-delta', {}, DELTA),
                  h('', {}, cementScrewFlowStr)
                ])
                : null
            ])
          ])
          : null
      ]),
      binControlContent && type != 'SBMBin'
        ? h('.bin-control-content', { style: { marginLeft: binMarginLeft, width: binSizeWidth } }, [
          binCanVibration === 1 && type != 'Lift'
            ? h('.buttonFontSize', {
              className: binVibrationButton,
              onMouseDown: _e => callApi([id, 'set_override', 'set_vibrate', 1]),
              onMouseUp: _e => callApi([id, 'set_override', 'set_vibrate']),
              title: props.fmt('{siloVibration}')
            }, h('i.icon-vibration'))
            : null,
          binOutputPump === 1 && type != 'Lift'
            ? h('', {
              className: binOutputPumpButton,
              onMouseDown: _e => canCallApi && callApi([id, 'set_override', 'set_pump_open', 1]),
              onMouseUp: _e => callApi([id, 'set_override', 'set_pump_open']),
              title: props.fmt('{binOutputPump}')
            }, h('i.fab.fa-rev'))
            : null
        ])
        : null
    ])
  ])
}

const renderNodeFlowmeters = props => {
  gloDebugReact && incDebugRenderCount('NodeFlowmeters', props)
  const dfs = props.data
  const id = dfs.id
  const materialType = dfs._material_type
  const countOfSilos = props.precalc.binsAndFlowmeters[materialType][id].silos.length
  const number = dfs._index
  const sizeCoeficientWidth = 5 * props.xxx_wide_mode_coef
  const sizeCoeficientHeight = 8
  const spaceBetween = number > 1 ? 0.3 : 0.09
  const sizeWidth = (sizeCoeficientWidth * countOfSilos + ((sizeCoeficientWidth * 0.05) * countOfSilos)) - spaceBetween + 'vw'
  const flowSizeWidth = (sizeCoeficientWidth * 0.8) + (sizeCoeficientWidth * 0.05) + 'vw'
  const sizeHeight = sizeCoeficientHeight + 'vh'
  const flowSizeHeight = (sizeCoeficientHeight * 0.84) + 'vh'
  const displayContentHeight = (sizeCoeficientHeight * 0.35) + 'vh'

  const valueStr = dfs.val_str
  const flowmeterTop = 'flowmeter-top ' + materialType
  const flowmeterSizeHeightTop = (sizeCoeficientHeight * 0.11) + 'vh'
  const flowmeterSizeHeightTopIcon = (sizeCoeficientHeight * 0.39) + 'vh'
  const rangePercent = dfs.range_percent
  const flowmeterTopLevel = 'linear-gradient(to right, var(--colorWeightProgress' + materialType + ') 0% ' + rangePercent + '%, var(--colorWeight' + materialType + ') ' + rangePercent + '% 100%)'

  const isFlow = dfs._is_flow
  const isWarning = dfs._is_warning
  const flowmeterDisplayContentClass = 'flowmeter-display-content' + (isWarning ? ' red' : '')


  return h('.flowmeterContent', { style: { marginLeft: spaceBetween + 'vw', width: sizeWidth } },
    h('.left', { style: { width: sizeWidth, height: sizeHeight } },
      h('.flowmeter-content', { style: { width: sizeWidth, height: sizeHeight } },
        h('.flowmeter', { style: { width: flowSizeWidth, height: sizeHeight } }, [
          h('.flowmeter-bottom-spacer', {}, []),
          h('.flowmeter-bottom-content', { style: { width: flowSizeWidth, height: flowmeterSizeHeightTop } }, [
            h('.flowmeter-bottom-left', { style: { height: flowmeterSizeHeightTop } }),
            h('.flowmeter-bottom-right', { style: { height: flowmeterSizeHeightTop } })
          ]),
          h('', {
            className: flowmeterDisplayContentClass,
            style: { width: flowSizeWidth, height: displayContentHeight }
          }, [
            isFlow ? h('.flowmeter-delta', {}, DELTA) : null,
            h('.flowmeter-display', {}, valueStr)
          ]),
          h('', {
            className: flowmeterTop,
            style: { width: flowSizeWidth, height: flowmeterSizeHeightTop, background: flowmeterTopLevel }
          }),
          h('.flowmeter-top-displays', { style: { width: flowSizeWidth, height: flowmeterSizeHeightTopIcon } }, [
            //h('.flowmeter-countdown', {}, dfs.countdown_str), TODO: not implemented, yet
            h('.flowmeter-batches', {}, dfs.seq_tot),
          ])
        ])
      )
    )
  )
}

const renderNodeMixerBay = props => {
  gloDebugReact && incDebugRenderCount('NodeMixerBay', props)
  const id = props.id
  const bayData = props.bayData
  const directionCloseReasonClass = (bayData.set_direction_manual === -1 ? ' reasonManual' : (bayData.set_direction === -1 ? ' reasonAutomat' : ''))
  const directionOpenReasonClass = (bayData.set_direction_manual === 1 ? ' reasonManual' : (bayData.set_direction === 1 ? ' reasonAutomat' : ''))
  const closingClass = 'mixerClosingOpening isOnDark isButton isControl'
    + (props.xxx_keys.CTRL ? ' hasCTRL' : '')
    + (bayData.sig_close_state === 1 ? ' signalActive' : '')
    + directionCloseReasonClass
  const openingClass = 'mixerClosingOpening isOnDark isButton isControl'
    + (props.xxx_keys.CTRL ? ' hasCTRL' : '')
    + (bayData.sig_open_state === 1 ? ' signalActive' : '')
    + directionOpenReasonClass
  const emptyingSemiDurationLeft = bayData.countdown_semi
  const emptyingFullDurationLeft = bayData.countdown_full
  // const mixerLockIcon = 'fa' + (bayData.is_locked ? ' fa-hand-paper faa-flash animated' : ' fa-lock-open')
  const mixerLockIcon = 'fa' + (bayData.is_locked ? ' fa-hand-paper' : ' fa-lock-open')
  const mixerLockClass = 'mixerBlockLock isOnDark isButton isControl'
    + (bayData.is_locked ? ' mixerBlockHand reasonManual' : ' mixerBlockLock green')
    + (props.xxx_keys.CTRL ? ' hasCTRL' : '')
  const bayPercent = bayData.open_percent
  const bayWidth = (100 - Math.min(bayPercent, 90)) + '%'
  // TODO: 'var(--colorVisualButtonTrue)' -> just use some color for "not open and not closed"
  // TODO: 'var(--colorRed)' -> TODO: define a special color for this
  const bayColor = bayPercent === 0 ? 'var(--colorGreen)' : (bayPercent < 90 ? 'var(--colorBaySemi)' : 'var(--colorRed)')

  const baySensors = bayData.sensors.map((x, idx, arr) => {
    const reasonClass = reasonToClassDot(x.reason)
    const title = (idx === 0 ? props.fmt('{mixerBayToClose}') : (idx === (arr.length - 1) ? props.fmt('{mixerBayToOpen}') : (props.fmt('{mixerBayTo}') + ' ' + x.position)))
    const icon = x.is_virtual ? 'fa-dot-circle' : 'fa-circle'
    return h('.mixerSensor.isOnDark.isButton.isControl' + (props.xxx_keys.CTRL ? '.hasCTRL' : '') + (x.state === 1 ? '.signalActive' : '') + reasonClass, {
      key: 'sensor-' + x.position,
      onMouseDown: _e => callApi([id, 'go_to_open' + bayData.cont_or_not + '_manual', x.position]),
      title
    }, h('i.fa.' + icon))
  })

  const sizeCoeficientHeight = 21
  const mixerBayContentHeight = (sizeCoeficientHeight * 0.04) + 'vh'

  return h('.mixerBay', {}, [
    h('.mixerBayControl', {}, [
      h('', {
        className: closingClass,
        onMouseDown: _e => callApi([id, 'set_direction' + bayData.cont_or_not + '_manual', -1]),
        onMouseUp: _e => callApi([id, 'unset_direction' + bayData.cont_or_not + '_manual']),
        title: props.fmt('{mixerBayClose}')
      }, semiNodeButtonClose),
      h('.mixerEmptyingDurationLeft', {}, emptyingSemiDurationLeft),
      h('', {
        className: mixerLockClass,
        onMouseDown: _e => callApi([id, 'lock_bay_toggle']),
        title: props.fmt('{mixerBayBlock}')
      }, h('i', { className: mixerLockIcon })),
      h('.mixerEmptyingDurationLeft', {}, emptyingFullDurationLeft),
      // h(".mixerEmptyingDurationLeft", {}, emptyingCountdown),
      h('', {
        className: openingClass,
        onMouseDown: _e => callApi([id, 'set_direction' + bayData.cont_or_not + '_manual', 1]),
        onMouseUp: _e => callApi([id, 'unset_direction' + bayData.cont_or_not + '_manual']),
        title: props.fmt('{mixerBayOpen}')
      }, semiNodeButtonOpen)
    ]),
    h('.mixer-bay', {}, [
      h('.mixer-bay-side'),
      h('.mixer-bay-middle', {}, [
        h('.mixerSensors', {}, baySensors),
        h('.mixer-bay-content', { style: { height: mixerBayContentHeight } }, [
          bayPercent !== null
          ? h('.mixer-bay-opening', { style: { width: bayWidth, backgroundColor: bayColor } })
          : null,
        ])
      ]),
      h('.mixer-bay-side')
    ])
  ])
}

const renderNodeMixers = props => {
  gloDebugReact && incDebugRenderCount('NodeMixers', props)
  const dfs = props.data
  const id = dfs.id
  const consistencyPercent = dfs.consistency_percent
  const consistencyStr = dfs.consistency_str
  // const consistencyHyperStr = dfs.consistency_hyper_str;
  const isCoverOpen = dfs.is_cover_open
  const coverClass = 'mixer-cover' + (isCoverOpen ? ' open' : ' close')
  const batchSize = dfs._vol_str
  const batchState = dfs.seq_tot
  const mixingDurationLeft = dfs.countdown
  // const emptyingSemiDurationLeft = dfs.countdown_semi;
  // const emptyingFullDurationLeft = dfs.countdown_full;
  // const emptyingCountdown = dfs.emptying_countdown;

  const canCallApi = (CTRL_ENABLED && props.xxx_keys.CTRL) || props.xxx_state_frozen

  const spz = dfs._veh_id
  const exhaustAvailable = dfs.can_exhaust
  const exhaustClass = 'mixer-exhaust-button isOnDark isButton isControl needsCTRL'
    + (dfs._is_exhausting === 1 ? ' signalActive' : '')
    + (props.xxx_keys.CTRL ? ' hasCTRL' : '')
    + (props.xxx_state_frozen ? ' isFrozen' : '')
    + reasonToClassSpace(dfs.set_exhaust_reason)
  const filterCleanStates = dfs.sigs_filter_clean_states || []
  const filterClean = [] // TODO: use map
  for (let i = 0; i < filterCleanStates.length; i++) {
    const valveState = filterCleanStates[i]
    const valveTitle = props.fmt('{filterClean}') + ' ' + (i + 1)
    const filterCleanClass = 'mixer-exhaust-button isOnDark isButton isControl needsCTRL'  // TODO: exhaust? really?
      + (props.xxx_keys.CTRL ? ' hasCTRL' : '')
      + (props.xxx_state_frozen ? ' isFrozen' : '')
      + (valveState ? ' signalActive' : '')
      + reasonToClassSpace(dfs.filter_clean_reasons[i])
    filterClean.push(h('', {
      className: filterCleanClass,
      title: valveTitle,
      onMouseDown: _e => canCallApi && callApi([id, 'set_filter_clean_manual', i]),
      onMouseUp: _e => callApi([id, 'unset_filter_clean_manual', i])
    }, h('i.icon-vibration')))
  }
  const hasMoistureProbe = dfs.moisture_str != null
  const moistureStr = hasMoistureProbe ? dfs.moisture_str : ''

  const totalRunDurationStr = dfs.total_run_duration_str
  const sinceLastMaintenanceStr = dfs.since_last_maintenance_str
  const totalRunDurationsStr = props.fmt('{TotalRunDuration}: ' + totalRunDurationStr + '\n{SinceLastMaintenance}: ' + sinceLastMaintenanceStr)

  const sizeCoeficientWidth = 20 * props.xxx_wide_mode_coef
  const sizeCoeficientHeight = 21

  const mixerWidth = sizeCoeficientWidth + 'vw'
  const mixerHeight = exhaustAvailable || filterClean.length > 0 ? (sizeCoeficientHeight * 1) + 'vh' : (sizeCoeficientHeight * 0.9) + 'vh'
  const mixerTopHeight = (sizeCoeficientHeight * 0.05) + 'vh'
  const mixerTop2Height = (sizeCoeficientHeight * 0.11) + 'vh'
  const mixerBodyHeight = (sizeCoeficientHeight * 0.41) + 'vh'
  const mixerBodyToolsHeight = (sizeCoeficientHeight * 0.10) + 'vh'
  const rotation = dfs.is_running
  const isStartingOrStopping = dfs.is_starting_or_stopping
  const mixerTurnOnOff = 'mixerButton isOnDark isButton isControl'
    + (props.xxx_keys.CTRL ? ' hasCTRL' : '')
    + (rotation ? ' red' : ' green')
  const mixerTurnOnOffButtonEvent = 'manual_' + (rotation ? 'stop' : 'start')
  const mixerRotation = (rotation ? 'mixer-run' : 'mixer-idle') + (isStartingOrStopping ? ' reasonAutomat' : '') // TODO: the reasonAutomat is here to somehow indicate motor start phase - maybe there's a cleaner way
  const autostopDurationLeft = dfs.autostop_timeout_to_go
  const canLubricate = dfs.can_lubricate
  //const mixerLubricationClass = ".mixerButton.isOnDark.isButton.isControl"
  const mixerLubricationClass = ".mixer-exhaust-button.isOnDark.isButton.isControl.needsCTRL"  // TODO: exhaust? really?
    + (props.xxx_keys.CTRL ? ".hasCTRL" : "")
    + (props.xxx_state_frozen ? ".isFrozen" : "")
    + (dfs.is_lubricating === 1 ? ".signalActive" : "")
    + reasonToClassDot(dfs.set_lubricate_reason)
  const hasWashing = dfs.has_washing
  const mixerWashingClass = ".mixer-exhaust-button.isOnDark.isButton.isControl.needsCTRL"  // TODO: exhaust? really?
    + (props.xxx_keys.CTRL ? ".hasCTRL" : "")
    + (props.xxx_state_frozen ? ".isFrozen" : "")
    + (dfs.is_washing ? ' signalActive' : (dfs.is_washing_ready ? ' signalActiveAnother' : ''))
    + reasonToClassDot(dfs.set_washing_reason)
  const mixerTemperature = false // TODO
  //const mixerConsistency = 'mixerConsistencyDisplay' + (props.xxx_keys.CTRL ? ' hasCTRL' : '')

  const consistencyPrintsStr = dfs.consistency_prints_str
  //const consistencyGraphW = 35
  //const consistencyGraphH = 300
  //const consistencyGraphPointsStr = dfs._consistency_graph ? dfs._consistency_graph.map(x => x[0].toFixed(2) + "," + (consistencyGraphH - x[1]).toFixed(2)).join(" ") : ""
  //const mixerConsistencyGraphWidth = (sizeCoeficientWidth / 2.25) + 'vw'
  //const mixerConsistencyWidth = (sizeCoeficientWidth / 1.81) - (sizeCoeficientWidth / 2.25) + 'vw'
  //const mixerDisplaysContentWidth = sizeCoeficientWidth - (sizeCoeficientWidth / 4) - ((sizeCoeficientWidth / 1.81) - (sizeCoeficientWidth / 4)) + 'vw'

  //const mixerConsistencyTimeValues = '10s/5050 20s/3440 28s/3040'
  const bays = dfs.bays.map((x, _idx) => rce(NodeMixerBay, { id, bayData: x, xxx_keys: props.xxx_keys, fmt: props.fmt }))
  // interleave bays with spacers - for zero-item case insert just spacer, for single-item case insert spacer before, for multiple-item case insert between element
  const baysAndSpacers = bays.reduce((accum, x, _idx, arr) => {
    if ((arr.length === 1) || (accum.length > 0)) {
      accum.push(h('.mixer-bay-spacer')) // TODO: in-place mutability!
    }
    accum.push(x) // TODO: in-place mutability!
    return accum
  }, dfs.bays.length === 0 ? [h('.mixer-bay-spacer', { style: { height: '4vh' } })] : []) // TODO: the hard-coded style is an ugly hack

  const gaugeValue = consistencyStr
  const gaugeMin = dfs.consistency_gauge_min
  const gaugeMax = dfs.consistency_gauge_max
  const gaugeTarget = dfs.consistency_gauge_target
  const gaugePercent = consistencyPercent
  const gaugeDegrees = gaugePercent * 1.8  // magic constant stepan found that fixes things
  const gaugeDeg = "rotate(" + gaugeDegrees + "deg)"

  return h('.content-mixer', {}, [
    h('.mixer-batch-content', { style: { width: mixerWidth } }, [
      h('.mixerBatch', { title: props.fmt('{sequence}') }, batchState)
    ]),
    h('.mixer-content', { style: { width: mixerWidth, height: mixerHeight } }, [
      h('.mixer-top', { style: { height: mixerTopHeight } }, [
        isCoverOpen != null ? h('', { className: coverClass }) : null
      ]),
      canLubricate || hasWashing || exhaustAvailable || filterClean.length > 0
        ? h('.mixer-top2', { style: { height: mixerTop2Height } }, [
          canLubricate
            ? h(mixerLubricationClass, {
              title: props.fmt("{MixerLubrication}"),
              onMouseDown: _e => canCallApi && callApi([id, 'set_override', 'set_lubricate', 1]),
              onMouseUp: _e => callApi([id, 'set_override', 'set_lubricate'])
            }, h('i.fa.fa-oil-can'))
            : null,
          hasWashing
            ? h(mixerWashingClass, {
              title: props.fmt("{MixerWashing}"),
              onMouseDown: _e => canCallApi && callApi([id, 'set_override', 'set_washing', 1]),
              onMouseUp: _e => callApi([id, 'set_override', 'set_washing'])
            }, h('i.fa.fa-shower'))
            : null,
          h("", {style: {"flex-grow": "1"}}),  // spacer
          filterClean.length > 0 ? filterClean : null,
          exhaustAvailable
            ? h('', {
              className: exhaustClass,
              title: props.fmt('{mixerExhaust}'),
              onMouseDown: _e => canCallApi && callApi([id, 'set_override', 'set_exhaust', 1]),
              onMouseUp: _e => callApi([id, 'set_override', 'set_exhaust'])
            },
            h('i.fa.fa-fan'))
            : null
        ])
        : null,
      h('.mixer-body', {
        style: { height: mixerBodyHeight },
        title: totalRunDurationsStr,
        onClick: _e => callApi([id, 'do_maintenance'])
      }, [
        h(".mixerConsistencyGraph-content", {}, [
          h(".mixerConsistencyPrints", {}, consistencyPrintsStr),
          //h("canvas.mixerConsistencyGraph", { width: "100px" }),
          /*h("svg", {
            viewBox: "0 0 " + consistencyGraphW + " " + consistencyGraphH,
            preserveAspectRatio: "none",
            style: { width: "100%", height: "70px", border: "solid 1px black" },
          }, [
            h("polyline", { stroke: "green", fill: "none", points: consistencyGraphPointsStr }),
          ])*/,
        ]),
        h('.mixerConsistencyDisplay-content', {}, [
          h(".gaugeContent", {
              onClick: e => { callApi([id, 'cons_tare']); e.stopPropagation() },
              title: props.fmt('{mixerConsistency}')
            }, [
            h('.gauge', {}, [
              h(".gauge-percentage", { style: { transform: gaugeDeg } }, []),
              h(".gauge-mask", {}, []),
              h(".gauge-value", {}, [gaugeValue]),
              gaugeTarget ?
              h('.gauge_target', {}, [
                h('i.fa.fa-sort-up'),
              ])
              : null,
            ]),
            h('.gauge_hint', {}, [
              h('.gauge_hint_item1', {}, gaugeMin),
              h('.gauge_hint_item2', {}, gaugeTarget),
              h('.gauge_hint_item3', {}, gaugeMax),
            ]),
          ]),
          // original plain-number consistency
          /*h('.mixerConsistency', {}, [
            h('', {
              className: mixerConsistency,
              onClick: e => { callApi([id, 'cons_tare']); e.stopPropagation() },
              title: props.fmt('{mixerConsistency}')
            }, consistencyStr)
          ]),*/
          // h(".mixerConsistencyHyperDisplay", {title: props.fmt("{mixerConsistencyHyper}")}, consistencyHyperStr),
          hasMoistureProbe ? h('.mixerMoistureProbeDisplay', {}, moistureStr) : null
        ]),
        h('.mixerDisplay-content', {}, [
          h('.mixerBatchSize', { title: props.fmt('{batchVolume}') }, batchSize),
          h('.mixerMixingDurationLeft', { title: props.fmt('{mixingDuration}') }, mixingDurationLeft),
          h('.mixerSpz', { title: props.fmt('{vehicleIdentificationNumber}') }, props.fmt(spz)) // we have to translate the value because of mixer washing - this field is being used to tell the user the mixer is being washed
        ])
      ]),
      h('.mixer-body-tools', { style: { height: mixerBodyToolsHeight } }, [
        h('.mixerToolsControl-content', {}, [
          h('.mixerControlButtons-content', {}, [
            h('.mixerRotationHole', { style: { height: mixerBodyToolsHeight } }, [
              h('.mixerRotationBackground', {}, [
                h('', { className: mixerRotation + ' reverse' })
              ])
            ]),
            h('.mixerAutostopDurationLeft', {}, autostopDurationLeft),
            h('', {
              className: mixerTurnOnOff,
              style: { height: mixerBodyToolsHeight },
              onClick: _e => callApi([id, mixerTurnOnOffButtonEvent]),
              title: props.fmt('{mixerOnOff}')
            }, h('i.fa.fa-power-off')),
            h('.mixerAutostopDurationLeft', {}, ''),  // just for symetry
            h('.mixerRotationHole', { style: { height: mixerBodyToolsHeight } }, [
              h('.mixerRotationBackground', {}, [
                h('', { className: mixerRotation })
              ])
            ])
          ])
        ]),
        mixerTemperature
          //? h('.mixerIcons.red', {}, h('i.fa.fa-thermometer-half.faa-flash.animated'))
          ? h('.mixerIcons.red', {}, h('i.fa.fa-thermometer-half'))
          : null
      ]),
      h('.mixer-body2', {}, baysAndSpacers)
    ])
  ])
}

const renderNodeMixerLift = props => {
  gloDebugReact && incDebugRenderCount('NodeMixerLift', props)
  const dfs = props.data
  const id = dfs.id
  // const sizeCoeficientWidth = 7.3;
  const sizeCoeficientHeight = 21

  const mixerLiftContentWidth = '6.5vw'
  const mixerLiftControlContentHeight = sizeCoeficientHeight + 'vh'
  const isTop = dfs.is_in_empty
  const isSemiTop = dfs.is_in_empty_semi
  const isWait = dfs.is_in_waiting
  const isDown = dfs.is_in_down
  const position = dfs._position
  const sigDirectionUp = dfs.sig_go_up_state
  const sigDirectionDown = dfs.sig_go_down_state
  const mixerCanVibration = dfs.can_vibrate
  const isVibrating = dfs._is_vibrating
  const emptyingSemiDurationLeft = dfs.countdown_semi
  const emptyingFullDurationLeft = dfs.countdown_full

  const strokeCount = dfs.stroke_count
  const strokeCountSinceLastMaintenance = dfs.stroke_count_since_last_maintenance
  const strokeCountStr = props.fmt('{StrokeCount}: ' + strokeCount + '\n{SinceLastMaintenance}: ' + strokeCountSinceLastMaintenance)

  const mixerLiftLockDown = dfs.is_locked_lift_down
  //const mixerLiftLockDownIcon = 'fa' + (mixerLiftLockDown ? ' fa-lock faa-flash animated' : ' fa-lock-open')
  const mixerLiftLockDownIcon = 'fa' + (mixerLiftLockDown ? ' fa-lock' : ' fa-lock-open')
  const mixerLiftLockDownClass = 'mixerLiftLock isButton isControl'
    + (mixerLiftLockDown ? ' red' : ' green')
    + (props.xxx_keys.CTRL ? ' hasCTRL' : '')
  const mixerLiftLockEmpty = dfs.is_locked_lift_empty
  //const mixerLiftLockEmptyIcon = 'fa' + (mixerLiftLockEmpty ? ' fa-lock faa-flash animated' : ' fa-lock-open')
  const mixerLiftLockEmptyIcon = 'fa' + (mixerLiftLockEmpty ? ' fa-lock' : ' fa-lock-open')
  const mixerLiftLockEmptyClass = 'mixerLiftControl isButton isControl'
    + (mixerLiftLockEmpty ? ' red' : ' green')
    + (props.xxx_keys.CTRL ? ' hasCTRL' : '')

  const mixerLiftFail = (position == null) // TODO: what is this?
  const mixerMiddle = 'mixerMiddle' + (position == null ? ' mixerCurrent mixerLiftFailState' : '') + (position > POS_DOWN && position < POS_WAITING ? ' mixerCurrent' : '')

  const mixerTop = 'mixerTop' + (position == POS_EMPTY ? ' mixerCurrent' : '')
  const mixerSemiTop = 'mixerSemiTop' + (position > POS_WAITING && position < POS_EMPTY ? ' mixerCurrentSemiTop' : '')
  const mixerWait = 'mixerWait' + (position == POS_WAITING ? ' mixerCurrent' : '')
  // const mixerMiddle = "mixerMiddle" + (position > 0 && position < 1 ? " mixerCurrent" : "");
  const mixerDown = 'mixerDown' + (position == POS_DOWN ? ' mixerCurrent' : '')

  const cancontrol = props.xxx_keys.CTRL ? ' hasCTRL' : ''
  const mixerTopSensor = 'mixerLiftControl isButton isControl needsCTRL'
    + (isTop ? ' mixerLiftSensorTop' : '')
    + cancontrol
    + (props.xxx_state_frozen ? ' isFrozen' : '')
    + reasonToClassSpace(dfs.lift_sensors.empty.reason)
  const mixerSemiTopSensor = 'mixerLiftControl isButton isControl needsCTRL'
    + (isSemiTop ? ' mixerLiftSensorSemiTop' : '')
    + cancontrol
    + (props.xxx_state_frozen ? ' isFrozen' : '')
    + reasonToClassSpace(dfs.lift_sensors.empty_semi.reason)
  const mixerWaitSensor = 'mixerLiftControl isButton isControl needsCTRL'
    + (isWait ? ' mixerLiftSensorWait' : '')
    + cancontrol
    + (props.xxx_state_frozen ? ' isFrozen' : '')
    + reasonToClassSpace(dfs.lift_sensors.waiting.reason)
  const mixerDownSensor = 'mixerLiftControl isButton isControl needsCTRL'
    + (isDown ? ' mixerLiftSensorDown' : '')
    + cancontrol
    + (props.xxx_state_frozen ? ' isFrozen' : '')
    + reasonToClassSpace(dfs.lift_sensors.down.reason)
  const directionUpReasonClass = (dfs.lift_set_direction_manual === 1 ? ' reasonManual' : (dfs.lift_set_direction === 1 ? ' reasonAutomat' : ''))
  const directionUp = 'mixerLiftControl isButton isControl needsCTRL'
    + (sigDirectionUp ? ' signalActive' : '')
    + cancontrol
    + (props.xxx_state_frozen ? ' isFrozen' : '')
    + directionUpReasonClass
  const directionDownReasonClass = (dfs.lift_set_direction_manual === -1 ? ' reasonManual' : (dfs.lift_set_direction === -1 ? ' reasonAutomat' : ''))
  const directionDown = 'mixerLiftControl isButton isControl needsCTRL'
    + (sigDirectionDown ? ' signalActive' : '')
    + cancontrol
    + (props.xxx_state_frozen ? ' isFrozen' : '')
    + directionDownReasonClass
  const mixerStop = 'mixerLiftControl mixerLiftControlStop isButton isControl needsCTRL'
    + cancontrol
    + (props.xxx_state_frozen ? ' isFrozen' : '')
  const mixerLiftVibrationButton = 'mixerLiftControl isButton isControl'
    + (isVibrating ? ' signalActive' : '')
    + cancontrol
    + reasonToClassSpace(dfs.set_vibrate_reason)

  const canCallApi = (CTRL_ENABLED && props.xxx_keys.CTRL) || props.xxx_state_frozen

  return h('.mixer-lift-content', { style: { width: mixerLiftContentWidth, height: mixerLiftControlContentHeight } }, [
    h('.mixer-positions-content', {}, [
      h('', { className: mixerTop }),
      h('', { className: mixerSemiTop }),
      h('', { className: mixerWait }),
      h('', { className: mixerMiddle }, [
        mixerLiftFail
          //? h('i.fa.fa-exclamation.faa-flash.animated.buttonFontSize13vw')
          ? h('i.fa.fa-exclamation.buttonFontSize13vw')
          : null
      ]),
      h('', {
        className: mixerDown,
        title: strokeCountStr,
        onClick: _e => callApi([id, 'reset_stroke_count'])
      })
    ]),
    h('.mixer-rail-content', {}, [
      h('.mixerRail')
    ]),
    h('.mixerLiftControl-content', { style: { height: mixerLiftControlContentHeight } }, [
      mixerCanVibration === 1
        ? h('.buttonFontSize', {
          className: mixerLiftVibrationButton,
          onMouseDown: _e => callApi([id, 'set_override', 'set_vibrate', 1]),
          onMouseUp: _e => callApi([id, 'set_override', 'set_vibrate']),
          title: props.fmt('{mixerLiftVibration}')
        }, h('i.icon-vibration'))
        : null,
      h('', {
        className: mixerLiftLockEmptyClass,
        onClick: _e => callApi([id, 'lock_lift_empty_toggle']),
        title: props.fmt('{mixerLiftLockEmpty}')
      }, h('i', { className: mixerLiftLockEmptyIcon })),
      h('', {
        className: mixerTopSensor,
        onClick: _e => canCallApi && callApi([id, 'go_to_position_manual', POS_EMPTY]),
        title: props.fmt('{mixerLiftToEmpty}')
      }, h('i.fa.fa-circle')),
      isSemiTop == null
        ? null
        : h('', {
          className: mixerSemiTopSensor,
          onClick: _e => canCallApi && callApi([id, 'go_to_position_manual', POS_EMPTY_SEMI])
        }, h('i.fa.fa-circle')),
      isWait == null
        ? null
        : h('', {
          className: mixerWaitSensor,
          onClick: _e => canCallApi && callApi([id, 'go_to_position_manual', POS_WAITING]),
          title: props.fmt('{mixerLiftToWaiting}')
        }, h('i.fa.fa-circle')),
      h('.buttonFontSize1vw', {
        className: directionUp,
        onMouseDown: _e => canCallApi && callApi([id, 'set_lift_direction_manual', 1]),
        onMouseUp: _e => callApi([id, 'unset_lift_direction_manual']),
        title: props.fmt('{mixerLiftUp}')
      }, h('i.fa.fa-angle-double-up')),
      h('.buttonFontSizeSmaller', {
        className: mixerStop,
        onClick: _e => canCallApi && callApi([id, 'stop_lift']),
        title: props.fmt('{mixerLiftStop}')
      }, h('i.far.fa-dot-circle')),
      h('.buttonFontSize1vw', {
        className: directionDown,
        onMouseDown: _e => canCallApi && callApi([id, 'set_lift_direction_manual', -1]),
        onMouseUp: _e => callApi([id, 'unset_lift_direction_manual']),
        title: props.fmt('{mixerLiftDown}')
      }, h('i.fa.fa-angle-double-down')),
      h('', {
        className: mixerDownSensor,
        onClick: _e => canCallApi && callApi([id, 'go_to_position_manual', POS_DOWN]),
        title: props.fmt('{mixerLiftToDown}')
      }, h('i.fa.fa-circle'))
    ]),
    h('.mixerLiftDurationLeft-content', { style: { height: mixerLiftControlContentHeight } }, [
      h('.mixerLiftDurationLeft', {}, emptyingFullDurationLeft),
      h('.mixerLiftDurationLeft', {}, emptyingSemiDurationLeft),
      h('.mixerLiftLock-content', {}, [
        h('', {
          className: mixerLiftLockDownClass,
          onClick: _e => callApi([id, 'lock_lift_down_toggle']),
          title: props.fmt('{mixerLiftLockDown}')
        }, h('i', { className: mixerLiftLockDownIcon }))
      ])
    ])
  ])
}

const renderNodeLifts = props => {
  gloDebugReact && incDebugRenderCount('NodeLifts', props)
  const dfs = props.data
  const id = dfs.id
  const isTop = dfs.is_in_empty
  const isSemiTop = dfs.is_in_empty_semi
  const isWait = dfs.is_in_waiting
  const isDown = dfs.is_in_down
  const position = dfs._position
  const sigDirectionUp = dfs.sig_go_up_state
  const sigDirectionDown = dfs.sig_go_down_state
  const liftCanVibration = dfs.can_vibrate
  const isVibrating = dfs._is_vibrating
  const topDuration = dfs.countdown_top_str
  const sequence = dfs.seq_tot
  // const val = dfs.val_str;
  const scaleValueStr = dfs.scale_val_str
  const liftIsNonEmpty = dfs._is_nonempty ? ' liftNonEmpty' : ''
  const bottomDuration = dfs.countdown_bottom_str

  const strokeCount = dfs.stroke_count
  const strokeCountSinceLastMaintenance = dfs.stroke_count_since_last_maintenance
  const strokeCountStr = props.fmt('{StrokeCount}: ' + strokeCount + '\n{SinceLastMaintenance}: ' + strokeCountSinceLastMaintenance)

  const sizeCoeficientWidth = 18 * props.xxx_wide_mode_coef
  const sizeCoeficientHeight = 34
  const liftWidthWithSpace = sizeCoeficientWidth + 1 + 'vw'
  const liftWidth = sizeCoeficientWidth + 'vw'
  const liftHeight = sizeCoeficientHeight + 'vh'
  const liftBottomDurationMarginTop = '30vh'
  const liftDownDisplayMarginTop = '24vh'
  const liftDownDisplayMarginLeft = '0.1vw'

  const liftTopSensorReasonClass = reasonToClassSpace(dfs.lift_sensors.empty.reason)
  const liftTopSensor = 'liftControl isButton isControl needsCTRL' + (isTop ? ' signalActive' : '') + (props.xxx_keys.CTRL ? ' hasCTRL' : '') + (props.xxx_state_frozen ? ' isFrozen' : '') + liftTopSensorReasonClass
  const liftSemiTopSensorReasonClass = reasonToClassSpace(dfs.lift_sensors.empty_semi.reason)
  const liftSemiTopSensor = 'liftControl isButton isControl needsCTRL' + (isSemiTop ? ' signalActive' : '') + (props.xxx_keys.CTRL ? ' hasCTRL' : '') + (props.xxx_state_frozen ? ' isFrozen' : '') + liftSemiTopSensorReasonClass
  const liftWaitSensorReasonClass = reasonToClassSpace(dfs.lift_sensors.waiting.reason)
  const liftWaitSensor = 'liftControl isButton isControl needsCTRL' + (isWait ? ' signalActive' : '') + (props.xxx_keys.CTRL ? ' hasCTRL' : '') + (props.xxx_state_frozen ? ' isFrozen' : '') + liftWaitSensorReasonClass
  const liftDownSensorReasonClass = reasonToClassSpace(dfs.lift_sensors.down.reason)
  const liftDownSensor = 'liftControl isButton isControl needsCTRL' + (isDown ? ' signalActive' : '') + (props.xxx_keys.CTRL ? ' hasCTRL' : '') + (props.xxx_state_frozen ? ' isFrozen' : '') + liftDownSensorReasonClass
  const liftVibrationReasonClass = reasonToClassSpace(dfs.set_vibrate_reason)
  const liftVibrationButton = 'liftControl isButton isControl' + (props.xxx_keys.CTRL ? ' hasCTRL' : '') + (isVibrating ? ' signalActive' : '') + liftVibrationReasonClass
  const directionUpReasonClass = (dfs.lift_set_direction_manual === 1 ? ' reasonManual' : (dfs.lift_set_direction === 1 ? ' reasonAutomat' : ''))
  const directionUp = 'liftControl isButton isControl needsCTRL' + (props.xxx_keys.CTRL ? ' hasCTRL' : '') + (props.xxx_state_frozen ? ' isFrozen' : '') + (sigDirectionUp ? ' signalActive' : '') + directionUpReasonClass
  const directionDownReasonClass = (dfs.lift_set_direction_manual === -1 ? ' reasonManual' : (dfs.lift_set_direction === -1 ? ' reasonAutomat' : ''))
  const directionDown = 'liftControl isButton isControl needsCTRL' + (props.xxx_keys.CTRL ? ' hasCTRL' : '') + (props.xxx_state_frozen ? ' isFrozen' : '') + (sigDirectionDown ? ' signalActive' : '') + directionDownReasonClass
  const liftStop = 'liftControl liftControlStop isButton isControl needsCTRL' + (props.xxx_keys.CTRL ? ' hasCTRL' : '') + (props.xxx_state_frozen ? ' isFrozen' : '')

  const liftTop = 'liftTop' + (position >= POS_EMPTY ? (' liftCurrentTop' + liftIsNonEmpty) : '')
  const liftSemiTop = 'liftSemiTop' + (position > POS_WAITING && position < POS_EMPTY ? (' liftCurrentSemiTop' + liftIsNonEmpty) : '')
  const liftWait = 'liftWait' + (position == POS_WAITING ? (' liftCurrent' + liftIsNonEmpty) : '')
  const liftMiddle = 'liftMiddle' + (position == null || position == POS_TOTAL ? ' liftCurrent liftFailState' : '') + (position > POS_DOWN && position < POS_WAITING ? (' liftCurrent' + liftIsNonEmpty) : '')
  const liftDown = 'liftDown' + (position == POS_DOWN ? (' liftCurrent' + liftIsNonEmpty) : '')
  const liftDownDisplayVisibility = (position == POS_DOWN ? 'visible' : 'hidden')
  const liftFail = (position == null || position == POS_TOTAL)

  const liftLockDown = dfs.is_locked_lift_down
  const liftLockDownIcon = 'fa' + (liftLockDown ? ' fa-lock liftLockRotated' : ' fa-lock-open liftLockRotated')
  const liftLockDownClass = 'liftControl isButton isControl'
    + (liftLockDown ? ' red' : ' green')
    + (props.xxx_keys.CTRL ? ' hasCTRL' : '')
  const liftLockEmpty = dfs.is_locked_lift_empty
  const liftLockEmptyIcon = 'fa' + (liftLockEmpty ? ' fa-lock liftLockRotated' : ' fa-lock-open liftLockRotated')
  const liftLockEmptyClass = 'liftControl isButton isControl'
    + (liftLockEmpty ? ' red' : ' green')
    + (props.xxx_keys.CTRL ? ' hasCTRL' : '')

  const canCallApi = (CTRL_ENABLED && props.xxx_keys.CTRL) || props.xxx_state_frozen

  return h('.content-lift', { style: { width: liftWidthWithSpace, height: liftHeight, marginLeft: '1vw' } }, [
    h('.lift-content', { style: { width: liftWidth, height: liftHeight } }, [
      h('.liftDisplay', { style: { marginTop: liftDownDisplayMarginTop, marginLeft: liftDownDisplayMarginLeft, visibility: liftDownDisplayVisibility } }, scaleValueStr),
      h('.liftTopDuration', { style: { marginLeft: liftWidth } }, topDuration),
      h('.liftBottomContent', { style: { marginTop: liftBottomDurationMarginTop } }, [
        h('.liftBottomSequence', {}, sequence),
        h('.liftBottomDuration', {}, bottomDuration)
      ]),
      h('', {
        className: liftDown,
        title: strokeCountStr,
        onClick: _e => callApi([id, 'reset_stroke_count'])
      }),
      h('', { className: liftMiddle }, liftFail
        //? h('i.fa.fa-question.faa-flash.animated.buttonFontSize13vw')
        ? h('i.fa.fa-question.buttonFontSize13vw')
        : null),
      h('', { className: liftWait }),
      h('', { className: liftSemiTop }),
      h('', { className: liftTop }),
      h('.liftRail-content', {}, [
        h('.liftRail'),
        h('.liftControl-content', {}, [
          liftCanVibration === 1
            ? h('.buttonFontSize', {
              className: liftVibrationButton,
              onMouseDown: _e => callApi([id, 'set_override', 'set_vibrate', 1]),
              onMouseUp: _e => callApi([id, 'set_override', 'set_vibrate']),
              title: props.fmt('{liftVibration}')
            }, h('i.icon-vibration'))
            : null,
          h('', {
            className: liftLockEmptyClass,
            onClick: _e => callApi([id, 'lock_lift_empty_toggle']),
            title: props.fmt('{liftLockEmpty}')
          }, h('i', { className: liftLockEmptyIcon })),
          h('', {
            className: liftTopSensor,
            onClick: _e => canCallApi && callApi([id, 'go_to_position_manual', POS_EMPTY]),
            title: props.fmt('{liftToEmpty}')
          }, h('i.fa.fa-circle')),
          isSemiTop != null
            ? h('', {
              className: liftSemiTopSensor,
              onClick: _e => canCallApi && callApi([id, 'go_to_position_manual', POS_EMPTY_SEMI])
            }, h('i.fa.fa-circle'))
            : null,
          isWait != null
            ? h('', {
              className: liftWaitSensor,
              onClick: _e => canCallApi && callApi([id, 'go_to_position_manual', POS_WAITING]),
              title: props.fmt('{liftToWaiting}')
            }, h('i.fa.fa-circle'))
            : null,
          h('.buttonFontSizeCalc07', {
            className: directionUp,
            onMouseDown: _e => canCallApi && callApi([id, 'set_lift_direction_manual', 1]),
            onMouseUp: _e => callApi([id, 'unset_lift_direction_manual']),
            title: props.fmt('{liftUp}')
          }, h('i.fa.fa-angle-double-up')),
          h('.buttonFontSizeCalc06', {
            className: liftStop,
            onClick: _e => canCallApi && callApi([id, 'stop_lift']),
            title: props.fmt('{liftStop}')
          }, h('i.far.fa-dot-circle')),
          h('.buttonFontSizeCalc07', {
            className: directionDown,
            onMouseDown: _e => canCallApi && callApi([id, 'set_lift_direction_manual', -1]),
            onMouseUp: _e => callApi([id, 'unset_lift_direction_manual']),
            title: props.fmt('{liftDown}')
          }, h('i.fa.fa-angle-double-down')),
          h('', {
            className: liftDownSensor,
            onClick: _e => canCallApi && callApi([id, 'go_to_position_manual', POS_DOWN]),
            title: props.fmt('{liftToDown}')
          }, h('i.fa.fa-circle')),
          h('', {
            className: liftLockDownClass,
            onClick: _e => callApi([id, 'lock_lift_down_toggle']),
            title: props.fmt('{liftLockDown}')
          }, h('i', { className: liftLockDownIcon }))
        ])
      ])
    ])
  ])
}

const renderNodeConveyers = props => {
  gloDebugReact && incDebugRenderCount('NodeConveyers', props)
  const dfs = props.data
  const id = dfs.id
  const silosLength = _.get(props.precalc.binsAndFlowmeters, ['Aggregate', id, 'silos']) ? _.get(props.precalc.binsAndFlowmeters, ['Aggregate', id, 'silos']).length : 0 // TODO: better in future
  const flowmetersLength = _.get(props.precalc.binsAndFlowmeters, ['Aggregate', id, 'flowmeters'], []).length
  const silosCount = silosLength > 0 ? silosLength : (flowmetersLength > 0 ? flowmetersLength : 1) // TODO: better in future
  const isRunning = dfs.is_running
  const conveyerType = dfs._is_inclined ? 'inclined' : 'horizontal' // TODO: get rid of this shit
  const conveyerIsFlowmeter = dfs.flow_str != null
  const isScrewConveyer = dfs._type === 'Screw'
  const flow = dfs.flow_str
  const scale = dfs.scale
  const scaleVal = dfs.scale_val_str
  const scaleReqVal = dfs.scale_req_str
  const batchState = dfs.seq_tot
  const fillPercentScale = dfs.scale_fill_within_bin_percent
  const horn = props.xxx_horn ? props.xxx_horn.id : null
  const hornEnable = (props.xxx_horn && props.xxx_horn.is_enabled) ? ' signalActive' : ''
  const conveyerControlMalfunctionHorn = 'conveyerIcon' + hornEnable
  // const conveyerRunState = "conveyerIcon " + (isRunning ? "green" : "");
  // const conveyerRunIcon = "fas fa-cog" + (isRunning ? " fa-spin" : "");
  const flushState = dfs._is_flushing
  const flushAvailable = dfs.can_flush
  const conveyerFlushReasonClass = reasonToClassSpace(dfs.set_flush_reason)
  const conveyerControlFlushButton = 'conveyerControl isButton isControl needsCTRL' + (flushState ? ' signalActive' : '') + (props.xxx_keys.CTRL ? ' hasCTRL' : '') + (props.xxx_state_frozen ? ' isFrozen' : '') + conveyerFlushReasonClass
  const vibrationState = dfs._is_vibrating
  const vibrationAvailable = dfs.can_vibrate
  const vibrationReasonClass = reasonToClassSpace(dfs.set_vibrate_reason)
  const conveyerControlVibrationButton = 'conveyerControl isButton isControl' + (vibrationState ? ' signalActive' : '') + (props.xxx_keys.CTRL ? ' hasCTRL' : '') + (props.xxx_state_frozen ? ' isFrozen' : '') + vibrationReasonClass
  const materialOnConveyer = dfs.is_empty_raw != null
  const materialOnConveyerEnable = dfs.is_empty_raw === 0
  const materialOnConveyerIconClass = 'fas fa-ellipsis-h' + (materialOnConveyerEnable ? ' signalActive' : '')

  const requirementState = dfs._set_requirement
  const requirementGranted = dfs.is_granted

  const displayContent = 'conveyerDisplayContent-' + conveyerType
  const hasOutputFlap = dfs.has_output_flap
  const hasOutputFlap2 = dfs.has_output_flap2
  const outputFlapState = dfs.is_output_flap_open
  const outputFlap2State = dfs.is_output_flap2_open
  const outputFlapClass = outputFlapState ? 'conveyerFlapOpened' : 'conveyerFlap'
  const outputFlap2Class = outputFlap2State ? 'conveyerFlapOpened' : 'conveyerFlap'
  const outputFlapButton = 'conveyerFlapButtons isButton isControl needsCTRL' + (props.xxx_keys.CTRL ? ' hasCTRL' : '') + (props.xxx_state_frozen ? ' isFrozen' : '')
  const outputFlap2Button = 'conveyerFlapButtons isButton isControl needsCTRL' + (props.xxx_keys.CTRL ? ' hasCTRL' : '') + (props.xxx_state_frozen ? ' isFrozen' : '')

  const conveyerClass = 'conveyer-' + conveyerType
  /* const isSlow = d.is_slow */
  const conveyerRightToLeft = dfs._right_to_left
  const conveyerRunClass = isRunning ? ('conveyer-run' /* + (isSlow ? ' slow' : '') */ + (conveyerRightToLeft ? ' reverse' : '')) : 'conveyer-idle'

  const conveyerRunReasonClass = reasonToClassSpace(dfs.set_open_reason)
  const conveyerControlStartButton = 'conveyerControl isButton isControl needsCTRL' + (isRunning ? ' red' : ' green') + (props.xxx_keys.CTRL ? ' hasCTRL' : '') + (props.xxx_state_frozen ? ' isFrozen' : '') + conveyerRunReasonClass
  const conveyerMoveMaterialReasonClass = reasonToClassSpace(dfs.set_move_material_reason)
  const conveyerControlMoveMaterialButton = 'conveyerControl isButton isControl' + (dfs.sig_start_state ? ' signalActive' : '') + (props.xxx_state_frozen ? ' isFrozen' : '') + conveyerMoveMaterialReasonClass
  const conveyerContent = 'conveyer-' + conveyerType + '-content'
  const conveyerControlContent = 'conveyer-' + conveyerType + '-control-content' + (conveyerRightToLeft ? '-righttoleft' : '')

  const sizeCoeficientWidth = dfs._is_inclined
    ? 15
    : (dfs.sink === 'Final'
      ? 20
      : (silosCount * 5) + (silosCount * 0.05) + (silosCount > 1 ? 0.5 : 0)
    ) * props.xxx_wide_mode_coef
  const sizeCoeficientHeight = dfs._is_inclined ? 34 : 10
  const conveyerWidthWithSpace = sizeCoeficientWidth + 0.2 + 'vw'
  const conveyerWidth = sizeCoeficientWidth + 'vw'
  const conveyerHeight = sizeCoeficientHeight + 'vh'

  const scaleBar = 'conveyer-scale-bar Aggregate'
  const levelScale = 'linear-gradient(to right, var(--colorWeightProgressAggregate) 0% ' + fillPercentScale + '%, var(--colorWeightAggregate) ' + fillPercentScale + '% 100%)'

  const canCallApi = (CTRL_ENABLED && props.xxx_keys.CTRL) || props.xxx_state_frozen

  return h('.contentConveyer', { style: { width: conveyerWidthWithSpace, height: conveyerHeight, marginLeft: dfs._is_inclined ? '1vw' : 0 } }, [
    h('.content-conveyer', { style: { width: conveyerWidth, height: conveyerHeight } }, [
      dfs._is_inclined // TODO: we should check for flap presence, not "is inclined"
        ? h('.conveyerFlaps', {}, [
          hasOutputFlap2
            ? h('.conveyerFlapL', {}, [
              h('', {
                className: outputFlap2Button,
                onMouseDown: _e => canCallApi && callApi([id, 'output_flap2_toggle'])
              }, [
                outputFlap2State ? h('i.fa.fa-arrow-up') : h('i.fa.fa-arrow-down')
              ]),
              h('', { className: outputFlap2Class })
            ])
            : null,
          hasOutputFlap
            ? h('.conveyerFlapR', {}, [
              h('', { className: outputFlapClass }),
              h('', {
                className: outputFlapButton,
                onMouseDown: _e => canCallApi && callApi([id, 'output_flap_toggle'])
              }, [
                outputFlapState ? h('i.fa.fa-arrow-up') : h('i.fa.fa-arrow-down')
              ])
            ])
            : null
        ])
        : null,
      h('.conveyer-content', { style: { width: conveyerWidth, height: conveyerHeight } }, [
        h('', { className: conveyerContent }, [
          h('', { className: displayContent }, [
            materialOnConveyer
              ? h('i', {
                className: materialOnConveyerIconClass,
                title: props.fmt('{conveyerHasMaterial}')
              })
              : null,
            requirementState != null
              ? h('.bin-top-request', {}, requirementState ? props.fmt('{requested}') : '')
              : h(''),
            requirementGranted != null
              ? h('.bin-top-request-accepted', {}, requirementGranted ? props.fmt('{switched}') : '')
              : h(''),
            scale ? h('.conveyerBatchStateDisplay', {}, batchState) : null
          ]),
          scale || conveyerIsFlowmeter
            ? h('', { className: scaleBar, style: { background: levelScale } })
            : null,
          h('', { className: conveyerClass }, [
            h('.conveyerMaterial', {
              style: {
                'width': dfs._material_width_percent + '%',
                'margin-left': dfs._material_left_percent + '%',
              }}),
            h('.conveyerRotationBackground', {}, [
              h('', { className: conveyerRunClass }),
            ]),
          ]),
          h('', { className: conveyerControlContent }, [
            h('.buttonFontSizeCalc06', {
              className: conveyerControlStartButton,
              title: props.fmt('{conveyerOnOff}'),
              onMouseDown: _e => canCallApi && callApi([id, 'toggle_open_invert'])
            }, h('i.far.fa-dot-circle')),
            (conveyerIsFlowmeter != true && isScrewConveyer != true)
              ? h('.buttonFontSizeCalc08', {
                className: conveyerControlMoveMaterialButton,
                title: props.fmt('{conveyerMoveMaterial}'),
                onMouseDown: _e => callApi([id, 'set_override', 'set_move_material', 1]),
                onMouseUp: _e => callApi([id, 'set_override', 'set_move_material'])
              }, h('i.fa.fa-angle-double-' + (conveyerRightToLeft ? 'left' : 'right')))
              : null,
            flushAvailable
              ? h('.buttonFontSizeSmaller', {
                className: conveyerControlFlushButton,
                title: props.fmt('{flushConveyer}'),
                onMouseDown: _e => canCallApi && callApi([id, 'set_override', 'set_flush', 1]),
                onMouseUp: _e => callApi([id, 'set_override', 'set_flush'])
              }, h('i.fas.fa-tint'))
              : null,
            vibrationAvailable
              ? h('.buttonFontSize', {
                className: conveyerControlVibrationButton,
                title: props.fmt('{vibrationConveyer}'),
                onMouseDown: _e => callApi([id, 'set_override', 'set_vibrate', 1]),
                onMouseUp: _e => callApi([id, 'set_override', 'set_vibrate'])
              }, h('i.icon-vibration'))
              : null,
            horn
              ? h('.buttonFontSizeSmaller', {
                className: conveyerControlMalfunctionHorn,
                title: props.fmt('{' + horn + '}')
              }, h('i.fa.fa-bullhorn'))
              : null,
            scale
              ? h('.conveyerDisplaysContent', {}, [
                h('.conveyerRequestDisplay', {}, scaleReqVal),
                h('.conveyerDisplay', {}, scaleVal)
              ])
              : null,
            conveyerIsFlowmeter
              ? h('.conveyerFlowDisplaysContent', {}, [
                h('.conveyer-delta', {}, DELTA),
                h('.conveyerFlowDisplay', {}, flow)
              ])
              : null
          ])
        ])
      ])
    ])
  ])
}

const renderNodeFreePaths = props => {
  gloDebugReact && incDebugRenderCount('NodeFreePaths', props)
  const d = props.data
  const sizeCoeficientWidth = 20 * props.xxx_wide_mode_coef
  const contentWidth = sizeCoeficientWidth + 'vw'
  const freePath = d._freepath
  const freePathClass = '.content-free-path.' + (freePath ? '.greenDark' : '.redDark')
  return h('freePaths-content', { style: { width: contentWidth } }, [
    h(freePathClass, {}, [
      h('i.fas.fa-shopping-cart')
    ])
  ])
}

// TODO: unfinished
// TODO: make this a component
/*const sensorHelper = props => {
  const reasonClass = reasonToClassDot(props.reason)
  //const title = (idx === 0 ? props.fmt('{mixerBayToClose}') : (idx === (arr.length - 1) ? props.fmt('{mixerBayToOpen}') : (props.fmt('{mixerBayTo}') + ' ' + x.position)))
  const iconClass = '.fa' + (x.is_virtual ? '.fa-dot-circle' : '.fa-circle')
  const activeClass = (x.state === 1 ? '.signalActive' : '')
  return h('.sensorButton.isButton.isControl.needsCTRL' + (props.xxx_keys.CTRL ? '.hasCTRL' : '') + reasonClass, {
    onMouseDown: props.onMouseDown,
    title: props.title,
  }, h('i.sensorLed' + activeClass + iconClass))
}*/

const renderNodeCart = props => {
  gloDebugReact && incDebugRenderCount('NodeCart', props)
  const d = props.data
  const id = d.id
  const seqTot = d.seq_to
  //const sizeCoeficientWidth = 20 * props.xxx_wide_mode_coef
  //const contentWidth = sizeCoeficientWidth + 'vw'

  const canCallApi = (CTRL_ENABLED && props.xxx_keys.CTRL) || props.xxx_state_frozen

  const openSensorClass = 'sensorButton isButton isControl'
    + (props.xxx_keys.CTRL ? ' hasCTRL' : '')
    //+ directionOpenReasonClass
  const openSensorIconClass = d.sig_is_open_state === 1 ? ' signalActive' : ''
    //+ directionOpenReasonClass
  const openingClass = 'sensorButton isButton isControl'
    + (props.xxx_keys.CTRL ? ' hasCTRL' : '')
    //+ directionOpenReasonClass
  const openingIconClass = d.sig_open_state === 1 ? ' signalActive' : ''
  const closedSensorClass = 'sensorButton isButton isControl'
    + (props.xxx_keys.CTRL ? ' hasCTRL' : '')
    //+ directionOpenReasonClass
  const closedSensorIconClass = d.sig_is_closed_state === 1 ? ' signalActive' : ''
    //+ directionOpenReasonClass
  const closingClass = 'sensorButton isButton isControl'
    + (props.xxx_keys.CTRL ? ' hasCTRL' : '')
    //+ directionCloseReasonClass
  const closingIconClass = d.sig_close_state === 1 ? ' signalActive' : ''

  const directionUpReasonClass = (d.set_direction_manual === 1 ? ' reasonManual' : (d.set_direction === 1 ? ' reasonAutomat' : ''))
  const directionDownReasonClass = (d.set_direction_manual === -1 ? ' reasonManual' : (d.set_direction === -1 ? ' reasonAutomat' : ''))
  const upClass = 'sensorButton isButton isControl'
    + (props.xxx_keys.CTRL ? ' hasCTRL' : '')
    + directionUpReasonClass
  const upIconClass = d.sig_is_going_up_state === 1 ? ' signalActive' : ''
  const downClass = 'sensorButton isButton isControl'
    + (props.xxx_keys.CTRL ? ' hasCTRL' : '')
    + directionDownReasonClass
  const downIconClass = d.sig_is_going_down_state === 1 ? ' signalActive' : ''
  //const cartStop = 'liftControl liftControlStop isButton isControl needsCTRL' + (props.xxx_keys.CTRL ? ' hasCTRL' : '') + (props.xxx_state_frozen ? ' isFrozen' : '')
  const cartStop = 'sensorButton cartControlStop isButton isControl needsCTRL' + (props.xxx_keys.CTRL ? ' hasCTRL' : '') + (props.xxx_state_frozen ? ' isFrozen' : '')

  //const isNonEmptyClass = d.is_empty ? ' liftNonEmpty' : ''
  const isNonEmptyClass = d.is_empty ? '' : ' liftNonEmpty'

  const sensors = d.sensors.map((x, _idx, _arr) => {
    const reasonClass = reasonToClassDot(x.reason)
    //const title = (idx === 0 ? props.fmt('{mixerBayToClose}') : (idx === (arr.length - 1) ? props.fmt('{mixerBayToOpen}') : (props.fmt('{mixerBayTo}') + ' ' + x.position)))
    const title = props.fmt("{cartToPosition} ") + (x.position + 1)  // TODO: use node's human_name? also, i don't like the +1 (it would be better if positions had names such as "hala 1", "pod michackou", "myti" and show these)
    const iconClass = '.fa' + (x.is_virtual ? '.fa-dot-circle' : '.fa-circle')
    const activeClass = (x.state === 1 ? '.signalActive' : '')
    return h('.sensorButton.isButton.isControl.needsCTRL' + (props.xxx_keys.CTRL ? '.hasCTRL' : '') + reasonClass, {
      key: 'sensor-' + x.position,
      onMouseDown: _e => canCallApi && callApi([id, 'go_to_position_manual', x.position]),
      title
    }, h('i.sensorLed' + activeClass + iconClass))
    // TODO: unfinished
    /*return sensorHelper({
      ...x,
      key: 'sensor-' + x.position,
      onMouseDown: _e => canCallApi && callApi([id, 'go_to_position_manual', x.position]),
      title,
    })*/
  })

  return h(".cart.m01vh", {}, [
    //h("", {}, "seq_tot: " + seqTot),
    h(".bin-top-batches", {}, seqTot),
    h(".hflex.sb.m01vh", {}, [
      h('', {
        className: downClass,
        //onMouseDown: _e => callApi([id, 'set_direction_manual', -1]),
        //onMouseUp: _e => callApi([id, 'unset_direction_manual']),
        title: props.fmt('{cartMotionLeft}')
      }, h("i.sensorAngle.fa.fa-angle-double-left", {className: downIconClass})),
      h(".hflex.fontColorButtonText", {}, [
        h("", {
          className: openSensorClass,
          title: props.fmt("{cartToOpen}"),
        }, h("i.sensorLed.fa.fa-circle", {className: openSensorIconClass})),
        h('', {
          className: openingClass,
          //onMouseDown: _e => callApi([id, 'set_direction_manual', 1]),
          //onMouseUp: _e => callApi([id, 'unset_direction_manual']),
          title: props.fmt('{cartOpen}')
        }, semiNodeButtonOpen),
      ]),
      h(".hflex.fontColorButtonText", {}, [
        h("", {
          className: closedSensorClass,
          title: props.fmt("{cartToClose}"),
        }, h("i.sensorLed.fa.fa-circle", {className: closedSensorIconClass})),
        h('', {
          className: closingClass,
          //onMouseDown: _e => callApi([id, 'set_direction_manual', -1]),
          //onMouseUp: _e => callApi([id, 'unset_direction_manual']),
          title: props.fmt('{cartClose}')
        }, semiNodeButtonClose),
      ]),
      //h('.buttonFontSizeCalc06', {
        //className: cartStop,
        //onClick: _e => canCallApi && callApi([id, 'stop_lift']),
        //title: props.fmt('{cartStop}')
      //}, h('i.far.fa-dot-circle')),
      h('', {
        className: upClass,
        //onMouseDown: _e => callApi([id, 'set_direction_manual', 1]),
        //onMouseUp: _e => callApi([id, 'unset_direction_manual']),
        title: props.fmt('{cartMotionRight}')
      }, h("i.sensorAngle.fa.fa-angle-double-right", {className: upIconClass})),
    ]),
    //h("", {}, "is_empty: " + d.is_empty),
    //h("", {}, "pos: " + d._position),
    //h("", {style: {width: "50%"}},
    // TODO: use node's human name for title?
    // TODO: liftCurrent class?
    h(".liftCurrent.m01vh", {className: isNonEmptyClass, title: props.fmt("{cart}"), style: {
      height: "30px",
      width: "40px",
      "--tran": d._position / d.pos_max,
      "--rot": d.open,
      //transform: "translateX(calc(200px * var(--tran))) translateY(calc(-10px * var(--tran))) rotate(calc(180deg * var(--rot)))",
      //transform: "translateX(calc(340px * var(--tran))) rotate(calc(180deg * var(--rot)))",
      //transformOrigin: "bottom right",
      position: "relative",
      //"--perc": d._position / d.pos_max * 100,
      "--ratio": d._position / d.pos_max,
      "--perc": (d._position / d.pos_max * 100) + "%",
      left: "calc(var(--perc) - (var(--ratio) * 40px))",
      transform: "rotate(calc(180deg * var(--rot)))",
    }}),
    //),
    h(".cartRail.m01vh"),
    h(".hflex.sa.m01vh", {}, sensors),
  ])
}

const renderNodeFunnels = props => {
  gloDebugReact && incDebugRenderCount('NodeFunnels', props)
  const dfs = props.data
  const id = dfs.id
  const sizeCoeficientWidth = 20 * props.xxx_wide_mode_coef
  // const sizeCoeficientHeight = 21;
  const funnelWidth = sizeCoeficientWidth + 'vw'
  const funnelOpened = dfs.open
  const truckCounterStr = dfs._truck_counter_str
  const truckFillStr = dfs._truck_fill_str
  const truckChangeCountdown = dfs._truck_change_countdown
  const sizeOfOpen = funnelOpened === 1 ? '35%' : '99%'
  const flushReasonClass = reasonToClassSpace(dfs.set_flush_reason)
  const flush = 'funnelButtons isButton isControl needsCTRL' + (dfs._is_flushing === 1 ? ' signalActive' : '') + (props.xxx_keys.CTRL ? ' hasCTRL' : '') + (props.xxx_state_frozen ? ' isFrozen' : '') + flushReasonClass
  const openStateClass = 'funnelButtons isButton isControl' + (dfs.sig_open_state ? ' signalActive' : '') + (props.xxx_keys.CTRL ? ' hasCTRL' : '') // TODO add reason
  const closeStateClass = 'funnelButtons isButton isControl' + (dfs.sig_close_state ? ' signalActive' : '') + (props.xxx_keys.CTRL ? ' hasCTRL' : '') // TODO add reason
  const isLocked = dfs.is_locked_bay
  const lockStateClass = 'funnelButtons isButton isControl' + (isLocked ? ' signalActive reasonManual' : ' green') + (props.xxx_keys.CTRL ? ' hasCTRL' : '')
  //const lockStateButtonIco = 'fa' + (isLocked ? ' fa-hand-paper faa-flash animated' : ' fa-lock-open')
  const lockStateButtonIco = 'fa' + (isLocked ? ' fa-hand-paper' : ' fa-lock-open')

  return h('.content-funnel', { style: { width: funnelWidth } }, [
    h('.funnel-flaps', {}, [
      h('', {
        className: lockStateClass,
        onMouseDown: _e => callApi([id, 'lock_bay_toggle']),
        title: props.fmt('{funnelBlock}')
      }, [
        h('i', { className: lockStateButtonIco })
      ]),
      h('', {
        className: closeStateClass,
        style: { marginLeft: '0vw' },
        onMouseDown: _e => callApi([id, 'go_to_open_manual', 0]),
        title: props.fmt('{funnelClose}')
      }, semiNodeButtonClose),
      h('.funnel-flap-left', {}, [
        h('.funnel-flapL', { style: { width: sizeOfOpen } })
      ]),
      h('.funnel-flap-right', {}, [
        h('.funnel-flapR', { style: { width: sizeOfOpen } })
      ]),
      h('', {
        className: openStateClass,
        style: { marginRight: '0vw' },
        onMouseDown: _e => callApi([id, 'go_to_open_manual', 1]),
        title: props.fmt('{funnelOpen}')
      }, semiNodeButtonOpen),
      h('', {
        className: flush,
        onMouseDown: _e => callApi([id, 'set_override', 'set_flush', 1]),
        onMouseUp: _e => callApi([id, 'set_override', 'set_flush']),
        title: props.fmt('{funnelFlush}')
      }, [
        h('i.fas.fa-tint')
      ])
    ]),
    h('.funnel-displays', {}, [
      h('.funnel-display-truck-fill', { title: props.fmt("{truckFillTooltip}") }, truckFillStr),
      h('.funnel-display-countdown', {}, truckChangeCountdown),
      h('.funnel-display-truck-counter', { title: props.fmt("{truckCounterTooltip}") }, truckCounterStr),
    ]),
  ])
}

const renderNodeChutes = props => {
  gloDebugReact && incDebugRenderCount('NodeChutes', props)
  const dfs = props.data
  const id = dfs.id
  const canVibrate = dfs.can_vibrate
  const vibrateReasonClass = reasonToClassSpace(dfs.set_vibrate_reason)
  const vibrateClass = 'chuteButtons isButton isControl' + (dfs._is_vibrating === 1 ? ' signalActive' : '') + (props.xxx_keys.CTRL ? ' hasCTRL' : '') + vibrateReasonClass
  const canFlush = dfs.can_flush
  const flushReasonClass = reasonToClassSpace(dfs.set_flush_reason)
  const flushClass = 'chuteButtons isButton isControl' + (dfs._is_flushing === 1 ? ' signalActive' : '') + (props.xxx_keys.CTRL ? ' hasCTRL' : '') + flushReasonClass
  const canExtend = dfs.can_extend
  const position = dfs.extend_percent
  const classPos0 = '.content-chute-extend.chute' + (position == 0 ? '' : '.hidden')
  const classPos05 = '.content-chute-extend.chute' + ((position > 0 && position < 100) ? '' : '.hidden')
  const classPos1 = '.content-chute-extend.chute' + (position == 100 ? '' : '.hidden')
  // TODO: reasons - we should probably just set reason for either extend or unextend, not both?
  const unextendStateClass = 'chuteButtons isButton isControl'
    + (dfs.sig_unextend_state ? ' signalActive' : '')
    + (props.xxx_keys.CTRL ? ' hasCTRL' : '')
    + reasonToClassSpace(dfs.set_extend_reason)
  const extendStateClass = 'chuteButtons isButton isControl'
    + (dfs.sig_extend_state ? ' signalActive' : '')
    + (props.xxx_keys.CTRL ? ' hasCTRL' : '')
    + reasonToClassSpace(dfs.set_extend_reason)

  const theChute = [
    h('.chute-top'),
    h('.chute-bottom-content', {}, [
      h('.chute-bottom.chute-bottom-left'),
      h('.chute-bottom.chute-bottom-right')
    ])
  ]

  return h('.vflex', {}, [
    h('.hflex', {}, [
      canVibrate
        ? h('', {
          className: vibrateClass,
          onMouseDown: _e => callApi([id, 'set_override', 'set_vibrate', 1]),
          onMouseUp: _e => callApi([id, 'set_override', 'set_vibrate']),
          title: props.fmt('{mixerBayVibration}')
        }, [
          h('i.icon-vibration')
        ])
        : null,
      canFlush
        ? h('', {
          className: flushClass,
          onMouseDown: _e => callApi([id, 'set_override', 'set_flush', 1]),
          onMouseUp: _e => callApi([id, 'set_override', 'set_flush']),
          title: props.fmt('{mixerBayFlush}')
        }, [
          h('i.fas.fa-tint')
        ])
        : null
    ]),
    canExtend
      ? h('.content-chute-extend', {}, [
        h(classPos0, {}, theChute),
        h('', {
          className: unextendStateClass,
          //onMouseDown: _e => callApi([id, 'set_override', 'go_to_extend', 0]),
          onMouseDown: _e => callApi([id, 'go_to_extend', 0]),
          title: props.fmt('{chuteUnextend}')
        }, [
          h('i.fa.fa-arrow-left')
        ]),
        h(classPos05, {}, theChute),
        h('', {
          className: extendStateClass,
          //onMouseDown: _e => callApi([id, 'set_override', 'go_to_extend', 1]),
          onMouseDown: _e => callApi([id, 'go_to_extend', 1]),
          title: props.fmt('{chuteExtend}')
        }, [
          h('i.fa.fa-arrow-right')
        ]),
        h(classPos1, {}, theChute)
      ])
      : null
  ])
}

const renderNodeContinuousConcreteSettings = props => {
  gloDebugReact && incDebugRenderCount('NodeContinuousConcreteSettings', props)
  const hourOutput = props.hour_output
  const truckCapacity = props.truck_capacity

  return h('.topContinuousSettingsContent', {}, [
    h('.topContinuousSettingsContentLine', {}, [
      h('.topContinuousSettingsContentText', {}, props.fmt('{HourOutput}')),
      h('.topContinuousSettingsContentButton.isButton.isOnDark', {
        onClick: _e => callApi(['control', 'hour_output_dec'])
      }, [
        h('i.fa.fa-minus')
      ]),
      h('.input.topContinuousSettingsContentInput', {
        title: props.fmt('{HourOutput}')
      }, hourOutput),
      h('.topContinuousSettingsContentButton.isButton.isOnDark', {
        onClick: _e => callApi(['control', 'hour_output_inc'])
      }, [
        h('i.fa.fa-plus')
      ])
    ]),
    h('.topContinuousSettingsContentLine', {}, [
      h('.topContinuousSettingsContentText', {}, props.fmt('{TruckCapacity}')),
      h('.topContinuousSettingsContentButton.isButton.isOnDark', {
        onClick: _e => callApi(['control', 'truck_capacity_dec'])
      }, [
        h('i.fa.fa-minus')
      ]),
      h('.input.topContinuousSettingsContentInput', {
        title: props.fmt('{TruckCapacity}')
      }, truckCapacity),
      h('.topContinuousSettingsContentButton.isButton.isOnDark', {
        onClick: _e => callApi(['control', 'truck_capacity_inc'])
      }, [
        h('i.fa.fa-plus')
      ])
    ]),
    h('.topContinuousSettingsContentLine', {}, [
      h('.topContinuousSettingsContentFlags', {}, [
        h('.topContinuousSettingsContentButton.isButton.isOnDark', {
          onClick: _e => callApi(['control', 'finish']),
          title: props.fmt('{finish}')
        }, [
          h('i.fa.fa-flag-checkered.red')
        ]),
        h('.topContinuousSettingsContentFlagsButton.isButton.isOnDark', {
          onClick: _e => callApi(['control', 'finish_truck']),
          title: props.fmt('{finishTruck}')
        }, [
          h('i.fa.fa-truck'),
          h('i.fa.fa-arrow-right'),
          h('i.fa.fa-flag-checkered')
        ])
      ]),
      h('.topContinuousSettingsContentFlagsButton.isButton.isOnDark', {
        onClick: _e => callApi(['control', 'add_truck']),
        title: props.fmt('{truckAdd}')
      }, [
        h('i.fa.fa-truck.green'),
        h('i.fa.fa-plus.green')
      ])
    ])
  ])
}

const renderNodeOrderOnHold = props => {
  gloDebugReact && incDebugRenderCount('NodeOrderOnHold', props)
  const orderOnHold = props.order_on_hold ? props.fmt('{order_on_hold}') : null
  const stopAfterBatch = props.stop_after_batch ? props.fmt('{stop_after_batch}') : null
  const plusStr = orderOnHold && stopAfterBatch ? ' + ' : null
  return h('.orderOnHold', {}, [orderOnHold, plusStr, stopAfterBatch])
}

const renderNodeFunctionsKey = props => {
  gloDebugReact && incDebugRenderCount('NodeFunctionsKey', props)
  // TODO: the following is kind of hacky
  let xkeys = props.xxx_keys
  if (props.xxx_mixer_is_locked) {
    xkeys = { ...xkeys, F2: true }
  }
  if (props.xxx_state_frozen) {
    xkeys = { ...xkeys, F10: true }
  }
  if (props.xxx_additionalWaterSiloOpen) {
    xkeys = { ...xkeys, F12: true }
  }
  if (props.xxx_horn_is_enabled) {
    xkeys = { ...xkeys, '*': true }
  }

  const ctrlPressed = props.xxx_keys.CTRL ? ' hasCTRL' : ''

  // const sizeCoeficientWidth = 44
  // const contentWidth = sizeCoeficientWidth + 'vw'

  return h('.functionKeys-content', {}, [
    h('.functionsKey-button.isButton.isControl', {
      className: ctrlPressed + (xkeys.F1 ? ' signalActive' : ''),
      title: props.fmt('{F1}'),
      onMouseDown: _e => props.openHelpWindow()
    }, [
      h('i.fas.fa-question'),
      h('.functionKey-hint', {}, 'F1')
    ]),
    h('.functionsKey-button.isButton.isControl', {
      className: ctrlPressed + (xkeys.F2 ? ' signalActive' : ''),
      title: props.fmt('{F2}'),
      onMouseDown: _e => actionF2(),
      onMouseUp: _e => actionF2up()
    }, [
      h('i.fa.fa-lock-open'),
      h('.functionKey-hint', {}, 'F2')
    ]),
    h('.functionsKey-button.isButton.isControl', {
      className: ctrlPressed + (xkeys.F3 ? ' signalActive' : ''),
      title: props.fmt('{F3}'),
      onMouseDown: _e => actionF3(),
      onMouseUp: _e => actionF3up()
    }, [
      h('i.fa.fa-history'),
      h('.functionKey-hint', {}, 'F3')
    ]),
    h('.functionsKey-button.isButton.isControl', {
      className: ctrlPressed + (xkeys.F4 ? ' signalActive' : ''),
      title: props.fmt('{F4}'),
      onMouseDown: _e => actionF4(),
      onMouseUp: _e => actionF4up()
    }, [
      h('', {}, semiNodeButtonClose),
      h('.functionKey-hint', {}, 'F4')
    ]),
    h('.functionsKey-button.isButton.isControl', {
      className: ctrlPressed + (xkeys.F5 ? ' signalActive' : ''),
      title: props.fmt('{F5}'),
      onMouseDown: _e => actionF5(),
      onMouseUp: _e => actionF5up()
    }, [
      h('', {}, semiNodeButtonOpen),
      h('.functionKey-hint', {}, 'F5')
    ]),
    h('.functionsKey-button.isButton.isControl', {
      className: ctrlPressed + (xkeys.F6 ? ' signalActive' : ''),
      title: props.fmt('{F6}'),
      onMouseDown: _e => actionF6(),
      onMouseUp: _e => actionF6up()
    }, [
      h('i.fa.fa-level-up-alt'),
      h('.functionKey-hint', {}, 'F6')
    ]),
    h('.functionsKey-button.isButton.isControl', {
      className: ctrlPressed + (xkeys.F7 ? ' signalActive' : ''),
      title: props.fmt('{F7}'),
      onMouseDown: _e => actionF7(),
      onMouseUp: _e => actionF7up()
    }, [
      h('i.icon-vibration'),
      h('.functionKey-hint', {}, 'F7')
    ]),
    h('.functionsKey-button.isButton.isControl', {
      className: ctrlPressed + (xkeys.F8 ? ' signalActive' : ''),
      title: props.fmt('{F8}'),
      onMouseDown: _e => actionF8(),
      onMouseUp: _e => actionF8up()
    }, [
      h('i.fa.fa-wind'),
      h('.functionKey-hint', {}, 'F8')
    ]),
    h('.functionsKey-button.isButton.isControl', {
      className: ctrlPressed + (xkeys.F9 ? ' signalActive' : ''),
      title: props.fmt('{F9}'),
      onMouseDown: _e => actionF9(),
      onMouseUp: _e => actionF9up()
    }, [
      h('i.fas.fa-ban'),
      h('.functionKey-hint', {}, 'F9')
    ]),
    h('.functionsKey-button.isButton.isControl', {
      className: ctrlPressed + (xkeys.F10 ? ' signalActive' : ''),
      title: props.fmt('{F10}'),
      onMouseDown: _e => actionF10(),
      onMouseUp: _e => actionF10up()
    }, [
      h('i.fa.fa-pause'),
      h('.functionKey-hint', {}, 'F10')
    ]),
    h('.functionsKey-button.isButton.isControl', {
      className: ctrlPressed + (xkeys.F11 ? ' signalActive' : ''),
      title: props.fmt('{F11}'),
      onMouseDown: _e => actionF11(),
      onMouseUp: _e => actionF11up()
    }, [
      h('i.fa.fa-tint-slash'),
      h('.functionKey-hint', {}, 'F11')
    ]),
    props.xxx_additionalWaterIsPresent
      ? h('.functionsKey-button.wider.isButton.isControl.hflex.sa', {
        className: ctrlPressed + (xkeys.F12 ? ' signalActive' : ''),
        onMouseDown: _e => actionF12(),
        onMouseUp: _e => actionF12up(),
        onMouseLeave: _e => actionF12up(),
        title: props.fmt('{additionalWater}')
      }, [
        h('.additionalWaterIconArea', {}, [
          h('i.fas.fa-tint'),
          h('.functionKey-hint', {}, 'F12')
        ]),
        h('.additionalWaterDisplay', {}, props.xxx_additionalWaterValueStr)
      ])
      : null,
    props.xxx_semaphore_id
      ? h('.functionsKey-button.wider.isButton.isControl.hflex.sa', { className: ctrlPressed }, [
        h('', {
          onMouseDown: _e => callApi([props.xxx_semaphore_id, 'set_red']),
          title: props.fmt('{semaphoreRed}')
        }, [
          h('i.fa.fa-circle.semaphore-font', {
            className: 'semaphore-red' + (props.xxx_semaphore_color === 'red' ? '-active' : '')
          })
        ]),
        h('', {
          className: ctrlPressed + '.semaphore-green' + (props.xxx_semaphore_color === 'green' ? '-active' : ''),
          onMouseDown: _e => callApi([props.xxx_semaphore_id, 'set_green']),
          title: props.fmt('{semaphoreGreen}')
        }, [
          h('i.fa.fa-circle.semaphore-font', {
            className: 'semaphore-green' + (props.xxx_semaphore_color === 'green' ? '-active' : '')
          })
        ])
      ])
      : null,
    props.xxx_horn_id
      ? h('.functionsKey-button.isButton.isControl', {
        className: ctrlPressed + (xkeys['*'] ? ' signalActive' : ''),
        onMouseDown: _e => actionStar(),
        onMouseUp: _e => actionStarUp(),
        title: props.fmt('{horn}')
      }, [
        h('i.fa.fa-bullhorn'),
        h('.horn-hint', {}, h('i.fas.fa-star-of-life'))
      ])
      : null
  ])
}

const renderNodeMessage = props => {
  gloDebugReact && incDebugRenderCount('NodeMessage', props)
  const d = props.data
  const options = []
  for (const idx in d.options) {
    const option = d.options[idx]
    const optionText = props.fmt(_.get(option, ['text'], option.id))
    let className = ''
    if (!props.xxx_keyboard_focus) {
      className = ''
    } else if (option.id === d.option_enter) {
      className = 'ready-for-enter'
    } else if (option.id === d.option_esc) {
      className = 'ready-for-esc'
    // TODO: find more elegant way to not offer keyboard shortcut for two-digit options
    } else if (option.id.indexOf('num_hack_') == 0 && option.text < 10) {
      className = 'ready-for-num'
    }
    if (option.id === d.answer) {
      className += ' selectedAnswer'
    }
    options.push(h('.message-button', {
      key: idx,
      className,
      onClick: _e => callApi([d.node_id, 'set_message_answer', d.k, option.id])
    }, optionText))
  }
  const type = 'message ' + d.level + (d.active === 0 ? ' inactive' : '')
  const textVals = d.text_vals || {}
  const text = props.fmt(d.text, textVals)
  if (props.xxx_keyboard_focus && !props.xxx_enterOrEscOrNumUsed) {
    if (d.option_enter && props.xxx_keys.ENTER) {
      props.setEnterOrEscUsed()
      callApi([d.node_id, 'set_message_answer', d.k, d.option_enter])
    } else if (d.option_esc && props.xxx_keys.ESC) {
      props.setEnterOrEscUsed()
      callApi([d.node_id, 'set_message_answer', d.k, d.option_esc])
    } else if (props.xxx_keys.ONE) {
      props.setEnterOrEscUsed()
      callApi([d.node_id, 'set_message_answer', d.k, "num_hack_1"])
    } else if (props.xxx_keys.TWO) {
      props.setEnterOrEscUsed()
      callApi([d.node_id, 'set_message_answer', d.k, "num_hack_2"])
    } else if (props.xxx_keys.THREE) {
      props.setEnterOrEscUsed()
      callApi([d.node_id, 'set_message_answer', d.k, "num_hack_3"])
    } else if (props.xxx_keys.FOUR) {
      props.setEnterOrEscUsed()
      callApi([d.node_id, 'set_message_answer', d.k, "num_hack_4"])
    } else if (props.xxx_keys.FIVE) {
      props.setEnterOrEscUsed()
      callApi([d.node_id, 'set_message_answer', d.k, "num_hack_5"])
    } else if (props.xxx_keys.SIX) {
      props.setEnterOrEscUsed()
      callApi([d.node_id, 'set_message_answer', d.k, "num_hack_6"])
    } else if (props.xxx_keys.SEVEN) {
      props.setEnterOrEscUsed()
      callApi([d.node_id, 'set_message_answer', d.k, "num_hack_7"])
    } else if (props.xxx_keys.EIGHT) {
      props.setEnterOrEscUsed()
      callApi([d.node_id, 'set_message_answer', d.k, "num_hack_8"])
    } else if (props.xxx_keys.NINE) {
      props.setEnterOrEscUsed()
      callApi([d.node_id, 'set_message_answer', d.k, "num_hack_9"])
    }
  }

  // the "key" stuff below is there only to silence react warning - investigate how to solve this better
  return h('.messageContent', {},
    h('', { className: type }, [
      h('.message-text', {}, text),
      h('.message-buttons', {}, options)
    ])
  )
}

const renderNodeLeftMenuCompressor = props => {
  gloDebugReact && incDebugRenderCount('NodeCompressor', props)
  const dfs = props.data
  const id = dfs.id
  const setRun = dfs.set_run
  const startStopAction = setRun ? 'stop' : 'start'
  const startStopLabel = props.fmt(setRun ? '{Stop}' : '{Start}')
  const startStopColor = setRun ? ' red' : ' green'
  return h('.leftMenuItem', {}, [
    h('.left-menu-title', {}, props.fmt('{' + id + '}')),
    h('.left-menu-button' + startStopColor, {
      onMouseDown: _e => callApi([id, startStopAction])
    }, startStopLabel)
  ])
}

const renderNodeLeftMenuScalesTare = props => {
  gloDebugReact && incDebugRenderCount('NodeLeftMenuScalesTare', props)
  const d = props.data
  const buttons = []
  for (const k in d) {
    const id = d[k]
    buttons.push(h('.left-menu-button', {
      key: id,
      onMouseDown: _e => callApi([id, 'tare'])
    }, props.fmt('{' + id + '}')))
  }
  return h('.leftMenuItem', {}, [
    buttons.length > 0 ? h('.left-menu-title', {}, props.fmt('{ScalesTare}')) : null,
    buttons
  ])
}

const renderNodeLeftMenuFlowmetersReset = props => {
  gloDebugReact && incDebugRenderCount('NodeLeftMenuFlowmetersReset', props)
  const d = props.data
  const buttons = []
  for (const k in d) {
    const id = d[k]
    buttons.push(h('.left-menu-button', {
      key: id,
      onMouseDown: _e => callApi([id, 'reset'])
    }, props.fmt('{' + id + '}')))
  }
  return h('.leftMenuItem', {}, [
    buttons.length > 0 ? h('.left-menu-title', {}, props.fmt('{FlowmetersReset}')) : null,
    buttons
  ])
}

// TODO: this handles multiple mixers but is actually kind of visually hard-coded for single mixer - find some elegant way to distinguish multiple mixers but make sure it still looks good for single mixer scenarios (which is far more common)
const renderNodeLeftMenuMixersWashing = props => {
  gloDebugReact && incDebugRenderCount('NodeLeftMenuMixersWashing', props)
  const d = props.data
  const buttons = []
  for (const k in d) {
    const id = d[k]
    buttons.push(h('.left-menu-button', {
      key: id,
      onMouseDown: _e => callApi(['control', 'mixer_washing_start', id])
    }, props.fmt('{Start}')))
    buttons.push(h('.left-menu-button', {
      key: id,
      onMouseDown: _e => callApi(['control', 'mixer_washing_stop', id])
    }, props.fmt('{Stop}')))
  }
  return h('.leftMenuItem', {}, [
    buttons.length > 0 ? h('.left-menu-title', {}, props.fmt('{MixerWashing}')) : null,
    buttons
  ])
}

/* const renderNodeModal = props => {
  gloDebugReact && incDebugRenderCount('NodeModal', props)
  const orderId = props.xxx_order_id;
  return h(".modal-content", {}, [
    h(".modal", {}, [
      h("iframe.iframeOrder#iframeOrder", {src: "../manager/order_dialog/"+orderId, frameborder: 0, width: "100%", height: "100%"}),
    ]),
  ])
} */

// TODO: currently unused - decouple ctrl/f10 logic from function below
/* function wrap_need_ctrl(func) {
  if (!keys['CTRL'] && !gloStateFrozen) {
    return () => {};
  }
  return func;
} */

// TODO: what an ugly name!
function sendTitle (msg) {
  document.title = msg
  document.title = 'control'
}

// Disable right click on page
document.addEventListener('contextmenu', event => event.preventDefault())
function click (e) {
  if (e.button === 2 || e.button === 3) {
    e.preventDefault()
    e.stopPropagation()
    return false
  }
}

const cloneAndDispatchEvent = e => {
  const x = new e.constructor(e.type, e)
  document.dispatchEvent(x)
}

const actionF2 = () => {
  callApi(['Mixer1', 'lock_toggle']) // TODO: hard-coded shit
  callApi(['control', 'function_key', 'F2', 1])
}

const actionF2up = () => {
  callApi(['control', 'function_key', 'F2', 0])
}

const actionF3 = () => {
  callApi(['control', 'finish_countdown'])
  callApi(['control', 'function_key', 'F3', 1])
}

const actionF3up = () => {
  callApi(['control', 'function_key', 'F3', 0])
}

const actionF4 = () => {
  callApi(['Mixer1', 'set_direction_manual_smart', -1]) // TODO: hard-coded shit
  callApi(['control', 'function_key', 'F4', 1])
}

const actionF4up = () => {
  callApi(['Mixer1', 'unset_direction_manual_smart']) // TODO: hard-coded shit
  callApi(['control', 'function_key', 'F4', 0])
}

const actionF5 = () => {
  callApi(['Mixer1', 'set_direction_manual_smart', 1]) // TODO: hard-coded shit
  callApi(['control', 'function_key', 'F5', 1])
}

const actionF5up = () => {
  callApi(['Mixer1', 'unset_direction_manual_smart']) // TODO: hard-coded shit
  callApi(['control', 'function_key', 'F5', 0])
}

const actionF6 = () => {
  callApi(['control', 'set_override', '_close_aggregate_silos_on_open', 1])
  callApi(['control', 'function_key', 'F6', 1])
}

const actionF6up = () => {
  callApi(['control', 'set_override', '_close_aggregate_silos_on_open'])
  callApi(['control', 'function_key', 'F6', 0])
}

const actionF7 = () => {
  callApi(['control', 'set_override', '_vibrate_silos_on_open', 1])
  callApi(['control', 'function_key', 'F7', 1])
}

const actionF7up = () => {
  callApi(['control', 'set_override', '_vibrate_silos_on_open'])
  callApi(['control', 'function_key', 'F7', 0])
}

const actionF8 = () => {
  callApi(['control', 'set_override', '_aerate_silos_on_open', 1])
  callApi(['control', 'function_key', 'F8', 1])
}

const actionF8up = () => {
  callApi(['control', 'set_override', '_aerate_silos_on_open'])
  callApi(['control', 'function_key', 'F8', 0])
}

const actionF9 = () => {
  callApi(['control', 'function_key', 'F9', 1])
}

const actionF9up = () => {
  callApi(['control', 'function_key', 'F9', 0])
}

const actionF10 = () => {
  callApi(['control', 'freeze_toggle'])
  callApi(['control', 'function_key', 'F10', 1])
}

const actionF10up = () => {
  callApi(['control', 'function_key', 'F10', 0])
}

const actionF11 = () => {
  callApi(['control', 'water_stop'])
  callApi(['control', 'function_key', 'F11', 1])
}

const actionF11up = () => {
  callApi(['control', 'function_key', 'F11', 0])
}

const actionF12 = () => {
  callApi(['WaterAdditionalSilo', 'set_open_invert'])
  callApi(['control', 'function_key', 'F12', 1])
}

const actionF12up = () => {
  callApi(['WaterAdditionalSilo', 'unset_open_invert'])
  callApi(['control', 'function_key', 'F12', 0])
}

const actionStar = () => {
  callApi(['TruckChangeHorn', 'set_override', 'set_enable', 1]) // TODO: hard-coded shit
}

const actionStarUp = () => {
  callApi(['TruckChangeHorn', 'set_override', 'set_enable']) // TODO: hard-coded shit
}

const actionSemaphoreToggle = () => {
  callApi(['Semaphore1', 'toggle']) // TODO: hard-coded shit
}

const actionM1 = () => {
  callApi(['Mixer1', 'manual_start']) // TODO: hard-coded shit
}

const actionM0 = () => {
  callApi(['Mixer1', 'manual_stop']) // TODO: hard-coded shit
}

const actionVB = () => {
  callApi(['Chute1', 'set_override', 'set_vibrate', 1]) // TODO: hard-coded shit
}

const actionVBup = () => {
  callApi(['Chute1', 'set_override', 'set_vibrate']) // TODO: hard-coded shit
}

const actionStopAfterBatch = () => {
  callApi(['control', 'stop_after_batch_toggle'])
}

const actionRecycledWaterDisabled = () => {
  callApi(['control', 'recycled_water_disabled_toggle'])
}

const actionOrderOnHold = () => {
  callApi(['control', 'order_on_hold_toggle'])
}

// TODO: make this a full-fledged component?
const semiNodeButtonClose = [h('i.fa.fa-arrow-right'), 'I', h('i.fa.fa-arrow-left')]
//const semiNodeButtonClose = h('.sensorLed', {}, [h('i.fa.fa-arrow-right'), 'I', h('i.fa.fa-arrow-left')])

// TODO: make this a full-fledged component?
const semiNodeButtonOpen = [h('i.fa.fa-arrow-left'), 'I', h('i.fa.fa-arrow-right')]
//const semiNodeButtonOpen = h('.sensorLed', {}, [h('i.fa.fa-arrow-left'), 'I', h('i.fa.fa-arrow-right')])

class NodeTopComponent extends React.Component {
  state = {}

  componentDidMount () {
    this.setState({
      viewportWidth: window.innerWidth,
      viewportHeight: window.innerHeight,
      theme: window.localStorage.getItem('theme')
    })
    window.addEventListener('resize', () => {
      console.log('resize')
      this.setState({
        viewportWidth: window.innerWidth,
        viewportHeight: window.innerHeight
      })
    })
  }

  render () {
    const props = this.props
    gloDebugReact && incDebugRenderCount('NodeTopComponent', props)
    const wideMode = (this.state.viewportWidth / this.state.viewportHeight) > (16 / 8) // this should actually be 16/9 but we need to add some reserve to account for possible window "widening" by taskbar, missing bottom pixel, etc.
    const aggregatesToNextLineCutoff = 15

    return rce(NodeLanguage, {
      xxx_menuIsOpened: this.state.menuIsOpened,
      xxx_modal_order_id: this.state.modalOrderId,
      xxx_is_help: this.state.isHelp,
      xxx_is_exit: this.state.isExit,
      xxx_is_dispatch: this.state.isDispatch,
      xxx_is_param: this.state.isParam,
      xxx_is_param_quick: this.state.isParamQuick,
      xxx_is_place: this.state.isPlace,
      xxx_is_language: this.state.isLanguage,
      xxx_wide_mode: wideMode,
      xxx_aggregatesToNextLineCutoff: aggregatesToNextLineCutoff,
      xxx_theme: this.state.theme,
      openExitDialog: () => this.setState({ isExit: true }),
      closeExitDialog: () => this.setState({ isExit: false }),
      openHelpWindow: () => this.setState({ isHelp: true }),
      closeHelpWindow: () => this.setState({ isHelp: false }),
      toggleHelpWindow: () => this.setState({ isHelp: !this.state.isHelp }),
      menuToggle: () => this.setState({ menuIsOpened: !this.state.menuIsOpened }),
      openLanguageDialog: () => this.setState({ isLanguage: true }),
      closeLanguageDialog: () => this.setState({ isLanguage: false }),
      dispatchOpenDialog: () => this.setState({ isDispatch: true }),
      dispatchCloseDialog: () => this.setState({ isDispatch: false }),
      paramOpenDialog: () => this.setState({ isParam: true }),
      paramCloseDialog: () => this.setState({ isParam: false }),
      paramQuickOpenDialog: () => this.setState({ isParamQuick: true }),
      paramQuickCloseDialog: () => this.setState({ isParamQuick: false }),
      placeOpenDialog: () => this.setState({ isPlace: true }),
      placeCloseDialog: () => this.setState({ isPlace: false }),
      managerOpenOrderDialog: x => this.setState({ modalOrderId: x }),
      managerCloseOrderDialog: () => this.setState({ modalOrderId: null }),
      themeToggle: () => {
        const newTheme = this.state.theme === 'day' ? 'night' : 'day'
        window.localStorage.setItem('theme', newTheme)
        this.setState({ theme: newTheme })
      }
    })
  }
}

class NodeKeyDetector extends React.PureComponent {
  state = {}

  componentDidMount () {
    // console.log('NodeKeyDetector componentDidMount');
    window.onkeydown = this.onkeydown.bind(this)
    window.onkeyup = this.onkeyup.bind(this)
  }

  onkeydown (e) {
    const props = this.props
    let keys = this.state
    let prevent = true
    if (e.repeat) {
      // nothing
    } else if (e.ctrlKey && e.keyCode === 'D'.charCodeAt(0)) {
      gloDebugReact = !gloDebugReact
    } else if (e.ctrlKey && e.keyCode === 'F'.charCodeAt(0)) {
      if (document.fullscreenElement) {
        document.exitFullscreen()
      } else {
        document.documentElement.requestFullscreen()
      }
    } else if (e.ctrlKey && e.keyCode === 'R'.charCodeAt(0)) {
      sendTitle('control:reload')
    } else if (e.keyCode === 13 || e.key === 'Enter') {
      // TODO: ugly
      if (props.xxx_is_help) {
        // return;
      } else if (props.xxx_is_exit) {
        // return;
      } else if (props.xxx_is_param) {
        // return;
      } else if (props.xxx_is_param_quick) {
        // return;
      } else if (props.xxx_is_place) {
        // return;
      } else if (props.xxx_modal_order_id) {
        // return;
      } else if (props.xxx_is_language) {
        // return;
      } else {
        keys = { ...keys, ENTER: true }
      }
    } else if (e.keyCode === 27 || e.key === 'Escape') {
      if (props.xxx_is_help) {
        props.closeHelpWindow()
        return
      } else if (props.xxx_is_exit) {
        props.closeExitDialog()
        return
      } else if (props.xxx_is_param) {
        props.paramCloseDialog()
        return
      } else if (props.xxx_is_param_quick) {
        props.paramQuickCloseDialog()
        return
      } else if (props.xxx_is_place) {
        props.placeCloseDialog()
        return
      } else if (props.xxx_modal_order_id) {
        props.managerCloseOrderDialog()
        return
      } else if (props.xxx_is_language) {
        props.closeLanguageDialog()
        return
      }
      keys = { ...keys, ESC: true }
    } else if (e.keyCode === 17 || e.key === 'Control') {
      keys = { ...keys, CTRL: true }
    } else if (e.keyCode === 18 || e.key === 'Alt') {
      keys = { ...keys, ALT: true }
    } else if (e.keyCode === 112 || e.key === 'F1') {
      keys = { ...keys, F1: true }
      props.toggleHelpWindow()
      keys = this.state // TODO: fuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuck
    } else if (e.keyCode === 113 || e.key === 'F2') {
      actionF2()
    } else if (e.keyCode === 114 || e.key === 'F3') {
      keys = { ...keys, F3: true }
      actionF3()
    } else if (e.keyCode === 115 || e.key === 'F4') {
      keys = { ...keys, F4: true }
      actionF4()
    } else if (e.keyCode === 116 || e.key === 'F5') {
      keys = { ...keys, F5: true }
      actionF5()
    } else if (e.keyCode === 117 || e.key === 'F6') {
      keys = { ...keys, F6: true }
      actionF6()
    } else if (e.keyCode === 118 || e.key === 'F7') {
      keys = { ...keys, F7: true }
      actionF7()
    } else if (e.keyCode === 119 || e.key === 'F8') {
      keys = { ...keys, F8: true }
      actionF8()
    } else if (e.keyCode === 120 || e.key === 'F9') {
      keys = { ...keys, F9: true }
      actionF9()
    } else if (e.keyCode === 121 || e.key === 'F10') {
      actionF10()
    } else if (e.keyCode === 122 || e.key === 'F11') {
      keys = { ...keys, F11: true }
      actionF11()
    } else if (e.keyCode === 123 || e.key === 'F12') {
      keys = { ...keys, F12: true }
      actionF12()
    } else if (e.keyCode === 106 || e.key === '*') {
      actionStar()
    } else if (e.keyCode === 77 || e.key === 'm') {
      keys = { ...keys, M: true }
    } else if ((e.keyCode === 96 || e.key === 'Numpad0' || e.keyCode === 48 || e.key === 'Digit0') && keys.M) {
      actionM0()
    } else if ((e.keyCode === 97 || e.key === 'Numpad1' || e.keyCode === 49 || e.key === 'Digit1') && keys.M) {
      actionM1()
    } else if ((e.keyCode === 97 || e.key === 'Numpad1' || e.keyCode === 49 || e.key === 'Digit1')) {
      keys = { ...keys, ONE: true }
    } else if ((e.keyCode === 98 || e.key === 'Numpad2' || e.keyCode === 50 || e.key === 'Digit2')) {
      keys = { ...keys, TWO: true }
    } else if ((e.keyCode === 99 || e.key === 'Numpad3' || e.keyCode === 51 || e.key === 'Digit3')) {
      keys = { ...keys, THREE: true }
    } else if ((e.keyCode === 100 || e.key === 'Numpad4' || e.keyCode === 52 || e.key === 'Digit4')) {
      keys = { ...keys, FOUR: true }
    } else if ((e.keyCode === 101 || e.key === 'Numpad5' || e.keyCode === 53 || e.key === 'Digit5')) {
      keys = { ...keys, FIVE: true }
    } else if ((e.keyCode === 102 || e.key === 'Numpad6' || e.keyCode === 54 || e.key === 'Digit6')) {
      keys = { ...keys, SIX: true }
    } else if ((e.keyCode === 103 || e.key === 'Numpad7' || e.keyCode === 55 || e.key === 'Digit7')) {
      keys = { ...keys, SEVEN: true }
    } else if ((e.keyCode === 104 || e.key === 'Numpad8' || e.keyCode === 56 || e.key === 'Digit8')) {
      keys = { ...keys, EIGHT: true }
    } else if ((e.keyCode === 105 || e.key === 'Numpad9' || e.keyCode === 57 || e.key === 'Digit9')) {
      keys = { ...keys, NINE: true }
    } else if ((e.keyCode === 37 || e.key === 'ArrowLeft') && keys.M) {
      keys = { ...keys, ArrowLeft: true }
      actionF4()
    } else if ((e.keyCode === 39 || e.key === 'ArrowRight') && keys.M) {
      keys = { ...keys, ArrowRight: true }
      actionF5()
    } else if ((e.keyCode === 86 || e.key === 'v') && keys.B) {
      keys = { ...keys, V: true }
      actionVB()
    } else if (e.keyCode === 86 || e.key === 'v') {
      keys = { ...keys, V: true }
    } else if ((e.keyCode === 66 || e.key === 'b') && keys.V) {
      keys = { ...keys, B: true }
      actionVB()
    } else if (e.keyCode === 66 || e.key === 'b') {
      keys = { ...keys, B: true }
      // stop after batch
    } else if ((e.keyCode === 83 || e.key === 's') && keys.Z) {
      keys = { ...keys, S: true }
      actionStopAfterBatch()
    } else if (e.keyCode === 83 || e.key === 's') {
      keys = { ...keys, S: true }
    } else if ((e.keyCode === 90 || e.key === 'z') && keys.S) {
      keys = { ...keys, Z: true }
      actionStopAfterBatch()
    } else if (e.keyCode === 90 || e.key === 'z') {
      keys = { ...keys, Z: true }
      // recycled water disabled
    } else if (e.keyCode === 111 || e.key === '/') {
      keys = { ...keys, '/': true }
      actionRecycledWaterDisabled()
      // OP objednavka prerusena "order-on-hold"
    } else if ((e.keyCode === 79 || e.key === 'o') && keys.P) {
      keys = { ...keys, O: true }
      actionOrderOnHold()
    } else if ((e.keyCode === 80 || e.key === 'p') && keys.O) {
      keys = { ...keys, P: true }
      actionOrderOnHold()
    } else if ((e.keyCode === 79 || e.key === 'o') && keys.K) {
      keys = { ...keys, O: true }
      actionSemaphoreToggle()
    } else if ((e.keyCode === 75 || e.key === 'k') && keys.O) {
      keys = { ...keys, K: true }
      actionSemaphoreToggle()
    } else if (e.keyCode === 79 || e.key === 'o') {
      keys = { ...keys, O: true }
    } else if (e.keyCode === 80 || e.key === 'p') {
      keys = { ...keys, P: true }
    } else if (e.keyCode === 75 || e.key === 'k') {
      keys = { ...keys, K: true }
    } else {
      prevent = false
    }
    if (prevent) {
      e.preventDefault()
    }
    this.setState(keys)
  }

  onkeyup (e) {
    let keys = this.state
    let prevent = true
    if (e.repeat) {
      // nothing
    } else if (e.keyCode === 13 || e.key === 'Enter') {
      // TODO: ignore when any modal is open?
      keys = { ...keys, ENTER: false, enterOrEscOrNumUsed: false }
    } else if (e.keyCode === 27 || e.key === 'Escape') {
      // TODO: ignore when any modal is open?
      keys = { ...keys, ESC: false, enterOrEscOrNumUsed: false }
    } else if (e.keyCode === 17 || e.key === 'Control') {
      keys = { ...keys, CTRL: false }
    } else if (e.keyCode === 18 || e.key === 'Alt') {
      keys = { ...keys, ALT: false }
    } else if (e.keyCode === 112 || e.key === 'F1') {
      keys = { ...keys, F1: false }
    } else if (e.keyCode === 113 || e.key === 'F2') {
      actionF2up()
    } else if (e.keyCode === 114 || e.key === 'F3') {
      keys = { ...keys, F3: false }
      actionF3up()
    } else if (e.keyCode === 115 || e.key === 'F4') {
      keys = { ...keys, F4: false }
      actionF4up()
    } else if (e.keyCode === 116 || e.key === 'F5') {
      keys = { ...keys, F5: false }
      actionF5up()
    } else if (e.keyCode === 117 || e.key === 'F6') {
      keys = { ...keys, F6: false }
      actionF6up()
    } else if (e.keyCode === 118 || e.key === 'F7') {
      keys = { ...keys, F7: false }
      actionF7up()
    } else if (e.keyCode === 119 || e.key === 'F8') {
      keys = { ...keys, F8: false }
      actionF8up()
    } else if (e.keyCode === 120 || e.key === 'F9') {
      keys = { ...keys, F9: false }
      actionF9up()
    } else if (e.keyCode === 121 || e.key === 'F10') {
      actionF10up()
    } else if (e.keyCode === 122 || e.key === 'F11') {
      keys = { ...keys, F11: false }
      actionF11up()
    } else if (e.keyCode === 123 || e.key === 'F12') {
      keys = { ...keys, F12: false }
      actionF12up()
    } else if (e.keyCode === 106 || e.key === '*') {
      actionStarUp()
    } else if ((e.keyCode === 37 || e.key === 'ArrowLeft') && keys.M) {
      keys = { ...keys, ArrowLeft: false }
      actionF4up()
    } else if ((e.keyCode === 77 || e.key === 'm') && keys.ArrowLeft) {
      keys = { ...keys, M: false }
      actionF4up()
    } else if ((e.keyCode === 39 || e.key === 'ArrowRight') && keys.M) {
      keys = { ...keys, ArrowRight: false }
      actionF5up()
    } else if ((e.keyCode === 77 || e.key === 'm') && keys.ArrowRight) {
      keys = { ...keys, M: false }
      actionF5up()
    } else if (e.keyCode === 77 || e.key === 'm') {
      keys = { ...keys, M: false }
    } else if (e.keyCode === 39 || e.key === 'ArrowRight') {
      keys = { ...keys, ArrowRight: false }
    } else if ((e.keyCode === 86 || e.key === 'v') && keys.B) {
      keys = { ...keys, V: false }
      actionVBup()
    } else if ((e.keyCode === 66 || e.key === 'b') && keys.V) {
      keys = { ...keys, B: false }
      actionVBup()
    } else if (e.keyCode === 86 || e.key === 'v') {
      keys = { ...keys, V: false }
    } else if (e.keyCode === 66 || e.key === 'b') {
      keys = { ...keys, B: false }
      // stop after batch
    } else if (e.keyCode === 83 || e.key === 's') {
      keys = { ...keys, S: false }
    } else if (e.keyCode === 90 || e.key === 'z') {
      keys = { ...keys, Z: false }
    } else if (e.keyCode === 75 || e.key === 'k') {
      keys = { ...keys, K: false }
      // OP objednavka prerusena "order-on-hold"
    } else if (e.keyCode === 79 || e.key === 'o') {
      keys = { ...keys, O: false }
    } else if (e.keyCode === 80 || e.key === 'p') {
      keys = { ...keys, P: false }
    } else if ((e.keyCode === 97 || e.key === 'Numpad1' || e.keyCode === 49 || e.key === 'Digit1')) {
      keys = { ...keys, ONE: false, enterOrEscOrNumUsed: false }
    } else if ((e.keyCode === 98 || e.key === 'Numpad2' || e.keyCode === 50 || e.key === 'Digit2')) {
      keys = { ...keys, TWO: false, enterOrEscOrNumUsed: false }
    } else if ((e.keyCode === 99 || e.key === 'Numpad3' || e.keyCode === 51 || e.key === 'Digit3')) {
      keys = { ...keys, THREE: false, enterOrEscOrNumUsed: false }
    } else if ((e.keyCode === 100 || e.key === 'Numpad4' || e.keyCode === 52 || e.key === 'Digit4')) {
      keys = { ...keys, FOUR: false, enterOrEscOrNumUsed: false }
    } else if ((e.keyCode === 101 || e.key === 'Numpad5' || e.keyCode === 53 || e.key === 'Digit5')) {
      keys = { ...keys, FIVE: false, enterOrEscOrNumUsed: false }
    } else if ((e.keyCode === 102 || e.key === 'Numpad6' || e.keyCode === 54 || e.key === 'Digit6')) {
      keys = { ...keys, SIX: false, enterOrEscOrNumUsed: false }
    } else if ((e.keyCode === 103 || e.key === 'Numpad7' || e.keyCode === 55 || e.key === 'Digit7')) {
      keys = { ...keys, SEVEN: false, enterOrEscOrNumUsed: false }
    } else if ((e.keyCode === 104 || e.key === 'Numpad8' || e.keyCode === 56 || e.key === 'Digit8')) {
      keys = { ...keys, EIGHT: false, enterOrEscOrNumUsed: false }
    } else if ((e.keyCode === 105 || e.key === 'Numpad9' || e.keyCode === 57 || e.key === 'Digit9')) {
      keys = { ...keys, NINE: false, enterOrEscOrNumUsed: false }
    } else {
      prevent = false
    }
    if (prevent) {
      e.preventDefault()
    }
    this.setState(keys)
  }

  render () {
    const props = this.props
    gloDebugReact && incDebugRenderCount('NodeKeyDetector', props)
    return rce(NodeEventSource, {
      ...props,
      xxx_keys: this.state,
      xxx_enterOrEscOrNumUsed: this.state.enterOrEscOrNumUsed,
      setEnterOrEscUsed: () => this.setState({ enterOrEscOrNumUsed: true })
    })
  }
}

class NodeLanguage extends React.Component {
  state = { d: {}, dCur: {}, cur: 'cs', avail: ['cs', 'en', 'de'] } // TODO: hard-coded shit

  init () {
    this.setState({ cur: window.localStorage.getItem('lang') || this.state.cur })
    fetch('./captions').then(res => res.json()).then(data => {
      console.log('lang:', data)
      data = by_key_to_by_lang(data)
      console.log('lang by lang:', data)
      this.setState({ d: data })
      this.setLang(this.state.cur)
    })
  }

  setLang (x) {
    this.setState({ cur: x, dCur: this.state.d[x] || {} })
    window.localStorage.setItem('lang', x)
  }

  componentDidMount () {
    this.init()
  }

  render () {
    const props = this.props
    gloDebugReact && incDebugRenderCount('NodeLanguage', props)
    return rce(NodeKeyDetector, {
      ...props,
      fmt: (x, moreVals = {}) => _fmt(x, { ...this.state.dCur, ...moreVals }),
      lang_set: this.setLang.bind(this),
      lang_avail: this.state.avail,
      lang_cur: this.state.cur
    })
  }
}

class NodeEventSource extends React.Component {
  state = { isConnected: false, doCrash: false }

  init () {
    const evtSource = new EventSource('stream')
    evtSource.onmessage = this.onmessage.bind(this)
    evtSource.onerror = this.onerror.bind(this)
    // this.setState({evtSource: evtSource, dateLastData: null});
    this.setState({ evtSource })
  }

  onmessage (e) {
    /*const dateNow = new Date()
    const dateDiff = this.state.dateLastData ? (dateNow - this.state.dateLastData) : null
    if (dateDiff && dateDiff > 1000) {
      console.log('sse dateDiff: ' + dateDiff)
    }*/
    const data = JSON.parse(e.data)
    // const data_new = data
    const newData = this.state.data ? { ...this.state.data, ...data } : data
    // const newData = this.state.data ? Object.assign(this.state.data, data) : data  // this is supposedly faster but it changes data in place which seems to fuck up react's change detection
    // const newData = this.state.data ? _.merge({}, this.state.data, data) : data;
    const precalc = this.state.precalc || precalcStuff(newData, this.props.xxx_aggregatesToNextLineCutoff)
    this.setState({
      data: newData,
      precalc,
      isConnected: true,
      /*dateLastData: dateNow*/
    })
  }

  onerror (_e) {
    console.error('sse error')
    /*const dateNow = new Date()
    const dateDiff = this.state.dateLastData ? dateNow - this.state.dateLastData : null
    if (dateDiff && dateDiff > 5000) {
      console.log('sse dateDiff: ' + dateDiff)
    }*/
    _.updateWith(gloDebugStuff, ['sse_errors'], x => x ? x + 1 : 1)
    this.state.evtSource.close()
    this.setState({ evtSource: null, isConnected: false })
    setTimeout(this.init.bind(this), 1000)
  }

  componentDidMount () {
    this.init()
  }

  render () {
    if (this.state.doCrash) {
      console.error("CRASH")
      null.fuck()
    }
    const props = this.props
    gloDebugReact && incDebugRenderCount('NodeEventSource', props)
    const isConnected = this.state.isConnected
    const state_frozen = _.get(this.state, ['data', 'control', '_is_frozen'])
    const state_is_pc = _.get(this.state, ['data', 'control', '_is_pc'])
    return rce(Main, {
      ...props,
      nodes: this.state.data,
      precalc: this.state.precalc,
      xxx_state_frozen: state_frozen,
      xxx_connected: isConnected,
      xxx_state_is_pc: state_is_pc,
      doCrash: () => { this.setState({ doCrash: true }) },
    })
  }
}

class Clock extends React.PureComponent {
  constructor () {
    super()
    this.state = { clockStr: '' }
  }

  componentDidMount () {
    setInterval(() => {
      this.setState({ clockStr: dateTimeString() })
    }, 1000)
  }

  render () {
    const props = this.props
    gloDebugReact && incDebugRenderCount('Clock', props)
    return h('.clock', {}, this.state.clockStr)
  }
}

// TODO: unify with other modals (below)
class ExitDialog extends React.Component {
  render () {
    const props = this.props
    gloDebugReact && incDebugRenderCount('ExitDialog', props)
    return h('.modal-content', {}, [
      h('.modal.modalBasic', {}, [
        h('.modal.modalHeader', {}, [
          h('.modal.modalTitle', {}, props.fmt('{exit}')),
          h('.modal.modalClose', {}, [
            h('i.fa.fa-window-close', { onClick: _e => { props.closeExitDialog() } })
          ])
        ]),
        h('.modal.modalBodyMain', {}, [
          h('', {}, props.fmt('{really_exit_control_program}'))
        ]),
        h('.modal.modalFooter', {}, [
          h('.modal.modalButton', { onClick: _e => { props.closeExitDialog() } }, props.fmt('{NO}')),
          //h('.modal.modalButton', { onClick: _e => { fetch('/loader/quit') } }, props.fmt('{YES}'))
          h('.modal.modalButton', { onClick: _e => { callApi(['control', 'loader_quit']) } }, props.fmt('{YES}'))
        ])
      ])
    ])
  }
}

// TODO: unify with other modals (below)
class LanguageDialog extends React.Component {
  render () {
    const props = this.props
    gloDebugReact && incDebugRenderCount('LanguageDialog', props)
    const availableLanguages = props.lang_avail
    const currentLanguage = props.lang_cur
    const languages = availableLanguages.map(lang =>
      h('tr', { key: lang, onClick: _e => { props.lang_set(lang); props.closeLanguageDialog() } }, [
        h('td', {}, [
          h('i.far' + (lang === currentLanguage ? '.fa-dot-circle' : '.fa-circle'), { style: { paddingRight: '0.5vw' } }, [])
        ]),
        h('td', {}, [
          h('i.fa.lang-lg.lang-lbl', { lang }, [])
        ])
      ])
    )
    return h('.modal-content', {}, [
      h('.modal.modalBasic', {}, [
        h('.modal.modalHeader', {}, [
          h('.modal.modalTitle', {}, ''),
          h('.modal.modalClose', {}, [
            h('i.fa.fa-window-close', { onClick: _e => { props.closeLanguageDialog() } })
          ])
        ]),
        h('.modal.modalBodyMain', {}, [
          h('table.info', {}, languages)
        ])
      ])
    ])
  }
}

// TOOD: unify with other modals (see below)
class NodeHelp extends React.Component {
  render () {
    const props = this.props
    gloDebugReact && incDebugRenderCount('NodeHelp', props)
    return h('.modal-content', {}, [
      h('.modal.modalBasic', {}, [
        h('.modal.modalHeader', {}, [
          h('.modal.modalTitle', {}, props.fmt('{F1}')),
          h('.modal.modalClose', {}, [
            h('i.fa.fa-window-close', { onClick: _e => { props.closeHelpWindow() } })
          ])
        ]),
        h('.modal.modalBodyMain', {}, [
          h('table.info', {}, [
            _.range(1, 13).map(i => h('tr', {}, [
              h('td', {}, '( F' + i + ' )'),
              h('td', {}, ' - ' + props.fmt('{F' + i + '}'))
            ])),
            h('tr', {}, h('td', { colspan: 2 }, '\u00a0')),
            h('tr', {}, [
              h('td', {}, 'Servisní číslo:\u00a0\u00a0'),
              h('td', {}, '+420 910 125 024')
            ]),
            h('tr', {}, h('td', { colspan: 2 }, '\u00a0')),
            h('tr.small', {}, h('td', { colspan: 2 }, 'Záložní kontakty')),
            h('tr.small', {}, h('td', { colspan: 2 }, '\u00a0')),
            h('tr.small', {}, [
              h('td', {}, 'Tomáš Malík:'),
              h('td', {}, '+420 770 102 184')
            ]),
            h('tr.small', {}, [
              h('td', {}, 'Vladimír Kantor:'),
              h('td', {}, '+420 724 160 710')
            ]),
            h('tr.small', {}, [
              h('td', {}, 'Štěpán Donáth:'),
              h('td', {}, '+420 724 290 240')
            ]),
            h('tr.small', {}, [
              h('td', {}, 'Pavel Podgorný:'),
              h('td', {}, '+420 606 370 344')
            ])
          ])
        ])
      ])
    ])
  }
}

class Modal extends React.Component {
  componentDidMount () {
    const el = window.document.getElementById('iframeOrder')
    el.onload = () => {
      el.contentDocument.body.addEventListener('keydown', cloneAndDispatchEvent)
      el.contentDocument.body.addEventListener('keyup', cloneAndDispatchEvent)
      el.contentDocument.getElementById('orderDialogEventContent').addEventListener('input', this.props.managerCloseOrderDialog)
    }
  }

  componentWillUnmount () {
    console.log('Modal componentWillUnmount')
    const el = window.document.getElementById('iframeOrder')
    el.contentDocument.getElementById('orderDialogEventContent').removeEventListener('input', this.props.managerCloseOrderDialog)
    el.contentDocument.dispatchEvent(new Event('dialog_closing'))
  }

  render () {
    const props = this.props
    gloDebugReact && incDebugRenderCount('Modal', props)
    const orderId = props.xxx_order_id
    return h('.modal-content', {}, [
      h('.modal.modalBasic.modalOrder', {}, [
        h('.modal.modalHeader', {}, [
          h('.modal.modalTitle', {}, props.fmt('{order}: ') + orderId),
          h('.modal.modalClose', {}, [
            h('i.fa.fa-window-close', { onClick: _e => { props.managerCloseOrderDialog() } })
          ])
        ]),
        h('.modal.modalBodyOrderDialog.modalBodyParam', {}, [ // TODO: param? what?
          h('iframe.iframeOrder', {
            id: 'iframeOrder',
            src: '../manager/order_dialog/' + orderId + '?lang=' + props.lang_cur,
            frameborder: 0,
            width: '100%',
            height: '100%',
          })
        ])
      ])
    ])
  }
}

// TODO: unify with Modal above
class IframeModal extends React.Component {
  constructor (props) {
    super(props)
    this.state = { innerWidth: 0, innerHeight: 0 }
  }

  componentDidMount () {
    if (this.props.noResize) {
      return
    }
    const el = window.document.getElementById('theIframe')
    el.onload = () => {
      console.log('iframe loaded')
      el.contentDocument.body.addEventListener('keydown', cloneAndDispatchEvent)
      el.contentDocument.body.addEventListener('keyup', cloneAndDispatchEvent)
      const ro = new ResizeObserver((_entries) => {
        const w = el.contentDocument.body.clientWidth
        const h = el.contentDocument.body.clientHeight
        console.log('iframe inner resize', w, h)
        this.setState({ innerWidth: w, innerHeight: h })
      })
      ro.observe(el.contentDocument.body)
      this.setState({ observer: ro })
    }
  }

  componentWillUnmount () {
    console.log('IframeModal componentWillUnmount')
    if (this.props.noResize) {
      return
    }
    // const el = window.document.getElementById("theIframe");
    if (this.state.observer) {
      this.state.observer.disconnect()
      this.setState({ observer: null })
    }
  }

  render () {
    const props = this.props
    gloDebugReact && incDebugRenderCount('IframeModal', props)
    return h('.modal-content', {}, [
      h('.modal.modalBasic' + props.extraClass, {}, [
        h('.modal.modalHeader', {}, [
          h('.modal.modalTitle', {}, props.title),
          h('.modal.modalClose', {}, [
            h('i.fa.fa-window-close', { onClick: props.onClose })
          ])
        ]),
        h('.modal.modalBody', { style: { height: (this.state.innerHeight - 1) + 'px' } }, [ // -1 is there to force scrollbar
          h('iframe', {
            id: 'theIframe',
            src: props.url,
            frameborder: 0,
            width: '100%',
            height: '100%',
          })
        ])
      ])
    ])
  }
}

// start watching for crashes after 5 seconds
setTimeout(() => {
  setInterval(() => {
    //console.debug("tick")
    const el = document.getElementById("main")
    if (!el) {
      console.log("crash detected, reloading")
      location.reload()
    }
  }, 1000)
}, 5000);

ReactDOM.render(rce(NodeTopComponent), document.getElementById('content'))
