/*
 * Copyright 2007 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.impl;

import com.intellij.lang.ASTNode;
import com.intellij.lang.LanguageParserDefinitions;
import com.intellij.lang.injection.MultiHostInjector;
import com.intellij.lang.injection.MultiHostRegistrar;
import com.intellij.lexer.Lexer;
import com.intellij.openapi.util.Key;
import com.intellij.openapi.util.Pair;
import com.intellij.openapi.util.TextRange;
import com.intellij.psi.PsiElement;
import com.intellij.psi.impl.source.xml.XmlAttributeValueImpl;
import com.intellij.psi.xml.XmlAttribute;
import com.intellij.psi.xml.XmlAttributeValue;
import com.intellij.psi.xml.XmlElementType;
import com.intellij.util.SmartList;
import org.intellij.lang.xpath.XPathTokenTypes;
import org.intellij.lang.xpath.xslt.XsltSupport;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;

import java.util.Collections;
import java.util.List;

final class XPathLanguageInjector implements MultiHostInjector {
    private static final Key<Pair<String, TextRange[]>> CACHED_FILES = Key.create("CACHED_FILES");

    public XPathLanguageInjector() {
    }

    private static TextRange @Nullable [] getCachedRanges(XmlAttribute attribute) {
        Pair<String, TextRange[]> pair;
        if ((pair = attribute.getUserData(CACHED_FILES)) != null) {
            if (!attribute.getValue().equals(pair.getFirst())) {
                attribute.putUserData(CACHED_FILES, null);
                return null;
            }
        } else {
            return null;
        }
        return pair.getSecond();
    }

    static final class AVTRange extends TextRange {
        final boolean myComplete;

        private AVTRange(int startOffset, int endOffset, boolean iscomplete) {
            super(startOffset, endOffset);
            myComplete = iscomplete;
        }

        public static AVTRange create(XmlAttribute attribute, int startOffset, int endOffset, boolean iscomplete) {
            return new AVTRange(attribute.displayToPhysical(startOffset), attribute.displayToPhysical(endOffset), iscomplete);
        }
    }

    private synchronized TextRange @NotNull [] getInjectionRanges(final XmlAttribute attribute, XsltChecker.LanguageLevel languageLevel) {
      final TextRange[] cachedFiles = getCachedRanges(attribute);
      if (cachedFiles != null) {
        return cachedFiles;
      }

      final String value = attribute.getDisplayValue();
      if (value == null) return TextRange.EMPTY_ARRAY;

      final TextRange[] ranges;
      if (XsltSupport.mayBeAVT(attribute)) {
        final List<TextRange> avtRanges = new SmartList<>();

        int i;
        int j = 0;
        Lexer lexer = null;
        while ((i = XsltSupport.getAVTOffset(value, j)) != -1) {
          if (lexer == null) {
            lexer = LanguageParserDefinitions.INSTANCE.forLanguage(languageLevel.getXPathVersion().getLanguage())
              .createLexer(attribute.getProject());
          }

          // "A right curly brace inside a Literal in an expression is not recognized as terminating the expression."
          lexer.start(value, i, value.length());
          j = -1;
          while (lexer.getTokenType() != null) {
            if (lexer.getTokenType() == XPathTokenTypes.RBRACE) {
              j = lexer.getTokenStart();
              break;
            }
            lexer.advance();
          }

          if (j != -1) {
            avtRanges.add(AVTRange.create(attribute, i, j + 1, j > i + 1));
          }
          else {
            // missing '}' error will be flagged by xpath parser
            avtRanges.add(AVTRange.create(attribute, i, value.length(), false));
            break;
          }
        }

        ranges = avtRanges.isEmpty() ? TextRange.EMPTY_ARRAY : avtRanges.toArray(TextRange.EMPTY_ARRAY);
      }
      else {
        ranges = new TextRange[]{ attribute.getValueTextRange() };
      }

      attribute.putUserData(CACHED_FILES, Pair.create(attribute.getValue(), ranges));

      return ranges;
    }

  @Override
  public @NotNull List<? extends Class<? extends PsiElement>> elementsToInjectIn() {
    return Collections.singletonList(XmlAttributeValue.class);
  }

  @Override
  public void getLanguagesToInject(@NotNull MultiHostRegistrar registrar, @NotNull PsiElement context) {
    XmlAttributeValueImpl value = (XmlAttributeValueImpl)context;
    PsiElement parent = value.getParent();
    if (!(parent instanceof XmlAttribute attribute)) return;
    if (!XsltSupport.isXPathAttribute(attribute)) return;

    ASTNode type = value.findChildByType(XmlElementType.XML_ENTITY_REF);
    if (type != null) return; // workaround for inability to inject into text with entity refs (e.g. IDEA-72972) TODO: fix it

    final XsltChecker.LanguageLevel languageLevel = XsltSupport.getXsltLanguageLevel(attribute.getContainingFile());
    final TextRange[] ranges = getInjectionRanges(attribute, languageLevel);
    for (TextRange range : ranges) {
      // workaround for http://www.jetbrains.net/jira/browse/IDEA-10096
      TextRange rangeInsideHost;
      String prefix;
      if (range instanceof AVTRange) {
        if (((AVTRange)range).myComplete) {
          rangeInsideHost = range.shiftRight(2).grown(-2);
          prefix = "";
        }
        else {
          // we need to keep the "'}' expected" parse error
          rangeInsideHost = range.shiftRight(2).grown(-1);
          prefix = "{";
        }
      }
      else {
        rangeInsideHost = range;
        prefix = "";
      }
      if (value.getTextRange().contains(rangeInsideHost.shiftRight(value.getTextRange().getStartOffset()))) {
        registrar.startInjecting(languageLevel.getXPathVersion().getLanguage())
                .addPlace(prefix, "", value, rangeInsideHost)
                .doneInjecting();
      }
    }
  }
}
