/*
 * 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 com.jetbrains.python.documentation.docstrings;

import com.google.common.collect.Maps;
import com.intellij.openapi.util.TextRange;
import com.intellij.openapi.util.text.StringUtil;
import com.jetbrains.python.psi.StructuredDocString;
import com.jetbrains.python.toolbox.Substring;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;

import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.regex.Matcher;
import java.util.regex.Pattern;


public abstract class TagBasedDocString extends DocStringLineParser implements StructuredDocString {
  protected final String myDescription;

  protected final Map<String, Substring> mySimpleTagValues = Maps.newHashMap();
  protected final Map<String, Map<Substring, Substring>> myArgTagValues = Maps.newHashMap();

  private static final Pattern RE_STRICT_TAG_LINE = Pattern.compile("([a-z]+)(\\s+:class:[^:]*|[^:]*)\\s*:\\s*?(.*)");
  private static final Pattern RE_LOOSE_TAG_LINE = Pattern.compile("([a-z]+)\\s+([a-zA-Z_0-9]*)\\s*:?\\s*?([^:]*)");
  private static final Pattern RE_ARG_TYPE = Pattern.compile("(.*?)\\s+([a-zA-Z_0-9]+)");

  public static String[] PARAM_TAGS = new String[]{"param", "parameter", "arg", "argument"};
  public static String[] PARAM_TYPE_TAGS = new String[]{"type"};
  public static String[] VARIABLE_TAGS = new String[]{"ivar", "cvar", "var"};

  public static String[] RAISES_TAGS = new String[]{"raises", "raise", "except", "exception"};
  public static String[] RETURN_TAGS = new String[]{"return", "returns"};
  @NotNull
  private final String myTagPrefix;

  public static String TYPE = "type";

  protected TagBasedDocString(@NotNull Substring docStringText, @NotNull String tagPrefix) {
    super(docStringText);
    myTagPrefix = tagPrefix;
    final StringBuilder builder = new StringBuilder();
    int lineno = 0;
    while (lineno < getLineCount()) {
      Substring line = getLine(lineno).trim();
      if (line.startsWith(tagPrefix)) {
        lineno = parseTag(lineno, tagPrefix);
      }
      else {
        builder.append(line.toString()).append("\n");
      }
      lineno++;
    }
    myDescription = builder.toString();
  }

  public abstract List<String> getAdditionalTags();

  @NotNull
  @Override
  public String getDescription() {
    return myDescription;
  }

  @Override
  public String getSummary() {
    final List<String> strings = StringUtil.split(StringUtil.trimLeading(myDescription), "\n", true, false);
    if (strings.size() > 1) {
      if (strings.get(1).isEmpty()) {
        return strings.get(0);
      }
    }
    return "";
  }

  @NotNull
  private Map<Substring, Substring> getTagValuesMap(String key) {
    Map<Substring, Substring> map = myArgTagValues.get(key);
    if (map == null) {
      map = Maps.newLinkedHashMap();
      myArgTagValues.put(key, map);
    }
    return map;
  }

  protected int parseTag(int lineno, String tagPrefix) {
    final Substring lineWithPrefix = getLine(lineno).trimLeft();
    if (lineWithPrefix.startsWith(tagPrefix)) {
      final Substring line = lineWithPrefix.substring(tagPrefix.length());
      final Matcher strictTagMatcher = RE_STRICT_TAG_LINE.matcher(line);
      final Matcher looseTagMatcher = RE_LOOSE_TAG_LINE.matcher(line);
      Matcher tagMatcher = null;
      if (strictTagMatcher.matches()) {
        tagMatcher = strictTagMatcher;
      }
      else if (looseTagMatcher.matches()) {
        tagMatcher = looseTagMatcher;
      }
      if (tagMatcher != null) {
        final Substring tagName = line.getMatcherGroup(tagMatcher, 1);
        Substring argName = line.getMatcherGroup(tagMatcher, 2).trim();
        final TextRange firstArgLineRange = line.getMatcherGroup(tagMatcher, 3).trim().getTextRange();
        final int linesCount = getLineCount();
        final int argStart = firstArgLineRange.getStartOffset();
        int argEnd = firstArgLineRange.getEndOffset();
        while (lineno + 1 < linesCount) {
          final Substring nextLine = getLine(lineno + 1).trim();
          if (nextLine.isEmpty() || nextLine.startsWith(tagPrefix)) {
            break;
          }
          argEnd = nextLine.getTextRange().getEndOffset();
          lineno++;
        }
        final Substring argValue = new Substring(argName.getSuperString(), argStart, argEnd);
        final String tagNameString = tagName.toString();
        if (argName.isEmpty()) {
          mySimpleTagValues.put(tagNameString, argValue);
        }
        else {
          if ("param".equals(tagNameString) || "parameter".equals(tagNameString) ||
              "arg".equals(tagNameString) || "argument".equals(tagNameString)) {
            // Process declarations that combine type and description, e.g. ":param int foo: bar"
            // as if there were both ":param foo: bar" and ":type foo: int"
            final Matcher argTypeMatcher = RE_ARG_TYPE.matcher(argName);
            if (argTypeMatcher.matches()) {
              final Substring argType = argName.getMatcherGroup(argTypeMatcher, 1).trim();
              argName = argName.getMatcherGroup(argTypeMatcher, 2);
              getTagValuesMap(TYPE).put(argName, argType);
            }
          }
          getTagValuesMap(tagNameString).put(argName, argValue);
        }
      }
    }
    return lineno;
  }

  protected static List<String> toUniqueStrings(List<?> objects) {
    final List<String> result = new ArrayList<>(objects.size());
    for (Object o : objects) {
      final String s = o.toString();
      if (!result.contains(s)) {
        result.add(s);
      }
    }
    return result;
  }

  @Nullable
  public Substring getTagValue(String... tagNames) {
    for (String tagName : tagNames) {
      final Substring value = mySimpleTagValues.get(tagName);
      if (value != null) {
        return value;
      }
    }
    return null;
  }

  @Nullable
  public Substring getTagValue(String tagName, @NotNull String argName) {
    final Map<Substring, Substring> argValues = myArgTagValues.get(tagName);
    return argValues != null ? argValues.get(new Substring(argName)) : null;
  }

  @Nullable
  public Substring getTagValue(String[] tagNames, @NotNull String argName) {
    for (String tagName : tagNames) {
      final Map<Substring, Substring> argValues = myArgTagValues.get(tagName);
      final Substring key = new Substring(argName);
      if (argValues != null && argValues.containsKey(key)) {
        return argValues.get(key);
      }
    }
    return null;
  }

  public List<Substring> getTagArguments(String... tagNames) {
    final List<Substring> result = new ArrayList<>();
    for (String tagName : tagNames) {
      final Map<Substring, Substring> map = myArgTagValues.get(tagName);
      if (map != null) {
        result.addAll(map.keySet());
      }
    }
    return result;
  }

  @NotNull
  @Override
  public List<String> getParameters() {
    return toUniqueStrings(getParameterSubstrings());
  }

  @NotNull
  @Override
  public List<Substring> getParameterSubstrings() {
    final List<Substring> results = new ArrayList<>();
    results.addAll(getTagArguments(PARAM_TAGS));
    results.addAll(getTagArguments(PARAM_TYPE_TAGS));
    return results;
  }

  @Override
  public @NotNull List<String> getAttributes() {
    return toUniqueStrings(getAttributeSubstrings());
  }

  @Override
  public @NotNull List<Substring> getAttributeSubstrings() {
    return new ArrayList<>(getTagArguments(VARIABLE_TAGS));
  }

  @Override
  protected boolean isBlockEnd(int lineNum) {
    return getLine(lineNum).trimLeft().startsWith(myTagPrefix);
  }
}
