/*
 * 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.blockEvaluator;

import com.intellij.openapi.vfs.VirtualFile;
import com.intellij.psi.PsiElement;
import com.intellij.psi.util.QualifiedName;
import com.jetbrains.python.PyNames;
import com.jetbrains.python.psi.*;
import com.jetbrains.python.psi.impl.PyEvaluator;
import com.jetbrains.python.psi.impl.PyPathEvaluator;
import com.jetbrains.python.psi.impl.PyPsiUtils;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;

import java.util.*;


public class PyBlockEvaluator {
  private final @NotNull PyEvaluationResult myEvaluationResult = new PyEvaluationResult();
  private final @NotNull PyEvaluationContext myContext;
  private final Set<PyFile> myVisitedFiles;
  private final Set<String> myDeclarationsToTrack = new HashSet<>();
  private String myCurrentFilePath;
  private Object myReturnValue;
  private boolean myEvaluateCollectionItems = true;

  /**
   * @param evaluationContext context, obtained via {@link #getContext()}. Pass it here to enable cache. See {@link PyEvaluationContext}
   *                          for more info
   * @see PyEvaluationContext
   */
  public PyBlockEvaluator(final @NotNull PyEvaluationContext evaluationContext) {
    this(new HashSet<>(), evaluationContext);
  }

  /**
   * Create evaluator with out of cache context
   */
  public PyBlockEvaluator() {
    this(new PyEvaluationContext());
  }

  private PyBlockEvaluator(final @NotNull Set<PyFile> visitedFiles, final @NotNull PyEvaluationContext evaluationContext) {
    myVisitedFiles = visitedFiles;
    myContext = evaluationContext;
  }

  public void trackDeclarations(String attrName) {
    myDeclarationsToTrack.add(attrName);
  }

  public void evaluate(PyElement element) {
    VirtualFile vFile = element.getContainingFile().getVirtualFile();
    myCurrentFilePath = vFile != null ? vFile.getPath() : null;
    if (myVisitedFiles.contains(element)) {
      return;
    }
    myVisitedFiles.add((PyFile)element.getContainingFile());
    PyElement statementContainer = element instanceof PyFunction ? ((PyFunction)element).getStatementList() : element;
    statementContainer.acceptChildren(new MyPyElementVisitor());
  }

  private void processExtendCall(PyCallExpression node, String nameBeingExtended) {
    PyExpression arg = node.getArguments()[0];

    Object value = myEvaluationResult.myNamespace.get(nameBeingExtended);
    if (value instanceof List) {
      Object argValue = prepareEvaluator().evaluate(arg);
      if (argValue != null) {
        myEvaluationResult.myNamespace.put(nameBeingExtended, prepareEvaluator().applyPlus(value, argValue));
      }
    }

    if (myDeclarationsToTrack.contains(nameBeingExtended)) {
      List<PyExpression> declarations = myEvaluationResult.myDeclarations.get(nameBeingExtended);
      if (declarations != null) {
        PyPsiUtils.sequenceToList(declarations, arg);
      }
    }
  }

  private void processUpdateCall(PyCallExpression node, String name) {
    Object value = myEvaluationResult.myNamespace.get(name);
    if (value instanceof Map) {
      Object argValue = prepareEvaluator().evaluate(node.getArguments()[0]);
      if (argValue instanceof Map) {
        ((Map)value).putAll((Map)argValue);
      }
    }
  }

  private PyEvaluator prepareEvaluator() {
    PyEvaluator evaluator = createEvaluator();
    evaluator.setNamespace(myEvaluationResult.myNamespace);
    evaluator.setEvaluateCollectionItems(myEvaluateCollectionItems);
    return evaluator;
  }

  protected PyEvaluator createEvaluator() {
    return new PyPathEvaluator(myCurrentFilePath);
  }

  public Object getValue(String name) {
    return myEvaluationResult.myNamespace.get(name);
  }

  public @Nullable String getValueAsString(String name) {
    Object value = myEvaluationResult.myNamespace.get(name);
    return value instanceof String ? (String)value : null;
  }

  public @Nullable List getValueAsList(String name) {
    Object value = myEvaluationResult.myNamespace.get(name);
    return value instanceof List ? (List)value : null;
  }

  public @NotNull List<String> getValueAsStringList(String name) {
    Object value = myEvaluationResult.myNamespace.get(name);
    if (value instanceof List valueList) {
      for (Object o : valueList) {
        if (o != null && !(o instanceof String)) {
          return Collections.emptyList();
        }
      }
      return (List<String>)value;
    }
    if (value instanceof String) {
      return Collections.singletonList((String)value);
    }
    return Collections.emptyList();
  }

  public Set<PyFile> getVisitedFiles() {
    return myVisitedFiles;
  }


  public Object getReturnValue() {
    return myReturnValue;
  }

  public void setEvaluateCollectionItems(boolean evaluateCollectionItems) {
    myEvaluateCollectionItems = evaluateCollectionItems;
  }

  public @NotNull List<PyExpression> getDeclarations(final @NotNull String name) {
    return myEvaluationResult.getDeclarations(name);
  }

  /**
   * @return so-called context. You may pass it to any instance of {@link PyBlockEvaluator}
   * to make instances share their cache
   */
  public @NotNull PyEvaluationContext getContext() {
    return myContext;
  }

  private class MyPyElementVisitor extends PyElementVisitor {
    @Override
    public void visitPyAssignmentStatement(@NotNull PyAssignmentStatement node) {
      PyExpression expression = node.getLeftHandSideExpression();
      if (expression instanceof PyTargetExpression) {
        String name = expression.getName();
        PyExpression value = ((PyTargetExpression)expression).findAssignedValue();
        myEvaluationResult.myNamespace.put(name, prepareEvaluator().evaluate(value));
        if (myDeclarationsToTrack.contains(name)) {
          List<PyExpression> declarations = new ArrayList<>();
          PyPsiUtils.sequenceToList(declarations, value);
          myEvaluationResult.myDeclarations.put(name, declarations);
        }
      }
      else if (expression instanceof PySubscriptionExpression) {
        PyExpression operand = ((PySubscriptionExpression)expression).getOperand();
        PyExpression indexExpression = ((PySubscriptionExpression)expression).getIndexExpression();
        if (operand instanceof PyReferenceExpression && ((PyReferenceExpression)operand).getQualifier() == null) {
          Object currentValue = myEvaluationResult.myNamespace.get(((PyReferenceExpression)operand).getReferencedName());
          if (currentValue instanceof Map) {
            Object mapKey = prepareEvaluator().evaluate(indexExpression);
            if (mapKey != null) {
              Object value = myEvaluateCollectionItems ? prepareEvaluator().evaluate(node.getAssignedValue()) : node.getAssignedValue();
              ((Map)currentValue).put(mapKey, value);
            }
          }
        }
      }
    }

    @Override
    public void visitPyAugAssignmentStatement(@NotNull PyAugAssignmentStatement node) {
      PyExpression target = node.getTarget();
      String name = target.getName();
      if (target instanceof PyReferenceExpression && !((PyReferenceExpression)target).isQualified() && name != null) {
        Object currentValue = myEvaluationResult.myNamespace.get(name);
        if (currentValue != null) {
          Object rhs = prepareEvaluator().evaluate(node.getValue());
          myEvaluationResult.myNamespace.put(name, prepareEvaluator().applyPlus(currentValue, rhs));
        }
        if (myDeclarationsToTrack.contains(name)) {
          List<PyExpression> declarations = myEvaluationResult.myDeclarations.get(name);
          if (declarations != null) {
            PyPsiUtils.sequenceToList(declarations, node.getValue());
          }
        }
      }
    }

    @Override
    public void visitPyExpressionStatement(@NotNull PyExpressionStatement node) {
      node.getExpression().accept(this);
    }

    @Override
    public void visitPyCallExpression(@NotNull PyCallExpression node) {
      PyExpression callee = node.getCallee();
      if (callee instanceof PyReferenceExpression calleeRef) {
        PyExpression qualifier = calleeRef.getQualifier();
        if (qualifier instanceof PyReferenceExpression qualifierRef) {
          if (!qualifierRef.isQualified()) {
            if (PyNames.EXTEND.equals(calleeRef.getReferencedName()) && node.getArguments().length == 1) {
              processExtendCall(node, qualifierRef.getReferencedName());
            }
            else if (PyNames.UPDATE.equals(calleeRef.getReferencedName()) && node.getArguments().length == 1) {
              processUpdateCall(node, qualifierRef.getReferencedName());
            }
          }
        }
      }
    }

    @Override
    public void visitPyFromImportStatement(final @NotNull PyFromImportStatement node) {
      if (node.isFromFuture()) return;
      final PsiElement source = PyUtil.turnDirIntoInit(node.resolveImportSource());
      if (source instanceof PyFile pyFile) {
        PyEvaluationResult newlyEvaluatedResult = myContext.getCachedResult(pyFile);

        if (newlyEvaluatedResult == null) {
          final PyBlockEvaluator importEvaluator = new PyBlockEvaluator(myVisitedFiles, myContext);
          importEvaluator.myDeclarationsToTrack.addAll(myDeclarationsToTrack);
          importEvaluator.evaluate(pyFile);
          newlyEvaluatedResult = importEvaluator.myEvaluationResult;
          myContext.cache(pyFile, newlyEvaluatedResult);
        }

        if (node.isStarImport()) {
          // TODO honor __all__ here
          myEvaluationResult.myNamespace.putAll(newlyEvaluatedResult.myNamespace);
          myEvaluationResult.myDeclarations.putAll(newlyEvaluatedResult.myDeclarations);
        }
        else {
          for (final PyImportElement element : node.getImportElements()) {
            final String nameOfVarInOurModule = element.getVisibleName();
            final QualifiedName nameOfVarInExternalModule = element.getImportedQName();
            if ((nameOfVarInOurModule == null) || (nameOfVarInExternalModule == null)) {
              continue;
            }

            final Object value = newlyEvaluatedResult.myNamespace.get(nameOfVarInExternalModule.toString());
            myEvaluationResult.myNamespace.put(nameOfVarInOurModule, value);
            final List<PyExpression> declarations = newlyEvaluatedResult.getDeclarations(nameOfVarInOurModule);
            if (myEvaluationResult.myDeclarations.containsKey(nameOfVarInOurModule)) {
              myEvaluationResult.myDeclarations.get(nameOfVarInOurModule).addAll(declarations);
            }
            else {
              myEvaluationResult.myDeclarations.put(nameOfVarInOurModule, declarations);
            }
          }
        }
      }
    }

    @Override
    public void visitPyIfStatement(@NotNull PyIfStatement node) {
      PyStatementList list = node.getIfPart().getStatementList();
      list.acceptChildren(this);
    }

    @Override
    public void visitPyReturnStatement(@NotNull PyReturnStatement node) {
      myReturnValue = prepareEvaluator().evaluate(node.getExpression());
    }
  }
}
