#
#
#           The Nim Compiler
#
#    See the file "copying.txt", included in this
#    distribution, for details about the copyright.
#
## code owner: Arne Döring
## e-mail: arne.doering@gmx.net

proc align(address, alignment: BiggestInt): BiggestInt =
  result = (address + (alignment - 1)) and not (alignment - 1)

proc align(address, alignment: int): int =
  result = (address + (alignment - 1)) and not (alignment - 1)


const
  ## a size is concidered "unknown" when it is an imported type from C
  ## or C++.
  szUnknownSize* = -3
  szIllegalRecursion* = -2
  szUncomputedSize* = -1

type IllegalTypeRecursionError = object of Exception

proc raiseIllegalTypeRecursion() =
  raise newException(IllegalTypeRecursionError, "illegal type recursion")

type
  OffsetAccum = object
    maxAlign: int
    offset: int

proc inc(arg: var OffsetAccum; value: int) =
  if unlikely(value == szIllegalRecursion):  raiseIllegalTypeRecursion()
  if value == szUnknownSize or arg.offset == szUnknownSize:
    arg.offset = szUnknownSize
  else:
    arg.offset += value

proc align(arg: var OffsetAccum; value: int) =
  if unlikely(value == szIllegalRecursion):  raiseIllegalTypeRecursion()
  if value == szUnknownSize or arg.maxAlign == szUnknownSize or arg.offset == szUnknownSize:
    arg.maxAlign = szUnknownSize
    arg.offset = szUnknownSize
  else:
    arg.maxAlign = max(value, arg.maxAlign)
    arg.offset = align(arg.offset, value)

proc finish(arg: var OffsetAccum) =
  if arg.maxAlign == szUnknownSize or arg.offset == szUnknownSize:
    arg.offset = szUnknownSize
  else:
    arg.offset = align(arg.offset, arg.maxAlign)

proc computeSizeAlign(conf: ConfigRef; typ: PType)

proc computeSubObjectAlign(conf: ConfigRef; n: PNode): BiggestInt =
  ## returns object alignment
  case n.kind
  of nkRecCase:
    assert(n.sons[0].kind == nkSym)
    result = computeSubObjectAlign(conf, n.sons[0])
    for i in 1 ..< sonsLen(n):
      let child = n.sons[i]
      case child.kind
      of nkOfBranch, nkElse:
        let align = computeSubObjectAlign(conf, child.lastSon)
        if align < 0:
          return align
        result = max(result, align)
      else:
        internalError(conf, "computeSubObjectAlign")
  of nkRecList:
    result = 1
    for i, child in n.sons:
      let align = computeSubObjectAlign(conf, n.sons[i])
      if align < 0:
        return align
      result = max(result, align)
  of nkSym:
    computeSizeAlign(conf, n.sym.typ)
    result = n.sym.typ.align
  else:
    result = 1


proc setOffsetsToUnknown(n: PNode) =
  if n.kind == nkSym and n.sym.kind == skField:
    n.sym.offset = szUnknownSize
  else:
    for i in 0 ..< safeLen(n):
      setOffsetsToUnknown(n[i])

proc computeObjectOffsetsFoldFunction(conf: ConfigRef; n: PNode,
                                      initialOffset: BiggestInt): tuple[offset, align: BiggestInt] =
  ## ``offset`` is the offset within the object, after the node has been written, no padding bytes added
  ## ``align`` maximum alignment from all sub nodes
  assert n != nil
  if n.typ != nil and n.typ.size == szIllegalRecursion:
    result.offset = szIllegalRecursion
    result.align = szIllegalRecursion
    return

  result.align = 1
  case n.kind
  of nkRecCase:
    assert(n.sons[0].kind == nkSym)
    let (kindOffset, kindAlign) = computeObjectOffsetsFoldFunction(conf, n.sons[0], initialOffset)

    var maxChildAlign: BiggestInt = if initialOffset == szUnknownSize: szUnknownSize else: 0
    for i in 1 ..< sonsLen(n):
      let child = n.sons[i]
      case child.kind
      of nkOfBranch, nkElse:
        # offset parameter cannot be known yet, it needs to know the alignment first
        let align = computeSubObjectAlign(conf, n.sons[i].lastSon)
        if align == szIllegalRecursion:
          result.offset = szIllegalRecursion
          result.align = szIllegalRecursion
          return
        if align == szUnknownSize or maxChildAlign == szUnknownSize:
          maxChildAlign = szUnknownSize
        else:
          maxChildAlign = max(maxChildAlign, align)
      else:
        internalError(conf, "computeObjectOffsetsFoldFunction(record case branch)")
    if maxChildAlign == szUnknownSize:
      setOffsetsToUnknown(n)
      result.align  = szUnknownSize
      result.offset = szUnknownSize
    else:
      # the union neds to be aligned first, before the offsets can be assigned
      let kindUnionOffset = align(kindOffset, maxChildAlign)
      var maxChildOffset: BiggestInt = 0
      for i in 1 ..< sonsLen(n):
        let (offset, align) = computeObjectOffsetsFoldFunction(conf, n.sons[i].lastSon, kindUnionOffset)
        maxChildOffset = max(maxChildOffset, offset)
      result.align = max(kindAlign, maxChildAlign)
      result.offset = maxChildOffset
  of nkRecList:
    result.align = 1 # maximum of all member alignments
    var offset = initialOffset
    for i, child in n.sons:
      let (newOffset, align) = computeObjectOffsetsFoldFunction(conf, child, offset)
      if newOffset == szIllegalRecursion:
        result.offset = szIllegalRecursion
        result.align = szIllegalRecursion
        return
      elif newOffset == szUnknownSize or offset == szUnknownSize:
        # if anything is unknown, the rest becomes unknown as well
        offset = szUnknownSize
        result.align = szUnknownSize
      else:
        offset = newOffset
        result.align = max(result.align, align)
    # final alignment
    if offset == szUnknownSize:
      result.offset = szUnknownSize
    else:
      result.offset = align(offset, result.align)
  of nkSym:
    var size = szUnknownSize
    var align = szUnknownSize
    if n.sym.bitsize == 0: # 0 represents bitsize not set
      computeSizeAlign(conf, n.sym.typ)
      size = n.sym.typ.size.int
      align = n.sym.typ.align.int

    result.align = align
    if initialOffset == szUnknownSize or size == szUnknownSize:
      n.sym.offset = szUnknownSize
      result.offset = szUnknownSize
    else:
      n.sym.offset = align(initialOffset, align).int
      result.offset = n.sym.offset + n.sym.typ.size
  else:
    result.align = szUnknownSize
    result.offset = szUnknownSize

proc computePackedObjectOffsetsFoldFunction(conf: ConfigRef; n: PNode, initialOffset: BiggestInt, debug: bool): BiggestInt =
  ## ``result`` is the offset within the object, after the node has been written, no padding bytes added
  case n.kind
  of nkRecCase:
    assert(n.sons[0].kind == nkSym)
    let kindOffset = computePackedObjectOffsetsFoldFunction(conf, n.sons[0], initialOffset, debug)
    # the union neds to be aligned first, before the offsets can be assigned
    let kindUnionOffset = kindOffset
    var maxChildOffset: BiggestInt = kindUnionOffset
    for i in 1 ..< sonsLen(n):
      let offset = computePackedObjectOffsetsFoldFunction(conf, n.sons[i].lastSon, kindUnionOffset, debug)
      if offset == szIllegalRecursion:
        return szIllegalRecursion
      if offset == szUnknownSize or maxChildOffset == szUnknownSize:
        maxChildOffset = szUnknownSize
      else:
        maxChildOffset = max(maxChildOffset, offset)
    result = maxChildOffset
  of nkRecList:
    result = initialOffset
    for i, child in n.sons:
      result = computePackedObjectOffsetsFoldFunction(conf, child, result, debug)
      if result == szIllegalRecursion:
        break
  of nkSym:
    var size = szUnknownSize
    if n.sym.bitsize == 0:
      computeSizeAlign(conf, n.sym.typ)
      size = n.sym.typ.size.int

    if initialOffset == szUnknownSize or size == szUnknownSize:
      n.sym.offset = szUnknownSize
      result = szUnknownSize
    else:
      n.sym.offset = int(initialOffset)
      result = initialOffset + n.sym.typ.size
  else:
    result = szUnknownSize

proc computeUnionObjectOffsetsFoldFunction(conf: ConfigRef; n: PNode, debug: bool): tuple[offset, align: BiggestInt] =
  ## ``result`` is the offset from the larget member of the union.
  case n.kind
  of nkRecCase:
    result.offset = szUnknownSize
    result.align = szUnknownSize
    localError(conf, n.info, "Illegal use of ``case`` in union type.")
    #internalError(conf, "Illegal use of ``case`` in union type.")
  of nkRecList:
    var maxChildOffset: BiggestInt = 0
    for i, child in n.sons:
      let (offset, align) = computeUnionObjectOffsetsFoldFunction(conf, child, debug)
      if offset == szIllegalRecursion or align == szIllegalRecursion:
        result.offset = szIllegalRecursion
        result.align = szIllegalRecursion
      elif offset == szUnknownSize or align == szUnknownSize:
        result.offset = szUnknownSize
        result.align = szUnknownSize
      else:
        assert offset != szUncomputedSize
        assert align != szUncomputedSize
        result.offset = max(result.offset, offset)
        result.align = max(result.align, align)
  of nkSym:
    var size = szUnknownSize
    var align = szUnknownSize
    if n.sym.bitsize == 0: # 0 represents bitsize not set
      computeSizeAlign(conf, n.sym.typ)
      size = n.sym.typ.size.int
      align = n.sym.typ.align.int

    result.align = align
    if size == szUnknownSize:
      n.sym.offset = szUnknownSize
      result.offset = szUnknownSize
    else:
      n.sym.offset = 0
      result.offset = n.sym.typ.size
  else:
    result.offset = szUnknownSize
    result.align = szUnknownSize

proc computeSizeAlign(conf: ConfigRef; typ: PType) =
  ## computes and sets ``size`` and ``align`` members of ``typ``
  assert typ != nil
  let hasSize = typ.size != szUncomputedSize
  let hasAlign = typ.align != szUncomputedSize

  if hasSize and hasAlign:
    # nothing to do, size and align already computed
    return

  # This function can only calculate both, size and align at the same time.
  # If one of them is already set this value is stored here and reapplied
  let revertSize = typ.size
  let revertAlign = typ.align
  defer:
    if hasSize:
      typ.size = revertSize
    if hasAlign:
      typ.align = revertAlign

  if typ.size == szIllegalRecursion or typ.align == szIllegalRecursion:
    # we are already computing the size of the type
    # --> illegal recursion in type
    return

  # mark computation in progress
  typ.size = szIllegalRecursion
  typ.align = szIllegalRecursion

  var maxAlign, sizeAccum, length: BiggestInt

  var tk = typ.kind
  case tk
  of tyProc:
    if typ.callConv == ccClosure:
      typ.size = 2 * conf.target.ptrSize
    else:
      typ.size = conf.target.ptrSize
    typ.align = int16(conf.target.ptrSize)

  of tyNil:
    typ.size = conf.target.ptrSize
    typ.align = int16(conf.target.ptrSize)

  of tyString:
    if conf.selectedGC == gcDestructors:
      typ.size = conf.target.ptrSize * 2
    else:
      typ.size = conf.target.ptrSize
    typ.align = int16(conf.target.ptrSize)

  of tyCString, tySequence, tyPtr, tyRef, tyVar, tyLent, tyOpenArray:
    let base = typ.lastSon
    if base == typ:
      # this is not the correct location to detect ``type A = ptr A``
      typ.size = szIllegalRecursion
      typ.align = szIllegalRecursion
      return

    typ.align = int16(conf.target.ptrSize)
    if typ.kind == tySequence and conf.selectedGC == gcDestructors:
      typ.size = conf.target.ptrSize * 2
    else:
      typ.size = conf.target.ptrSize

  of tyArray:
    computeSizeAlign(conf, typ.sons[1])
    let elemSize = typ.sons[1].size
    if elemSize < 0:
      typ.size = elemSize
      typ.align = int16(elemSize)
    else:
      typ.size = lengthOrd(conf, typ.sons[0]) * elemSize
      typ.align = typ.sons[1].align

  of tyUncheckedArray:
    let base = typ.lastSon
    computeSizeAlign(conf, base)
    typ.size = 0
    typ.align = base.align
  of tyEnum:
    if firstOrd(conf, typ) < 0:
      typ.size = 4              # use signed int32
      typ.align = 4
    else:
      length = lastOrd(conf, typ)   # BUGFIX: use lastOrd!
      if length + 1 < `shl`(1, 8):
        typ.size = 1
        typ.align = 1
      elif length + 1 < `shl`(1, 16):
        typ.size = 2
        typ.align = 2
      elif length + 1 < `shl`(BiggestInt(1), 32):
        typ.size = 4
        typ.align = 4
      else:
        typ.size = 8
        typ.align = 8
  of tySet:
    if typ.sons[0].kind == tyGenericParam:
      typ.size = szUncomputedSize
      typ.align = szUncomputedSize # in original version this was 1
    else:
      length = lengthOrd(conf, typ.sons[0])
      if length <= 8:
        typ.size = 1
      elif length <= 16:
        typ.size = 2
      elif length <= 32:
        typ.size = 4
      elif length <= 64:
        typ.size = 8
      elif align(length, 8) mod 8 == 0:
        typ.size = align(length, 8) div 8
      else:
        typ.size = align(length, 8) div 8 + 1
    typ.align = int16(typ.size)
  of tyRange:
    computeSizeAlign(conf, typ.sons[0])
    typ.size = typ.sons[0].size
    typ.align = typ.sons[0].align
  of tyTuple:
    try:
      var accum = OffsetAccum(maxAlign: 1)
      for i in 0 ..< sonsLen(typ):
        let child = typ.sons[i]
        computeSizeAlign(conf, child)
        accum.align(child.align)
        if typ.n != nil: # is named tuple (has field symbols)?
          let sym = typ.n[i].sym
          sym.offset = accum.offset
        accum.inc(int(child.size))
      accum.finish
      typ.size = accum.offset
      typ.align = int16(accum.maxAlign)
    except IllegalTypeRecursionError:
      typ.size = szIllegalRecursion
      typ.align = szIllegalRecursion
  of tyObject:
    var headerSize: BiggestInt
    var headerAlign: int16
    if typ.sons[0] != nil:
      # compute header size
      if conf.cmd == cmdCompileToCpp:
        # if the target is C++ the members of this type are written
        # into the padding byets at the end of the parent type. At the
        # moment it is not supported to calculate that.
        headerSize = szUnknownSize
        headerAlign = szUncomputedSize
      else:
        var st = typ.sons[0]
        while st.kind in skipPtrs:
          st = st.sons[^1]
        computeSizeAlign(conf, st)
        if st.size == szIllegalRecursion:
          typ.size = st.size
          typ.align = st.align
          return
        headerSize = st.size
        headerAlign = st.align
    elif isObjectWithTypeFieldPredicate(typ):
      # this branch is taken for RootObj
      headerSize = conf.target.intSize
      headerAlign = conf.target.intSize.int16
    else:
      headerSize = 0
      headerAlign = 1
    let (offset, align) =
      if tfUnion in typ.flags:
        if tfPacked in typ.flags:
          let info = if typ.sym != nil: typ.sym.info else: unknownLineInfo()
          localError(conf, info, "type may not be packed and union at the same time.")
          (BiggestInt(szUnknownSize), BiggestInt(szUnknownSize))
        else:
          computeUnionObjectOffsetsFoldFunction(conf, typ.n, false)
      elif tfPacked in typ.flags:
        (computePackedObjectOffsetsFoldFunction(conf, typ.n, headerSize, false), BiggestInt(1))
      else:
        computeObjectOffsetsFoldFunction(conf, typ.n, headerSize)
    if offset == szIllegalRecursion:
      typ.size = szIllegalRecursion
      typ.align = szIllegalRecursion
      return
    if offset == szUnknownSize or (
        typ.sym != nil and
        typ.sym.flags * {sfCompilerProc, sfImportc} == {sfImportc}):
      typ.size = szUnknownSize
      typ.align = szUnknownSize
      return
    # header size is already in size from computeObjectOffsetsFoldFunction
    # maxAlign is probably not changed at all from headerAlign
    if tfPacked in typ.flags:
      typ.size = offset
      typ.align = 1
    else:
      typ.align = int16(max(align, headerAlign))
      typ.size = align(offset, typ.align)
  of tyInferred:
    if typ.len > 1:
      computeSizeAlign(conf, typ.lastSon)
      typ.size = typ.lastSon.size
      typ.align = typ.lastSon.align

  of tyGenericInst, tyDistinct, tyGenericBody, tyAlias, tySink, tyOwned:
    computeSizeAlign(conf, typ.lastSon)
    typ.size = typ.lastSon.size
    typ.align = typ.lastSon.align

  of tyTypeClasses:
    if typ.isResolvedUserTypeClass:
      computeSizeAlign(conf, typ.lastSon)
      typ.size = typ.lastSon.size
      typ.align = typ.lastSon.align
    else:
      typ.size = szUncomputedSize
      typ.align = szUncomputedSize

  of tyTypeDesc:
    computeSizeAlign(conf, typ.base)
    typ.size = typ.base.size
    typ.align = typ.base.align

  of tyForward:
    # is this really illegal recursion, or maybe just unknown?
    typ.size = szIllegalRecursion
    typ.align = szIllegalRecursion

  of tyStatic:
    if typ.n != nil:
      computeSizeAlign(conf, typ.lastSon)
      typ.size = typ.lastSon.size
      typ.align = typ.lastSon.align
    else:
      typ.size = szUncomputedSize
      typ.align = szUncomputedSize
  else:
    typ.size = szUncomputedSize
    typ.align = szUncomputedSize

template foldSizeOf*(conf: ConfigRef; n: PNode; fallback: PNode): PNode =
  let config = conf
  let node = n
  let typ = node[1].typ
  computeSizeAlign(config, typ)
  let size = typ.size
  if size >= 0:
    let res = newIntNode(nkIntLit, size)
    res.info = node.info
    res.typ = node.typ
    res
  else:
    fallback

template foldAlignOf*(conf: ConfigRef; n: PNode; fallback: PNode): PNode =
  let config = conf
  let node = n
  let typ = node[1].typ
  computeSizeAlign(config, typ)
  let align = typ.align
  if align >= 0:
    let res = newIntNode(nkIntLit, align)
    res.info = node.info
    res.typ = node.typ
    res
  else:
    fallback

template foldOffsetOf*(conf: ConfigRef; n: PNode; fallback: PNode): PNode =
  ## Returns an int literal node of the given offsetof expression in `n`.
  ## Falls back to `fallback`, if the `offsetof` expression can't be processed.
  let config = conf
  let node : PNode = n
  var dotExpr: PNode
  block findDotExpr:
    if node[1].kind == nkDotExpr:
      dotExpr = node[1]
    elif node[1].kind == nkCheckedFieldExpr:
      dotExpr = node[1][0]
    else:
      localError(config, node.info, "can't compute offsetof on this ast")

  assert dotExpr != nil
  let value = dotExpr[0]
  let member = dotExpr[1]
  computeSizeAlign(config, value.typ)
  let offset = member.sym.offset
  if offset >= 0:
    let tmp = newIntNode(nkIntLit, offset)
    tmp.info = node.info
    tmp.typ = node.typ
    tmp
  else:
    fallback
