public List<String> onTabComplete(
      CommandSender commandSender, Command command, String s, String[] strings) {

    for (TabCompleter completer : delegateCompleters) {
      List<String> list = completer.onTabComplete(commandSender, command, s, strings);
      if (list != null) return list;
    }

    String expression = strings[strings.length - 1];
    TreeSet<String> result = new TreeSet<String>();
    Caller caller = plugin.getCallerService().getCaller(commandSender);
    WorkspaceService service = plugin.getWorkspaceService();
    String workspaceName = service.getWorkspaceName(commandSender);
    Workspace workspace = service.getWorkspace(workspaceName);
    LinkedList<String> tokens = new LinkedList<String>();
    boolean needHelp = expression.endsWith("?");
    if (needHelp) expression = expression.substring(0, expression.length() - 1);
    Collections.addAll(tokens, expression.split("\\."));
    if (expression.endsWith(".")) tokens.add("");

    if (needHelp) {
      getHelp(caller, workspace, tokens);
      return Collections.singletonList(expression);
    }

    String firstToken = tokens.pollFirst();
    if (firstToken == null) firstToken = "";
    MetaClass callerScriptMetaClass = InvokerHelper.getMetaClass(CallerScript.class);
    MetaClass workspaceMetaClass = InvokerHelper.getMetaClass(Workspace.class);
    Map workspaceVars = null;
    if (workspace != null) workspaceVars = workspace.getBinding().getVariables();
    Map globalVars = service.getBinding().getVariables();
    PreparedScriptProperties properties = new PreparedScriptProperties();
    properties.setCaller(caller);
    properties.setServer(plugin.getServer());
    properties.setWorkspace(workspace);

    if (tokens.isEmpty()) { // get current method or class
      for (MetaProperty metaProperty : callerScriptMetaClass.getProperties()) {
        String name = metaProperty.getName();
        if (name.contains(firstToken)) result.add(name);
      }
      for (String name : service.getImportTabCompleteClasses().keySet()) {
        if (name.contains(firstToken)) result.add(name);
      }
      for (MetaMethod metaMethod : callerScriptMetaClass.getMetaMethods()) {
        if (metaMethod.getDeclaringClass().getTheClass().equals(Object.class)) continue;
        String name = metaMethod.getName();
        if (name.contains(firstToken)) {
          String methodEnd = "(";
          if (metaMethod.isValidMethod(new Class[] {Closure.class})) methodEnd = "{";
          else if (metaMethod.getParameterTypes().length == 0) methodEnd = "()";
          result.add(name + methodEnd);
        }
        int args = metaMethod.getParameterTypes().length;
        if ((name.startsWith("get") && args == 0 || name.startsWith("set") && args == 1)
            && name.length() > 3) {
          String propertyName = getPropertyName(name);
          if (propertyName != null && propertyName.contains(firstToken)) result.add(propertyName);
        }
      }
      for (MetaMethod metaMethod : workspaceMetaClass.getMetaMethods()) {
        if (metaMethod.getDeclaringClass().getTheClass().equals(Object.class)) continue;
        String name = metaMethod.getName();
        if (name.contains(firstToken)) {
          String methodEnd = "(";
          if (metaMethod.isValidMethod(new Class[] {Closure.class})) methodEnd = "{";
          else if (metaMethod.getParameterTypes().length == 0) methodEnd = "()";
          result.add(name + methodEnd);
        }
      }
      for (Method method : CallerScript.class.getMethods()) {
        if (method.getDeclaringClass().equals(Object.class)) continue;
        String name = method.getName();
        if (name.contains(firstToken)) {
          String methodEnd = "(";
          Class<?>[] types = method.getParameterTypes();
          if (types.length == 1 && Closure.class.isAssignableFrom(types[0])) methodEnd = "{";
          else if (types.length == 0) methodEnd = "()";
          result.add(name + methodEnd);
          int args = method.getParameterTypes().length;
          if ((name.startsWith("get") && args == 0 || name.startsWith("set") && args == 1)
              && name.length() > 3) {
            String propertyName = getPropertyName(name);
            if (propertyName != null && propertyName.contains(firstToken)) result.add(propertyName);
          }
        }
      }
      for (Method method : Workspace.class.getMethods()) {
        if (method.getDeclaringClass().equals(Object.class)) continue;
        String name = method.getName();
        if (name.contains(firstToken)) {
          String methodEnd = "(";
          Class<?>[] types = method.getParameterTypes();
          if (types.length == 1 && Closure.class.isAssignableFrom(types[0])) methodEnd = "{";
          else if (types.length == 0) methodEnd = "()";
          result.add(name + methodEnd);
        }
      }
      if (workspaceVars != null)
        for (Object key : workspaceVars.keySet()) {
          String name = key.toString();
          if (name.contains(firstToken)) result.add(name);
        }
      if (globalVars != null)
        for (Object key : globalVars.keySet()) {
          String name = key.toString();
          if (name.contains(firstToken)) result.add(name);
        }
      for (GroovyObject modifier : CallerScript.getDynamicModifiers()) {
        Object[] params = {properties};
        try {
          Map<?, ?> map =
              (Map) modifier.getMetaClass().invokeMethod(modifier, "getPropertyMapFor", params);
          for (Object key : map.keySet()) {
            String name = key.toString();
            if (name.contains(firstToken)) result.add(name);
          }
        } catch (Exception ignored) {
        }
        try {
          Map<?, ?> map =
              (Map) modifier.getMetaClass().invokeMethod(modifier, "getMethodMapFor", params);
          for (Object key : map.keySet()) {
            String name = key.toString();
            if (name.contains(firstToken)) result.add(name + "(");
          }
        } catch (Exception ignored) {
        }
      }
      if (globalVars != null)
        for (Object key : globalVars.keySet()) {
          String name = key.toString();
          if (name.contains(firstToken)) result.add(name);
        }
      return new ArrayList<String>(result);
    }

    // get metaclass of first token
    MetaClass metaClass =
        getFirstTokenMeta(
            caller,
            firstToken,
            commandSender,
            service,
            callerScriptMetaClass,
            workspaceMetaClass,
            workspace,
            workspaceVars,
            globalVars,
            properties);
    boolean classHook =
        tokens.size() <= 1 && service.getImportTabCompleteClasses().containsKey(firstToken);

    if (metaClass == null) return null;
    metaClass = skipTokens(tokens, metaClass);
    if (metaClass == null) return null;

    // select property or method of last metaclass
    String token = tokens.pollFirst();
    Class theClass = metaClass.getTheClass();
    String inputPrefix = expression.substring(0, expression.lastIndexOf('.')) + ".";
    for (MetaProperty metaProperty : metaClass.getProperties()) {
      String name = metaProperty.getName();
      if (name.startsWith(token)) result.add(inputPrefix + name);
    }
    for (MetaMethod metaMethod : metaClass.getMetaMethods()) {
      if (metaMethod.getDeclaringClass().getTheClass().equals(Object.class)) continue;
      String name = metaMethod.getName();
      if (name.startsWith(token)) {
        String methodEnd = "(";
        if (metaMethod.isValidMethod(new Class[] {Closure.class})) methodEnd = "{";
        else if (metaMethod.getNativeParameterTypes().length == 0) methodEnd = "()";
        result.add(inputPrefix + name + methodEnd);
      }
      int args = metaMethod.getParameterTypes().length;
      if ((name.startsWith("get") && args == 0 || name.startsWith("set") && args == 1)
          && name.length() > 3) {
        String propertyName = getPropertyName(name);
        if (propertyName != null && propertyName.startsWith(token))
          result.add(inputPrefix + propertyName);
      }
    }
    for (Method method : theClass.getMethods()) {
      if (method.getDeclaringClass().equals(Object.class)) continue;
      String name = method.getName();
      if (name.startsWith(token)) {
        String methodEnd = "(";
        Class<?>[] types = method.getParameterTypes();
        if (types.length == 1 && Closure.class.isAssignableFrom(types[0])) methodEnd = "{";
        if (types.length == 0) methodEnd = "()";
        result.add(inputPrefix + name + methodEnd);
      }
      int args = method.getParameterTypes().length;
      if ((name.startsWith("get") && args == 0 || name.startsWith("set") && args == 1)
          && name.length() > 3) {
        String propertyName = getPropertyName(name);
        if (propertyName != null && propertyName.startsWith(token))
          result.add(inputPrefix + propertyName);
      }
    }
    if (Enum.class.isAssignableFrom(theClass)) {
      Enum[] enumValues = getEnumValues(theClass);
      if (enumValues != null)
        for (Enum anEnum : enumValues) {
          String name = anEnum.name();
          if (name.startsWith(token)) result.add(inputPrefix + name);
        }
    }
    if (classHook) {
      for (MetaProperty metaProperty : InvokerHelper.getMetaClass(Class.class).getProperties()) {
        String name = metaProperty.getName();
        if (name.startsWith(token)) result.add(inputPrefix + name);
      }
      for (Method method : Class.class.getMethods()) {
        if (method.getDeclaringClass().equals(Object.class)) continue;
        String name = method.getName();
        if (name.startsWith(token)) {
          String methodEnd = "(";
          Class<?>[] types = method.getParameterTypes();
          if (types.length == 1 && Closure.class.isAssignableFrom(types[0])) methodEnd = "{";
          if (types.length == 0) methodEnd = "()";
          result.add(inputPrefix + name + methodEnd);
        }
        int args = method.getParameterTypes().length;
        if ((name.startsWith("get") && args == 0 || name.startsWith("set") && args == 1)
            && name.length() > 3) {
          String propertyName = getPropertyName(name);
          if (propertyName != null && propertyName.startsWith(token))
            result.add(inputPrefix + propertyName);
        }
      }
    }
    return new ArrayList<String>(result);
  }
 private void createMetaMethodFromClass(Map<CachedClass, List<MetaMethod>> map, Class aClass) {
   try {
     MetaMethod method = (MetaMethod) aClass.newInstance();
     final CachedClass declClass = method.getDeclaringClass();
     List<MetaMethod> arr = map.get(declClass);
     if (arr == null) {
       arr = new ArrayList<MetaMethod>(4);
       map.put(declClass, arr);
     }
     arr.add(method);
     instanceMethods.add(method);
   } catch (InstantiationException e) {
     /* ignore */
   } catch (IllegalAccessException e) {
     /* ignore */
   }
 }
  private void registerMethods(
      final Class theClass,
      final boolean useMethodWrapper,
      final boolean useInstanceMethods,
      Map<CachedClass, List<MetaMethod>> map) {
    if (useMethodWrapper) {
      // Here we instantiate objects representing MetaMethods for DGM methods.
      // Calls for such meta methods done without reflection, so more effectively.

      try {
        List<GeneratedMetaMethod.DgmMethodRecord> records =
            GeneratedMetaMethod.DgmMethodRecord.loadDgmInfo();

        for (GeneratedMetaMethod.DgmMethodRecord record : records) {
          Class[] newParams = new Class[record.parameters.length - 1];
          System.arraycopy(record.parameters, 1, newParams, 0, record.parameters.length - 1);

          MetaMethod method =
              new GeneratedMetaMethod.Proxy(
                  record.className,
                  record.methodName,
                  ReflectionCache.getCachedClass(record.parameters[0]),
                  record.returnType,
                  newParams);
          final CachedClass declClass = method.getDeclaringClass();
          List<MetaMethod> arr = map.get(declClass);
          if (arr == null) {
            arr = new ArrayList<MetaMethod>(4);
            map.put(declClass, arr);
          }
          arr.add(method);
          instanceMethods.add(method);
        }
      } catch (Throwable e) {
        e.printStackTrace();
        // we print the error, but we don't stop with an exception here
        // since it is more comfortable this way for development
      }
    } else {
      CachedMethod[] methods = ReflectionCache.getCachedClass(theClass).getMethods();

      for (CachedMethod method : methods) {
        final int mod = method.getModifiers();
        if (Modifier.isStatic(mod)
            && Modifier.isPublic(mod)
            && method.getCachedMethod().getAnnotation(Deprecated.class) == null) {
          CachedClass[] paramTypes = method.getParameterTypes();
          if (paramTypes.length > 0) {
            List<MetaMethod> arr = map.get(paramTypes[0]);
            if (arr == null) {
              arr = new ArrayList<MetaMethod>(4);
              map.put(paramTypes[0], arr);
            }
            if (useInstanceMethods) {
              final NewInstanceMetaMethod metaMethod = new NewInstanceMetaMethod(method);
              arr.add(metaMethod);
              instanceMethods.add(metaMethod);
            } else {
              final NewStaticMetaMethod metaMethod = new NewStaticMetaMethod(method);
              arr.add(metaMethod);
              staticMethods.add(metaMethod);
            }
          }
        }
      }
    }
  }