// Copyright 2017 Google Inc.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//     https://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package com.google.devtools.intellij.ijaas.handlers;

import com.google.common.collect.Ordering;
import com.google.common.util.concurrent.SettableFuture;
import com.google.devtools.intellij.ijaas.BaseHandler;
import com.google.devtools.intellij.ijaas.handlers.JavaCompleteHandler.Request;
import com.google.devtools.intellij.ijaas.handlers.JavaCompleteHandler.Response;
import com.intellij.codeInsight.completion.CodeCompletionHandlerBase;
import com.intellij.codeInsight.completion.CompletionPhase;
import com.intellij.codeInsight.completion.CompletionProgressIndicator;
import com.intellij.codeInsight.completion.CompletionType;
import com.intellij.codeInsight.completion.impl.CompletionServiceImpl;
import com.intellij.codeInsight.lookup.LookupElement;
import com.intellij.codeInsight.lookup.LookupElementPresentation;
import com.intellij.codeInsight.lookup.impl.LookupImpl;
import com.intellij.lang.java.JavaLanguage;
import com.intellij.openapi.application.Application;
import com.intellij.openapi.application.ApplicationManager;
import com.intellij.openapi.command.CommandProcessor;
import com.intellij.openapi.editor.Editor;
import com.intellij.openapi.editor.EditorFactory;
import com.intellij.openapi.project.Project;
import com.intellij.openapi.project.ProjectLocator;
import com.intellij.openapi.util.Ref;
import com.intellij.openapi.util.io.FileUtil;
import com.intellij.openapi.vfs.LocalFileSystem;
import com.intellij.openapi.vfs.VirtualFile;
import com.intellij.psi.PsiClass;
import com.intellij.psi.PsiDocumentManager;
import com.intellij.psi.PsiElement;
import com.intellij.psi.PsiFile;
import com.intellij.psi.PsiFileFactory;
import com.intellij.psi.PsiKeyword;
import com.intellij.psi.PsiMethod;
import com.intellij.psi.PsiVariable;
import java.io.File;
import java.util.ArrayList;
import java.util.Collections;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.atomic.AtomicReference;
import javax.annotation.Nullable;

public class JavaCompleteHandler extends BaseHandler<Request, Response> {
  @Override
  protected Class<Request> requestClass() {
    return Request.class;
  }

  @Override
  protected Response handle(Request request) {
    Project project = findProject(request.file);
    if (project == null) {
      throw new RuntimeException("Cannot find the target project");
    }
    SettableFuture<Response> responseFuture = SettableFuture.create();
    Application application = ApplicationManager.getApplication();
    Ref<PsiFile> psiFileRef = new Ref<>();
    application.runReadAction(
        () -> {
          psiFileRef.set(
              PsiFileFactory.getInstance(project)
                  .createFileFromText(JavaLanguage.INSTANCE, request.text));
        });
    PsiFile psiFile = psiFileRef.get();

    application.invokeAndWait(
        () -> {
          Editor editor =
              EditorFactory.getInstance()
                  .createEditor(
                      PsiDocumentManager.getInstance(project).getDocument(psiFile), project);
          editor.getCaretModel().moveToOffset(request.offset);
          CommandProcessor.getInstance()
              .executeCommand(
                  project,
                  () -> {
                    CodeCompletionHandlerBase handler =
                        new CodeCompletionHandlerBase(CompletionType.BASIC) {
                          @Override
                          protected void completionFinished(
                              CompletionProgressIndicator indicator, boolean hasModifiers) {
                            CompletionServiceImpl.setCompletionPhase(
                                new CompletionPhase.ItemsCalculated(indicator));
                            Response response = new Response();
                            LookupImpl lookup = indicator.getLookup();
                            for (LookupElement item : lookup.getItems()) {
                              PsiElement psi = item.getPsiElement();
                              if (psi == null) {
                                continue;
                              }
                              Completion c = new Completion();
                              LookupElementPresentation presentation =
                                  new LookupElementPresentation();
                              item.renderElement(presentation);
                              c.word =
                                  item.getLookupString().substring(lookup.getPrefixLength(item));
                              if (psi instanceof PsiMethod) {
                                PsiMethod m = (PsiMethod) psi;
                                if (m.getParameterList().getParametersCount() == 0) {
                                  c.word += "()";
                                } else {
                                  c.word += '(';
                                }
                                c.menu =
                                    presentation.getTypeText() + " - " + presentation.getTailText();
                                c.kind = Completion.FUNCTION;
                              } else if (psi instanceof PsiKeyword) {
                                c.kind = Completion.KEYWORD;
                              } else if (psi instanceof PsiClass) {
                                c.menu = presentation.getTailText();
                                c.kind = Completion.TYPE;
                              } else if (psi instanceof PsiVariable) {
                                c.menu = presentation.getTypeText();
                                c.kind = Completion.VARIABLE;
                              } else {
                                c.menu = psi.getClass().getSimpleName();
                                c.kind = "";
                              }
                              response.completions.add(c);
                            }
                            responseFuture.set(response);
                          }
                        };
                    handler.invokeCompletion(project, editor);
                  },
                  null,
                  null);
        });
    try {
      Response response = responseFuture.get();
      Collections.sort(response.completions, new CompletionOrdering());
      return response;
    } catch (ExecutionException e) {
      throw new RuntimeException(e);
    } catch (InterruptedException e) {
      Thread.currentThread().interrupt();
      throw new RuntimeException(e);
    }
  }

  @Nullable
  private Project findProject(String file) {
    LocalFileSystem localFileSystem = LocalFileSystem.getInstance();
    ProjectLocator projectLocator = ProjectLocator.getInstance();
    AtomicReference<Project> ret = new AtomicReference<>();
    FileUtil.processFilesRecursively(
        new File(file),
        (f) -> {
          VirtualFile vf = localFileSystem.findFileByIoFile(f);
          if (vf != null) {
            ret.set(projectLocator.guessProjectForFile(vf));
            return false;
          }
          return true;
        });
    return ret.get();
  }

  public static class Request {
    String file;
    String text;
    int offset;
  }

  public static class Response {
    ArrayList<Completion> completions = new ArrayList<>();
  }

  public class Completion {
    public static final String VARIABLE = "v";
    public static final String FUNCTION = "f";
    public static final String TYPE = "t";
    public static final String KEYWORD = "k";

    public String word;
    public String menu;
    public String kind;
  }

  private static class CompletionOrdering extends Ordering<Completion> {
    @Override
    public int compare(Completion arg0, Completion arg1) {
      boolean arg0Keyword = arg0.kind.equals(Completion.KEYWORD);
      boolean arg1Keyword = arg1.kind.equals(Completion.KEYWORD);
      if (arg0Keyword && !arg1Keyword) {
        return 1;
      } else if (!arg0Keyword && arg1Keyword) {
        return -1;
      }
      return arg0.word.compareTo(arg1.word);
    }
  }
}