/**
   * Get array string values mapped with their PsiElements
   *
   * <p>["value", "value2"]
   */
  @NotNull
  public static Map<String, PsiElement> getArrayValuesAsMap(
      @NotNull ArrayCreationExpression arrayCreationExpression) {

    List<PsiElement> arrayValues =
        PhpPsiUtil.getChildren(
            arrayCreationExpression,
            new Condition<PsiElement>() {
              @Override
              public boolean value(PsiElement psiElement) {
                return psiElement.getNode().getElementType() == PhpElementTypes.ARRAY_VALUE;
              }
            });

    if (arrayValues == null) {
      return Collections.emptyMap();
    }

    Map<String, PsiElement> keys = new HashMap<String, PsiElement>();
    for (PsiElement child : arrayValues) {
      String stringValue = PhpElementsUtil.getStringValue(child.getFirstChild());
      if (stringValue != null && StringUtils.isNotBlank(stringValue)) {
        keys.put(stringValue, child);
      }
    }

    return keys;
  }
  @Nullable
  public static ArrayCreationExpression getCompletableArrayCreationElement(PsiElement psiElement) {

    // array('<test>' => '')
    if (PhpPatterns.psiElement(PhpElementTypes.ARRAY_KEY).accepts(psiElement.getContext())) {
      PsiElement arrayKey = psiElement.getContext();
      if (arrayKey != null) {
        PsiElement arrayHashElement = arrayKey.getContext();
        if (arrayHashElement instanceof ArrayHashElement) {
          PsiElement arrayCreationExpression = arrayHashElement.getContext();
          if (arrayCreationExpression instanceof ArrayCreationExpression) {
            return (ArrayCreationExpression) arrayCreationExpression;
          }
        }
      }
    }

    // on array creation key dont have value, so provide completion here also
    // array('foo' => 'bar', '<test>')
    if (PhpPatterns.psiElement(PhpElementTypes.ARRAY_VALUE).accepts(psiElement.getContext())) {
      PsiElement arrayKey = psiElement.getContext();
      if (arrayKey != null) {
        PsiElement arrayCreationExpression = arrayKey.getContext();
        if (arrayCreationExpression instanceof ArrayCreationExpression) {
          return (ArrayCreationExpression) arrayCreationExpression;
        }
      }
    }

    return null;
  }
  private void parseTypes2(
      Map<String, Collection<String>> map, Iterable<ArrayHashElement> elements, String parent) {
    Collection<String> types = map.get(parent);
    if (types == null) {
      types = new ArrayList<String>();
      map.put(parent, types);
    }

    for (ArrayHashElement element : elements) {
      PhpPsiElement key = element.getKey();
      if (key instanceof StringLiteralExpression) {
        // key
        String keyName = ((StringLiteralExpression) key).getContents();
        types.add(keyName);

        String fullKeyName = parent.length() > 0 ? (parent + "." + keyName) : keyName;

        // value
        PhpPsiElement val = element.getValue();
        if (val instanceof ArrayCreationExpression) { // recursive
          Iterable<ArrayHashElement> subElements =
              ((ArrayCreationExpression) val).getHashElements();
          parseTypes2(map, subElements, fullKeyName);

        } else if (val instanceof FieldReference) { // reference to a field, where it's defined
          String classFqn = ((ClassReference) ((FieldReference) val).getClassReference()).getFQN();
          for (PhpClass phpClass :
              PhpIndex.getInstance(element.getProject()).getClassesByFQN(classFqn)) {
            Field field = phpClass.findFieldByName(((FieldReference) val).getNameCS(), false);
            if (field.getDefaultValue() instanceof ArrayCreationExpression) {
              Iterable<ArrayHashElement> subElements =
                  ((ArrayCreationExpression) field.getDefaultValue()).getHashElements();
              parseTypes2(map, subElements, fullKeyName);
            }
          }

        } else { // get value type
          parseValueType(val);

          // try annotation
          PsiElement el2 = element;
          while (el2 != null
              && (el2 instanceof LeafPsiElement
                      && ((LeafPsiElement) el2).getElementType() == PhpTokenTypes.opCOMMA
                  || el2 instanceof PsiWhiteSpace)) {
            el2 = el2.getNextSibling();
          }
          if (el2 instanceof PsiComment) {
            System.out.println("Comment for " + fullKeyName + ": " + el2.getText());
          }
        }
      }
    }
  }
  /**
   * Try to visit possible class name for PsiElements with text like "Foo\|Bar", "Foo|\Bar",
   * "\Foo|\Bar" Cursor must have position in PsiElement
   *
   * @param psiElement the element context, cursor should be in it
   * @param cursorOffset current cursor editor eg from completion context
   * @param visitor callback on matching class
   */
  public static void visitNamespaceClassForCompletion(
      PsiElement psiElement, int cursorOffset, ClassForCompletionVisitor visitor) {

    int cursorOffsetClean = cursorOffset - psiElement.getTextOffset();
    if (cursorOffsetClean < 1) {
      return;
    }

    String content = psiElement.getText();
    int length = content.length();
    if (!(length >= cursorOffsetClean)) {
      return;
    }

    String beforeCursor = content.substring(0, cursorOffsetClean);
    boolean isValid;

    // espend\|Container, espend\Cont|ainer <- fallback to last full namespace
    // espend|\Container <- only on known namespace "espend"
    String namespace = beforeCursor;

    // if no backslash or its equal in first position, fallback on namespace completion
    int lastSlash = beforeCursor.lastIndexOf("\\");
    if (lastSlash <= 0) {
      isValid = PhpIndexUtil.hasNamespace(psiElement.getProject(), beforeCursor);
    } else {
      isValid = true;
      namespace = beforeCursor.substring(0, lastSlash);
    }

    if (!isValid) {
      return;
    }

    // format namespaces and add prefix for fluent completion
    String prefix = "";
    if (namespace.startsWith("\\")) {
      prefix = "\\";
    } else {
      namespace = "\\" + namespace;
    }

    // search classes in current namespace and child namespaces
    for (PhpClass phpClass :
        PhpIndexUtil.getPhpClassInsideNamespace(psiElement.getProject(), namespace)) {
      String presentableFQN = phpClass.getPresentableFQN();
      if (presentableFQN != null
          && fr.adrienbrault.idea.symfony2plugin.util.StringUtils.startWithEqualClassname(
              presentableFQN, beforeCursor)) {
        visitor.visit(phpClass, presentableFQN, prefix);
      }
    }
  }
  @Nullable
  public static MethodReferenceBag getMethodParameterReferenceBag(
      PsiElement psiElement, int wantIndex) {

    PsiElement variableContext = psiElement.getContext();
    if (!(variableContext instanceof ParameterList)) {
      return null;
    }

    ParameterList parameterList = (ParameterList) variableContext;
    if (!(parameterList.getContext() instanceof MethodReference)) {
      return null;
    }

    ParameterBag currentIndex = PsiElementUtils.getCurrentParameterIndex(psiElement);
    if (currentIndex == null) {
      return null;
    }

    if (wantIndex >= 0 && currentIndex.getIndex() != wantIndex) {
      return null;
    }

    return new MethodReferenceBag(
        parameterList, (MethodReference) parameterList.getContext(), currentIndex);
  }
  @Nullable
  public static PsiElement getArrayKeyValueInsideSignaturePsi(
      PsiElement psiElementInsideClass, String callTo[], String methodName, String keyName) {
    PhpClass phpClass = PsiTreeUtil.getParentOfType(psiElementInsideClass, PhpClass.class);
    if (phpClass == null) {
      return null;
    }

    String className = phpClass.getPresentableFQN();
    if (className == null) {
      return null;
    }

    for (String s : callTo) {
      // @TODO: replace signature
      PsiElement arrayKeyValueInsideSignature =
          PhpElementsUtil.getArrayKeyValueInsideSignaturePsi(
              psiElementInsideClass.getProject(),
              "#M#C\\" + className + "." + s,
              methodName,
              keyName);
      if (arrayKeyValueInsideSignature != null) {
        return arrayKeyValueInsideSignature;
      }
    }

    return null;
  }
  /**
   * Find first variable declaration in parent scope of a given variable:
   *
   * <p>function() { $event = new FooEvent(); dispatch('foo', $event); }
   */
  @Nullable
  public static String getFirstVariableTypeInScope(@NotNull Variable variable) {

    // parent search scope, eg Method else fallback to a grouped statement
    PsiElement searchScope = PsiTreeUtil.getParentOfType(variable, Function.class);
    if (searchScope == null) {
      searchScope = PsiTreeUtil.getParentOfType(variable, GroupStatement.class);
    }

    if (searchScope == null) {
      return null;
    }

    final String name = variable.getName();
    if (name == null) {
      return null;
    }

    final String[] result = {null};
    searchScope.acceptChildren(
        new PsiRecursiveElementVisitor() {
          @Override
          public void visitElement(PsiElement element) {
            if (element instanceof Variable && name.equals(((Variable) element).getName())) {
              PsiElement assignmentExpression = element.getParent();
              if (assignmentExpression instanceof AssignmentExpression) {
                PhpPsiElement value = ((AssignmentExpression) assignmentExpression).getValue();
                if (value instanceof NewExpression) {
                  ClassReference classReference = ((NewExpression) value).getClassReference();
                  if (classReference != null) {
                    String classSignature = classReference.getFQN();
                    if (StringUtils.isNotBlank(classSignature)) {
                      result[0] = classSignature;
                    }
                  }
                }
              }
            }

            super.visitElement(element);
          }
        });

    return result[0];
  }
  public static String getPrevSiblingAsTextUntil(
      PsiElement psiElement, ElementPattern pattern, boolean includeMatching) {
    String prevText = "";

    for (PsiElement child = psiElement.getPrevSibling();
        child != null;
        child = child.getPrevSibling()) {
      if (pattern.accepts(child)) {
        if (includeMatching) {
          return child.getText() + prevText;
        }
        return prevText;
      } else {
        prevText = child.getText() + prevText;
      }
    }

    return prevText;
  }
  /** "DateTime", DateTime::class */
  @Nullable
  public static PhpClass resolvePhpClassOnPsiElement(@NotNull PsiElement psiElement) {

    String dataClass = null;
    if (psiElement instanceof ClassConstantReference) {
      PsiElement lastChild = psiElement.getLastChild();
      // @TODO: FOO::class find PhpElementTyp: toString provides "class"
      if ("class".equals(lastChild.getText())) {
        PhpExpression classReference = ((ClassConstantReference) psiElement).getClassReference();
        if (classReference instanceof PhpReference) {
          dataClass = ((PhpReference) classReference).getFQN();
        }
      }
    } else {
      dataClass = getStringValue(psiElement);
    }

    if (dataClass == null) {
      return null;
    }

    return getClassInterface(psiElement.getProject(), dataClass);
  }
  @Nullable
  private static String getStringValue(@Nullable PsiElement psiElement, int depth) {

    if (psiElement == null || ++depth > 5) {
      return null;
    }

    if (psiElement instanceof StringLiteralExpression) {
      String resolvedString = ((StringLiteralExpression) psiElement).getContents();
      if (StringUtils.isEmpty(resolvedString)) {
        return null;
      }

      return resolvedString;
    }

    if (psiElement instanceof Field) {
      return getStringValue(((Field) psiElement).getDefaultValue(), depth);
    }

    if (psiElement instanceof PhpReference) {

      PsiReference psiReference = psiElement.getReference();
      if (psiReference == null) {
        return null;
      }

      PsiElement ref = psiReference.resolve();
      if (ref instanceof PhpReference) {
        return getStringValue(psiElement, depth);
      }

      if (ref instanceof Field) {
        PsiElement resolved = ((Field) ref).getDefaultValue();

        if (resolved instanceof StringLiteralExpression) {
          return ((StringLiteralExpression) resolved).getContents();
        }
      }
    }

    return null;
  }