require 'concurrent/synchronization/abstract_struct'
require 'concurrent/synchronization'

module Concurrent

  # An thread-safe variation of Ruby's standard `Struct`. Values can be set at
  # construction or safely changed at any time during the object's lifecycle.
  #
  # @see http://ruby-doc.org/core-2.2.0/Struct.html Ruby standard library `Struct`
  module MutableStruct
    include Synchronization::AbstractStruct

    # @!macro struct_new
    #
    #   Factory for creating new struct classes.
    #
    #   ```
    #   new([class_name] [, member_name]+>) -> StructClass click to toggle source
    #   new([class_name] [, member_name]+>) {|StructClass| block } -> StructClass
    #   new(value, ...) -> obj
    #   StructClass[value, ...] -> obj
    #   ```
    #
    #   The first two forms are used to create a new struct subclass `class_name`
    #   that can contain a value for each   member_name . This subclass can be
    #   used to create instances of the structure like any other  Class .
    #
    #   If the `class_name` is omitted an anonymous struct class will be created.
    #   Otherwise, the name of this struct will appear as a constant in the struct class,
    #   so it must be unique for all structs under this base class and must start with a
    #   capital letter. Assigning a struct class to a constant also gives the class
    #   the name of the constant.
    #
    #   If a block is given it will be evaluated in the context of `StructClass`, passing
    #   the created class as a parameter. This is the recommended way to customize a struct.
    #   Subclassing an anonymous struct creates an extra anonymous class that will never be used.
    #
    #   The last two forms create a new instance of a struct subclass. The number of value
    #   parameters must be less than or equal to the number of attributes defined for the
    #   struct. Unset parameters default to nil. Passing more parameters than number of attributes
    #   will raise an `ArgumentError`.
    #
    #   @see http://ruby-doc.org/core-2.2.0/Struct.html#method-c-new Ruby standard library `Struct#new`

    # @!macro struct_values
    #
    #   Returns the values for this struct as an Array.
    #
    #   @return [Array] the values for this struct
    #
    def values
      synchronize { ns_values }
    end
    alias_method :to_a, :values

    # @!macro struct_values_at
    #
    #   Returns the struct member values for each selector as an Array.
    #
    #   A selector may be either an Integer offset or a Range of offsets (as in `Array#values_at`).
    #
    #   @param [Fixnum, Range] indexes the index(es) from which to obatin the values (in order)
    def values_at(*indexes)
      synchronize { ns_values_at(indexes) }
    end

    # @!macro struct_inspect
    #
    #   Describe the contents of this struct in a string.
    #
    #   @return [String] the contents of this struct in a string
    def inspect
      synchronize { ns_inspect }
    end
    alias_method :to_s, :inspect

    # @!macro struct_merge
    #
    #   Returns a new struct containing the contents of `other` and the contents
    #   of `self`. If no block is specified, the value for entries with duplicate
    #   keys will be that of `other`. Otherwise the value for each duplicate key
    #   is determined by calling the block with the key, its value in `self` and
    #   its value in `other`.
    #
    #   @param [Hash] other the hash from which to set the new values
    #   @yield an options block for resolving duplicate keys
    #   @yieldparam [String, Symbol] member the name of the member which is duplicated
    #   @yieldparam [Object] selfvalue the value of the member in `self`
    #   @yieldparam [Object] othervalue the value of the member in `other`
    #
    #   @return [Synchronization::AbstractStruct] a new struct with the new values
    #
    #   @raise [ArgumentError] of given a member that is not defined in the struct
    def merge(other, &block)
      synchronize { ns_merge(other, &block) }
    end

    # @!macro struct_to_h
    #
    #   Returns a hash containing the names and values for the struct’s members.
    #
    #   @return [Hash] the names and values for the struct’s members
    def to_h
      synchronize { ns_to_h }
    end

    # @!macro struct_get
    #
    #   Attribute Reference
    #
    #   @param [Symbol, String, Integer] member the string or symbol name of the member
    #     for which to obtain the value or the member's index
    #
    #   @return [Object] the value of the given struct member or the member at the given index.
    #
    #   @raise [NameError] if the member does not exist
    #   @raise [IndexError] if the index is out of range.
    def [](member)
      synchronize { ns_get(member) }
    end

    # @!macro struct_equality
    #
    #   Equality
    #
    #   @return [Boolean] true if other has the same struct subclass and has
    #     equal member values (according to `Object#==`)
    def ==(other)
      synchronize { ns_equality(other) }
    end

    # @!macro struct_each
    #
    #   Yields the value of each struct member in order. If no block is given
    #   an enumerator is returned.
    #
    #   @yield the operation to be performed on each struct member
    #   @yieldparam [Object] value each struct value (in order)
    def each(&block)
      return enum_for(:each) unless block_given?
      synchronize { ns_each(&block) }
    end

    # @!macro struct_each_pair
    #
    #   Yields the name and value of each struct member in order. If no block is
    #   given an enumerator is returned.
    #
    #   @yield the operation to be performed on each struct member/value pair
    #   @yieldparam [Object] member each struct member (in order)
    #   @yieldparam [Object] value each struct value (in order)
    def each_pair(&block)
      return enum_for(:each_pair) unless block_given?
      synchronize { ns_each_pair(&block) }
    end

    # @!macro struct_select
    #
    #   Yields each member value from the struct to the block and returns an Array
    #   containing the member values from the struct for which the given block
    #   returns a true value (equivalent to `Enumerable#select`).
    #
    #   @yield the operation to be performed on each struct member
    #   @yieldparam [Object] value each struct value (in order)
    #
    #   @return [Array] an array containing each value for which the block returns true
    def select(&block)
      return enum_for(:select) unless block_given?
      synchronize { ns_select(&block) }
    end

    # @!macro struct_set
    #
    #   Attribute Assignment
    #
    #   Sets the value of the given struct member or the member at the given index.
    #
    #   @param [Symbol, String, Integer] member the string or symbol name of the member
    #     for which to obtain the value or the member's index
    #
    #   @return [Object] the value of the given struct member or the member at the given index.
    #
    #   @raise [NameError] if the name does not exist
    #   @raise [IndexError] if the index is out of range.
    def []=(member, value)
      if member.is_a? Integer
        length = synchronize { @values.length }
        if member >= length
          raise IndexError.new("offset #{member} too large for struct(size:#{length})")
        end
        synchronize { @values[member] = value }
      else
        send("#{member}=", value)
      end
    rescue NoMethodError
      raise NameError.new("no member '#{member}' in struct")
    end

    # @!macro struct_new
    def self.new(*args, &block)
      clazz_name = nil
      if args.length == 0
        raise ArgumentError.new('wrong number of arguments (0 for 1+)')
      elsif args.length > 0 && args.first.is_a?(String)
        clazz_name = args.shift
      end
      FACTORY.define_struct(clazz_name, args, &block)
    end

    FACTORY = Class.new(Synchronization::LockableObject) do
      def define_struct(name, members, &block)
        synchronize do
          clazz = Synchronization::AbstractStruct.define_struct_class(MutableStruct, Synchronization::LockableObject, name, members, &block)
          members.each_with_index do |member, index|
            clazz.send :remove_method, member
            clazz.send(:define_method, member) do
              synchronize { @values[index] }
            end
            clazz.send(:define_method, "#{member}=") do |value|
              synchronize { @values[index] = value }
            end
          end
          clazz
        end
      end
    end.new
    private_constant :FACTORY
  end
end
