class Fancy
  class AST

    class MethodDef < Rubinius::AST::Define
      def initialize(line, method_ident, args, body, access = :public)
        body = AST::ExpressionList.new(line) unless body

        @line = line
        @name = method_ident.method_name
        @arguments = args
        @body = body
        @access = access
        generate_ivar_assignment

        if body.empty?
          body.unshift_expression Rubinius::AST::NilLiteral.new(line)
        end
      end

      def generate_ivar_assignment
        @arguments.required.reverse.each do |name|
          if name.to_s =~ /^@/
            ident = Fancy::AST::Identifier.new(line, name.to_s)
            value = Rubinius::AST::LocalVariableAccess.new(line, name)
            asign = Fancy::AST::Assignment.new(line, ident, value)
            body.unshift_expression(asign)
          end
        end
      end

      def bytecode(g)
        if @name.to_s =~ /^initialize:(\S)+/
          define_constructor_class_method g
        end
        if @name.to_s =~ /^unknown_message:with_params:$/
          define_method_missing(g)
        end

        docstring = @body.shift_docstring
        super(g)
        MethodDef.set_docstring(g, docstring, @line, @arguments.names)
      end


      # Sets fancy documentation for the object currently
      # on top of the stack
      def self.set_docstring(g, docstring, line, argnames = [])
        # prevent invoking documentation: when doesnt makes sense.
        return unless docstring
        local = StackLocal.new
        local.set!(g)
        ms = MessageSend.new(line,
                            Identifier.new(line, "Fancy::Documentation"),
                            Identifier.new(line, "for_method:on_class:is:"),
                            MessageArgs.new(line, local, Self.new(line), docstring))
        ms.bytecode(g)
        meta = HashLiteral.new(line,
          SymbolLiteral.new(line, :argnames),
          ArrayLiteral.new(line, *argnames.map{|arg|StringLiteral.new(line, arg.to_s)})
        )
        ms = MessageSend.new(line,
                             Nothing.new,
                             Identifier.new(line, "meta:"),
                             MessageArgs.new(line, meta))
        ms.bytecode(g)
        g.pop
      end

      # defines a class method names "new:foo:" if we're defining a
      # method named e.g. "initialize:foo:" (a constructor method).
      def define_constructor_class_method(g)
        method_ident = Rubinius::AST::StringLiteral.new(@line, @name.to_s[11..-1])
        ms = MessageSend.new(@line,
                             Rubinius::AST::Self.new(@line),
                             Identifier.new(@line, "define_constructor_class_method:"),
                             MessageArgs.new(@line, method_ident))
        ms.bytecode(g)
        g.pop
      end

      def define_method_missing(g)
        MessageSend.new(@line,
                        Rubinius::AST::Self.new(@line),
                        Identifier.new(@line, "define_forward_method_missing"),
                        MessageArgs.new(@line)).bytecode(g)
        g.pop
      end
    end

    SymbolLiteral = Rubinius::AST::SymbolLiteral

    class Nothing
      def bytecode(g)
      end
    end

    class StackLocal
      def set!(g)
        # expects value to be on top of stack
        @local = g.new_stack_local
        g.set_stack_local @local
      end

      def bytecode(g)
        g.push_stack_local @local
      end
    end

    class MethodArgs < Rubinius::AST::FormalArguments
      def initialize(line, *args)
        super(line, args.map(&:to_sym), nil, nil)
      end
    end

  end
end
