# helpers: truthy, coerce_to, yield1, yieldX, deny_frozen_access
# backtick_javascript: true
# use_strict: true

module ::Enumerable
  %x{
    function comparableForPattern(value) {
      if (value.length === 0) {
        value = [nil];
      }

      if (value.length > 1) {
        value = [value];
      }

      return value;
    }
  }

  def all?(pattern = undefined, &block)
    if `pattern !== undefined`
      each do |*value|
        comparable = `comparableForPattern(value)`

        return false unless pattern.public_send(:===, *comparable)
      end
    elsif block_given?
      each do |*value|
        unless yield(*value)
          return false
        end
      end
    else
      each do |*value|
        unless ::Opal.destructure(value)
          return false
        end
      end
    end

    true
  end

  def any?(pattern = undefined, &block)
    if `pattern !== undefined`
      each do |*value|
        comparable = `comparableForPattern(value)`

        return true if pattern.public_send(:===, *comparable)
      end
    elsif block_given?
      each do |*value|
        if yield(*value)
          return true
        end
      end
    else
      each do |*value|
        if ::Opal.destructure(value)
          return true
        end
      end
    end

    false
  end

  def chunk(&block)
    return to_enum(:chunk) { enumerator_size } unless block_given?

    ::Enumerator.new do |yielder|
      %x{
        var previous = nil, accumulate = [];

        function releaseAccumulate() {
          if (accumulate.length > 0) {
            #{yielder.yield(`previous`, `accumulate`)}
          }
        }

        self.$each.$$p = function(value) {
          var key = $yield1(block, value);

          if (key === nil) {
            releaseAccumulate();
            accumulate = [];
            previous = nil;
          } else {
            if (previous === nil || previous === key) {
              accumulate.push(value);
            } else {
              releaseAccumulate();
              accumulate = [value];
            }

            previous = key;
          }
        }

        self.$each();

        releaseAccumulate();
      }
    end
  end

  def chunk_while(&block)
    ::Kernel.raise ::ArgumentError, 'no block given' unless block_given?

    slice_when { |before, after| !(yield before, after) }
  end

  def collect(&block)
    return enum_for(:collect) { enumerator_size } unless block_given?

    %x{
      var result = [];

      self.$each.$$p = function() {
        var value = $yieldX(block, arguments);

        result.push(value);
      };

      self.$each();

      return result;
    }
  end

  def collect_concat(&block)
    return enum_for(:collect_concat) { enumerator_size } unless block_given?
    map(&block).flatten(1)
  end

  def compact
    to_a.compact
  end

  def count(object = undefined, &block)
    result = 0

    %x{
      if (object != null && block !== nil) {
        #{warn('warning: given block not used')}
      }
    }

    if `object != null`
      block = ::Kernel.proc do |*args|
        ::Opal.destructure(args) == object
      end
    elsif block.nil?
      block = ::Kernel.proc { true }
    end

    each do |*args|
      `result++` if `$yieldX(block, args)`
    end

    result
  end

  def cycle(n = nil, &block)
    unless block_given?
      return enum_for(:cycle, n) do
        if n.nil?
          respond_to?(:size) ? ::Float::INFINITY : nil
        else
          n = ::Opal.coerce_to!(n, ::Integer, :to_int)
          n > 0 ? enumerator_size * n : 0
        end
      end
    end

    unless n.nil?
      n = ::Opal.coerce_to! n, ::Integer, :to_int

      return if `n <= 0`
    end

    %x{
      var all = [], i, length, value;

      self.$each.$$p = function() {
        var param = #{::Opal.destructure(`arguments`)},
            value = $yield1(block, param);

        all.push(param);
      }

      self.$each();

      if (all.length === 0) {
        return nil;
      }

      if (n === nil) {
        while (true) {
          for (i = 0, length = all.length; i < length; i++) {
            value = $yield1(block, all[i]);
          }
        }
      }
      else {
        while (n > 1) {
          for (i = 0, length = all.length; i < length; i++) {
            value = $yield1(block, all[i]);
          }

          n--;
        }
      }
    }
  end

  def detect(ifnone = undefined, &block)
    return enum_for :detect, ifnone unless block_given?

    each do |*args|
      value = ::Opal.destructure(args)
      if yield(value)
        return value
      end
    end

    %x{
      if (ifnone !== undefined) {
        if (typeof(ifnone) === 'function') {
          return ifnone();
        } else {
          return ifnone;
        }
      }
    }

    nil
  end

  def drop(number)
    number = `$coerce_to(number, #{::Integer}, 'to_int')`

    if `number < 0`
      ::Kernel.raise ::ArgumentError, 'attempt to drop negative size'
    end

    %x{
      var result  = [],
          current = 0;

      self.$each.$$p = function() {
        if (number <= current) {
          result.push(#{::Opal.destructure(`arguments`)});
        }

        current++;
      };

      self.$each()

      return result;
    }
  end

  def drop_while(&block)
    return enum_for :drop_while unless block_given?

    %x{
      var result   = [],
          dropping = true;

      self.$each.$$p = function() {
        var param = #{::Opal.destructure(`arguments`)};

        if (dropping) {
          var value = $yield1(block, param);

          if (!$truthy(value)) {
            dropping = false;
            result.push(param);
          }
        }
        else {
          result.push(param);
        }
      };

      self.$each();

      return result;
    }
  end

  def each_cons(n, &block)
    if `arguments.length != 1`
      ::Kernel.raise ::ArgumentError, "wrong number of arguments (#{`arguments.length`} for 1)"
    end

    n = ::Opal.try_convert n, ::Integer, :to_int

    if `n <= 0`
      ::Kernel.raise ::ArgumentError, 'invalid size'
    end

    unless block_given?
      return enum_for(:each_cons, n) do
        enum_size = enumerator_size
        if enum_size.nil?
          nil
        elsif enum_size == 0 || enum_size < n
          0
        else
          enum_size - n + 1
        end
      end
    end

    %x{
      var buffer = [];

      self.$each.$$p = function() {
        var element = #{::Opal.destructure(`arguments`)};
        buffer.push(element);
        if (buffer.length > n) {
          buffer.shift();
        }
        if (buffer.length == n) {
          $yield1(block, buffer.slice(0, n));
        }
      }

      self.$each();

      return self;
    }
  end

  def each_entry(*data, &block)
    unless block_given?
      return to_enum(:each_entry, *data) { enumerator_size }
    end

    %x{
      self.$each.$$p = function() {
        var item = #{::Opal.destructure(`arguments`)};

        $yield1(block, item);
      }

      self.$each.apply(self, data);

      return self;
    }
  end

  def each_slice(n, &block)
    n = `$coerce_to(#{n}, #{::Integer}, 'to_int')`

    if `n <= 0`
      ::Kernel.raise ::ArgumentError, 'invalid slice size'
    end

    return enum_for(:each_slice, n) { respond_to?(:size) ? (size / n).ceil : nil } unless block_given?

    %x{
      var slice = []

      self.$each.$$p = function() {
        var param = #{::Opal.destructure(`arguments`)};

        slice.push(param);

        if (slice.length === n) {
          $yield1(block, slice);
          slice = [];
        }
      };

      self.$each();

      // our "last" group, if smaller than n then won't have been yielded
      if (slice.length > 0) {
        $yield1(block, slice);
      }
    }

    self
  end

  def each_with_index(*args, &block)
    return enum_for(:each_with_index, *args) { enumerator_size } unless block_given?

    %x{
      var index = 0;

      self.$each.$$p = function() {
        var param = #{::Opal.destructure(`arguments`)};

        block(param, index);

        index++;
      };

      self.$each.apply(self, args);
    }

    self
  end

  def each_with_object(object, &block)
    return enum_for(:each_with_object, object) { enumerator_size } unless block_given?

    %x{
      self.$each.$$p = function() {
        var param = #{::Opal.destructure(`arguments`)};

        block(param, object);
      };

      self.$each();
    }

    object
  end

  def entries(*args)
    %x{
      var result = [];

      self.$each.$$p = function() {
        result.push(#{::Opal.destructure(`arguments`)});
      };

      self.$each.apply(self, args);

      return result;
    }
  end

  def filter_map(&block)
    return enum_for(:filter_map) { enumerator_size } unless block_given?

    map(&block).select(&:itself)
  end

  def find_all(&block)
    return enum_for(:find_all) { enumerator_size } unless block_given?

    %x{
      var result = [];

      self.$each.$$p = function() {
        var param = #{::Opal.destructure(`arguments`)},
            value = $yield1(block, param);

        if ($truthy(value)) {
          result.push(param);
        }
      };

      self.$each();

      return result;
    }
  end

  def find_index(object = undefined, &block)
    return enum_for :find_index if `object === undefined && block === nil`

    %x{
      if (object != null && block !== nil) {
        #{warn('warning: given block not used')}
      }
    }

    index = 0

    if `object != null`
      each do |*value|
        if ::Opal.destructure(value) == object
          return index
        end

        `index += 1`
      end
    else
      each do |*value|
        if yield(*value)
          return index
        end

        `index += 1`
      end
    end

    nil
  end

  def first(number = undefined)
    if `number === undefined`
      each do |value|
        return value
      end
      nil
    else
      result = []
      number = `$coerce_to(number, #{::Integer}, 'to_int')`

      if `number < 0`
        ::Kernel.raise ::ArgumentError, 'attempt to take negative size'
      end

      if `number == 0`
        return []
      end

      current = 0

      each do |*args|
        `result.push(#{::Opal.destructure(args)})`

        if `number <= ++current`
          return result
        end
      end

      result
    end
  end

  def grep(pattern, &block)
    result = []

    each do |*value|
      cmp = `comparableForPattern(value)`
      next unless pattern.__send__(:===, *cmp)
      if block_given?
        value = [value] if value.length > 1
        value = yield(*value)
      elsif value.length <= 1
        value = value[0]
      end

      result.push(value)
    end

    result
  end

  def grep_v(pattern, &block)
    result = []

    each do |*value|
      cmp = `comparableForPattern(value)`
      next if pattern.__send__(:===, *cmp)
      if block_given?
        value = [value] if value.length > 1
        value = yield(*value)
      elsif value.length <= 1
        value = value[0]
      end

      result.push(value)
    end

    result
  end

  def group_by(&block)
    return enum_for(:group_by) { enumerator_size } unless block_given?

    hash = {}

    %x{
      var result;

      self.$each.$$p = function() {
        var param = #{::Opal.destructure(`arguments`)},
            value = $yield1(block, param);

        #{(hash[`value`] ||= []) << `param`};
      }

      self.$each();

      if (result !== undefined) {
        return result;
      }
    }

    hash
  end

  def include?(obj)
    each do |*args|
      if ::Opal.destructure(args) == obj
        return true
      end
    end

    false
  end

  def inject(object = undefined, sym = undefined, &block)
    %x{
      var result = object;

      if (block !== nil && sym === undefined) {
        self.$each.$$p = function() {
          var value = #{::Opal.destructure(`arguments`)};

          if (result === undefined) {
            result = value;
            return;
          }

          value = $yieldX(block, [result, value]);

          result = value;
        };
      }
      else {
        if (sym === undefined) {
          if (!#{::Symbol === object}) {
            #{::Kernel.raise ::TypeError, "#{object.inspect} is not a Symbol"};
          }

          sym    = object;
          result = undefined;
        }

        self.$each.$$p = function() {
          var value = #{::Opal.destructure(`arguments`)};

          if (result === undefined) {
            result = value;
            return;
          }

          result = #{`result`.__send__ sym, `value`};
        };
      }

      self.$each();

      return result == undefined ? nil : result;
    }
  end

  def lazy
    ::Enumerator::Lazy.new(self, enumerator_size) do |enum, *args|
      enum.yield(*args)
    end
  end

  def enumerator_size
    respond_to?(:size) ? size : nil
  end

  def max(n = undefined, &block)
    %x{
      if (n === undefined || n === nil) {
        var result, value;

        self.$each.$$p = function() {
          var item = #{::Opal.destructure(`arguments`)};

          if (result === undefined) {
            result = item;
            return;
          }

          if (block !== nil) {
            value = $yieldX(block, [item, result]);
          } else {
            value = #{`item` <=> `result`};
          }

          if (value === nil) {
            #{::Kernel.raise ::ArgumentError, 'comparison failed'};
          }

          if (value > 0) {
            result = item;
          }
        }

        self.$each();

        if (result === undefined) {
          return nil;
        } else {
          return result;
        }
      }

      n = $coerce_to(n, #{::Integer}, 'to_int');
    }

    sort(&block).reverse.first(n)
  end

  def max_by(n = nil, &block)
    return enum_for(:max_by, n) { enumerator_size } unless block

    unless n.nil?
      return sort_by(&block).reverse.take n
    end

    %x{
      var result,
          by;

      self.$each.$$p = function() {
        var param = #{::Opal.destructure(`arguments`)},
            value = $yield1(block, param);

        if (result === undefined) {
          result = param;
          by     = value;
          return;
        }

        if (#{`value` <=> `by`} > 0) {
          result = param
          by     = value;
        }
      };

      self.$each();

      return result === undefined ? nil : result;
    }
  end

  def min(n = nil, &block)
    unless n.nil?
      if block_given?
        return sort { |a, b| yield a, b }.take n
      else
        return sort.take n
      end
    end

    %x{
      var result;

      if (block !== nil) {
        self.$each.$$p = function() {
          var param = #{::Opal.destructure(`arguments`)};

          if (result === undefined) {
            result = param;
            return;
          }

          var value = block(param, result);

          if (value === nil) {
            #{::Kernel.raise ::ArgumentError, 'comparison failed'};
          }

          if (value < 0) {
            result = param;
          }
        };
      }
      else {
        self.$each.$$p = function() {
          var param = #{::Opal.destructure(`arguments`)};

          if (result === undefined) {
            result = param;
            return;
          }

          if (#{::Opal.compare(`param`, `result`)} < 0) {
            result = param;
          }
        };
      }

      self.$each();

      return result === undefined ? nil : result;
    }
  end

  def min_by(n = nil, &block)
    return enum_for(:min_by, n) { enumerator_size } unless block

    unless n.nil?
      return sort_by(&block).take n
    end

    %x{
      var result,
          by;

      self.$each.$$p = function() {
        var param = #{::Opal.destructure(`arguments`)},
            value = $yield1(block, param);

        if (result === undefined) {
          result = param;
          by     = value;
          return;
        }

        if (#{`value` <=> `by`} < 0) {
          result = param
          by     = value;
        }
      };

      self.$each();

      return result === undefined ? nil : result;
    }
  end

  def minmax(&block)
    block ||= ::Kernel.proc { |a, b| a <=> b }

    %x{
      var min = nil, max = nil, first_time = true;

      self.$each.$$p = function() {
        var element = #{::Opal.destructure(`arguments`)};
        if (first_time) {
          min = max = element;
          first_time = false;
        } else {
          var min_cmp = #{block.call(`min`, `element`)};

          if (min_cmp === nil) {
            #{::Kernel.raise ::ArgumentError, 'comparison failed'}
          } else if (min_cmp > 0) {
            min = element;
          }

          var max_cmp = #{block.call(`max`, `element`)};

          if (max_cmp === nil) {
            #{::Kernel.raise ::ArgumentError, 'comparison failed'}
          } else if (max_cmp < 0) {
            max = element;
          }
        }
      }

      self.$each();

      return [min, max];
    }
  end

  def minmax_by(&block)
    return enum_for(:minmax_by) { enumerator_size } unless block

    %x{
      var min_result = nil,
          max_result = nil,
          min_by,
          max_by;

      self.$each.$$p = function() {
        var param = #{::Opal.destructure(`arguments`)},
            value = $yield1(block, param);

        if ((min_by === undefined) || #{`value` <=> `min_by`} < 0) {
          min_result = param;
          min_by     = value;
        }

        if ((max_by === undefined) || #{`value` <=> `max_by`} > 0) {
          max_result = param;
          max_by     = value;
        }
      };

      self.$each();

      return [min_result, max_result];
    }
  end

  def none?(pattern = undefined, &block)
    if `pattern !== undefined`
      each do |*value|
        comparable = `comparableForPattern(value)`

        return false if pattern.public_send(:===, *comparable)
      end
    elsif block_given?
      each do |*value|
        if yield(*value)
          return false
        end
      end
    else
      each do |*value|
        item = ::Opal.destructure(value)

        return false if item
      end
    end

    true
  end

  def one?(pattern = undefined, &block)
    count = 0

    if `pattern !== undefined`
      each do |*value|
        comparable = `comparableForPattern(value)`

        if pattern.public_send(:===, *comparable)
          count += 1
          return false if count > 1
        end
      end
    elsif block_given?
      each do |*value|
        next unless yield(*value)
        count += 1

        return false if count > 1
      end
    else
      each do |*value|
        next unless ::Opal.destructure(value)
        count += 1

        return false if count > 1
      end
    end

    count == 1
  end

  def partition(&block)
    return enum_for(:partition) { enumerator_size } unless block_given?

    %x{
      var truthy = [], falsy = [], result;

      self.$each.$$p = function() {
        var param = #{::Opal.destructure(`arguments`)},
            value = $yield1(block, param);

        if ($truthy(value)) {
          truthy.push(param);
        }
        else {
          falsy.push(param);
        }
      };

      self.$each();

      return [truthy, falsy];
    }
  end

  def reject(&block)
    return enum_for(:reject) { enumerator_size } unless block_given?

    %x{
      var result = [];

      self.$each.$$p = function() {
        var param = #{::Opal.destructure(`arguments`)},
            value = $yield1(block, param);

        if (!$truthy(value)) {
          result.push(param);
        }
      };

      self.$each();

      return result;
    }
  end

  def reverse_each(&block)
    return enum_for(:reverse_each) { enumerator_size } unless block_given?

    %x{
      var result = [];

      self.$each.$$p = function() {
        result.push(arguments);
      };

      self.$each();

      for (var i = result.length - 1; i >= 0; i--) {
        $yieldX(block, result[i]);
      }

      return result;
    }
  end

  def slice_before(pattern = undefined, &block)
    if `pattern === undefined && block === nil`
      ::Kernel.raise ::ArgumentError, 'both pattern and block are given'
    end

    if `pattern !== undefined && block !== nil || arguments.length > 1`
      ::Kernel.raise ::ArgumentError, "wrong number of arguments (#{`arguments.length`} expected 1)"
    end

    ::Enumerator.new do |e|
      %x{
        var slice = [];

        if (block !== nil) {
          if (pattern === undefined) {
            self.$each.$$p = function() {
              var param = #{::Opal.destructure(`arguments`)},
                  value = $yield1(block, param);

              if ($truthy(value) && slice.length > 0) {
                #{e << `slice`};
                slice = [];
              }

              slice.push(param);
            };
          }
          else {
            self.$each.$$p = function() {
              var param = #{::Opal.destructure(`arguments`)},
                  value = block(param, #{pattern.dup});

              if ($truthy(value) && slice.length > 0) {
                #{e << `slice`};
                slice = [];
              }

              slice.push(param);
            };
          }
        }
        else {
          self.$each.$$p = function() {
            var param = #{::Opal.destructure(`arguments`)},
                value = #{pattern === `param`};

            if ($truthy(value) && slice.length > 0) {
              #{e << `slice`};
              slice = [];
            }

            slice.push(param);
          };
        }

        self.$each();

        if (slice.length > 0) {
          #{e << `slice`};
        }
      }
    end
  end

  def slice_after(pattern = undefined, &block)
    if `pattern === undefined && block === nil`
      ::Kernel.raise ::ArgumentError, 'both pattern and block are given'
    end

    if `pattern !== undefined && block !== nil || arguments.length > 1`
      ::Kernel.raise ::ArgumentError, "wrong number of arguments (#{`arguments.length`} expected 1)"
    end

    if `pattern !== undefined`
      block = ::Kernel.proc { |e| pattern === e }
    end

    ::Enumerator.new do |yielder|
      %x{
        var accumulate;

        self.$each.$$p = function() {
          var element = #{::Opal.destructure(`arguments`)},
              end_chunk = $yield1(block, element);

          if (accumulate == null) {
            accumulate = [];
          }

          if ($truthy(end_chunk)) {
            accumulate.push(element);
            #{yielder.yield(`accumulate`)};
            accumulate = null;
          } else {
            accumulate.push(element)
          }
        }

        self.$each();

        if (accumulate != null) {
          #{yielder.yield(`accumulate`)};
        }
      }
    end
  end

  def slice_when(&block)
    ::Kernel.raise ::ArgumentError, 'wrong number of arguments (0 for 1)' unless block_given?

    ::Enumerator.new do |yielder|
      %x{
        var slice = nil, last_after = nil;

        self.$each_cons.$$p = function() {
          var params = #{::Opal.destructure(`arguments`)},
              before = params[0],
              after = params[1],
              match = $yieldX(block, [before, after]);

          last_after = after;

          if (slice === nil) {
            slice = [];
          }

          if ($truthy(match)) {
            slice.push(before);
            #{yielder.yield(`slice`)};
            slice = [];
          } else {
            slice.push(before);
          }
        }

        self.$each_cons(2);

        if (slice !== nil) {
          slice.push(last_after);
          #{yielder.yield(`slice`)};
        }
      }
    end
  end

  def sort(&block)
    ary = to_a
    block = ->(a, b) { a <=> b } unless block_given?
    ary.sort(&block)
  end

  def sort_by(&block)
    return enum_for(:sort_by) { enumerator_size } unless block_given?

    dup = map do
      arg = ::Opal.destructure(`arguments`)
      [yield(arg), arg]
    end
    dup.sort! { |a, b| `a[0]` <=> `b[0]` }
    dup.map! { |i| `i[1]` }
  end

  # This method implements the Kahan summation algorithm if it is possible to apply one.
  def sum(initial = 0)
    result = initial
    compensation = 0

    each do |*args|
      item = if block_given?
               yield(*args)
             else
               ::Opal.destructure(args)
             end

      if ![::Float::INFINITY, -::Float::INFINITY].include?(item) && item.respond_to?(:-)
        y = item - compensation
        t = result + y
        compensation = (t - result) - y
        result = t
      else
        result += item
      end
    end

    result
  end

  def take(num)
    first(num)
  end

  def take_while(&block)
    return enum_for :take_while unless block

    result = []

    each do |*args|
      value = ::Opal.destructure(args)

      unless yield(value)
        return result
      end

      `result.push(value)`
    end
  end

  def uniq(&block)
    hash = {}

    each do |*args|
      value = ::Opal.destructure(args)

      produced = if block_given?
                   yield(value)
                 else
                   value
                 end

      unless hash.key?(produced)
        hash[produced] = value
      end
    end

    hash.values
  end

  def tally(hash = undefined)
    `if (hash && hash !== nil) { $deny_frozen_access(hash); }`

    out = group_by(&:itself).transform_values(&:count)
    if hash
      out.each { |k, v| hash[k] = hash.fetch(k, 0) + v }
      hash
    else
      out
    end
  end

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

    %x{
      var hash = #{{}};

      self.$each.$$p = function() {
        var param = #{::Opal.destructure(`arguments`)};
        var ary = #{::Opal.coerce_to?(`param`, ::Array, :to_ary)}, key, val;
        if (!ary.$$is_array) {
          #{::Kernel.raise ::TypeError, "wrong element type #{`param`.class} (expected array)"}
        }
        if (ary.length !== 2) {
          #{::Kernel.raise ::ArgumentError, "element has wrong array length (expected 2, was #{`ary`.length})"}
        }
        key = ary[0];
        val = ary[1];

        Opal.hash_put(hash, key, val);
      };

      self.$each.apply(self, args);

      return hash;
    }
  end

  def to_set(klass = Set, *args, &block)
    klass.new(self, *args, &block)
  end

  def zip(*others, &block)
    to_a.zip(*others)
  end

  alias find detect
  alias filter find_all
  alias flat_map collect_concat
  alias map collect
  alias member? include?
  alias reduce inject
  alias select find_all
  alias to_a entries
end
