# backtick_javascript: true

require 'corelib/enumerable'

class ::Struct
  include ::Enumerable

  def self.new(const_name, *args, keyword_init: false, &block)
    if const_name
      if const_name.class == ::String && const_name[0].upcase != const_name[0]
        # Fast track so that we skip needlessly going thru exceptions
        # in most cases.
        args.unshift(const_name)
        const_name = nil
      else
        begin
          const_name = ::Opal.const_name!(const_name)
        rescue ::TypeError, ::NameError
          args.unshift(const_name)
          const_name = nil
        end
      end
    end

    args.map do |arg|
      ::Opal.coerce_to!(arg, ::String, :to_str)
    end

    klass = ::Class.new(self) do
      args.each { |arg| define_struct_attribute(arg) }

      class << self
        def new(*args)
          instance = allocate
          `#{instance}.$$data = {}`
          instance.initialize(*args)
          instance
        end

        alias_method :[], :new
      end
    end

    klass.module_eval(&block) if block
    `klass.$$keyword_init = keyword_init`

    if const_name
      ::Struct.const_set(const_name, klass)
    end

    klass
  end

  def self.define_struct_attribute(name)
    if self == ::Struct
      ::Kernel.raise ::ArgumentError, 'you cannot define attributes to the Struct class'
    end

    members << name

    define_method name do
      `self.$$data[name]`
    end

    define_method "#{name}=" do |value|
      `self.$$data[name] = value`
    end
  end

  def self.members
    if self == ::Struct
      ::Kernel.raise ::ArgumentError, 'the Struct class has no members'
    end

    @members ||= []
  end

  def self.inherited(klass)
    members = @members

    klass.instance_eval do
      @members = members
    end
  end

  def initialize(*args)
    if `#{self.class}.$$keyword_init`
      kwargs = args.last || {}

      if args.length > 1 || `(args.length === 1 && !kwargs.$$is_hash)`
        ::Kernel.raise ::ArgumentError, "wrong number of arguments (given #{args.length}, expected 0)"
      end

      extra = kwargs.keys - self.class.members
      if extra.any?
        ::Kernel.raise ::ArgumentError, "unknown keywords: #{extra.join(', ')}"
      end

      self.class.members.each do |name|
        self[name] = kwargs[name]
      end
    else
      if args.length > self.class.members.length
        ::Kernel.raise ::ArgumentError, 'struct size differs'
      end

      self.class.members.each_with_index do |name, index|
        self[name] = args[index]
      end
    end
  end

  def initialize_copy(from)
    %x{
      self.$$data = {}
      var keys = Object.keys(from.$$data), i, max, name;
      for (i = 0, max = keys.length; i < max; i++) {
        name = keys[i];
        self.$$data[name] = from.$$data[name];
      }
    }
  end

  def self.keyword_init?
    `self.$$keyword_init`
  end

  def members
    self.class.members
  end

  def hash
    [self.class, to_a].hash
  end

  def [](name)
    if ::Integer === name
      ::Kernel.raise ::IndexError, "offset #{name} too small for struct(size:#{self.class.members.size})" if name < -self.class.members.size
      ::Kernel.raise ::IndexError, "offset #{name} too large for struct(size:#{self.class.members.size})" if name >= self.class.members.size

      name = self.class.members[name]
    elsif ::String === name
      %x{
        if(!self.$$data.hasOwnProperty(name)) {
          #{::Kernel.raise ::NameError.new("no member '#{name}' in struct", name)}
        }
      }
    else
      ::Kernel.raise ::TypeError, "no implicit conversion of #{name.class} into Integer"
    end

    name = ::Opal.coerce_to!(name, ::String, :to_str)
    `self.$$data[name]`
  end

  def []=(name, value)
    if ::Integer === name
      ::Kernel.raise ::IndexError, "offset #{name} too small for struct(size:#{self.class.members.size})" if name < -self.class.members.size
      ::Kernel.raise ::IndexError, "offset #{name} too large for struct(size:#{self.class.members.size})" if name >= self.class.members.size

      name = self.class.members[name]
    elsif ::String === name
      ::Kernel.raise ::NameError.new("no member '#{name}' in struct", name) unless self.class.members.include?(name.to_sym)
    else
      ::Kernel.raise ::TypeError, "no implicit conversion of #{name.class} into Integer"
    end

    name = ::Opal.coerce_to!(name, ::String, :to_str)
    `self.$$data[name] = value`
  end

  def ==(other)
    return false unless other.instance_of?(self.class)

    %x{
      var recursed1 = {}, recursed2 = {};

      function _eqeq(struct, other) {
        var key, a, b;

        recursed1[#{`struct`.__id__}] = true;
        recursed2[#{`other`.__id__}] = true;

        for (key in struct.$$data) {
          a = struct.$$data[key];
          b = other.$$data[key];

          if (#{::Struct === `a`}) {
            if (!recursed1.hasOwnProperty(#{`a`.__id__}) || !recursed2.hasOwnProperty(#{`b`.__id__})) {
              if (!_eqeq(a, b)) {
                return false;
              }
            }
          } else {
            if (!#{`a` == `b`}) {
              return false;
            }
          }
        }

        return true;
      }

      return _eqeq(self, other);
    }
  end

  def eql?(other)
    return false unless other.instance_of?(self.class)

    %x{
      var recursed1 = {}, recursed2 = {};

      function _eqeq(struct, other) {
        var key, a, b;

        recursed1[#{`struct`.__id__}] = true;
        recursed2[#{`other`.__id__}] = true;

        for (key in struct.$$data) {
          a = struct.$$data[key];
          b = other.$$data[key];

          if (#{::Struct === `a`}) {
            if (!recursed1.hasOwnProperty(#{`a`.__id__}) || !recursed2.hasOwnProperty(#{`b`.__id__})) {
              if (!_eqeq(a, b)) {
                return false;
              }
            }
          } else {
            if (!#{`a`.eql?(`b`)}) {
              return false;
            }
          }
        }

        return true;
      }

      return _eqeq(self, other);
    }
  end

  def each
    return enum_for(:each) { size } unless block_given?

    self.class.members.each { |name| yield self[name] }
    self
  end

  def each_pair
    return enum_for(:each_pair) { size } unless block_given?

    self.class.members.each { |name| yield [name, self[name]] }
    self
  end

  def length
    self.class.members.length
  end

  def to_a
    self.class.members.map { |name| self[name] }
  end

  `var inspect_stack = []`

  def inspect
    result = '#<struct '

    if `inspect_stack`.include? __id__
      result + ':...>'
    else
      `inspect_stack` << __id__
      pushed = true

      if ::Struct === self && self.class.name
        result += "#{self.class} "
      end

      result += each_pair.map do |name, value|
        "#{name}=#{Opal.inspect(value)}"
      end.join ', '

      result += '>'

      result
    end
  ensure
    `inspect_stack.pop()` if pushed
  end

  def to_h(*args, &block)
    return map(&block).to_h(*args) if block_given?

    self.class.members.each_with_object({}) { |name, h| h[name] = self[name] }
  end

  def values_at(*args)
    args = args.map { |arg| `arg.$$is_range ? #{arg.to_a} : arg` }.flatten
    %x{
      var result = [];
      for (var i = 0, len = args.length; i < len; i++) {
        if (!args[i].$$is_number) {
          #{::Kernel.raise ::TypeError, "no implicit conversion of #{`args[i]`.class} into Integer"}
        }
        result.push(#{self[`args[i]`]});
      }
      return result;
    }
  end

  def dig(key, *keys)
    item = if `key.$$is_string && self.$$data.hasOwnProperty(key)`
             `self.$$data[key] || nil`
           end

    %x{
      if (item === nil || keys.length === 0) {
        return item;
      }
    }

    unless item.respond_to?(:dig)
      ::Kernel.raise ::TypeError, "#{item.class} does not have #dig method"
    end

    item.dig(*keys)
  end

  alias size length
  alias to_s inspect
  alias values to_a
end
