/*
 * Copyright 2000-2016 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.intellij.application.options.colors.highlighting;

import com.intellij.openapi.editor.colors.TextAttributesKey;
import com.intellij.openapi.util.TextRange;
import com.intellij.openapi.util.text.StringUtil;
import com.intellij.util.containers.ContainerUtil;
import com.intellij.util.containers.Stack;
import org.jetbrains.annotations.Nullable;

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

public class HighlightsExtractor {
  private final Map<String,TextAttributesKey> myTags;
  private final Map<String,TextAttributesKey> myInlineElements;
  private int myStartOffset;
  private int myEndOffset;

  private int mySkippedLen;
  private int myIndex;
  private boolean myIsOpeningTag;

  private List<TextRange> mySkipped = new ArrayList<>();

  public HighlightsExtractor(@Nullable Map<String, TextAttributesKey> tags) {
    this(tags, null);
  }

  public HighlightsExtractor(@Nullable Map<String, TextAttributesKey> tags, @Nullable Map<String, TextAttributesKey> inlineElements) {
    myTags = tags;
    myInlineElements = inlineElements;
  }

  public String extractHighlights(String text, List<HighlightData> highlights) {
    mySkipped.clear();
    if (ContainerUtil.isEmpty(myTags) && ContainerUtil.isEmpty(myInlineElements)) return text;
    resetIndices();
    Stack<HighlightData> highlightsStack = new Stack<>();
    while (true) {
      String tagName = findTagName(text);
      if (tagName == null || myIndex < 0) break;
      String tagNameWithoutParameters = StringUtil.substringBefore(tagName, " ");
      if (myInlineElements != null && tagNameWithoutParameters != null && myInlineElements.containsKey(tagNameWithoutParameters)) {
        mySkippedLen += tagName.length() + 2;
        String hintText = tagName.substring(tagNameWithoutParameters.length()).trim();
        highlights.add(new InlineElementData(myStartOffset - mySkippedLen, myInlineElements.get(tagNameWithoutParameters), hintText));
      }
      else if (myTags != null && myTags.containsKey(tagName)) {
        if (myIsOpeningTag) {
          mySkippedLen += tagName.length() + 2;
          HighlightData highlightData = new HighlightData(myStartOffset - mySkippedLen, myTags.get(tagName));
          highlightsStack.push(highlightData);
        } else {
          HighlightData highlightData = highlightsStack.pop();
          highlightData.setEndOffset(myEndOffset - mySkippedLen);
          mySkippedLen += tagName.length() + 3;
          highlights.add(highlightData);
        }
      }
    }

    return cutDefinedTags(text);
  }

  private String findTagName(String text) {
    myIsOpeningTag = true;
    int openTag = text.indexOf('<', myIndex);
    if (openTag == -1) {
      return null;
    }
    while (text.charAt(openTag + 1) == '<') {
      openTag++;
    }
    if (text.charAt(openTag + 1) == '/') {
      myIsOpeningTag = false;
      openTag++;
    }
    if (!isValidTagFirstChar(text.charAt(openTag + 1))) {
      myIndex = openTag + 1;
      return "";
    }

    int closeTag = text.indexOf('>', openTag + 1);
    if (closeTag == -1) return null;
    int i = text.indexOf('<', openTag + 1);
    if (i != -1 && i < closeTag) {
      myIndex = i;
      return "";
    }
    final String tagName = text.substring(openTag + 1, closeTag);

    if (myIsOpeningTag) {
      myStartOffset = openTag + tagName.length() + 2;
      if (myTags != null && myTags.containsKey(tagName) ||
          myInlineElements != null && myInlineElements.containsKey(StringUtil.substringBefore(tagName, " "))) {
        mySkipped.add(TextRange.from(openTag, tagName.length() + 2));
      }
    } else {
      myEndOffset = openTag - 1;
      if (myTags != null && myTags.containsKey(tagName)) {
        mySkipped.add(TextRange.from(openTag - 1, tagName.length() + 3));
      }
    }
    myIndex = Math.max(myStartOffset, myEndOffset + 1);
    return tagName;
  }
  
  private static boolean isValidTagFirstChar(char c) {
    return Character.isLetter(c) || c == '_';
  }

  private String cutDefinedTags(String text) {
    StringBuilder builder = new StringBuilder(text);
    for (int i = mySkipped.size() - 1; i >= 0; i--) {
      TextRange range = mySkipped.get(i);
      builder.delete(range.getStartOffset(), range.getEndOffset());
    }
    return builder.toString();
  }

  private void resetIndices() {
    myIndex = 0;
    myStartOffset = 0;
    myEndOffset = 0;
    mySkippedLen = 0;
  }
}
