/*
 * Copyright 2000-2017 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.jetbrains.python.inspections;

import com.intellij.codeInspection.LocalInspectionToolSession;
import com.intellij.codeInspection.ProblemsHolder;
import com.intellij.psi.PsiElementVisitor;
import com.intellij.psi.tree.TokenSet;
import com.intellij.util.containers.ContainerUtil;
import com.jetbrains.python.PyNames;
import com.jetbrains.python.PyPsiBundle;
import com.jetbrains.python.PyTokenTypes;
import com.jetbrains.python.inspections.quickfix.AugmentedAssignmentQuickFix;
import com.jetbrains.python.psi.*;
import com.jetbrains.python.psi.impl.PyBuiltinCache;
import com.jetbrains.python.psi.impl.PyPsiUtils;
import com.jetbrains.python.psi.resolve.PyResolveContext;
import com.jetbrains.python.psi.types.PyStructuralType;
import com.jetbrains.python.psi.types.PyType;
import com.jetbrains.python.psi.types.PyTypeChecker;
import com.jetbrains.python.psi.types.TypeEvalContext;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;

import java.util.Arrays;
import java.util.List;
import java.util.Set;

/**
 * User: catherine
 * <p>
 * Inspection to detect assignments that can be replaced with augmented assignments.
 */
public final class PyAugmentAssignmentInspection extends PyInspection {

  private static final @NotNull TokenSet OPERATIONS = TokenSet.create(PyTokenTypes.PLUS, PyTokenTypes.MINUS, PyTokenTypes.MULT,
                                                                      PyTokenTypes.FLOORDIV, PyTokenTypes.DIV, PyTokenTypes.PERC,
                                                                      PyTokenTypes.AND,
                                                                      PyTokenTypes.OR, PyTokenTypes.XOR, PyTokenTypes.LTLT,
                                                                      PyTokenTypes.GTGT,
                                                                      PyTokenTypes.EXP);
  private static final @NotNull TokenSet COMMUTATIVE_OPERATIONS =
    TokenSet.create(PyTokenTypes.PLUS, PyTokenTypes.MULT, PyTokenTypes.OR, PyTokenTypes.AND);

  private static final @NotNull List<String> SEQUENCE_METHODS = Arrays.asList(PyNames.LEN, PyNames.ITER, PyNames.GETITEM, PyNames.CONTAINS);

  @Override
  public @NotNull PsiElementVisitor buildVisitor(@NotNull ProblemsHolder holder,
                                                 boolean isOnTheFly,
                                                 @NotNull LocalInspectionToolSession session) {
    return new Visitor(holder, PyInspectionVisitor.getContext(session));
  }

  private static class Visitor extends PyInspectionVisitor {

    Visitor(@Nullable ProblemsHolder holder,
            @NotNull TypeEvalContext context) {
      super(holder, context);
    }

    @Override
    public void visitPyAssignmentStatement(final @NotNull PyAssignmentStatement node) {
      final PyExpression target = node.getLeftHandSideExpression();
      final PyBinaryExpression value = PyUtil.as(node.getAssignedValue(), PyBinaryExpression.class);

      if (target != null && value != null && node.getTargets().length == 1) {
        final PyExpression leftExpression = value.getLeftExpression();
        final PyExpression rightExpression = PyPsiUtils.flattenParens(value.getRightExpression());

        if (leftExpression == null || rightExpression == null) {
          return;
        }

        final String targetText = target.getText();
        final PyExpression mainOperandExpression;
        final PyExpression otherOperandExpression;
        final boolean changedParts;

        if (targetText.equals(leftExpression.getText())) {
          mainOperandExpression = leftExpression;
          otherOperandExpression = rightExpression;
          changedParts = false;
        }
        else if (targetText.equals(rightExpression.getText())) {
          mainOperandExpression = rightExpression;
          otherOperandExpression = leftExpression;
          changedParts = true;
        }
        else {
          return;
        }

        final PyElementType operator = value.getOperator();
        if (operator != null && assignmentCanBeReplaced(mainOperandExpression, otherOperandExpression, operator, changedParts)) {
          registerProblem(node, PyPsiBundle.message("INSP.assignment.can.be.replaced.with.augmented.assignment"),
                          new AugmentedAssignmentQuickFix());
        }
      }
    }

    private boolean assignmentCanBeReplaced(@NotNull PyExpression mainOperandExpression,
                                            @NotNull PyExpression otherOperandExpression,
                                            @NotNull PyElementType operator,
                                            boolean changedParts) {
      if (!(mainOperandExpression instanceof PyReferenceExpression || mainOperandExpression instanceof PySubscriptionExpression)) {
        return false;
      }

      if (!changedParts && OPERATIONS.contains(operator) || changedParts && COMMUTATIVE_OPERATIONS.contains(operator)) {
        final PyType otherOperandType = myTypeEvalContext.getType(otherOperandExpression);

        if (!PyTypeChecker.isUnknown(otherOperandType, myTypeEvalContext)) {
          if (changedParts) {
            if (hasAnySequenceMethod(otherOperandType, otherOperandExpression)) {
              return false;
            }

            final PyType mainOperandType = myTypeEvalContext.getType(mainOperandExpression);
            if (mainOperandType != null && hasAnySequenceMethod(mainOperandType, mainOperandExpression)) {
              return false;
            }
          }

          return isNumeric(otherOperandType, PyBuiltinCache.getInstance(otherOperandExpression)) ||
                 hasAnySequenceMethod(otherOperandType, otherOperandExpression);
        }
      }

      return false;
    }

    private boolean hasAnySequenceMethod(@NotNull PyType type, @NotNull PyExpression location) {
      if (type instanceof PyStructuralType) {
        final Set<String> attributeNames = ((PyStructuralType)type).getAttributeNames();

        return SEQUENCE_METHODS.stream().anyMatch(attributeNames::contains);
      }

      final PyResolveContext resolveContext = PyResolveContext.defaultContext(myTypeEvalContext);

      return !SEQUENCE_METHODS
        .stream()
        .map(method -> type.resolveMember(method, location, AccessDirection.READ, resolveContext))
        .allMatch(ContainerUtil::isEmpty);
    }

    private boolean isNumeric(@NotNull PyType type, @NotNull PyBuiltinCache cache) {
      return PyTypeChecker.match(cache.getComplexType(), type, myTypeEvalContext);
    }
  }
}
