module RubySMB::Dcerpc::Ndr

  require 'ruby_smb/field'

  # NDR Syntax
  UUID = '8a885d04-1ceb-11c9-9fe8-08002b104860'
  VER_MAJOR = 2
  VER_MINOR = 0

  #####################################
  #        NDR Primitive Types        #
  #####################################

  # Signed and unsigned integers use BinData primitives directly

  # [Booleans](https://pubs.opengroup.org/onlinepubs/9629399/chap14.htm#tagcjh_19_02_03)
  class NdrBoolean < BinData::Uint32le
    default_parameters byte_align: 4

    def assign(val)
      super(value_to_int(val))
    end

    def snapshot
      return _value == 0 ? false : true
    end


    private

    def value_to_binary_string(val)
      super(value_to_int(val))
    end

    def value_to_int(val)
      val = val.snapshot if val.respond_to?(:snapshot)
      case(val)
      when FalseClass
        return 0
      when TrueClass
        return 1
      when Integer
        # Any non-zero value is TRUE, let's assume the caller knows what he's doing
        return val
      else
        raise ArgumentError.new(
          "Type mismatch (#{val.class}). Expecting FalseClass, TrueClass or Integer"
        )
      end
    end
  end

  # [Characters](https://pubs.opengroup.org/onlinepubs/9629399/chap14.htm#tagcjh_19_02_04)
  class NdrChar < BinData::String
    default_parameter(length: 1, byte_align: 1)
  end

  class NdrWideChar < RubySMB::Field::String16
    default_parameter(length: 2, byte_align: 2)
  end

  # An NDR Enum type as defined in
  # [Transfer Syntax NDR - Enumerated Types](https://pubs.opengroup.org/onlinepubs/9629399/chap14.htm#tagcjh_19_02_05_01)
  class NdrEnum < BinData::Int16le
    default_parameters byte_align: 2
  end

  # [Integers](https://pubs.opengroup.org/onlinepubs/9629399/chap14.htm#tagcjh_19_02_05)
  # This will define the eight Integers accepted by the NDR protocol:
  # - NdrInt8
  # - NdrUint8
  # - NdrInt16
  # - NdrUint16
  # - NdrInt32
  # - NdrUint32
  # - NdrInt64
  # - NdrUint64
  {Int8: 1, Uint8: 1, Int16le: 2, Uint16le: 2, Int32le: 4, Uint32le: 4, Int64le: 8, Uint64le: 8}.each do |klass, nb_bytes|
    new_klass_name = "Ndr#{klass.to_s.chomp('le')}"
    unless self.const_defined?(new_klass_name)
      new_klass = Class.new(BinData.const_get(klass)) do
        default_parameters byte_align: nb_bytes
      end
      self.const_set(new_klass_name, new_klass)
      BinData::RegisteredClasses.register(new_klass_name, new_klass)
    end
  end

  class NdrFileTime < RubySMB::Field::FileTime
    # Note that the original Microsoft FILETIME structure is composed of two
    # DWORDs, whereas RubySMB implementation uses an Uint64 field. For this
    # reason, the alignement is set to the size of a DWORD (4 bytes) to match
    # Microsoft structures.
    default_parameters byte_align: 4
  end



  #####################################
  #       NDR Constructed Types       #
  #####################################

  module ConstructedTypePlugin
    def initialize_instance
      @deferred_ptrs = []
      super
    end

    def defer_ptr(ref)
      @deferred_ptrs << ref
    end

    def has_deferred_ptrs?
      !!@deferred_ptrs&.any?
    end

    def write_ptr(io)
      @deferred_ptrs.each do |ptr_ref|
        ptr_ref.do_write(io, is_deferred: true)
      end
      @deferred_ptrs.clear
    end

    def read_ptr(io)
      @deferred_ptrs.each do |ptr_ref|
        ptr_ref.do_read(io, is_deferred: true)
      end
      @deferred_ptrs.clear
    end

    def is_deferring(obj)
      @deferred_ptrs.any? { |e| e.equal?(obj) }
    end

    def get_top_level_constructed_type
      return self if is_a?(PointerPlugin)

      res = nil
      if parent&.is_a?(ConstructedTypePlugin)
        res = parent.get_top_level_constructed_type
      end
      return res || self
    end

    def do_num_bytes_ptr(struct_offset)
      sum = 0
      @deferred_ptrs.each do |ptr_ref|
        sum += ptr_ref.do_num_bytes(struct_offset + sum, is_deferred: true)
      end
      @deferred_ptrs.clear
      sum
    end
  end


  #
  # Arrays
  #

  module ArrayClassPlugin
    module ExtendArrayPlugin
      def initialize_shared_instance
        super
        extend ArrayPlugin
      end
    end
    module ExtendConfPlugin
      def initialize_shared_instance
        super
        extend ConfPlugin
      end
    end
    module ExtendVarPlugin
      def initialize_shared_instance
        super
        extend VarPlugin
      end
    end
    def self.extended(target)
      target.default_parameters(
        :read_until => lambda { index == (@obj.read_until_index - 1) },
        # Set :byte_align to 4 bytes by default, which is the size of the `size
        # information` field (:max_count or :offset/:actual_count). If the
        # elements set with :type are greater, this parameter will be
        # updated later in NdrArrayArgProcessor::sanitize_parameters!
        :byte_align => 4
      )
      target.arg_processor :ndr_array
      class_name = target.to_s.split('::').last
      if class_name.include?('NdrVar') || class_name.include?('NdrConfVar')
        target.include ExtendVarPlugin
      end
      if class_name.include?('NdrConf')
        target.include ExtendConfPlugin
        target.extend ConfClassPlugin
      end
      target.include ExtendArrayPlugin
    end
  end

  module ArrayPlugin
    include ConstructedTypePlugin

    def align_element_size(offset)
      align = eval_parameter(:type).instance_variable_get(:@obj_params)[:byte_align]
      align ? (align - (offset % align)) % align : 0
    end

    def should_process_max_count?
      # :max_count has already been processed if the parent structure is an
      # NdrStruct, but this is not the case if we are dealing with a pointer
      !parent.is_a?(NdrStruct) || self.is_a?(PointerPlugin)
    end

    def do_write(io)
      if is_a?(ConfPlugin) && should_process_max_count?
        io.writebytes([@max_count].pack('L<'))
      end

      if is_a?(VarPlugin)
        io.writebytes([@offset].pack('L<'))
        io.writebytes([@actual_count].pack('L<'))
      end

      unless empty?
        io.writebytes("\x00" * align_element_size(io.offset))
        super
      end

      if has_deferred_ptrs?
        write_ptr(io)
      end
    end

    def has_elements_to_read?
      # When reading a binary stream, the only elements that indicate the array
      # has elements to read are :actual_count, :max_count or :initial_length
      # parameter, depending on the type of NDR Array:
      # 1. When :actual_count is present (NdrVarArray and NdrConfVarArray), it
      #    indicates the actual number of elements passed
      # 2. When only :max_count is present (NdrConfArray), we're assuming the
      #    maximum number of elements is the actual number of elements in this
      #    array
      # 3. None of them are present, but :initial_length parameter has been
      #    set, meaning we are delaing with a fixed array without any embedded
      #    size information (NdrFixArray)
      (@actual_count&.> 0) ||
      @actual_count.nil? && (@max_count&.> 0) ||
      @actual_count.nil? && @max_count.nil? && (eval_parameter(:initial_length)&.> 0)
    end

    def do_read(io)
      if is_a?(ConfPlugin) && should_process_max_count?
        max_count = io.readbytes(4).unpack1('L<')
        BinData.trace_message do |tracer|
          tracer.trace_obj("#{debug_name}.max_count", max_count.to_s)
        end
        set_max_count(max_count)
      end

      if is_a?(VarPlugin)
        @offset = io.readbytes(4).unpack('L<').first
        BinData.trace_message do |tracer|
          tracer.trace_obj("#{debug_name}.offset", @offset.to_s)
        end
        @actual_count = @read_until_index = io.readbytes(4).unpack('L<').first
        BinData.trace_message do |tracer|
          tracer.trace_obj("#{debug_name}.actual_count", @actual_count.to_s)
        end
      end

      if has_elements_to_read?
        io.seekbytes(align_element_size(io.offset))
        super
      end

      if has_deferred_ptrs?
        read_ptr(io)
      end
    end

    def do_num_bytes(struct_offset = 0)
      sum = 0

      if is_a?(ConfPlugin) && should_process_max_count?
        sum += 4
      end

      if is_a?(VarPlugin)
        sum += 8
      end

      unless empty?
        sum += align_element_size(struct_offset + sum)
        sum += super()
      end

      if has_deferred_ptrs?
        sum += do_num_bytes_ptr(struct_offset + sum)
      end
      sum
    end

    def sum_num_bytes_below_index(index)
      (0...index).inject(0) do |sum, i|
        nbytes = 0
        if elements[i].has_parameter?(:byte_align) && elements[i].respond_to?(:bytes_to_align)
          nbytes = elements[i].bytes_to_align(elements[i], sum.ceil)
        end
        nbytes += elements[i].do_num_bytes

        if nbytes.is_a?(Integer)
          sum.ceil + nbytes
        else
          sum + nbytes
        end
      end
    end
  end

  module ConfClassPlugin; end

  module ConfPlugin
    attr_accessor :read_until_index, :max_count

    def initialize_instance
      @read_until_index = 0
      @max_count = 0
      @max_count_set = false
      super
    end

    def insert(index, *objs)
      obj = super
      @max_count = length unless @max_count_set
      obj
    end

    def slice_index(index)
      obj = super
      @max_count = length unless @max_count_set
      obj
    end

    def []=(index, value)
      obj = super
      @max_count = length unless @max_count_set
      obj
    end

    def set_max_count(val)
      @max_count = @read_until_index = val
      @max_count_set = true
    end
  end

  module VarPlugin
    attr_accessor :read_until_index, :actual_count, :offset

    def initialize_instance
      @read_until_index = 0
      @actual_count = 0
      @offset = 0
      super
    end

    def insert(index, *objs)
      obj = super
      @actual_count = length
      obj
    end

    def slice_index(index)
      obj = super
      @actual_count = length
      obj
    end

    def []=(index, value)
      obj = super
      @actual_count = length
      obj
    end
  end

  # This ArgProcessor needs to inherit from BinData::ArrayArgProcessor to make
  # sure the ArrayArgProcessor `sanitize_parameters!` is called. This will
  # perform proper Array-related sanity checks on the given parameters.
  class ::BinData::NdrArrayArgProcessor < BinData::ArrayArgProcessor
    def sanitize_parameters!(obj_class, params)
      res = super

      type_class = params[:type]
      # Let the BinData::Array sanitization routine deal with "no type provided"
      return res unless type_class

      type_class, type_params  = params[:type] if type_class.is_a?(Array)
      if type_class.has_parameter?(:byte_align)
        # According to NDR alignemnt rules for arrays: Array alignment is the
        # largest alignment of the array element type and the size information
        # type, if any.
        # So, here, we pick the greatest value between the size of the `size
        # information` field (:max_count or :offset/:actual_count), which is 4
        # bytes for 32-bit NDR, and the element type size
        byte_align = type_class.instance_variable_get(:@obj_params)[:byte_align]
        if obj_class < NdrFixArray
          # Fixed size arrays doesn't have size information
          params[:byte_align] = byte_align
        else
          params[:byte_align] = [4, byte_align].max
        end
        return res
      elsif type_params&.key?(:byte_align)
        return res
      end

      raise ArgumentError.new(
        "NDR Arrays must only include elements with the `:byte_align` "\
        "parameter set. This makes sure the whole structure is correctly "\
        "aligned. Use a predefined NDR element instead, or provide the "\
        "`:byte_align` parameter in `:type` (Faulty element type: #{params[:type]})"
      )
    end
  end

  # [Uni-dimensional Fixed Arrays](https://pubs.opengroup.org/onlinepubs/9629399/chap14.htm#tagcjh_19_03_03_01)
  class NdrFixArray < BinData::Array
    arg_processor :ndr_array

    def initialize_shared_instance
      super
      extend ArrayPlugin
    end

    def insert(index, *objs)
      fixed_size = get_parameter(:initial_length)
      if (length + objs.size) != fixed_size
        raise ArgumentError, "Can't add new elements to a NdrFixArray (set to #{fixed_size} elements)"
      else
        super
      end
    end

    def append_new_element
      fixed_size = get_parameter(:initial_length)
      raise ArgumentError, "Can't add new elements to a NdrFixArray (set to #{fixed_size} elements)"
    end
  end

  # Specific implementation for fixed array of bytes, which can be set from an array of unit8 or a string
  class NdrFixedByteArray < NdrFixArray
    default_parameters(type: :ndr_uint8, byte_align: 1)

    def assign(val)
      val = val.bytes if val.is_a?(String)
      super(val.to_ary)
    end
  end

  # [Uni-dimensional Conformant Arrays](https://pubs.opengroup.org/onlinepubs/9629399/chap14.htm#tagcjh_19_03_03_02)
  class NdrConfArray < BinData::Array
    extend ArrayClassPlugin
  end

  # [Uni-dimensional Varying Arrays](https://pubs.opengroup.org/onlinepubs/9629399/chap14.htm#tagcjh_19_03_03_03)
  class NdrVarArray < BinData::Array
    extend ArrayClassPlugin
  end

  # Uni-dimensional Conformant-varying Arrays
  class NdrConfVarArray < BinData::Array
    extend ArrayClassPlugin
  end

  # TODO: Multi-dimensional Arrays



  #
  # Strings
  #

  module StringClassPlugin
    module ExtendVarStringPlugin
      def initialize_shared_instance
        super
        extend VarStringPlugin
      end
    end
    module ExtendConfStringPlugin
      def initialize_shared_instance
        super
        extend ConfStringPlugin
      end
    end
    def self.extended(target)
      target.default_parameters byte_align: 4
      char_size = 1
      char_size = 2 if target < RubySMB::Field::String16 || target < RubySMB::Field::Stringz16
      if target < BinData::Stringz
        target.default_parameters(:max_length => lambda { @obj.actual_count * char_size })
      else
        target.default_parameters(:length => lambda { @obj.actual_count * char_size })
      end
      target.include ExtendVarStringPlugin
      class_name = target.to_s.split('::').last
      if class_name.include?('NdrConfVar')
        target.include ExtendConfStringPlugin
        target.extend ConfClassPlugin
      end
    end
  end

  module ConfStringPlugin
    attr_accessor :max_count

    def initialize_instance
      @max_count = 0
      if has_parameter?(:initial_value)
        set_max_count(get_max_count(eval_parameter(:initial_value)))
      end
      super
    end

    def should_process_max_count?
      # :max_count has already been processed if the parent structure is an
      # NdrStruct, but this is not the case if we are dealing with a pointer
      !parent.is_a?(NdrStruct) || self.is_a?(PointerPlugin)
    end

    def do_write(io)
      if should_process_max_count?
        io.writebytes([@max_count].pack('L<'))
      end
      super
    end

    def do_read(io)
      if should_process_max_count?
        max_count = io.readbytes(4).unpack1('L<')
        BinData.trace_message do |tracer|
          tracer.trace_obj("#{debug_name}.max_count", max_count.to_s)
        end
        set_max_count(max_count)
      end
      super
    end

    def assign(val)
      if val.is_a?(ConfStringPlugin)
        @max_count = val.max_count
      else
        set_max_count(get_max_count(val))
      end
      super
    end

    def do_num_bytes
      sum = 0
      if should_process_max_count?
        # add max_count size
        sum += 4
      end
      sum + super
    end

    def get_max_count(val)
      if is_a?(BinData::Stringz)
        max_count = val.to_s.strip.length
        # Add one to count the terminator. According to
        # https://pubs.opengroup.org/onlinepubs/9629399/chap14.htm#tagcjh_19_03_04_02,
        # the NDR String must contain at least one element, the terminator. So,
        # add one even if it is an empty string.
        max_count += 1
        return max_count
      else
        return val.to_s.length
      end
    end

    def set_max_count(val)
      @max_count = val
    end
  end

  module VarStringPlugin
    attr_accessor :actual_count, :offset

    def initialize_instance
      @offset = 0
      @actual_count = 0
      if has_parameter?(:initial_value)
        update_actual_count(eval_parameter(:initial_value))
      end
      super
    end

    def do_write(io)
      io.writebytes([@offset].pack('L<'))
      io.writebytes([@actual_count].pack('L<'))
      super if @actual_count > 0
    end

    def do_read(io)
      @offset = io.readbytes(4).unpack('L<').first
       BinData.trace_message do |tracer|
        tracer.trace_obj("#{debug_name}.offset", @offset.to_s)
      end
      @actual_count = io.readbytes(4).unpack('L<').first
      BinData.trace_message do |tracer|
        tracer.trace_obj("#{debug_name}.actual_count", @actual_count.to_s)
      end
      super if @actual_count > 0
    end

    def assign(val)
      update_actual_count(val)
      super
    end

    def do_num_bytes
      @actual_count > 0 ? (8 + super) : 8
    end

    def update_actual_count(val)
      if is_a?(BinData::Stringz)
        @actual_count = val.to_s.strip.length
        # Add one to count the terminator. According to
        # https://pubs.opengroup.org/onlinepubs/9629399/chap14.htm#tagcjh_19_03_04,
        # the NDR String must contain at least one element, the terminator. So,
        # add one even if it is an empty string.
        @actual_count += 1
      else
        @actual_count = val.to_s.length
      end
    end
  end

  # [Varying Strings](https://pubs.opengroup.org/onlinepubs/9629399/chap14.htm#tagcjh_19_03_04_01)
  class NdrVarString < BinData::String
    extend StringClassPlugin
  end

  class NdrVarStringz < BinData::Stringz
    extend StringClassPlugin
  end

  class NdrVarWideString < RubySMB::Field::String16
    extend StringClassPlugin
  end

  class NdrVarWideStringz < RubySMB::Field::Stringz16
    extend StringClassPlugin
  end

  # [Conformant and Varying Strings](https://pubs.opengroup.org/onlinepubs/9629399/chap14.htm#tagcjh_19_03_04_02)
  class NdrConfVarString < BinData::String
    extend StringClassPlugin
  end

  class NdrConfVarStringz < BinData::Stringz
    extend StringClassPlugin
  end

  class NdrConfVarWideString < RubySMB::Field::String16
    extend StringClassPlugin
  end

  class NdrConfVarWideStringz < RubySMB::Field::Stringz16
    extend StringClassPlugin
  end

  # TODO:[Arrays of Strings](https://pubs.opengroup.org/onlinepubs/9629399/chap14.htm#tagcjh_19_03_05)
  # Microsoft DCERPC uses array of pointers for strings. I couldn't find any reference to array of strings.


  #
  # [Structures](https://pubs.opengroup.org/onlinepubs/9629399/chap14.htm#tagcjh_19_03_06)
  #

  module StructPlugin
    include ConstructedTypePlugin

    def should_process_max_count?
      # According to the NDR definition for Structures Containing a Conformant
      # Array:
      #
      # "In the NDR representation of a structure that contains a
      # conformant array, the unsigned long integers that give maximum element
      # counts for dimensions of the array are moved to the beginning of the
      # structure, and the array elements appear in place at the end of the
      # structure. If a structure that contains a conformant array itself a
      # member of another structure, the maximum element counts are further
      # moved to the beginning of the containing structure. This construction
      # iterates through all enclosing structures."
      #
      # This only applies if the current object is the top level structure (no
      # parent). Note that if it is a pointer to a structure and the current
      # object is being deferred, :max_count still need to be processed since
      # it had not been moved to the beginning of the parent structure.
      klass = is_a?(PointerPlugin) ? self.class.superclass : self.class
      parent_obj = nil
      # TODO: possible issue: parent can be a BinData::Choice, and won't be
      # detected as a ConstructedTypePlugin, even if the embeding structure is.
      # Check this with a BinData::Choice that points to a structure embedding
      # a conformant structure
      if parent&.is_a?(ConstructedTypePlugin)
        parent_obj = parent.get_top_level_constructed_type
      end
      klass.has_conformant_array && (parent_obj.nil? || parent_obj.is_deferring(self))
    end

    def do_write(io)
      if should_process_max_count?
        max_count = retrieve_max_count
        io.writebytes([max_count].pack('L<')) if max_count

        # Align the structure according to the alignment rules for the structure
        if respond_to?(:referent_bytes_align)
          io.writebytes("\x00" * referent_bytes_align(io.offset))
        elsif has_parameter?(:byte_align)
          io.writebytes("\x00" * bytes_to_align(self, io.offset))
        end
      end

      super

      if has_deferred_ptrs?
        write_ptr(io)
      end
    end

    def do_read(io)
      if should_process_max_count?
        max_count = io.readbytes(4).unpack1('L<')
        BinData.trace_message do |tracer|
          tracer.trace_obj("#{debug_name}.max_count", max_count.to_s)
        end
        set_max_count(max_count)

        # Align the structure according to the alignment rules for the structure
        if respond_to?(:referent_bytes_align)
          io.seekbytes(referent_bytes_align(io.offset))
        elsif has_parameter?(:byte_align)
          io.seekbytes(bytes_to_align(self, io.offset))
        end
      end

      super

      if has_deferred_ptrs?
        read_ptr(io)
      end
    end

    def retrieve_max_count
      obj = self[field_names.last]
      return obj.length if obj.is_a?(ConfPlugin)
      return obj.get_max_count(obj) if obj.is_a?(ConfStringPlugin)
      if obj.is_a?(NdrStruct)
        return obj.retrieve_max_count
      end
    end

    def set_max_count(val)
      obj = self[field_names.last]
      obj.set_max_count(val)
    end

    def do_num_bytes
      sum = 0

      if should_process_max_count?
        # count max_count (4 bytes)
        max_count = retrieve_max_count
        sum += 4 if max_count

        if respond_to?(:referent_bytes_align)
          sum += referent_bytes_align(sum)
        elsif has_parameter?(:byte_align)
          sum += bytes_to_align(self, sum)
        end
      end

      sum += super

      if has_deferred_ptrs?
        sum += do_num_bytes_ptr(sum)
      end

      sum
    end

    def bytes_to_align(obj, rel_offset)
      if obj.is_a?(PointerPlugin)
        # Pointers are always 4-bytes aligned
        return (4 - (rel_offset % 4)) % 4
      end
      if obj.is_a?(ConfPlugin)
        # `max_count` should have been handled at the beginning of the structure
        # already. We need to fix `rel_offset` since it includes the
        # `max_count` 4 bytes, plus the possible padding bytes needed to align
        # the structure. This is required because BinData Struct is not
        # aware of `max_count` and consider the first field to be the beginning
        # of the structure instead. We have to make sure the alignment is
        # calculated from the beginning of the structure.
        align = eval_parameter(:byte_align)
        pad_length = (align - (4 % align)) % align
        rel_offset += (4 + pad_length)

        # We need to handle another corner case, which is a Conformant array
        # (not Varying). The size information (max_count) has been place in
        # from of the structure and no other size information is present before
        # the actual elements of the array. Therefore, the alignment must be
        # done according to the rules of the elements. Since a NdrArray has its
        # default :byte_align value set to 4 (:max_count size), we have to make
        # sure the element size is used instead.
        unless obj.is_a?(VarPlugin)
          return obj.align_element_size(rel_offset)
        end
      end
      is_a?(BinData::ByteAlignPlugin) ? super : 0

    end
  end

  class NdrStruct < BinData::Record
    # Caller must specify :byte_align according to the type of the largest element in the structure.
    # See https://pubs.opengroup.org/onlinepubs/9629399/chap14.htm#tagcjh_19_03_02
    #
    # "The alignment of a structure in the octet stream is the largest of the
    # alignments of the fields it contains. These fields may also be
    # constructed types. The same alignment rules apply recursively to nested
    # constructed types."
    mandatory_parameters(:byte_align)

    class << self; attr_reader :has_conformant_array end

    def self.validate_conformant_array(field)
      if @has_conformant_array
        raise ArgumentError.new(
          "Invalid structure #{self}: Conformant array or embedded structure "\
          "with Conformant array must be the last member of the structure"
        )
      end
      obj_proto = field.last.prototype
      obj_class = obj_proto.instance_variable_get(:@obj_class)
      @has_conformant_array = true if obj_class < NdrStruct && obj_class.has_conformant_array
      if obj_class.is_a?(ConfClassPlugin) && !obj_class.is_a?(PointerClassPlugin)
        @has_conformant_array = true
      end
    end

    def self.method_missing(symbol, *args, &block)
      field = super
      if field.is_a?(::Array) && field.last.is_a?(BinData::SanitizedField)
        unless field.last.has_parameter?(:byte_align) || field.last.instantiate.bit_aligned?
          raise ArgumentError.new(
            "NDR Structures must only include elements with the `:byte_align` "\
            "parameter set. This makes sure the whole structure is correctly "\
            "aligned. Use a predefined NDR element instead, or provide the "\
            "`:byte_align` parameter when defining the structure "\
            "(Faulty element: #{field.last.name})"
          )
        end
        validate_conformant_array(field)
      end
      field
    end

    def initialize_shared_instance
      super
      extend StructPlugin
    end
  end

  #
  # [Unions](https://pubs.opengroup.org/onlinepubs/9629399/chap14.htm#tagcjh_19_03_08)
  #

  # TODO: Unions
  # TODO: Pipes

  #
  # [Pointers](https://pubs.opengroup.org/onlinepubs/9629399/chap14.htm#tagcjh_19_03_10)
  #

  module PointerClassPlugin
    module ExtendPointerPlugin
      def initialize_shared_instance
        super
        extend PointerPlugin
      end
    end
    def self.extended(target)
      target.default_parameters byte_align: 4
      target.arg_processor :ndr_pointer
      target.include ExtendPointerPlugin
    end
  end


  module TopLevelPlugin
    module TopLevelClassMethods
      def pos
        @@pos
      end
      def increment_pos
        @@pos += 1
      end
      def reset_pos
        @@pos = 0
      end
    end

    def self.extended(target)
      target.class.extend(TopLevelClassMethods)
      target.class.reset_pos
    end

    def initialize_instance
      super
      @standalone_ptr = false
    end

    def do_write(io, is_deferred: false)
      # If for whatever reasons, the #pos value has been modified, reset it to
      # make sure the pointer ref_id will start from INITIAL_REF_ID
      self.class.reset_pos if is_top_level_ptr || @standalone_ptr
      if is_deferred
        super(io, is_deferred: is_deferred)
      else
        super(io)
      end
      # Since #pos has been incremented for each embedded pointer, let's reset
      # it to go back to its initial state
      self.class.reset_pos if is_top_level_ptr || @standalone_ptr
    end

    def to_binary_s
      @standalone_ptr = true
      res = super
      @standalone_ptr = false
      res
    end

    def set_top_level_ptr
      @top_level_ptr = true
      #update_ref_ids
    end

    def unset_top_level_ptr
      @top_level_ptr = false
    end

    def is_top_level_ptr
      !!@top_level_ptr
    end

    def num_bytes
      @standalone_ptr = true
      res = super
      @standalone_ptr = false
      res
    end
  end

  # Windows SMB client uses 0x00020000 as an initial reference ID, but it is
  # rejected by the server on the Windows Server 2003. On this version, only
  # 0x00000001 seems to be accepted. So, we need to use this value to maintain
  # compatibility.
  INITIAL_REF_ID = 0x00000001

  module PointerPlugin
    attr_accessor :ref_id

    def initialize_instance
      if @ref_id.nil?
        if eval_parameter(:initial_value)
          instantiate_referent
        else
          @ref_id = 0
        end
      end
      extend_top_level_class
      super
    end

    def extend_top_level_class
      current = self
      loop do
        current.extend(TopLevelPlugin) unless current.is_a?(TopLevelPlugin)
        if current.parent.nil?
          current.set_top_level_ptr unless current.is_top_level_ptr
          break
        else
          current.unset_top_level_ptr if current.is_top_level_ptr
          current = current.parent
        end
      end
    end

    def snapshot
      if is_alias?
        fetch_alias_referent
      elsif is_null_ptr? && !eval_parameter(:initial_value)
        :null
      else
        super
      end
    end

    def referent_bytes_align(offset)
      align = self.class.superclass.default_parameters[:byte_align]
      align = eval_parameter(:referent_byte_align) unless align
      (align - (offset % align)) % align
    end

    def write_ref_id(io)
      if is_alias?
        ref_field = fetch_alias_referent
        if ref_field
          if ref_field.class != self.class
            raise ArgumentError, "Pointer points to a different referent type: #{ref_field.class} (set to #{obj.class})"
          end
          @ref_id = ref_field.ref_id
        end
      elsif @ref_id != 0 || (is_null_ptr? && eval_parameter(:initial_value))
        @ref_id = INITIAL_REF_ID + self.class.pos
        self.class.increment_pos unless @standalone_ptr
      end
      io.writebytes([@ref_id].pack('L<'))
    end

    def do_write(io, is_deferred: false)
      if is_deferred
        if is_a?(NdrStruct) && self.class.superclass.has_conformant_array
          # align :max_count since it will be placed in front of the structure.
          # The structure itself will be properly aligned later.
          align = (4 - (io.offset % 4)) % 4
          io.writebytes("\x00" * align)
        else
          io.writebytes("\x00" * referent_bytes_align(io.offset))
        end
      else
        write_ref_id(io)
        parent_obj = nil
        if parent&.is_a?(ConstructedTypePlugin)
          parent_obj = parent.get_top_level_constructed_type
        end
        if parent_obj && @ref_id != 0 && !@standalone_ptr
          parent_obj.defer_ptr(self)
          return
        end
      end
      super(io) unless (is_null_ptr? && !eval_parameter(:initial_value)) || is_alias?
    end

    def do_read(io, is_deferred: false)
      if is_deferred
        if is_a?(NdrStruct) && self.class.superclass.has_conformant_array
          # align :max_count since it will be placed in front of the structure.
          # The structure itself will be properly aligned later.
          align = (4 - (io.offset % 4)) % 4
          io.seekbytes(align)
        else
          io.seekbytes(referent_bytes_align(io.offset))
        end
      else
        @ref_id = io.readbytes(4).unpack('L<').first
        BinData.trace_message do |tracer|
          tracer.trace_obj("#{debug_name}.ref_id", @ref_id.to_s)
        end
        parent_obj = nil
        if parent&.is_a?(ConstructedTypePlugin)
          parent_obj = parent.get_top_level_constructed_type
        end
        if parent_obj && @ref_id != 0
          parent_obj.defer_ptr(self)
          return
        end
      end
      super(io) unless is_null_ptr? || is_alias?
    end

    def assign(val)
      if val == :null
        @ref_id = 0
      elsif is_alias?
        ref_field = fetch_alias_referent
        raise ArgumentError, "Referent of alias pointer does not exist: #{get_parameter(:ref_to)}" unless ref_field
        ref_field.assign(val)
      else
        instantiate_referent if is_null_ptr?
        super
      end
    end

    def is_alias?
      has_parameter?(:ref_to)
    end

    def fetch_alias_referent(current: parent, ref: get_parameter(:ref_to), name: nil)
      return if current.nil?
      if current.get_parameter(:ref_to) == ref
        raise ArgumentError.new(
          "Pointer alias refering to #{ref} cannot be found. This referent "\
          "should appears before the alias in the stream"
        )
      end
      return current if name == ref
      res = nil
      case current
      when ArrayPlugin
        current.each do |element|
          res = fetch_alias_referent(current: element, ref: ref, name: name)
          break if res
        end
      when BinData::Record, BinData::Struct
        current.each_pair do |name, field|
          res = fetch_alias_referent(current: field, ref: ref, name: name)
          break if res
        end
      end
      return res
    end

    def do_num_bytes(struct_offset = 0, is_deferred: false)
      sum = 0
      if is_deferred
        if is_a?(NdrStruct) && self.class.superclass.has_conformant_array
          # align :max_count since it will be placed in front of the structure.
          # The structure itself will be properly aligned later.
          align = (4 - (struct_offset % 4)) % 4
          sum += align
        else
          sum += referent_bytes_align(struct_offset)
        end
      else
        # add ref_id size
        sum += 4

        parent_obj = nil
        if parent&.is_a?(ConstructedTypePlugin)
          parent_obj = parent.get_top_level_constructed_type
        end
        if parent_obj && @ref_id != 0 && !@standalone_ptr
          parent_obj.defer_ptr(self)
          # only return ref_id size, the actual referent size will be added later
          return sum
        end
      end
      unless (is_null_ptr? && !eval_parameter(:initial_value)) || is_alias?
        if is_a?(ArrayPlugin)
          sum += super(struct_offset + sum)
        else
          sum += super()
        end
      end

      sum
    end

    def instantiate_referent
      @ref_id = INITIAL_REF_ID
    end

    def is_null_ptr?
      @ref_id == 0
    end

    def insert(index, *objs)
      obj = super
      # If we just pushed a new element and it was a null pointer (ref_id==0),
      # we will initialize the ref_id to make sure it is not considered a null
      # pointer anymore
      if is_null_ptr? && is_a?(BinData::Array) && !empty?
        instantiate_referent
      end
      obj
    end
  end

  class ::BinData::NdrPointerArgProcessor < BinData::BaseArgProcessor
    def sanitize_parameters!(obj_class, params)
      obj_klass = obj_class
      obj_klass = obj_class.superclass if obj_class.superclass.arg_processor == self
      res = obj_class.superclass.arg_processor.sanitize_parameters!(obj_klass, params)

      return res if obj_class.superclass.default_parameters[:byte_align]
      return res if params[:referent_byte_align]

      raise ArgumentError.new(
        "NDR Pointers referent must have `:byte_align` parameter set. This "\
        "makes sure the whole structure is correctly aligned. Use a predefined "\
        "NDR element instead, or provide the `:referent_byte_align` parameter "\
        "when defining the structure (Faulty pointer class: #{obj_class})"
      )
    end

    def extract_args(obj_class, obj_args)
      obj_class = obj_class.superclass if obj_class.superclass.arg_processor == self
      obj_class.superclass.arg_processor.extract_args(obj_class, obj_args)
    end
  end

  # Pointers to NDR integers. This defined four pointers:
  # - NdrUint8Ptr
  # - NdrUint16Ptr
  # - NdrUint32Ptr
  # - NdrUint64Ptr
  {NdrUint8: 1, NdrUint16: 2, NdrUint32: 4, NdrUint64: 8}.each do |klass, align|
    new_klass_name = "#{klass.to_s}Ptr"
    unless self.const_defined?(new_klass_name)
      new_klass = Class.new(const_get(klass)) do
        extend PointerClassPlugin
      end
      self.const_set(new_klass_name, new_klass)
      BinData::RegisteredClasses.register(new_klass_name, new_klass)
    end
  end

  # Pointers to other classes
  class NdrCharPtr < NdrChar
    extend PointerClassPlugin
  end

  class NdrBooleanPtr < NdrBoolean
    extend PointerClassPlugin
  end

  class NdrStringPtr < NdrConfVarString
    extend PointerClassPlugin
  end

  class NdrStringzPtr < NdrConfVarStringz
    extend PointerClassPlugin
  end

  class NdrWideStringPtr < NdrConfVarWideString
    extend PointerClassPlugin
  end

  class NdrWideStringzPtr < NdrConfVarWideStringz
    extend PointerClassPlugin
  end


  # ArrayPtr definitions:
  #   If the type is Ndr*ArrayPtr, it's a ConfVarArray (Uni-dimensional Conformant-varying Array)
  #   If the type is Ndr*ConfArrayPtr, it's a ConfArray (Uni-dimensional Conformant Array)
  class NdrByteArrayPtr < NdrConfVarArray
    default_parameters type: :ndr_uint8
    extend PointerClassPlugin

    def assign(val)
      val = val.bytes if val.is_a?(String)
      super(val.to_ary)
    end
  end

  class NdrByteConfArrayPtr < NdrConfArray
    default_parameters type: :ndr_uint8
    extend PointerClassPlugin

    def assign(val)
      val = val.bytes if val.is_a?(String)
      super(val.to_ary)
    end
  end

  %i[ Uint8 Uint16 Uint32 Uint64 ].each do |klass|
    new_klass_name = "Ndr#{klass}ArrayPtr"
    unless self.const_defined?(new_klass_name)
      new_klass = Class.new(NdrConfVarArray) do
        default_parameters type: "ndr_#{klass}".downcase.to_sym
        extend PointerClassPlugin
      end
      self.const_set(new_klass_name, new_klass)
      BinData::RegisteredClasses.register(new_klass_name, new_klass)
    end

    new_klass_name = "Ndr#{klass}ConfArrayPtr"
    unless self.const_defined?(new_klass_name)
      new_klass = Class.new(NdrConfArray) do
        default_parameters type: "ndr_#{klass}".downcase.to_sym
        extend PointerClassPlugin
      end
      self.const_set(new_klass_name, new_klass)
      BinData::RegisteredClasses.register(new_klass_name, new_klass)
    end
  end

  class NdrFileTimePtr < NdrFileTime
    extend PointerClassPlugin
  end

  class UuidPtr < RubySMB::Dcerpc::Uuid
    default_parameter referent_byte_align: 4
    extend PointerClassPlugin
  end

  # An NDR Context Handle representation as defined in
  # [IDL Data Type Declarations - Basic Type Declarations](http://pubs.opengroup.org/onlinepubs/9629399/apdxn.htm#tagcjh_34_01)
  class NdrContextHandle < BinData::Primitive
    default_parameters byte_align: 4
    endian :little

    uint32 :context_handle_attributes
    uuid   :context_handle_uuid

    def get
      {:context_handle_attributes => context_handle_attributes, :context_handle_uuid => context_handle_uuid}
    end

    def set(handle)
      if handle.is_a?(Hash)
        self.context_handle_attributes = handle[:context_handle_attributes]
        self.context_handle_uuid = handle[:context_handle_uuid]
      elsif handle.is_a?(NdrContextHandle)
        read(handle.to_binary_s)
      else
        read(handle.to_s)
      end
    end
  end

  # (IDL/NDR) Pickles as defined in
  # [(IDL/NDR) # Pickles](https://pubs.opengroup.org/onlinepubs/9668899/chap2.htm#tagcjh_05_01_07)
  # and
  # [2.2.6 Type Serialization Version # 1](https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-rpce/9a1d0f97-eac0-49ab-a197-f1a581c2d6a0)

  # [2.2.6.1 Common Type Header for the Serialization Stream](https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-rpce/6d75d40e-e2d2-4420-b9e9-8508a726a9ae)
  class TypeSerialization1CommonTypeHeader < BinData::Record
    default_parameter byte_align: 8
    endian :little

    uint8  :version, initial_value: 1
    uint8  :endianness, initial_value: 0x10
    uint16 :common_header_length, initial_value: 8
    uint32 :filler, initial_value: 0xCCCCCCCC
  end

  # [2.2.6.2 Private Header for Constructed Type](https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-rpce/63949ba8-bc88-4c0c-9377-23f14b197827)
  class TypeSerialization1PrivateHeader < BinData::Record
    default_parameter byte_align: 8
    endian :little

    uint32 :object_buffer_length, initial_value: -> { buffer_length(@obj) }
    uint32 :filler, initial_value: 0x00000000

    def buffer_length(obj)
      parent.respond_to?(:field_length) ? parent.field_length(obj.parent) : 0
    end
  end

  # [2.2.6 Type Serialization Version 1](https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-rpce/9a1d0f97-eac0-49ab-a197-f1a581c2d6a0)
  #
  # This structure is not meant to be instantiated directly. Instead, the
  # structure with the fields to be serialized needs to inherit from
  # TypeSerialization1. This class will take care of adding the
  # TypeSerialization1PrivateHeader fields in front of any NDR constructed type
  # structures and setting the buffer length field (:object_buffer_length) to
  # the correct value.
  #
  # Example:
  #
  # class TestStruct < RubySMB::Dcerpc::Ndr::NdrStruct
  #   default_parameters byte_align: 8
  #   endian :little

  #   rpc_unicode_string :full_name
  #   ndr_uint32 :user_id
  # end

  # class TestTypeSerialization1 < RubySMB::Dcerpc::Ndr::TypeSerialization1
  #   default_parameter byte_align: 8
  #   endian :little
  #
  #   test_struct :data1
  #   uint32 :value1, initial_value: 5
  #   uint32 :value2, initial_value: 6
  #   test_struct :data2
  #   uint32 :value3, initial_value: 7
  #   test_struct :data3
  # end
  #
  # This will result in the following structure:
  # {
  #   :common_header => {:version=>1, :endianness=>16, :common_header_length=>8, :filler=>3435973836},
  #   :private_header1 => {:object_buffer_length=>12, :filler=>0},
  #   :data1 => {:full_name=>{:buffer_length=>0, :maximum_length=>0, :buffer=>:null}, :user_id=>0},
  #   :value1 => 5,
  #   :value2 => 6,
  #   :private_header2 => {:object_buffer_length=>12, :filler=>0},
  #   :data2 => {:full_name=>{:buffer_length=>0, :maximum_length=>0, :buffer=>:null}, :user_id=>0},
  #   :value3 => 7,
  #   :private_header3 => {:object_buffer_length=>12, :filler=>0},
  #   :data3 => {:full_name=>{:buffer_length=>0, :maximum_length=>0, :buffer=>:null}, :user_id=>0}
  # }
  #
  class TypeSerialization1 < BinData::Record
    PRIVATE_HEADER_BASE_NAME = 'private_header'.freeze

    default_parameter byte_align: 8
    endian :little
    search_prefix :type_serialization1

    common_type_header :common_header

    def field_length(obj)
      length = 0
      index = find_index_of(obj)
      if index
        each_pair {|n, o| length = o.num_bytes if n == field_names[index + 1]}
      end
      length
    end

    def self.method_missing(symbol, *args, &block)
      return super if dsl_parser.respond_to?(symbol)

      klass = BinData::RegisteredClasses.lookup(symbol, {endian: dsl_parser.endian, search_prefix: dsl_parser.search_prefix})
      if klass.new.is_a?(ConstructedTypePlugin)
        # We cannot have two fields with the same name. So, here we look for
        # existing TypeSerialization1PrivateHeader fields and just increment
        # the ending number of the last TypeSerialization1PrivateHeader field
        # to make the new name unique.
        names = dsl_parser.fields.find_all do |field|
          field.prototype.instance_variable_get(:@obj_class) == TypeSerialization1PrivateHeader
        end.map(&:name).sort
        if names.empty?
          new_name = "#{PRIVATE_HEADER_BASE_NAME}1"
        else
          num = names.last.match(/#{PRIVATE_HEADER_BASE_NAME}(\d)$/)[1].to_i
          new_name = "#{PRIVATE_HEADER_BASE_NAME}#{num + 1}"
        end

        super(:private_header, new_name.to_sym)
      end

      super
    end
  end

end

