/*
 * 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 org.intellij.plugins.intelliLang.inject.groovy;

import com.intellij.lang.Language;
import com.intellij.lang.injection.ConcatenationAwareInjector;
import com.intellij.lang.injection.MultiHostRegistrar;
import com.intellij.openapi.project.Project;
import com.intellij.openapi.util.Ref;
import com.intellij.openapi.util.TextRange;
import com.intellij.openapi.util.Trinity;
import com.intellij.psi.*;
import com.intellij.psi.impl.source.tree.injected.InjectedLanguageUtil;
import com.intellij.psi.search.LocalSearchScope;
import com.intellij.psi.search.searches.ReferencesSearch;
import com.intellij.psi.tree.IElementType;
import com.intellij.psi.util.PsiTreeUtil;
import com.intellij.util.containers.ContainerUtil;
import gnu.trove.THashSet;
import org.intellij.plugins.intelliLang.Configuration;
import org.intellij.plugins.intelliLang.inject.InjectedLanguage;
import org.intellij.plugins.intelliLang.inject.InjectorUtils;
import org.intellij.plugins.intelliLang.inject.LanguageInjectionSupport;
import org.intellij.plugins.intelliLang.inject.TemporaryPlacesRegistry;
import org.intellij.plugins.intelliLang.inject.config.BaseInjection;
import org.intellij.plugins.intelliLang.inject.java.InjectionCache;
import org.intellij.plugins.intelliLang.inject.java.JavaLanguageInjectionSupport;
import org.intellij.plugins.intelliLang.util.AnnotationUtilEx;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import org.jetbrains.plugins.groovy.codeInspection.utils.ControlFlowUtils;
import org.jetbrains.plugins.groovy.lang.lexer.GroovyTokenTypes;
import org.jetbrains.plugins.groovy.lang.psi.GroovyFileBase;
import org.jetbrains.plugins.groovy.lang.psi.api.GroovyResolveResult;
import org.jetbrains.plugins.groovy.lang.psi.api.auxiliary.modifiers.annotation.GrAnnotationNameValuePair;
import org.jetbrains.plugins.groovy.lang.psi.api.statements.arguments.GrArgumentList;
import org.jetbrains.plugins.groovy.lang.psi.api.statements.blocks.GrCodeBlock;
import org.jetbrains.plugins.groovy.lang.psi.api.statements.branch.GrReturnStatement;
import org.jetbrains.plugins.groovy.lang.psi.api.statements.expressions.*;
import org.jetbrains.plugins.groovy.lang.psi.api.statements.expressions.literals.GrStringInjection;
import org.jetbrains.plugins.groovy.lang.psi.api.types.GrCodeReferenceElement;

import java.util.Collection;
import java.util.LinkedList;
import java.util.List;

public class GrConcatenationAwareInjector implements ConcatenationAwareInjector {
  private final LanguageInjectionSupport mySupport;
  private final Configuration myConfiguration;
  private final Project myProject;

  @SuppressWarnings("UnusedParameters")
  public GrConcatenationAwareInjector(Configuration configuration, Project project, TemporaryPlacesRegistry temporaryPlacesRegistry) {
    myConfiguration = configuration;
    myProject = project;
    mySupport = InjectorUtils.findNotNullInjectionSupport(GroovyLanguageInjectionSupport.GROOVY_SUPPORT_ID);
  }

  @Override
  public void getLanguagesToInject(@NotNull final MultiHostRegistrar registrar, @NotNull final PsiElement... operands) {
    if (operands.length == 0) return;

    final PsiFile file = operands[0].getContainingFile();

    if (!(file instanceof GroovyFileBase)) return;

    new InjectionProcessor(myConfiguration, mySupport, operands) {
      @Override
      protected void processInjection(Language language,
                                      List<Trinity<PsiLanguageInjectionHost, InjectedLanguage, TextRange>> list,
                                      boolean settingsAvailable,
                                      boolean unparsable) {
        InjectorUtils.registerInjection(language, list, file, registrar);
        InjectorUtils.registerSupport(mySupport, settingsAvailable, registrar);
        InjectorUtils.putInjectedFileUserData(registrar, InjectedLanguageUtil.FRANKENSTEIN_INJECTION, unparsable);
      }

      @Override
      protected boolean areThereInjectionsWithName(String methodName, boolean annoOnly) {
        if (methodName == null) return false;
        if (getAnnotatedElementsValue().contains(methodName)) {
          return true;
        }
        if (!annoOnly && getXmlAnnotatedElementsValue().contains(methodName)) {
          return true;
        }
        return false;
      }

    }.processInjections();
  }

  @NotNull
  private static String getStringPresentation(@Nullable PsiElement operand) {
    if (operand instanceof GrStringInjection) {
      return operand.getText();
    }
    return "missingValue";
  }

  static class InjectionProcessor {
    private final Configuration myConfiguration;
    private final LanguageInjectionSupport mySupport;
    private final PsiElement[] myOperands;
    private boolean myShouldStop;
    private boolean myUnparsable;

    public InjectionProcessor(@NotNull Configuration configuration, @NotNull LanguageInjectionSupport support, @NotNull PsiElement... operands) {
      myConfiguration = configuration;
      mySupport = support;
      myOperands = operands;
    }

    void processInjections() {
      final PsiElement firstOperand = myOperands[0];
      final PsiElement topBlock = ControlFlowUtils.findControlFlowOwner(firstOperand);
      final LocalSearchScope searchScope = new LocalSearchScope(new PsiElement[]{topBlock instanceof GrCodeBlock
                                                                                 ? topBlock : firstOperand.getContainingFile()}, "", true);
      final THashSet<PsiModifierListOwner> visitedVars = new THashSet<>();
      final LinkedList<PsiElement> places = new LinkedList<>();
      places.add(firstOperand);
      final GrInjectionUtil.AnnotatedElementVisitor visitor = new GrInjectionUtil.AnnotatedElementVisitor() {
        @Override
        public boolean visitMethodParameter(GrExpression expression, GrCall methodCall) {
          final GrArgumentList list = methodCall.getArgumentList();
          assert list != null;

          final String methodName;
          if (methodCall instanceof GrMethodCall) {
            GrExpression invoked = ((GrMethodCall)methodCall).getInvokedExpression();
            final String referenceName = invoked instanceof GrReferenceExpression? ((GrReferenceExpression)invoked).getReferenceName() : null;
            if ("super".equals(referenceName) || "this".equals(referenceName)) { // constructor call
              final PsiClass psiClass = PsiTreeUtil.getParentOfType(methodCall, PsiClass.class, true);
              final PsiClass psiTargetClass = "super".equals(referenceName)? psiClass == null ? null : psiClass.getSuperClass() : psiClass;
              methodName = psiTargetClass == null? null : psiTargetClass.getName();
            }
            else {
              methodName = referenceName;
            }
          }
          else if (methodCall instanceof GrNewExpression) {
            final GrCodeReferenceElement classRef = ((GrNewExpression)methodCall).getReferenceElement();
            methodName = classRef == null ? null : classRef.getReferenceName();
          }
          else {
            methodName = null;
          }
          if (methodName != null && areThereInjectionsWithName(methodName, false)) {
            final GroovyResolveResult result = methodCall.advancedResolve();
            PsiElement element = result.getElement();
            if (element instanceof PsiMethod) {
              PsiMethod method = (PsiMethod)element;
              final PsiParameter[] parameters = method.getParameterList().getParameters();
              int index = GrInjectionUtil.findParameterIndex(expression, methodCall);
              if (index >= 0) {
                process(parameters[index], method, index);
              }
            }
          }
          return false;
        }

        @Override
        public boolean visitMethodReturnStatement(GrReturnStatement parent, PsiMethod method) {
          if (areThereInjectionsWithName(method.getName(), false)) {
            process(method, method, -1);
          }
          return false;
        }

        @Override
        public boolean visitVariable(PsiVariable variable) {
          if (myConfiguration.getAdvancedConfiguration().getDfaOption() != Configuration.DfaOption.OFF && visitedVars.add(variable)) {
            ReferencesSearch.search(variable, searchScope).forEach(psiReference -> {
              final PsiElement element = psiReference.getElement();
              if (element instanceof GrExpression) {
                final GrExpression refExpression = (GrExpression)element;
                places.add(refExpression);
                if (!myUnparsable) {
                  myUnparsable = checkUnparsableReference(refExpression);
                }
              }
              return true;
            });
          }
          if (!processCommentInjections(variable)) {
            myShouldStop = true;
          }
          else {
            process(variable, null, -1);
          }
          return false;
        }

        @Override
        public boolean visitAnnotationParameter(GrAnnotationNameValuePair nameValuePair, PsiAnnotation psiAnnotation) {
          final String paramName = nameValuePair.getName();
          final String methodName = paramName != null ? paramName : PsiAnnotation.DEFAULT_REFERENCED_METHOD_NAME;
          if (areThereInjectionsWithName(methodName, false)) {
            final PsiReference reference = nameValuePair.getReference();
            final PsiElement element = reference == null ? null : reference.resolve();
            if (element instanceof PsiMethod) {
              process((PsiMethod)element, (PsiMethod)element, -1);
            }
          }
          return false;
        }

        @Override
        public boolean visitReference(GrReferenceExpression expression) {
          if (myConfiguration.getAdvancedConfiguration().getDfaOption() == Configuration.DfaOption.OFF) return true;
          final PsiElement e = expression.resolve();
          if (e instanceof PsiVariable) {
            if (e instanceof PsiParameter) {
              final PsiParameter p = (PsiParameter)e;
              final PsiElement declarationScope = p.getDeclarationScope();
              final PsiMethod method = declarationScope instanceof PsiMethod ? (PsiMethod)declarationScope : null;
              final PsiParameterList parameterList = method == null ? null : method.getParameterList();
              // don't check catchblock parameters & etc.
              if (!(parameterList == null || parameterList != e.getParent()) &&
                  areThereInjectionsWithName(method.getName(), false)) {
                final int parameterIndex = parameterList.getParameterIndex((PsiParameter)e);
                process((PsiModifierListOwner)e, method, parameterIndex);
              }
            }
            visitVariable((PsiVariable)e);
          }
          return !myShouldStop;
        }
      };

      while (!places.isEmpty() && !myShouldStop) {
        final PsiElement curPlace = places.removeFirst();
        GrInjectionUtil.visitAnnotatedElements(curPlace, visitor);
      }

    }

    private void process(final PsiModifierListOwner owner, PsiMethod method, int paramIndex) {
      if (!processAnnotationInjections(owner)) {
        myShouldStop = true;
      }
      for (BaseInjection injection : myConfiguration.getInjections(JavaLanguageInjectionSupport.JAVA_SUPPORT_ID)) {
        if (injection.acceptsPsiElement(owner)) {
          if (!processXmlInjections(injection, owner, method, paramIndex)) {
            myShouldStop = true;
            break;
          }
        }
      }
    }

    private boolean processAnnotationInjections(final PsiModifierListOwner annoElement) {
      if (annoElement instanceof PsiParameter) {
        final PsiElement scope = ((PsiParameter)annoElement).getDeclarationScope();
        if (scope instanceof PsiMethod && !areThereInjectionsWithName(((PsiNamedElement)scope).getName(), true)) {
          return true;
        }
      }
      final PsiAnnotation[] annotations =
        GrConcatenationInjector.getAnnotationFrom(annoElement, myConfiguration.getAdvancedConfiguration().getLanguageAnnotationPair(), true, true);
      if (annotations.length > 0) {
        return processAnnotationInjectionInner(annoElement, annotations);
      }
      return true;
    }


    private static boolean checkUnparsableReference(GrExpression expression) {
      final PsiElement parent = expression.getParent();
      if (parent instanceof GrAssignmentExpression) {
        final GrAssignmentExpression assignmentExpression = (GrAssignmentExpression)parent;
        final IElementType operation = assignmentExpression.getOperationTokenType();
        if (assignmentExpression.getLValue() == expression && operation == GroovyTokenTypes.mPLUS_ASSIGN ) {
          return true;
        }
      }
      else if (parent instanceof GrBinaryExpression) {
        return true;
      }
      return false;

    }

    protected boolean areThereInjectionsWithName(String methodName, boolean annoOnly) {
      return true;
    }

    protected boolean processCommentInjectionInner(PsiVariable owner, PsiElement comment, BaseInjection injection) {
      processInjectionWithContext(injection, false);
      return false;
    }

    protected boolean processAnnotationInjectionInner(PsiModifierListOwner owner, PsiAnnotation[] annotations) {
      final String id = AnnotationUtilEx.calcAnnotationValue(annotations, "value");
      final String prefix = AnnotationUtilEx.calcAnnotationValue(annotations, "prefix");
      final String suffix = AnnotationUtilEx.calcAnnotationValue(annotations, "suffix");
      final BaseInjection injection = new BaseInjection(JavaLanguageInjectionSupport.JAVA_SUPPORT_ID);
      if (prefix != null) injection.setPrefix(prefix);
      if (suffix != null) injection.setSuffix(suffix);
      if (id != null) injection.setInjectedLanguageId(id);
      processInjectionWithContext(injection, false);
      return false;
    }

    protected boolean processXmlInjections(BaseInjection injection, PsiModifierListOwner owner, PsiMethod method, int paramIndex) {
      processInjectionWithContext(injection, true);
      if (injection.isTerminal()) {
        return false;
      }
      return true;
    }

    private boolean processCommentInjections(PsiVariable owner) {
      Ref<PsiElement> causeRef = Ref.create();
      PsiElement anchor = owner.getFirstChild() instanceof PsiComment?
                          (owner.getModifierList() != null? owner.getModifierList() : owner.getTypeElement()) : owner;
      if (anchor == null) return true;
      BaseInjection injection = mySupport.findCommentInjection(anchor, causeRef);
      return injection == null || processCommentInjectionInner(owner, causeRef.get(), injection);
    }


    private void processInjectionWithContext(BaseInjection injection, boolean settingsAvailable) {
      Language language = InjectorUtils.getLanguage(injection);
      if (language == null) return;

      String languageID = language.getID();
      List<Trinity<PsiLanguageInjectionHost, InjectedLanguage, TextRange>> list = ContainerUtil.newArrayList();

      boolean unparsable = false;

      String prefix = "";
      //String suffix = "";
      for (int i = 0; i < myOperands.length; i++) {
        PsiElement operand = myOperands[i];
        final ElementManipulator<PsiElement> manipulator = ElementManipulators.getManipulator(operand);
        if (manipulator == null) {
          unparsable = true;
          prefix += getStringPresentation(operand);
          if (i == myOperands.length - 1) {
            Trinity<PsiLanguageInjectionHost, InjectedLanguage, TextRange> last = ContainerUtil.getLastItem(list);
            assert last != null;
            InjectedLanguage injected = last.second;
            list.set(list.size() - 1, Trinity.create(last.first, InjectedLanguage.create(injected.getID(), injected.getPrefix(), prefix, false), last.third));
          }
        }
        else {
          InjectedLanguage injectedLanguage = InjectedLanguage.create(languageID, prefix, "", false);
          TextRange range = manipulator.getRangeInElement(operand);
          PsiLanguageInjectionHost host = (PsiLanguageInjectionHost)operand;
          list.add(Trinity.create(host, injectedLanguage, range));
          prefix = "";
        }
      }

      if (!list.isEmpty()) {
        processInjection(language, list, settingsAvailable, unparsable);
      }
    }

    protected void processInjection(Language language,
                                    List<Trinity<PsiLanguageInjectionHost, InjectedLanguage, TextRange>> list,
                                    boolean settingsAvailable, boolean unparsable) {
    }
  }

  public Collection<String> getAnnotatedElementsValue() {
    // note: external annotations not supported
    return InjectionCache.getInstance(myProject).getAnnoIndex();
  }

  private Collection<String> getXmlAnnotatedElementsValue() {
    return InjectionCache.getInstance(myProject).getXmlIndex();
  }
}
