/*
 * 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;

import com.intellij.openapi.application.PathMacros;
import com.intellij.openapi.components.*;
import com.intellij.openapi.diagnostic.Logger;
import com.intellij.openapi.util.text.StringUtil;
import com.intellij.util.containers.ContainerUtil;
import com.intellij.util.containers.hash.LinkedHashMap;
import gnu.trove.THashMap;
import gnu.trove.THashSet;
import org.jdom.Element;
import org.jetbrains.annotations.NonNls;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import org.jetbrains.jps.model.serialization.JpsGlobalLoader;
import org.jetbrains.jps.model.serialization.PathMacroUtil;

import java.util.Collection;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.locks.ReentrantReadWriteLock;

@State(
  name = "PathMacrosImpl",
  storages = @Storage(value = "path.macros.xml", roamingType = RoamingType.PER_OS)
)
public class PathMacrosImpl extends PathMacros implements PersistentStateComponent<Element> {
  private static final Logger LOG = Logger.getInstance(PathMacrosImpl.class);

  private final Map<String, String> myLegacyMacros = new THashMap<>();
  private final Map<String, String> myMacros = new LinkedHashMap<>();
  private int myModificationStamp = 0;
  private final ReentrantReadWriteLock myLock = new ReentrantReadWriteLock();
  private final List<String> myIgnoredMacros = ContainerUtil.createLockFreeCopyOnWriteList();

  private static final String MACRO_ELEMENT = JpsGlobalLoader.PathVariablesSerializer.MACRO_TAG;
  private static final String NAME_ATTR = JpsGlobalLoader.PathVariablesSerializer.NAME_ATTRIBUTE;
  private static final String VALUE_ATTR = JpsGlobalLoader.PathVariablesSerializer.VALUE_ATTRIBUTE;

  @NonNls
  public static final String IGNORED_MACRO_ELEMENT = "ignoredMacro";

  private static final Set<String> SYSTEM_MACROS = new THashSet<>();
  @NonNls public static final String EXT_FILE_NAME = "path.macros";

  static {
    SYSTEM_MACROS.add(PathMacroUtil.APPLICATION_HOME_DIR);
    SYSTEM_MACROS.add(PathMacroUtil.APPLICATION_PLUGINS_DIR);
    SYSTEM_MACROS.add(PathMacroUtil.PROJECT_DIR_MACRO_NAME);
    SYSTEM_MACROS.add(PathMacroUtil.MODULE_DIR_MACRO_NAME);
    SYSTEM_MACROS.add(PathMacroUtil.USER_HOME_NAME);
  }

  @SuppressWarnings("SpellCheckingInspection")
  private static final Set<String> ourToolsMacros = ContainerUtil.immutableSet(
    "ClasspathEntry",
    "Classpath",
    "ColumnNumber",
    "ContentRoot",
    "FileClass",
    "FileDir",
    "FileParentDir",
    "FileDirName",
    "FileDirPathFromParent",
    "FileDirRelativeToProjectRoot",
    "/FileDirRelativeToProjectRoot",
    "FileDirRelativeToSourcepath",
    "/FileDirRelativeToSourcepath",
    "FileExt",
    "FileFQPackage",
    "FileName",
    "FileNameWithoutExtension",
    "FileNameWithoutAllExtensions",
    "FilePackage",
    "FilePath",
    "UnixSeparators",
    "FilePathRelativeToProjectRoot",
    "/FilePathRelativeToProjectRoot",
    "FilePathRelativeToSourcepath",
    "/FilePathRelativeToSourcepath",
    "FilePrompt",
    "FileRelativeDir",
    "/FileRelativeDir",
    "FileRelativePath",
    "/FileRelativePath",
    "FileEncoding",
    "JavaDocPath",
    "JDKPath",
    "LineNumber",
    "ModuleFileDir",
    "ModuleFilePath",
    "ModuleName",
    "AffectedModuleNames",
    "IsMake",
    "ModuleSourcePath",
    "ModuleSdkPath",
    "OutputPath",
    "PhpExecutable",
    "ProjectFileDir",
    "ProjectFilePath",
    "ProjectName",
    "Projectpath",
    "Prompt",
    "SourcepathEntry",
    "Sourcepath",
    "SHOW_CHANGES",
    "ClipboardContent",
    "SelectedText",
    "SelectionStartLine",
    "SelectionEndLine",
    "SelectionStartColumn",
    "SelectionEndColumn",
    "PyInterpreterDirectory",
    "ExecutableByFileExt"
  );

  public PathMacrosImpl() {
  }

  public static PathMacrosImpl getInstanceEx() {
    return (PathMacrosImpl)getInstance();
  }

  @Override
  public Set<String> getUserMacroNames() {
    myLock.readLock().lock();
    try {
      return new THashSet<>(myMacros.keySet()); // keyset should not escape the lock
    }
    finally {
      myLock.readLock().unlock();
    }
  }

  public static Set<String> getToolMacroNames() {
    return ourToolsMacros;
  }

  @Override
  public Set<String> getSystemMacroNames() {
    return SYSTEM_MACROS;
  }

  @Override
  public Collection<String> getIgnoredMacroNames() {
    return myIgnoredMacros;
  }

  @Override
  public void setIgnoredMacroNames(@NotNull final Collection<String> names) {
    myIgnoredMacros.clear();
    myIgnoredMacros.addAll(names);
  }

  @Override
  public void addIgnoredMacro(@NotNull String name) {
    if (!myIgnoredMacros.contains(name)) myIgnoredMacros.add(name);
  }

  public int getModificationStamp() {
    myLock.readLock().lock();
    try {
      return myModificationStamp;
    }
    finally {
      myLock.readLock().unlock();
    }
  }

  @Override
  public boolean isIgnoredMacroName(@NotNull String macro) {
    return myIgnoredMacros.contains(macro);
  }

  @Override
  public Set<String> getAllMacroNames() {
    return ContainerUtil.union(getUserMacroNames(), getSystemMacroNames());
  }

  @Override
  public String getValue(String name) {
    try {
      myLock.readLock().lock();
      return myMacros.get(name);
    }
    finally {
      myLock.readLock().unlock();
    }
  }

  @Override
  public void removeAllMacros() {
    try {
      myLock.writeLock().lock();
      myMacros.clear();
    }
    finally {
      myModificationStamp++;
      myLock.writeLock().unlock();
    }
  }

  @Override
  public Collection<String> getLegacyMacroNames() {
    try {
      myLock.readLock().lock();
      // keyset should not escape the lock
      return new THashSet<>(myLegacyMacros.keySet());
    }
    finally {
      myLock.readLock().unlock();
    }
  }

  @Override
  public void setMacro(@NotNull String name, @NotNull String value) {
    if (StringUtil.isEmptyOrSpaces(value)) {
      return;
    }

    try {
      myLock.writeLock().lock();
      myMacros.put(name, value);
    }
    finally {
      myModificationStamp++;
      myLock.writeLock().unlock();
    }
  }

  @Override
  public void addLegacyMacro(@NotNull String name, @NotNull String value) {
    try {
      myLock.writeLock().lock();
      myLegacyMacros.put(name, value);
      myMacros.remove(name);
    }
    finally {
      myModificationStamp++;
      myLock.writeLock().unlock();
    }
  }

  @Override
  public void removeMacro(String name) {
    try {
      myLock.writeLock().lock();
      final String value = myMacros.remove(name);
      LOG.assertTrue(value != null);
    }
    finally {
      myModificationStamp++;
      myLock.writeLock().unlock();
    }
  }

  @Nullable
  @Override
  public Element getState() {
    try {
      Element element = new Element("state");
      myLock.writeLock().lock();

      for (Map.Entry<String, String> entry : myMacros.entrySet()) {
        String value = entry.getValue();
        if (!StringUtil.isEmptyOrSpaces(value)) {
          final Element macro = new Element(MACRO_ELEMENT);
          macro.setAttribute(NAME_ATTR, entry.getKey());
          macro.setAttribute(VALUE_ATTR, value);
          element.addContent(macro);
        }
      }

      for (final String macro : myIgnoredMacros) {
        final Element macroElement = new Element(IGNORED_MACRO_ELEMENT);
        macroElement.setAttribute(NAME_ATTR, macro);
        element.addContent(macroElement);
      }
      return element;
    }
    finally {
      myLock.writeLock().unlock();
    }
  }

  @Override
  public void loadState(Element element) {
    try {
      myLock.writeLock().lock();

      for (Element macro : element.getChildren(MACRO_ELEMENT)) {
        final String name = macro.getAttributeValue(NAME_ATTR);
        String value = macro.getAttributeValue(VALUE_ATTR);
        if (name == null || value == null) {
          continue;
        }

        if (SYSTEM_MACROS.contains(name)) {
          continue;
        }

        if (value.length() > 1 && value.charAt(value.length() - 1) == '/') {
          value = value.substring(0, value.length() - 1);
        }

        myMacros.put(name, value);
      }

      for (Element macroElement : element.getChildren(IGNORED_MACRO_ELEMENT)) {
        String ignoredName = macroElement.getAttributeValue(NAME_ATTR);
        if (!StringUtil.isEmpty(ignoredName) && !myIgnoredMacros.contains(ignoredName)) {
          myIgnoredMacros.add(ignoredName);
        }
      }
    }
    finally {
      myModificationStamp++;
      myLock.writeLock().unlock();
    }
  }

  public void addMacroReplacements(ReplacePathToMacroMap result) {
    for (String name : getUserMacroNames()) {
      String value = getValue(name);
      if (!StringUtil.isEmptyOrSpaces(value)) {
        result.addMacroReplacement(value, name);
      }
    }
  }

  public void addMacroExpands(ExpandMacroToPathMap result) {
    for (String name : getUserMacroNames()) {
      String value = getValue(name);
      if (!StringUtil.isEmptyOrSpaces(value)) {
        result.addMacroExpand(name, value);
      }
    }

    myLock.readLock().lock();
    try {
      for (Map.Entry<String, String> entry : myLegacyMacros.entrySet()) {
        result.addMacroExpand(entry.getKey(), entry.getValue());
      }
    }
    finally {
      myLock.readLock().unlock();
    }
  }
}
