/*
 * Copyright 2005 Sascha Weinreuter
 *
 * 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.lang.xpath.xslt.context;

import com.intellij.codeInsight.intention.IntentionAction;
import com.intellij.psi.PsiElement;
import com.intellij.psi.PsiReference;
import com.intellij.psi.impl.source.resolve.ResolveCache;
import com.intellij.psi.util.PsiTreeUtil;
import com.intellij.psi.xml.XmlFile;
import com.intellij.psi.xml.XmlTag;
import org.intellij.lang.xpath.context.VariableContext;
import org.intellij.lang.xpath.psi.XPathElement;
import org.intellij.lang.xpath.psi.XPathVariable;
import org.intellij.lang.xpath.psi.XPathVariableReference;
import org.intellij.lang.xpath.psi.impl.ResolveUtil;
import org.intellij.lang.xpath.xslt.XsltSupport;
import org.intellij.lang.xpath.xslt.impl.XsltIncludeIndex;
import org.intellij.lang.xpath.xslt.psi.XsltElementFactory;
import org.intellij.lang.xpath.xslt.psi.XsltParameter;
import org.intellij.lang.xpath.xslt.psi.XsltTemplate;
import org.intellij.lang.xpath.xslt.psi.XsltVariable;
import org.intellij.lang.xpath.xslt.quickfix.CreateParameterFix;
import org.intellij.lang.xpath.xslt.quickfix.CreateVariableFix;
import org.intellij.lang.xpath.xslt.util.ElementProcessor;
import org.intellij.lang.xpath.xslt.util.XsltCodeInsightUtil;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;

import java.util.ArrayList;
import java.util.List;

public class XsltVariableContext implements VariableContext<XsltVariable> {
    public static final XsltVariableContext INSTANCE = new XsltVariableContext();

    private final ResolveCache.Resolver RESOLVER = (psiReference, incompleteCode) -> resolveInner((XPathVariableReference)psiReference);

    @Override
    public XsltVariable @NotNull [] getVariablesInScope(XPathElement element) {
        final XmlTag context = getContextTagImpl(element);
        final VariantsProcessor processor = new VariantsProcessor(context);

        ResolveUtil.treeWalkUp(processor, context);
        processForwardGlobals(context, processor);
        return processor.getResult();
    }

    @Override
    public XPathVariable resolve(final XPathVariableReference reference) {
        return (XPathVariable) ResolveCache.getInstance(reference.getProject()).resolveWithCaching(reference, RESOLVER, false, false);
    }

    private @Nullable XPathVariable resolveInner(XPathVariableReference reference) {
        final XmlTag context = getContextTagImpl(reference);
        final VariableResolveProcessor processor = new VariableResolveProcessor(reference.getReferencedName(), context);

        final XPathVariable variable = (XPathVariable)ResolveUtil.treeWalkUp(processor, context);
        if (variable != null) {
          return variable;
        }
        if (!processForwardGlobals(context, processor)) {
          final XmlFile file = PsiTreeUtil.getParentOfType(context, XmlFile.class, true);
          if (file != null) {
            XsltIncludeIndex.processBackwardDependencies(file, xmlFile -> {
              processor.processExternalFile(xmlFile, context);
              return processor.shouldContinue();
            });
          }
        }
        return (XPathVariable)processor.getResult();
    }

    private static boolean processForwardGlobals(XmlTag context, VariableProcessor processor) {
      while (context != null && !XsltSupport.isTopLevelElement(context)) {
        context = context.getParentTag();
      }
      while (context != null && processor.shouldContinue()) {
        processor.process(context);
        context = PsiTreeUtil.getNextSiblingOfType(context, XmlTag.class);
      }
      return !processor.shouldContinue();
    }

    protected @Nullable XmlTag getContextTagImpl(XPathElement element) {
        return PsiTreeUtil.getContextOfType(element, XmlTag.class, true);
    }

    @Override
    public IntentionAction @NotNull [] getUnresolvedVariableFixes(XPathVariableReference reference) {
        return new IntentionAction[] {
                new CreateVariableFix(reference),
                new CreateParameterFix(reference)
        };
    }

    @Override
    public boolean isReferenceTo(PsiElement element, XPathVariableReference reference) {
        if (element instanceof XsltParameter) {
            final XsltTemplate template = XsltCodeInsightUtil.getTemplate(element, false);
            if (template == null || template.getMatchExpression() == null) return false;

            final XPathVariable t = reference.resolve();
            if (t == null) return false;
            final PsiReference[] references = element.getReferences();
            for (PsiReference r : references) {
                if (r.isReferenceTo(t)) return true;
            }
        }
        return false;
    }

    @Override
    public boolean canResolve() {
        return true;
    }

    abstract static class VariableProcessor extends ElementProcessor<XmlTag> {
        VariableProcessor(XmlTag context) {
            super(context);
        }

        @Override
        protected boolean followImport() {
            return true;
        }

        @Override
        protected final void processTemplate(XmlTag tag) {
            // not interested
        }

        protected abstract void processVarOrParamImpl(XmlTag tag);

        @Override
        protected final void processVarOrParam(XmlTag tag) {
            if (tag != myRoot) {
                processVarOrParamImpl(tag);
            }
        }

        @Override
        protected abstract boolean shouldContinue();
    }

    static class VariantsProcessor extends VariableProcessor {
        private final List<XsltVariable> myNames = new ArrayList<>();

        VariantsProcessor(XmlTag context) {
            super(context);
        }

        public XsltVariable[] getResult() {
            return myNames.toArray(new XsltVariable[0]);
        }

        @Override
        protected void processVarOrParamImpl(XmlTag tag) {
            if (XsltSupport.isVariableOrParam(tag)) {
                myNames.add(XsltElementFactory.getInstance().wrapElement(tag, XsltVariable.class));
            }
        }

        @Override
        protected boolean shouldContinue() {
            return true;
        }
    }

    static class VariableResolveProcessor extends VariableProcessor implements ResolveUtil.ResolveProcessor {
        private final String myName;
        private PsiElement myResult = null;

        VariableResolveProcessor(final String name, XmlTag context) {
            super(context);
            myName = name;
        }

        @Override
        public PsiElement getResult() {
            return myResult;
        }

        @Override
        protected boolean shouldContinue() {
            return myResult == null;
        }

        @Override
        protected void processVarOrParamImpl(XmlTag tag) {
            if (XsltSupport.isVariableOrParam(tag)) {
                final String name = tag.getAttributeValue("name");
                if (myName.equals(name)) {
                    myResult = XsltElementFactory.getInstance().wrapElement(tag, XsltVariable.class);
                }
            }
        }
    }
}
