/*
 * Copyright 2000-2012 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;

import com.intellij.openapi.components.PathMacroMap;
import com.intellij.openapi.util.text.StringUtil;
import com.intellij.util.ArrayUtil;
import com.intellij.util.Function;
import com.intellij.util.containers.ContainerUtil;
import com.intellij.util.containers.ContainerUtilRt;
import gnu.trove.TObjectIntHashMap;
import org.jetbrains.annotations.NonNls;

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

/**
 * @author Eugene Zhuravlev
 *         Date: Dec 6, 2004
 *
 * @see PathMacrosImpl#addMacroReplacements(ReplacePathToMacroMap)
 * @see com.intellij.openapi.components.PathMacroManager
 */
public class ReplacePathToMacroMap extends PathMacroMap {
  private List<String> myPathsIndex = null;
  private final Map<String, String> myMacroMap = ContainerUtilRt.newLinkedHashMap();

  @NonNls private static final String[] PROTOCOLS;
  static {
    List<String> protocols = new ArrayList<String>();
    protocols.add("file");
    for (PathMacroExpendableProtocolBean bean : PathMacroExpendableProtocolBean.EP_NAME.getExtensions()) {
      protocols.add(bean.protocol);
    }
    PROTOCOLS = ArrayUtil.toStringArray(protocols);
  }

  public void addMacroReplacement(String path, String macroName) {
    addReplacement(quotePath(path), "$" + macroName + "$", true);
  }

  public void addReplacement(String path, String macroExpr, boolean overwrite) {
    path = StringUtil.trimEnd(path, "/");
    putIfAbsent(path, macroExpr, overwrite);
    for (String protocol : PROTOCOLS) {
      putIfAbsent(protocol + ":" + path, protocol + ":" + macroExpr, overwrite);
      putIfAbsent(protocol + ":/" + path, protocol + ":/" + macroExpr, overwrite);
      putIfAbsent(protocol + "://" + path, protocol + "://" + macroExpr, overwrite);
    }
  }

  private void putIfAbsent(final String path, final String substitution, final boolean overwrite) {
    if (overwrite || !myMacroMap.containsKey(path)) {
      myMacroMap.put(path, substitution);
    }
  }

  @Override
  public String substitute(String text, boolean caseSensitive) {
    if (text == null) {
      //noinspection ConstantConditions
      return null;
    }

    for (final String path : getPathIndex()) {
      text = replacePathMacro(text, path, caseSensitive);
    }
    return text;
  }

  private String replacePathMacro(String text, final String path, boolean caseSensitive) {
    if (text.length() < path.length() || path.isEmpty()) {
      return text;
    }

    boolean startsWith = caseSensitive ? text.startsWith(path) : StringUtil.startsWithIgnoreCase(text, path);

    if (!startsWith) return text;

    //check that this is complete path (ends with "/" or "!/")
    // do not collapse partial paths, i.e. do not substitute "/a/b/cd" in paths like "/a/b/cdeFgh"
    int endOfOccurrence = path.length();
    final boolean isWindowsRoot = path.endsWith(":/");
    if (!isWindowsRoot &&
        endOfOccurrence < text.length() &&
        text.charAt(endOfOccurrence) != '/' &&
        !text.substring(endOfOccurrence).startsWith("!/")) {
      return text;
    }

    return myMacroMap.get(path) + text.substring(endOfOccurrence);
  }

  @Override
  public String substituteRecursively(String text, final boolean caseSensitive) {
    for (final String path : getPathIndex()) {
      text = replacePathMacroRecursively(text, path, caseSensitive);
    }
    return text;
  }

  private String replacePathMacroRecursively(String text, final String path, boolean caseSensitive) {
    if (text.length() < path.length()) {
      return text;
    }

    if (path.isEmpty()) return text;

    final StringBuilder newText = new StringBuilder();
    final boolean isWindowsRoot = path.endsWith(":/");
    int i = 0;
    while (i < text.length()) {
      int occurrenceOfPath = caseSensitive ? text.indexOf(path, i) : StringUtil.indexOfIgnoreCase(text, path, i);
      if (occurrenceOfPath >= 0) {
        int endOfOccurrence = occurrenceOfPath + path.length();
        if (!isWindowsRoot &&
            endOfOccurrence < text.length() &&
            text.charAt(endOfOccurrence) != '/' &&
            text.charAt(endOfOccurrence) != '\"' &&
            text.charAt(endOfOccurrence) != ' ' &&
            !text.substring(endOfOccurrence).startsWith("!/")) {
          newText.append(text.substring(i, endOfOccurrence));
          i = endOfOccurrence;
          continue;
        }
        if (occurrenceOfPath > 0) {
          char prev = text.charAt(occurrenceOfPath - 1);
          if (Character.isLetterOrDigit(prev) || prev == '_') {
            newText.append(text.substring(i, endOfOccurrence));
            i = endOfOccurrence;
            continue;
          }
        }
      }
      if (occurrenceOfPath < 0) {
        if (newText.length() == 0) {
          return text;
        }
        newText.append(text.substring(i));
        break;
      }
      else {
        newText.append(text.substring(i, occurrenceOfPath));
        newText.append(myMacroMap.get(path));
        i = occurrenceOfPath + path.length();
      }
    }
    return newText.toString();
  }

  private static int getIndex(final Map.Entry<String, String> s) {
    final String replacement = s.getValue();
    if (replacement.contains("..")) return 1;
    if (replacement.contains("$" + PathMacrosImpl.USER_HOME_MACRO_NAME + "$")) return 1;
    if (replacement.contains("$" + PathMacrosImpl.MODULE_DIR_MACRO_NAME + "$")) return 3;
    if (replacement.contains("$" + PathMacrosImpl.PROJECT_DIR_MACRO_NAME + "$")) return 3;
    return 2;
  }

  private static int stripPrefix(String key) {
    key = StringUtil.trimStart(key, "jar:");
    key = StringUtil.trimStart(key, "file:");
    while (key.startsWith("/")) {
      key = key.substring(1);
    }
    return key.length();
  }

  public List<String> getPathIndex() {
    if (myPathsIndex == null || myPathsIndex.size() != myMacroMap.size()) {
      List<Map.Entry<String, String>> entries = new ArrayList<Map.Entry<String, String>>(myMacroMap.entrySet());

      final TObjectIntHashMap<Map.Entry<String, String>> weights = new TObjectIntHashMap<Map.Entry<String, String>>();
      for (Map.Entry<String, String> entry : entries) {
        weights.put(entry, getIndex(entry) * 512 + stripPrefix(entry.getKey()));
      }

      ContainerUtil.sort(entries, new Comparator<Map.Entry<String, String>>() {
        @Override
        public int compare(final Map.Entry<String, String> o1, final Map.Entry<String, String> o2) {
          return weights.get(o2) - weights.get(o1);
        }
      });
      myPathsIndex = ContainerUtil.map2List(entries, new Function<Map.Entry<String, String>, String>() {
        @Override
        public String fun(Map.Entry<String, String> entry) {
          return entry.getKey();
        }
      });
    }
    return myPathsIndex;
  }

  public boolean equals(Object obj) {
    if (obj == this) return true;
    if (!(obj instanceof ReplacePathToMacroMap)) return false;

    return myMacroMap.equals(((ReplacePathToMacroMap)obj).myMacroMap);
  }

  public int hashCode() {
    return myMacroMap.hashCode();
  }

  public void put(String path, String replacement) {
    myMacroMap.put(path, replacement);
  }

}
