/*
 * Copyright 2000-2016 JetBrains s.r.o.
 *
 * 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
 *
 * http://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.intellij.codeInspection.deadCode;

import com.intellij.analysis.AnalysisScope;
import com.intellij.codeInspection.*;
import com.intellij.codeInspection.ex.EntryPointsManagerImpl;
import com.intellij.codeInspection.ex.GlobalInspectionContextImpl;
import com.intellij.codeInspection.ex.InspectionToolWrapper;
import com.intellij.codeInspection.ex.Tools;
import com.intellij.codeInspection.reference.*;
import com.intellij.codeInspection.ui.InspectionToolPresentation;
import com.intellij.codeInspection.unusedSymbol.UnusedSymbolLocalInspection;
import com.intellij.codeInspection.unusedSymbol.UnusedSymbolLocalInspectionBase;
import com.intellij.psi.*;
import com.intellij.psi.controlFlow.DefUseUtil;
import com.intellij.ui.ScrollPaneFactory;
import com.intellij.ui.TitledSeparator;
import com.intellij.ui.components.JBLabel;
import com.intellij.ui.components.JBRadioButton;
import com.intellij.ui.components.JBTabbedPane;
import com.intellij.util.ObjectUtils;
import com.intellij.util.ui.JBUI;
import gnu.trove.THashSet;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import org.jetbrains.annotations.TestOnly;
import org.jetbrains.uast.UClass;
import org.jetbrains.uast.UDeclaration;
import org.jetbrains.uast.UExpression;
import org.jetbrains.uast.UMethod;

import javax.swing.*;
import java.awt.*;
import java.awt.event.ActionListener;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.Set;

public class UnusedDeclarationInspection extends UnusedDeclarationInspectionBase {
  private final UnusedParametersInspection myUnusedParameters = new UnusedParametersInspection();

  public UnusedDeclarationInspection() { }

  @TestOnly
  public UnusedDeclarationInspection(boolean enabledInEditor) {
    super(enabledInEditor);
  }

  @Nullable
  @Override
  public String getAlternativeID() {
    return UnusedSymbolLocalInspectionBase.UNUSED_PARAMETERS_SHORT_NAME;
  }

  @Override
  public void runInspection(@NotNull AnalysisScope scope,
                            @NotNull InspectionManager manager,
                            @NotNull GlobalInspectionContext globalContext,
                            @NotNull ProblemDescriptionsProcessor problemDescriptionsProcessor) {
    if (myLocalInspectionBase.PARAMETER) {
      globalContext.getRefManager().iterate(new RefVisitor() {
        @Override public void visitElement(@NotNull RefEntity refEntity) {
          if (!(refEntity instanceof RefMethod) ||
              !globalContext.shouldCheck(refEntity, UnusedDeclarationInspection.this) ||
              !UnusedDeclarationPresentation.compareVisibilities((RefMethod)refEntity, myLocalInspectionBase.getParameterVisibility())) {
            return;
          }
          CommonProblemDescriptor[] descriptors = myUnusedParameters.checkElement(refEntity, scope, manager, globalContext, problemDescriptionsProcessor);
          if (descriptors != null) {
            problemDescriptionsProcessor.addProblemElement(refEntity, descriptors);
          }
        }
      });
    }
    super.runInspection(scope, manager, globalContext, problemDescriptionsProcessor);
  }

  @Nullable
  @Override
  public RefGraphAnnotator getAnnotator(@NotNull RefManager refManager) {
    return new UnusedVariablesGraphAnnotator(InspectionManager.getInstance(refManager.getProject()), refManager);
  }

  @Override
  public boolean queryExternalUsagesRequests(@NotNull InspectionManager manager,
                                             @NotNull GlobalInspectionContext globalContext,
                                             @NotNull ProblemDescriptionsProcessor problemDescriptionsProcessor) {
    final boolean requests = super.queryExternalUsagesRequests(manager, globalContext, problemDescriptionsProcessor);
    if (!requests) {
      myUnusedParameters.queryExternalUsagesRequests(manager, globalContext, problemDescriptionsProcessor);
    }
    return requests;
  }

  @Nullable
  @Override
  public String getHint(@NotNull QuickFix fix) {
    return myUnusedParameters.getHint(fix);
  }

  @Nullable
  @Override
  public QuickFix getQuickFix(String hint) {
    return myUnusedParameters.getQuickFix(hint);
  }

  @Override
  protected UnusedSymbolLocalInspectionBase createUnusedSymbolLocalInspection() {
    //noinspection deprecation
    return new UnusedSymbolLocalInspection();
  }

  @Override
  public JComponent createOptionsPanel() {
    JTabbedPane tabs = new JBTabbedPane(SwingConstants.TOP);
    tabs.add("Members to report", ScrollPaneFactory.createScrollPane(myLocalInspectionBase.createOptionsPanel(), true));
    tabs.add("Entry points", ScrollPaneFactory.createScrollPane(new OptionsPanel(), true));
    return tabs;
  }

  private class OptionsPanel extends JPanel {
    private final JCheckBox myMainsCheckbox;
    private final JCheckBox myAppletToEntries;
    private final JCheckBox myServletToEntries;
    private final JCheckBox myNonJavaCheckbox;

    private OptionsPanel() {
      super(new GridBagLayout());
      GridBagConstraints gc = new GridBagConstraints();
      gc.weightx = 1;
      gc.weighty = 0;

      gc.fill = GridBagConstraints.HORIZONTAL;
      gc.anchor = GridBagConstraints.NORTHWEST;
      gc.gridx = 0;
      gc.gridy = 0;
      gc.gridwidth = 2;
      add(new JBLabel("When entry points are in test sources, mark callees as:"), gc);
      gc.gridy++;

      final JBRadioButton asEntryPoint = new JBRadioButton("used", isTestEntryPoints());
      final JBRadioButton asUnused = new JBRadioButton("unused", !isTestEntryPoints());
      final ButtonGroup group = new ButtonGroup();
      group.add(asEntryPoint);
      group.add(asUnused);
      final ActionListener listener = e -> setTestEntryPoints(asEntryPoint.isSelected());
      asEntryPoint.addActionListener(listener);
      asUnused.addActionListener(listener);

      gc.gridwidth = 1;
      gc.weightx = 0;
      add(asEntryPoint, gc);
      gc.gridx = 1;
      gc.weightx = 1;
      add(asUnused, gc);

      gc.gridx = 0;
      gc.gridy++;

      gc.gridwidth = 2;
      add(new TitledSeparator(), gc);
      gc.gridy++;
      add(new JBLabel("Entry points:"), gc);
      gc.insets = JBUI.insets(5, 0, 0, 0);
      gc.gridy++;

      add(createBtnPanel(), gc);
      gc.gridy++;
      gc.insets = JBUI.insets(0, 5, 2, 0);

      myMainsCheckbox = new JCheckBox(InspectionsBundle.message("inspection.dead.code.option.main"));
      myMainsCheckbox.setSelected(ADD_MAINS_TO_ENTRIES);
      myMainsCheckbox.addActionListener(e -> ADD_MAINS_TO_ENTRIES = myMainsCheckbox.isSelected());


      add(myMainsCheckbox, gc);
      gc.gridy++;

      myAppletToEntries = new JCheckBox(InspectionsBundle.message("inspection.dead.code.option.applet"));
      myAppletToEntries.setSelected(ADD_APPLET_TO_ENTRIES);
      myAppletToEntries.addActionListener(e -> ADD_APPLET_TO_ENTRIES = myAppletToEntries.isSelected());
      add(myAppletToEntries, gc);
      gc.gridy++;

      myServletToEntries = new JCheckBox(InspectionsBundle.message("inspection.dead.code.option.servlet"));
      myServletToEntries.setSelected(ADD_SERVLET_TO_ENTRIES);
      myServletToEntries.addActionListener(e -> ADD_SERVLET_TO_ENTRIES = myServletToEntries.isSelected());
      add(myServletToEntries, gc);
      gc.gridy++;

      for (final EntryPoint extension : myExtensions) {
        if (extension.showUI()) {
          final JCheckBox extCheckbox = new JCheckBox(extension.getDisplayName());
          extCheckbox.setSelected(extension.isSelected());
          extCheckbox.addActionListener(e -> extension.setSelected(extCheckbox.isSelected()));
          add(extCheckbox, gc);
          gc.gridy++;
        }
      }

      myNonJavaCheckbox =
      new JCheckBox(InspectionsBundle.message("inspection.dead.code.option.external"));
      myNonJavaCheckbox.setSelected(ADD_NONJAVA_TO_ENTRIES);
      myNonJavaCheckbox.addActionListener(e -> ADD_NONJAVA_TO_ENTRIES = myNonJavaCheckbox.isSelected());

      gc.weighty = 1;
      add(myNonJavaCheckbox, gc);
    }

    private JPanel createBtnPanel() {
      final JPanel btnPanel = new JPanel(new GridBagLayout());
      GridBagConstraints constraints = new GridBagConstraints();
      constraints.anchor = GridBagConstraints.NORTHWEST;
      constraints.fill = GridBagConstraints.NONE;
      constraints.weightx = 0;
      btnPanel.add(EntryPointsManagerImpl.createConfigureClassPatternsButton(), constraints);
      constraints.gridx = 1;
      btnPanel.add(EntryPointsManagerImpl.createConfigureAnnotationsButton(), constraints);
      constraints.fill = GridBagConstraints.HORIZONTAL;
      constraints.weightx = 1;
      btnPanel.add(Box.createHorizontalBox(), constraints);
      return btnPanel;
    }
  }

  private class UnusedVariablesGraphAnnotator extends RefGraphAnnotator {
    private final InspectionManager myInspectionManager;
    private final GlobalInspectionContextImpl myContext;
    private final Map<String, Tools> myTools;

    UnusedVariablesGraphAnnotator(InspectionManager inspectionManager, RefManager refManager) {
      myInspectionManager = inspectionManager;
      myContext = (GlobalInspectionContextImpl)((RefManagerImpl)refManager).getContext();
      myTools = myContext.getTools();
    }

    @Override
    public void onReferencesBuild(RefElement refElement) {
      if (refElement instanceof RefClass) {
        UClass uClass = ((RefClass)refElement).getUastElement();
        if (uClass != null) {
          for (PsiClassInitializer initializer : uClass.getInitializers()) {
            findUnusedVariables(initializer.getBody(), refElement, uClass);
          }
        }
      }
      else if (refElement instanceof RefMethod) {
        UDeclaration element = ((RefMethod)refElement).getUastElement();
        if (element instanceof UMethod) {
          UExpression body = ((UMethod)element).getUastBody();
          if (body != null) {
            //TODO resolve
            findUnusedVariables((PsiCodeBlock)body.getJavaPsi(), refElement, element);
          }
        }
      }
    }

    private void findUnusedVariables(PsiCodeBlock body, RefElement refElement, PsiElement element) {
      if (body == null) return;
      Tools tools = myTools.get(getShortName());
      if (tools.isEnabled(element)) {
        InspectionToolWrapper toolWrapper = tools.getInspectionTool(element);
        InspectionToolPresentation presentation = myContext.getPresentation(toolWrapper);
        if (((UnusedDeclarationInspection)toolWrapper.getTool()).getSharedLocalInspectionTool().LOCAL_VARIABLE) {
          List<CommonProblemDescriptor> descriptors = new ArrayList<>();

          final Set<PsiVariable> usedVariables = new THashSet<>();
          List<DefUseUtil.Info> unusedDefs = DefUseUtil.getUnusedDefs(body, usedVariables);

          if (unusedDefs != null && !unusedDefs.isEmpty()) {

            for (DefUseUtil.Info info : unusedDefs) {
              PsiElement parent = info.getContext();
              PsiVariable psiVariable = info.getVariable();

              if (parent instanceof PsiDeclarationStatement || parent instanceof PsiResourceVariable) {
                if (!info.isRead() && !SuppressionUtil.inspectionResultSuppressed(psiVariable, UnusedDeclarationInspection.this)) {
                  descriptors.add(createProblemDescriptor(psiVariable));
                }
              }
            }

          }
          body.accept(new JavaRecursiveElementWalkingVisitor() {
            @Override
            public void visitClass(PsiClass aClass) { }

            @Override
            public void visitLambdaExpression(PsiLambdaExpression expression) {} //todo

            @Override
            public void visitLocalVariable(PsiLocalVariable variable) {
              if (!usedVariables.contains(variable) && variable.getInitializer() == null &&
                  !SuppressionUtil.inspectionResultSuppressed(variable, UnusedDeclarationInspection.this)) {
                descriptors.add(createProblemDescriptor(variable));
              }
            }
          });
          if (!descriptors.isEmpty()) {
            presentation.addProblemElement(refElement, descriptors.toArray(CommonProblemDescriptor.EMPTY_ARRAY));
          }
        }
      }
    }

    private ProblemDescriptor createProblemDescriptor(PsiVariable psiVariable) {
      PsiElement toHighlight = ObjectUtils.notNull(psiVariable.getNameIdentifier(), psiVariable);
      return myInspectionManager.createProblemDescriptor(
        toHighlight,
        InspectionsBundle.message("inspection.unused.assignment.problem.descriptor1", "<code>#ref</code> #loc"), (LocalQuickFix)null,
        ProblemHighlightType.LIKE_UNUSED_SYMBOL, false);
    }
  }
}
