# Class for packed binary data, with defined bitfields and accessors for them.
# See {intro.txt}[link:../doc/files/intro_txt.html] for an overview.
#
# Data after the end of the defined fields is accessible using the +rest+
# declaration. See examples/ip.rb. Nested fields can be declared using +nest+.
# See examples/nest.rb.
#
# Note that all string methods are still available: length, grep, etc.
# The String#replace method is useful.
#
class BitStruct < String
  VERSION = "0.17"

  require 'bit-struct/field'
  NULL_FIELD = Field.new(0, 0, :null, :display_name => "null field")

  # Raised when a field is added after an instance has been created. Fields
  # cannot be added after this point.
  class ClosedClassError < StandardError; end

  # Raised if the chosen field name is not allowed, either because another
  # field by that name exists, or because a method by that name exists.
  class FieldNameError < StandardError; end

  @default_options = {}

  @initial_value = nil
  @closed = nil
  @rest_field = nil
  @note = nil

  class << self
    def inherited cl
      cl.instance_eval do
        @initial_value = nil
        @closed = nil
        @rest_field = nil
        @note = nil
      end
    end

    # ------------------------
    # :section: field access methods
    #
    # For introspection and metaprogramming.
    #
    # ------------------------

    # Return the list of fields for this class.
    def fields
      @fields ||= self == BitStruct ? [] : superclass.fields.dup
    end

    # Return the list of fields defined by this class, not inherited
    # from the superclass.
    def own_fields
      @own_fields ||= []
    end

    # Add a field to the BitStruct (usually, this is only used internally).
    def add_field(name, length, opts = {})
      round_byte_length ## just to make sure this has been calculated
      ## before adding anything

      name = name.to_sym

      if @closed
        raise ClosedClassError, "Cannot add field #{name}: " +
          "The definition of the #{self.inspect} BitStruct class is closed."
      end

      if fields.find {|f|f.name == name}
        raise FieldNameError, "Field #{name} is already defined as a field."
      end

      if instance_methods(true).find {|m| m == name}
        if opts[:allow_method_conflict] || opts["allow_method_conflict"]
          warn "Field #{name} is already defined as a method."
        else
          raise FieldNameError,"Field #{name} is already defined as a method."
        end
      end

      field_class = opts[:field_class]

      prev = fields[-1] || NULL_FIELD
      offset = prev.offset + prev.length
      field = field_class.new(offset, length, name, opts)
      field.add_accessors_to(self)
      fields << field
      own_fields << field
      @bit_length += field.length
      @round_byte_length = (bit_length/8.0).ceil

      if @initial_value
        diff = @round_byte_length - @initial_value.length
        if diff > 0
          @initial_value << "\0" * diff
        end
      end

      field
    end

    def parse_options(ary, default_name, default_field_class) # :nodoc:
      opts = ary.grep(Hash).first || {}
      opts = default_options.merge(opts)

      opts[:display_name]  = ary.grep(String).first || default_name
      opts[:field_class]   = ary.grep(Class).first || default_field_class

      opts
    end

    # Get or set the hash of default options for the class, which apply to all
    # fields. Changes take effect immediately, so can be used alternatingly with
    # blocks of field declarations. If +h+ is provided, update the default
    # options with that hash. Default options are inherited.
    #
    # This is especially useful with the <tt>:endian => val</tt> option.
    def default_options h = nil
      @default_options ||= superclass.default_options.dup
      if h
        @default_options.merge! h
      end
      @default_options
    end

    # Length, in bits, of this object.
    def bit_length
      @bit_length ||= fields.inject(0) {|a, f| a + f.length}
    end

    # Length, in bytes (rounded up), of this object.
    def round_byte_length
      @round_byte_length ||= (bit_length/8.0).ceil
    end

    def closed! # :nodoc:
      @closed = true
    end

    def field_by_name name
      @field_by_name ||= {}
      field = @field_by_name[name]
      unless field
        field = fields.find {|f| f.name == name}
        @field_by_name[name] = field if field
      end
      field
    end
  end

  # Return the list of fields for this class.
  def fields
    self.class.fields
  end

  # Return the rest field for this class.
  def rest_field
    self.class.rest_field
  end

  # Return the field with the given name.
  def field_by_name name
    self.class.field_by_name name
  end

  # ------------------------
  # :section: metadata inspection methods
  #
  # Methods to textually describe the format of a BitStruct subclass.
  #
  # ------------------------

  class << self
    # Default format for describe. Fields are byte, type, name, size,
    # and description.
    DESCRIBE_FORMAT = "%8s: %-12s %-14s[%4s] %s"

    # Can be overridden to use a different format.
    def describe_format
      DESCRIBE_FORMAT
    end

    # Textually describe the fields of this class of BitStructs.
    # Returns a printable table (array of line strings), based on +fmt+,
    # which defaults to #describe_format, which defaults to +DESCRIBE_FORMAT+.
    def describe(fmt = nil, opts = {})
      if fmt.kind_of? Hash
        opts = fmt; fmt = nil
      end

      if block_given?
        fields.each do |field|
          field.describe(opts) do |desc|
            yield desc
          end
        end
        nil

      else
        fmt ||= describe_format

        result = []

        unless opts[:omit_header]
          result << fmt % ["byte", "type", "name", "size", "description"]
          result << "-"*70
        end

        fields.each do |field|
          field.describe(opts) do |desc|
            result << fmt % desc
          end
        end

        unless opts[:omit_footer]
          result << @note if @note
        end

        result
      end
    end

    # Subclasses can use this to append a string (or several) to the #describe
    # output. Notes are not cumulative with inheritance. When used with no
    # arguments simply returns the note string
    def note(*str)
      @note = str unless str.empty?
      @note
    end
  end

  # ------------------------
  # :section: initialization and conversion methods
  #
  # ------------------------

  # Initialize the string with the given string or bitstruct, or with a hash of
  # field=>value pairs, or with the defaults for the BitStruct subclass, or
  # with an IO or other object with a #read method. Fields can be strings or
  # symbols. Finally, if a block is given, yield the instance for modification
  # using accessors.
  def initialize(value = nil)   # :yields: instance
    self << self.class.initial_value

    case value
    when Hash
      value.each do |k, v|
        send "#{k}=", v
      end

    when nil

    else
      if value.respond_to?(:read)
        value = value.read(self.class.round_byte_length)
      end

      self[0, value.length] = value
    end

    self.class.closed!
    yield self if block_given?
  end

  DEFAULT_TO_H_OPTS = {
    :convert_keys   => :to_sym,
    :include_rest   => true
  }

  # Returns a hash of {name=>value,...} for each field. By default, include
  # the rest field.
  # Keys are symbols derived from field names using +to_sym+, unless
  # <tt>opts[:convert_keys]<\tt> is set to some other method name.
  def to_h(opts = DEFAULT_TO_H_OPTS)
    converter = opts[:convert_keys] || :to_sym

    fields_for_to_h = fields
    if opts[:include_rest] and (rest_field = self.class.rest_field)
      fields_for_to_h += [rest_field]
    end

    fields_for_to_h.inject({}) do |h,f|
      h[f.name.send(converter)] = send(f.name)
      h
    end
  end

  # Returns an array of values of the fields of the BitStruct. By default,
  # include the rest field.
  def to_a(include_rest = true)
    ary =
      fields.map do |f|
        send(f.name)
      end

    if include_rest and (rest_field = self.class.rest_field)
      ary << send(rest_field.name)
    end
    ary
  end

  if "a"[0].kind_of? String
    def [](*args)
      if args.size == 1 and args[0].kind_of?(Integer)
        super.ord
      else
        super
      end
    end

    def []=(*args)
      if args.size == 2 and (i=args[0]).kind_of?(Integer)
        super(i, args[1].chr)
      else
        super
      end
    end
  end

  class << self
    # The unique "prototype" object from which new instances are copied.
    # The fields of this instance can be modified in the class definition
    # to set default values for the fields in that class. (Otherwise, defaults
    # defined by the fields themselves are used.) A copy of this object is
    # inherited in subclasses, which they may override using defaults and
    # by writing to the initial_value object itself.
    #
    # If called with a block, yield the initial value object before returning
    # it. Useful for customization within a class definition.
    #
    def initial_value   # :yields: the initial value
      unless @initial_value
        iv = defined?(superclass.initial_value) ?
          superclass.initial_value.dup : ""
        if iv.length < round_byte_length
          iv << "\0" * (round_byte_length - iv.length)
        end

        @initial_value = "" # Serves as initval while the real initval is inited
        @initial_value = new(iv)
        @closed = false # only creating the first _real_ instance closes.

        fields.each do |field|
          @initial_value.send("#{field.name}=", field.default) if field.default
        end
      end
      yield @initial_value if block_given?
      @initial_value
    end

    # Take +data+ (a string or BitStruct) and parse it into instances of
    # the +classes+, returning them in an array. The classes can be given
    # as an array or a separate arguments. (For parsing a string into a _single_
    # BitStruct instance, just use the #new method with the string as an arg.)
    def parse(data, *classes)
      classes.flatten.map do |c|
        c.new(data.slice!(0...c.round_byte_length))
      end
    end

    # Join the given structs (array or multiple args) as a string.
    # Actually, the inherited String#+ instance method is the same, as is using
    # Array#join.
    def join(*structs)
      structs.flatten.map {|struct| struct.to_s}.join("")
    end
  end

  # ------------------------
  # :section: inspection methods
  #
  # ------------------------

  DEFAULT_INSPECT_OPTS = {
    :format           => "#<%s %s>",
    :field_format     => "%s=%s",
    :separator        => ", ",
    :field_name_meth  => :name,
    :include_rest     => true,
    :brackets         => ["[", "]"],
    :include_class    => true,
    :simple_format    => "<%s>"
  }

  DETAILED_INSPECT_OPTS = {
    :format           => "%s:\n%s",
    :field_format     => "%30s = %s",
    :separator        => "\n",
    :field_name_meth  => :display_name,
    :include_rest     => true,
    :brackets         => [nil, "\n"],
    :include_class    => true,
    :simple_format    => "\n%s"
  }

  # A standard inspect method which does not add newlines.
  def inspect_with_options(opts = DEFAULT_INSPECT_OPTS)
    field_format = opts[:field_format]
    field_name_meth = opts[:field_name_meth]

    fields_for_inspect = fields.select {|field| field.inspectable?}
    if opts[:include_rest] and (rest_field = self.class.rest_field)
      fields_for_inspect << rest_field
    end

    ary = fields_for_inspect.map do |field|
      field_format %
       [field.send(field_name_meth),
        field.inspect_in_object(self, opts)]
    end

    body = ary.join(opts[:separator])

    if opts[:include_class]
      opts[:format] % [self.class, body]
    else
      opts[:simple_format] % body
    end
  end

  alias inspect inspect_with_options

  # A more visually appealing inspect method that puts each field/value on
  # a separate line. Very useful when output is scrolling by on a screen.
  #
  # (This is actually a convenience method to call #inspect with the
  # DETAILED_INSPECT_OPTS opts.)
  def inspect_detailed
    inspect(DETAILED_INSPECT_OPTS)
  end

  # ------------------------
  # :section: field declaration methods
  #
  # ------------------------

  # Define accessors for a variable length substring from the end of
  # the defined fields to the end of the BitStruct. The _rest_ may behave as
  # a String or as some other String or BitStruct subclass.
  #
  # This does not add a field, which is useful because a superclass can have
  # a rest method which accesses subclass data. In particular, #rest does
  # not affect the #round_byte_length class method. Of course, any data
  # in rest does add to the #length of the BitStruct, calculated as a string.
  # Also, _rest_ is not inherited.
  #
  # The +ary+ argument(s) work as follows:
  #
  # If a class is provided, use it for the Field class (String by default).
  # If a string is provided, use it for the display_name (+name+ by default).
  # If a hash is provided, use it for options.
  #
  # *Warning*: the rest reader method returns a copy of the field, so
  # accessors on that returned value do not affect the original rest field.
  #
  def self.rest(name, *ary)
    if @rest_field
      raise ArgumentError, "Duplicate rest field: #{name.inspect}."
    end

    opts = parse_options(ary, name, String)
    offset = round_byte_length
    byte_range = offset..-1
    class_eval do
      field_class = opts[:field_class]
      define_method name do ||
        field_class.new(self[byte_range])
      end

      define_method "#{name}=" do |val|
        self[byte_range] = val
      end

      @rest_field = Field.new(offset, -1, name, {
        :display_name => opts[:display_name],
        :rest_class => field_class
      })
    end
  end

  # Not included with the other fields, but accessible separately.
  def self.rest_field; @rest_field; end
end
