# helpers: truthy, coerce_to, const_set, Object, return_ivar, assign_ivar, ivar, deny_frozen_access, freeze, prop, jsid, each_ivar
# backtick_javascript: true
# use_strict: true

class ::Module
  %x{
    function ensure_symbol_or_string(name) {
      if (name.$$is_string) {
        return name;
      };
      var converted_name = #{::Opal.try_convert(`name`, ::String, :to_str)};
      if (converted_name.$$is_string) {
        return converted_name;
      } else if (converted_name === nil) {
        #{::Kernel.raise ::TypeError, "#{`name`} is not a symbol nor a string"}
      } else {
        #{::Kernel.raise ::TypeError, "can't convert #{`name`.class} to String (#{`name`.class}#to_str gives #{`converted_name`.class}"}
      }
    }
  }

  def self.allocate
    %x{
      var module = Opal.allocate_module(nil, function(){});
      // Link the prototype of Module subclasses
      if (self !== Opal.Module) Object.setPrototypeOf(module, self.$$prototype);
      return module;
    }
  end

  def initialize(&block)
    module_eval(&block) if block_given?
  end

  def ===(object)
    return false if `object == null`

    `Opal.is_a(object, self)`
  end

  def <(other)
    unless ::Module === other
      ::Kernel.raise ::TypeError, 'compared with non class/module'
    end

    # class cannot be a descendant of itself
    %x{
      var working = self,
          ancestors,
          i, length;

      if (working === other) {
        return false;
      }

      for (i = 0, ancestors = Opal.ancestors(self), length = ancestors.length; i < length; i++) {
        if (ancestors[i] === other) {
          return true;
        }
      }

      for (i = 0, ancestors = Opal.ancestors(other), length = ancestors.length; i < length; i++) {
        if (ancestors[i] === self) {
          return false;
        }
      }

      return nil;
    }
  end

  def <=(other)
    equal?(other) || self < other
  end

  def >(other)
    unless ::Module === other
      ::Kernel.raise ::TypeError, 'compared with non class/module'
    end

    other < self
  end

  def >=(other)
    equal?(other) || self > other
  end

  def <=>(other)
    %x{
      if (self === other) {
        return 0;
      }
    }

    unless ::Module === other
      return nil
    end

    lt = self < other
    return nil if lt.nil?
    lt ? -1 : 1
  end

  def alias_method(newname, oldname)
    `$deny_frozen_access(self)`

    newname = `$coerce_to(newname, #{::String}, 'to_str')`
    oldname = `$coerce_to(oldname, #{::String}, 'to_str')`
    `Opal.alias(self, newname, oldname)`

    self
  end

  def alias_native(mid, jsid = mid)
    `$deny_frozen_access(self)`

    `Opal.alias_native(self, mid, jsid)`

    self
  end

  def ancestors
    `Opal.ancestors(self)`
  end

  def append_features(includer)
    `$deny_frozen_access(includer)`

    `Opal.append_features(self, includer)`
    self
  end

  def attr_accessor(*names)
    attr_reader(*names)
    attr_writer(*names)
  end

  def attr(*args)
    %x{
      if (args.length == 2 && (args[1] === true || args[1] === false)) {
        #{warn 'optional boolean argument is obsoleted', uplevel: 1}

        args[1] ? #{attr_accessor(`args[0]`)} : #{attr_reader(`args[0]`)};
        return nil;
      }
    }

    attr_reader(*args)
  end

  def attr_reader(*names)
    %x{
      $deny_frozen_access(self);

      var proto = self.$$prototype;

      for (var i = names.length - 1; i >= 0; i--) {
        var name = names[i],
            id   = $jsid(name),
            ivar = $ivar(name);

        var body = $return_ivar(ivar);

        // initialize the instance variable as nil
        Opal.prop(proto, ivar, nil);

        body.$$parameters = [];
        body.$$arity = 0;

        Opal.defn(self, id, body);
      }
    }

    nil
  end

  def attr_writer(*names)
    %x{
      $deny_frozen_access(self);

      var proto = self.$$prototype;

      for (var i = names.length - 1; i >= 0; i--) {
        var name = names[i],
            id   = $jsid(name + '='),
            ivar = $ivar(name);

        var body = $assign_ivar(ivar)

        body.$$parameters = [['req']];
        body.$$arity = 1;

        // initialize the instance variable as nil
        Opal.prop(proto, ivar, nil);

        Opal.defn(self, id, body);
      }
    }

    nil
  end

  def autoload(const, path)
    %x{
      $deny_frozen_access(self);

      if (!#{Opal.const_name?(const)}) {
        #{::Kernel.raise ::NameError, "autoload must be constant name: #{const}"}
      }

      if (path == "") {
        #{::Kernel.raise ::ArgumentError, 'empty file name'}
      }

      if (!self.$$const.hasOwnProperty(#{const})) {
        if (!self.$$autoload) {
          self.$$autoload = {};
        }
        Opal.const_cache_version++;
        self.$$autoload[#{const}] = { path: #{path}, loaded: false, required: false, success: false, exception: false };

        if (self.$const_added && !self.$const_added.$$pristine) {
          self.$const_added(#{const});
        }
      }
      return nil;
    }
  end

  def autoload?(const)
    %x{
      if (self.$$autoload && self.$$autoload[#{const}] && !self.$$autoload[#{const}].required && !self.$$autoload[#{const}].success) {
        return self.$$autoload[#{const}].path;
      }

      var ancestors = self.$ancestors();

      for (var i = 0, length = ancestors.length; i < length; i++) {
        if (ancestors[i].$$autoload && ancestors[i].$$autoload[#{const}] && !ancestors[i].$$autoload[#{const}].required && !ancestors[i].$$autoload[#{const}].success) {
          return ancestors[i].$$autoload[#{const}].path;
        }
      }
      return nil;
    }
  end

  def class_variables
    `Object.keys(Opal.class_variables(self))`
  end

  def class_variable_get(name)
    name = ::Opal.class_variable_name!(name)

    `Opal.class_variable_get(self, name, false)`
  end

  def class_variable_set(name, value)
    `$deny_frozen_access(self)`

    name = ::Opal.class_variable_name!(name)

    `Opal.class_variable_set(self, name, value)`
  end

  def class_variable_defined?(name)
    name = ::Opal.class_variable_name!(name)

    `Opal.class_variables(self).hasOwnProperty(name)`
  end

  def const_added(name)
  end
  ::Opal.pristine self, :const_added

  def remove_class_variable(name)
    `$deny_frozen_access(self)`

    name = ::Opal.class_variable_name!(name)

    %x{
      if (Opal.hasOwnProperty.call(self.$$cvars, name)) {
        var value = self.$$cvars[name];
        delete self.$$cvars[name];
        return value;
      } else {
        #{::Kernel.raise ::NameError, "cannot remove #{name} for #{self}"}
      }
    }
  end

  def constants(inherit = true)
    `Opal.constants(self, inherit)`
  end

  def self.constants(inherit = undefined)
    %x{
      if (inherit == null) {
        var nesting = (self.$$nesting || []).concat($Object),
            constant, constants = {},
            i, ii;

        for(i = 0, ii = nesting.length; i < ii; i++) {
          for (constant in nesting[i].$$const) {
            constants[constant] = true;
          }
        }
        return Object.keys(constants);
      } else {
        return Opal.constants(self, inherit)
      }
    }
  end

  def self.nesting
    `self.$$nesting || []`
  end

  # check for constant within current scope
  # if inherit is true or self is Object, will also check ancestors
  def const_defined?(name, inherit = true)
    name = Opal.const_name!(name)

    ::Kernel.raise ::NameError.new("wrong constant name #{name}", name) unless name =~ ::Opal::CONST_NAME_REGEXP

    %x{
      var module, modules = [self], module_constants, i, ii;

      // Add up ancestors if inherit is true
      if (inherit) {
        modules = modules.concat(Opal.ancestors(self));

        // Add Object's ancestors if it's a module – modules have no ancestors otherwise
        if (self.$$is_module) {
          modules = modules.concat([$Object]).concat(Opal.ancestors($Object));
        }
      }

      for (i = 0, ii = modules.length; i < ii; i++) {
        module = modules[i];
        if (module.$$const[#{name}] != null) { return true; }
        if (
          module.$$autoload &&
          module.$$autoload[#{name}] &&
          !module.$$autoload[#{name}].required &&
          !module.$$autoload[#{name}].success
        ) {
          return true;
        }
      }

      return false;
    }
  end

  def const_get(name, inherit = true)
    name = Opal.const_name!(name)

    %x{
      if (name.indexOf('::') === 0 && name !== '::'){
        name = name.slice(2);
      }
    }

    if `name.indexOf('::') != -1 && name != '::'`
      return name.split('::').inject(self) { |o, c| o.const_get(c) }
    end

    ::Kernel.raise ::NameError.new("wrong constant name #{name}", name) unless name =~ ::Opal::CONST_NAME_REGEXP

    %x{
      if (inherit) {
        return Opal.$$([self], name);
      } else {
        return Opal.const_get_local(self, name);
      }
    }
  end

  def const_missing(name)
    full_const_name = self == ::Object ? name : "#{self.name}::#{name}"

    ::Kernel.raise ::NameError.new("uninitialized constant #{full_const_name}", name)
  end

  def const_set(name, value)
    `$deny_frozen_access(self)`

    name = ::Opal.const_name!(name)

    if name !~ ::Opal::CONST_NAME_REGEXP || name.start_with?('::')
      ::Kernel.raise ::NameError.new("wrong constant name #{name}", name)
    end

    `$const_set(self, name, value)`

    value
  end

  def public_constant(const_name)
  end

  def define_method(name, method = undefined, &block)
    %x{
      $deny_frozen_access(self);

      if (method === undefined && block === nil)
        #{::Kernel.raise ::ArgumentError, 'tried to create a Proc object without a block'}

      name = ensure_symbol_or_string(name);
    }

    if `method !== undefined`
      block = case method
              when ::Proc
                method

              when ::Method
                `#{method.to_proc}.$$unbound`

              when ::UnboundMethod
                `Opal.wrap_method_body(method.$$method)`

              else
                ::Kernel.raise ::TypeError, "wrong argument type #{method.class} (expected Proc/Method/UnboundMethod)"
              end

      if `!method.$$is_proc`
        owner = method.owner
        if `owner.$$is_class` && !(self <= owner) # rubocop:disable Style/InverseMethods
          message = `owner.$$is_singleton` ? "can't bind singleton method to a different class" : "bind argument must be a subclass of #{owner}"
          ::Kernel.raise ::TypeError, message
        end
      end
    end

    %x{
      if (typeof(Proxy) !== 'undefined') {
        var meta = Object.create(null)

        block.$$proxy_target = block
        block = new Proxy(block, {
          apply: function(target, self, args) {
            var old_name = target.$$jsid, old_lambda = target.$$is_lambda;
            target.$$jsid = name;
            target.$$is_lambda = true;
            try {
              return target.apply(self, args);
            } catch(e) {
              if (e === target.$$brk || e === target.$$ret) return e.$v;
              throw e;
            } finally {
              target.$$jsid = old_name;
              target.$$is_lambda = old_lambda;
            }
          }
        })
      }

      block.$$jsid        = name;
      block.$$s           = null;
      block.$$def         = block;
      block.$$define_meth = true;

      return Opal.defn(self, $jsid(name), block);
    }
  end

  def freeze
    # Specialized version of freeze, because the $$base_module property needs to be
    # accessible despite the frozen status

    return self if frozen?

    %x{
      if (!self.hasOwnProperty('$$base_module')) { $prop(self, '$$base_module', null); }

      return $freeze(self);
    }
  end

  def remove_method(*names)
    %x{
      for (var i = 0; i < names.length; i++) {
        var name = ensure_symbol_or_string(names[i]);
        $deny_frozen_access(self);

        Opal.rdef(self, "$" + name);
      }
    }

    self
  end

  def singleton_class?
    `!!self.$$is_singleton`
  end

  def include(*mods)
    %x{
      for (var i = mods.length - 1; i >= 0; i--) {
        var mod = mods[i];

        if (!mod.$$is_module) {
          #{::Kernel.raise ::TypeError, "wrong argument type #{`mod`.class} (expected Module)"};
        }

        #{`mod`.append_features self};
        #{`mod`.included self};
      }
    }

    self
  end

  def included_modules
    `Opal.included_modules(self)`
  end

  def include?(mod)
    %x{
      if (!mod.$$is_module) {
        #{::Kernel.raise ::TypeError, "wrong argument type #{`mod`.class} (expected Module)"};
      }

      var i, ii, mod2, ancestors = Opal.ancestors(self);

      for (i = 0, ii = ancestors.length; i < ii; i++) {
        mod2 = ancestors[i];
        if (mod2 === mod && mod2 !== self) {
          return true;
        }
      }

      return false;
    }
  end

  def instance_method(name)
    %x{
      var meth = self.$$prototype[$jsid(name)];

      if (!meth || meth.$$stub) {
        #{::Kernel.raise ::NameError.new("undefined method `#{name}' for class `#{self.name}'", name)};
      }

      return #{::UnboundMethod.new(self, `meth.$$owner || #{self}`, `meth`, name)};
    }
  end

  def instance_methods(include_super = true)
    %x{
      if ($truthy(#{include_super})) {
        return Opal.instance_methods(self);
      } else {
        return Opal.own_instance_methods(self);
      }
    }
  end

  def included(mod)
  end

  def extended(mod)
  end

  def extend_object(object)
    `$deny_frozen_access(object)`
    nil
  end

  def method_added(*)
  end

  def method_removed(*)
  end

  def method_undefined(*)
  end

  def module_eval(*args, &block)
    if block.nil? && `!!Opal.compile`
      ::Kernel.raise ::ArgumentError, 'wrong number of arguments (0 for 1..3)' unless (1..3).cover? args.size

      string, file, _lineno = *args
      default_eval_options = { file: file || '(eval)', eval: true }
      compiling_options = __OPAL_COMPILER_CONFIG__.merge(default_eval_options)
      compiled = ::Opal.compile string, compiling_options
      block = ::Kernel.proc do
        %x{new Function("Opal,self", "return " + compiled)(Opal, self)}
      end
    elsif args.any?
      ::Kernel.raise ::ArgumentError, "wrong number of arguments (#{args.size} for 0)" \
                                      "\n\n  NOTE:If you want to enable passing a String argument please add \"require 'opal-parser'\" to your script\n"
    end

    %x{
      var old = block.$$s,
          result;

      block.$$s = null;
      result = block.apply(self, [self]);
      block.$$s = old;

      return result;
    }
  end

  def module_exec(*args, &block)
    %x{
      if (block === nil) {
        #{::Kernel.raise ::LocalJumpError, 'no block given'}
      }

      var block_self = block.$$s, result;

      block.$$s = null;
      result = block.apply(self, args);
      block.$$s = block_self;

      return result;
    }
  end

  def method_defined?(method)
    %x{
      var body = self.$$prototype[$jsid(method)];
      return (!!body) && !body.$$stub;
    }
  end

  def module_function(*methods)
    %x{
      $deny_frozen_access(self);

      if (methods.length === 0) {
        self.$$module_function = true;
        return nil;
      }
      else {
        for (var i = 0, length = methods.length; i < length; i++) {
          var meth = methods[i],
              id   = $jsid(meth),
              func = self.$$prototype[id];

          Opal.defs(self, id, func);
        }
        return methods.length === 1 ? methods[0] : methods;
      }

      return self;
    }
  end

  def name
    %x{
      if (self.$$full_name) {
        return self.$$full_name;
      }

      var result = [], base = self;

      while (base) {
        // Give up if any of the ancestors is unnamed
        if (base.$$name === nil || base.$$name == null) return nil;

        result.unshift(base.$$name);

        base = base.$$base_module;

        if (base === $Object) {
          break;
        }
      }

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

      return self.$$full_name = result.join('::');
    }
  end

  def prepend(*mods)
    %x{
      if (mods.length === 0) {
        #{::Kernel.raise ::ArgumentError, 'wrong number of arguments (given 0, expected 1+)'}
      }

      for (var i = mods.length - 1; i >= 0; i--) {
        var mod = mods[i];

        if (!mod.$$is_module) {
          #{::Kernel.raise ::TypeError, "wrong argument type #{`mod`.class} (expected Module)"};
        }

        #{`mod`.prepend_features self};
        #{`mod`.prepended self};
      }
    }

    self
  end

  def prepend_features(prepender)
    %x{
      $deny_frozen_access(prepender);

      if (!self.$$is_module) {
        #{::Kernel.raise ::TypeError, "wrong argument type #{self.class} (expected Module)"};
      }

      Opal.prepend_features(self, prepender)
    }
    self
  end

  def prepended(mod)
  end

  def remove_const(name)
    `$deny_frozen_access(self)`

    `Opal.const_remove(self, name)`
  end

  def to_s
    `Opal.Module.$name.call(self)` || "#<#{`self.$$is_module ? 'Module' : 'Class'`}:0x#{__id__.to_s(16)}>"
  end

  def undef_method(*names)
    %x{
      for (var i = 0; i < names.length; i++) {
        var name = ensure_symbol_or_string(names[i]);
        $deny_frozen_access(self);

        Opal.udef(self, "$" + name);
      }
    }

    self
  end

  def instance_variables
    consts = constants
    %x{
      var result = [];

      $each_ivar(self, function(name) {
        if (name !== 'constructor' && !#{consts.include?(`name`)}) {
          result.push('@' + name);
        }
      });

      return result;
    }
  end

  %x{
    function copyInstanceMethods(from, to) {
      var i, method_names = Opal.own_instance_methods(from);
      for (i = 0; i < method_names.length; i++) {
        var name = method_names[i],
            jsid = $jsid(name),
            body = from.$$prototype[jsid],
            wrapped = Opal.wrap_method_body(body);

        wrapped.$$jsid = name;
        Opal.defn(to, jsid, wrapped);
      }
    }

    function copyIncludedModules(from, to) {
      var modules = from.$$own_included_modules;
      for (var i = modules.length - 1; i >= 0; i--) {
        Opal.append_features(modules[i], to);
      }
    }

    function copyPrependedModules(from, to) {
      var modules = from.$$own_prepended_modules;
      for (var i = modules.length - 1; i >= 0; i--) {
        Opal.prepend_features(modules[i], to);
      }
    }
  }

  def initialize_copy(other)
    %x{
      copyInstanceMethods(other, self);
      copyIncludedModules(other, self);
      copyPrependedModules(other, self);
      self.$$cloned_from = other.$$cloned_from.concat(other);
    }
    copy_class_variables(other)
    copy_constants(other)
  end

  def initialize_dup(other)
    super
    # Unlike other classes, Module's singleton methods are copied on Object#dup.
    copy_singleton_methods(other)
  end

  def copy_class_variables(other)
    %x{
      for (var name in other.$$cvars) {
        self.$$cvars[name] = other.$$cvars[name];
      }
    }
  end

  def copy_constants(other)
    %x{
      var name, other_constants = other.$$const;

      for (name in other_constants) {
        $const_set(self, name, other_constants[name]);
      }
    }
  end

  def refine(klass, &block)
    refinement_module, m, klass_id = self, nil, nil
    %x{
      klass_id = Opal.id(klass);
      if (typeof self.$$refine_modules === "undefined") {
        self.$$refine_modules = Object.create(null);
      }
      if (typeof self.$$refine_modules[klass_id] === "undefined") {
        m = self.$$refine_modules[klass_id] = #{::Refinement.new};
      }
      else {
        m = self.$$refine_modules[klass_id];
      }
      m.refinement_module = refinement_module
      m.refined_class = klass
    }
    m.class_exec(&block)
    m
  end

  def refinements
    %x{
      var refine_modules = self.$$refine_modules, hash = #{{}};;
      if (typeof refine_modules === "undefined") return hash;
      for (var id in refine_modules) {
        hash['$[]='](refine_modules[id].refined_class, refine_modules[id]);
      }
      return hash;
    }
  end

  # Compiler overrides this method
  def using(mod)
    ::Kernel.raise 'Module#using is not permitted in methods'
  end

  alias class_eval module_eval
  alias class_exec module_exec
  alias inspect to_s
end

class ::Refinement < ::Module
  attr_reader :refined_class

  def inspect
    if @refinement_module
      "#<refinement:#{@refined_class.inspect}@#{@refinement_module.inspect}>"
    else
      super
    end
  end
end
