class Module
  def self.new(&block)
    %x{
      function AnonModule(){}
      var klass         = Opal.boot(Opal.Module, AnonModule);
      klass.$$name      = nil;
      klass.$$class     = Opal.Module;
      klass.$$dep       = []
      klass.$$is_module = true;
      klass.$$proto     = {};

      // inherit scope from parent
      Opal.create_scope(Opal.Module.$$scope, klass);

      if (block !== nil) {
        var block_self = block.$$s;
        block.$$s = null;
        block.call(klass);
        block.$$s = block_self;
      }

      return klass;
    }
  end

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

    `Opal.is_a(object, self)`
  end

  def <(other)
    # class cannot be a descendant of itself
    %x{
      var working = self;
      
      if (working === other) {
        return false;
      }
      
      while (working) {
        if (working === other) {
          return true;
        }

        working = working.$$parent;
      }

      return false;
    }
  end

  def alias_method(newname, oldname)
    `Opal.alias(self, newname, oldname)`

    self
  end

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

    self
  end

  def ancestors
    %x{
      var parent = self,
          result = [];

      while (parent) {
        result.push(parent);
        result = result.concat(parent.$$inc);

        parent = parent.$$super;
      }

      return result;
    }
  end

  def append_features(klass)
    `Opal.append_features(self, klass)`
    self
  end

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

  alias attr attr_accessor

  def attr_reader(*names)
    %x{
      var proto = self.$$proto;

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

        // the closure here is needed because name will change at the next
        // cycle, I wish we could use let.
        var body = (function(ivar) {
          return function() {
            if (this[ivar] == null) {
              return nil;
            }
            else {
              return this[ivar];
            }
          };
        })(ivar);

        // initialize the instance variable as nil
        proto[ivar] = nil;

        if (self.$$is_singleton) {
          proto.constructor.prototype[id] = body;
        }
        else {
          Opal.defn(self, id, body);
        }
      }
    }

    nil
  end

  def attr_writer(*names)
    %x{
      var proto = self.$$proto;

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

        // the closure here is needed because name will change at the next
        // cycle, I wish we could use let.
        var body = (function(ivar){
          return function(value) {
            return this[ivar] = value;
          }
        })(ivar);

        // initialize the instance variable as nil
        proto[ivar] = nil;

        if (self.$$is_singleton) {
          proto.constructor.prototype[id] = body;
        }
        else {
          Opal.defn(self, id, body);
        }
      }
    }

    nil
  end

  def autoload(const, path)
    %x{
      var autoloaders;

      if (!(autoloaders = self.$$autoload)) {
        autoloaders = self.$$autoload = {};
      }

      autoloaders[#{const}] = #{path};
      return nil;
    }
  end

  def class_variable_get(name)
    name = Opal.coerce_to!(name, String, :to_str)
    raise NameError, 'class vars should start with @@' if `name.length < 3 || name.slice(0,2) !== '@@'`
    %x{
      var value = Opal.cvars[name.slice(2)];
      #{raise NameError, 'uninitialized class variable @@a in' if `value == null`}
      return value;
    }
  end

  def class_variable_set(name, value)
    name = Opal.coerce_to!(name, String, :to_str)
    raise NameError if `name.length < 3 || name.slice(0,2) !== '@@'`
    %x{
      Opal.cvars[name.slice(2)] = value;
      return value;
    }
  end

  def constants
    `self.$$scope.constants.slice(0)`
  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)
    raise NameError, "wrong constant name #{name}" unless name =~ /^[A-Z]\w*$/

    %x{
      var scopes = [self.$$scope];

      if (inherit || self === Opal.Object) {
        var parent = self.$$super;

        while (parent !== Opal.BasicObject) {
          scopes.push(parent.$$scope);

          parent = parent.$$super;
        }
      }

      for (var i = 0, length = scopes.length; i < length; i++) {
        if (scopes[i].hasOwnProperty(name)) {
          return true;
        }
      }

      return false;
    }
  end

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

    raise NameError, "wrong constant name #{name}" unless `/^[A-Z]\w*$/.test(name)`

    %x{
      var scopes = [self.$$scope];

      if (inherit || self == Opal.Object) {
        var parent = self.$$super;

        while (parent !== Opal.BasicObject) {
          scopes.push(parent.$$scope);

          parent = parent.$$super;
        }
      }

      for (var i = 0, length = scopes.length; i < length; i++) {
        if (scopes[i].hasOwnProperty(name)) {
          return scopes[i][name];
        }
      }

      return #{const_missing name};
    }
  end

  def const_missing(name)
    %x{
      if (self.$$autoload) {
        var file = self.$$autoload[name];

        if (file) {
          self.$require(file);

          return #{const_get name};
        }
      }
    }

    raise NameError, "uninitialized constant #{self}::#{name}"
  end

  def const_set(name, value)
    raise NameError, "wrong constant name #{name}" unless name =~ /^[A-Z]\w*$/

    begin
      name = name.to_str
    rescue
      raise TypeError, 'conversion with #to_str failed'
    end

    `Opal.casgn(self, name, value)`

    value
  end

  def define_method(name, method = undefined, &block)
    if `method === undefined && block === nil`
      raise ArgumentError, "tried to create a Proc object without a block"
    end

    block ||= case method
      when Proc
        method

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

      when UnboundMethod
        lambda {|*args|
          bound = method.bind(self)
          bound.call(*args)
        }

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

    %x{
      var id = '$' + name;

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

      Opal.def(self, id, block);

      return name;
    }
  end

  def remove_method(*names)
    %x{
      for (var i = 0, length = names.length; i < length; i++) {
        Opal.rdef(self, "$" + names[i]);
      }
    }

    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 === self) {
          continue;
        }

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

    self
  end

  def include?(mod)
    %x{
      for (var cls = self; cls; cls = cls.$$super) {
        for (var i = 0; i != cls.$$inc.length; i++) {
          var mod2 = cls.$$inc[i];
          if (mod === mod2) {
            return true;
          }
        }
      }
      return false;
    }
  end

  def instance_method(name)
    %x{
      var meth = self.$$proto['$' + name];

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

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

  def instance_methods(include_super = true)
    %x{
      var methods = [],
          proto   = self.$$proto;

      for (var prop in proto) {
        if (prop.charAt(0) !== '$') {
          continue;
        }

        if (typeof(proto[prop]) !== "function") {
          continue;
        }

        if (proto[prop].$$stub) {
          continue;
        }

        if (!self.$$is_mod) {
          if (self !== Opal.BasicObject && proto[prop] === Opal.BasicObject.$$proto[prop]) {
            continue;
          }

          if (!include_super && !proto.hasOwnProperty(prop)) {
            continue;
          }

          if (!include_super && proto[prop].$$donated) {
            continue;
          }
        }

        methods.push(prop.substr(1));
      }

      return methods;
    }
  end

  def included(mod)
  end

  def extended(mod)
  end

  def method_added(*)
  end

  def method_removed(*)
  end

  def method_undefined(*)
  end

  def module_eval(&block)
    raise ArgumentError, 'no block given' unless block

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

      block.$$s = null;
      result = block.call(self);
      block.$$s = old;

      return result;
    }
  end

  alias class_eval module_eval

  def module_exec(&block)
    %x{
      if (block === nil) {
        throw new Error("no block given");
      }

      var block_self = block.$$s, result;

      block.$$s = null;
      result = block.apply(self, $slice.call(arguments));
      block.$$s = block_self;

      return result;
    }
  end

  alias class_exec module_exec

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

  def module_function(*methods)
    %x{
      if (methods.length === 0) {
        self.$$module_function = true;
      }
      else {
        for (var i = 0, length = methods.length; i < length; i++) {
          var meth = methods[i],
              id   = '$' + meth,
              func = self.$$proto[id];

          Opal.defs(self, id, func);
        }
      }

      return self;
    }
  end

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

      var result = [], base = self;

      while (base) {
        if (base.$$name === nil) {
          return result.length === 0 ? nil : result.join('::');
        }

        result.unshift(base.$$name);

        base = base.$$base_module;

        if (base === Opal.Object) {
          break;
        }
      }

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

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

  def remove_class_variable(*)
  end

  def remove_const(name)
    %x{
      var old = self.$$scope[name];
      delete self.$$scope[name];
      return old;
    }
  end

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

  def undef_method(*names)
    %x{
      for (var i = 0, length = names.length; i < length; i++) {
        Opal.udef(self, "$" + names[i]);
      }
    }

    self
  end
end
