/*
 * Copyright 2000-2014 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.psi.impl;

import com.google.common.collect.Collections2;
import com.google.common.collect.Queues;
import com.intellij.lang.ASTFactory;
import com.intellij.lang.ASTNode;
import com.intellij.psi.tree.IElementType;
import com.intellij.psi.tree.TokenSet;
import com.intellij.psi.util.PsiTreeUtil;
import com.intellij.util.ArrayUtil;
import com.intellij.util.IncorrectOperationException;
import com.jetbrains.NotNullPredicate;
import com.jetbrains.python.FunctionParameter;
import com.jetbrains.python.PyElementTypes;
import com.jetbrains.python.PyTokenTypes;
import com.jetbrains.python.PythonDialectsTokenSetProvider;
import com.jetbrains.python.psi.*;
import com.jetbrains.python.psi.resolve.PyResolveContext;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;

import java.util.*;

public class PyArgumentListImpl extends PyElementImpl implements PyArgumentList {

  // Filters all expressions but keyword arguments
  private static final NoKeyArguments NO_KEY_ARGUMENTS = new NoKeyArguments();

  public PyArgumentListImpl(ASTNode astNode) {
    super(astNode);
  }

  @Override
  protected void acceptPyVisitor(PyElementVisitor pyVisitor) {
    pyVisitor.visitPyArgumentList(this);
  }

  @Override
  @NotNull
  public Collection<PyExpression> getArgumentExpressions() {
    final PyExpression[] arguments = getArguments();
    final Collection<PyExpression> result = new ArrayList<PyExpression>(arguments.length);
    for (final PyExpression expression : arguments) {
      if (expression instanceof PyKeywordArgument) {
        final PyExpression valueExpression = ((PyKeywordArgument)expression).getValueExpression();
        result.add(valueExpression);
      }
      if (expression instanceof PyReferenceExpression) {
        result.add(expression);
      }
    }
    return result;
  }

  @NotNull
  public PyExpression[] getArguments() {
    return childrenToPsi(PythonDialectsTokenSetProvider.INSTANCE.getExpressionTokens(), PyExpression.EMPTY_ARRAY);
  }

  @Nullable
  public PyKeywordArgument getKeywordArgument(String name) {
    ASTNode node = getNode().getFirstChildNode();
    while (node != null) {
      if (node.getElementType() == PyElementTypes.KEYWORD_ARGUMENT_EXPRESSION) {
        PyKeywordArgument arg = (PyKeywordArgument)node.getPsi();
        String keyword = arg.getKeyword();
        if (keyword != null && keyword.equals(name)) return arg;
      }
      node = node.getTreeNext();
    }
    return null;
  }

  @Override
  public void addArgument(@NotNull final PyExpression arg) {
    final PyElementGenerator generator = new PyElementGeneratorImpl(getProject());

    // Adds param to appropriate place
    final Deque<PyKeywordArgument> keywordArguments = getKeyWordArguments();
    final Deque<PyExpression> parameters = getParameters();

    if (keywordArguments.isEmpty() && parameters.isEmpty()) {
      generator.insertItemIntoListRemoveRedundantCommas(this, null, arg);
      return;
    }


    if (arg instanceof PyKeywordArgument) {
      if (parameters.isEmpty()) {
        generator.insertItemIntoListRemoveRedundantCommas(this, keywordArguments.getLast(), arg);
      }
      else {
        if (keywordArguments.isEmpty()) {
          generator.insertItemIntoListRemoveRedundantCommas(this, parameters.getLast(), arg);
        }
        else {
          generator.insertItemIntoListRemoveRedundantCommas(this, keywordArguments.getLast(), arg);
        }
      }
    }
    else {
      if (parameters.isEmpty()) {
        generator.insertItemIntoListRemoveRedundantCommas(this, null, arg);
      }
      else {
        generator.insertItemIntoListRemoveRedundantCommas(this, parameters.getLast(), arg);
      }
    }
  }


  /**
   * @return parameters (as opposite to keyword arguments)
   */
  @NotNull
  private Deque<PyExpression> getParameters() {
    final PyExpression[] childrenOfType = PsiTreeUtil.getChildrenOfType(this, PyExpression.class);
    if (childrenOfType == null) {
      return new ArrayDeque<PyExpression>(0);
    }
    return Queues.newArrayDeque(Collections2.filter(Arrays.asList(childrenOfType), NO_KEY_ARGUMENTS));
  }

  /**
   * @return keyword arguments (as opposite to parameters)
   */
  @NotNull
  private Deque<PyKeywordArgument> getKeyWordArguments() {
    return Queues.newArrayDeque(PsiTreeUtil.findChildrenOfType(this, PyKeywordArgument.class));
  }

  public void addArgumentFirst(PyExpression arg) {
    ASTNode node = getNode();
    ASTNode[] pars = node.getChildren(TokenSet.create(PyTokenTypes.LPAR));
    if (pars.length == 0) {
      // there's no starting paren
      try {
        add(arg);
      }
      catch (IncorrectOperationException e1) {
        throw new IllegalStateException(e1);
      }
    }
    else {
      ASTNode before = PyPsiUtils.getNextNonWhitespaceSibling(pars[0]);
      ASTNode anchorBefore;
      if (before != null && elementPrecedesElementsOfType(before, PythonDialectsTokenSetProvider.INSTANCE.getExpressionTokens())) {
        ASTNode comma = createComma();
        node.addChild(comma, before);
        node.addChild(ASTFactory.whitespace(" "), before);
        anchorBefore = comma;
      }
      else {
        anchorBefore = before;
      }
      ASTNode argNode = arg.getNode();
      if (anchorBefore == null) {
        node.addChild(argNode);
      }
      else {
        node.addChild(argNode, anchorBefore);
      }
    }
  }

  /**
   * @return newly created comma
   */
  @NotNull
  private ASTNode createComma() {
    return PyElementGenerator.getInstance(getProject()).createComma();
  }

  private static boolean elementPrecedesElementsOfType(ASTNode before, TokenSet expressions) {
    ASTNode node = before;
    while (node != null) {
      if (expressions.contains(node.getElementType())) return true;
      node = node.getTreeNext();
    }
    return false;
  }

  private void addArgumentLastWithoutComma(PyExpression arg) {
    ASTNode par = getClosingParen();
    if (par == null) {
      // there's no ending paren
      try {
        add(arg);
      }
      catch (IncorrectOperationException e1) {
        throw new IllegalStateException(e1);
      }
    }
    else {
      getNode().addChild(arg.getNode(), par);
    }
  }

  @Nullable
  public ASTNode getClosingParen() {
    ASTNode node = getNode();
    final ASTNode[] children = node.getChildren(TokenSet.create(PyTokenTypes.RPAR));
    return children.length == 0 ? null : children[children.length - 1];
  }

  private void addArgumentNode(PyExpression arg, ASTNode beforeThis, boolean commaFirst) {
    ASTNode comma = PyElementGenerator.getInstance(getProject()).createComma();
    ASTNode node = getNode();
    ASTNode argNode = arg.getNode();
    if (commaFirst) {
      node.addChild(comma, beforeThis);
      node.addChild(ASTFactory.whitespace(" "), beforeThis);
      node.addChild(argNode, beforeThis);
    }
    else {
      node.addChild(argNode, beforeThis);
      node.addChild(comma, beforeThis);
      node.addChild(ASTFactory.whitespace(" "), beforeThis);
    }
  }

  public void addArgumentAfter(PyExpression argument, @Nullable PyExpression afterThis) {
    if (afterThis == null) {
      addArgumentFirst(argument);
      return;
    }
    boolean good = false;
    for (PyExpression expression : getArguments()) {
      if (expression == afterThis) {
        good = true;
        break;
      }
    }
    if (!good) {
      throw new IllegalArgumentException("Expression " + afterThis + " is not an argument (" + Arrays.toString(getArguments()) + ")");
    }
    // CASES:
    ASTNode node = afterThis.getNode().getTreeNext();
    while (node != null) {
      IElementType type = node.getElementType();
      if (type == PyTokenTypes.RPAR) {
        // 1: Nothing, just add
        addArgumentNode(argument, node, true);
        break;
      }
      else if (PythonDialectsTokenSetProvider.INSTANCE.getExpressionTokens().contains(type)) {
        // 2: After some argument followed by comma: after comma, add element, add comma
        // 3: After some argument not followed by comma: add comma, add element
        addArgumentNode(argument, node, true);
        break;
      }
      else if (type == PyTokenTypes.COMMA) {
        ASTNode next = PyPsiUtils.getNextNonWhitespaceSibling(node);
        if (next == null) {
          addArgumentLastWithoutComma(argument);
        }
        else if (next.getElementType() == PyTokenTypes.RPAR) {
          addArgumentNode(argument, next, false);
        }
        else {
          addArgumentNode(argument, next, false);
        }
        break;
      }
      node = node.getTreeNext();
    }
  }

  @Nullable
  public PyCallExpression getCallExpression() {
    return PsiTreeUtil.getParentOfType(this, PyCallExpression.class);
  }

  @Override
  public void deleteChildInternal(@NotNull ASTNode node) {
    if (ArrayUtil.contains(node.getPsi(), getArguments())) {
      PyPsiUtils.deleteAdjacentCommaWithWhitespaces(this, node.getPsi());
    }
    super.deleteChildInternal(node);
  }

  @NotNull
  public CallArgumentsMapping analyzeCall(PyResolveContext resolveContext) {
    return analyzeCall(resolveContext, 0);
  }

  @NotNull
  public CallArgumentsMapping analyzeCall(PyResolveContext resolveContext, int offset) {
    final CallArgumentsMappingImpl ret = new CallArgumentsMappingImpl(this);
    // declaration-based checks
    // proper arglist is: [positional,...][name=value,...][*tuple,][**dict]
    // where "positional" may be a tuple of nested "positional" parameters, too.
    // following the spec: http://docs.python.org/ref/calls.html
    PyCallExpression call = getCallExpression();
    if (call != null) {
      PyCallExpression.PyMarkedCallee resolvedCallee = call.resolveCallee(resolveContext, offset);
      if (resolvedCallee != null) {
        ret.mapArguments(resolvedCallee, resolveContext.getTypeEvalContext());
      }
      else {
        ret.verifyArguments();
      }
    }
    return ret;
  }

  private static class NoKeyArguments extends NotNullPredicate<PyExpression> {
    @Override
    protected boolean applyNotNull(@NotNull final PyExpression input) {
      return (PsiTreeUtil.getParentOfType(input, PyKeywordArgument.class) == null) && !(input instanceof PyKeywordArgument);
    }
  }

  @Nullable
  @Override
  public PyExpression getValueExpressionForParam(@NotNull final FunctionParameter parameter) {
    final String parameterName = parameter.getName();
    if (parameterName != null) {
      final PyKeywordArgument kwarg = getKeywordArgument(parameterName);
      if (kwarg != null) {
        return kwarg.getValueExpression();
      }
    }

    final PyExpression[] arguments = getArguments();
    final int position = parameter.getPosition();
    if ((position != FunctionParameter.POSITION_NOT_SUPPORTED) && (arguments.length > position)) {
      final PyExpression result = arguments[position];
      if (result instanceof PyKeywordArgument) {
        ((PyKeywordArgument)result).getValueExpression();
      }
      else {
        return result;
      }
    }

    return null;
  }
}
