// Copyright 2000-2024 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
package com.intellij.lang.ant.dom;

import com.intellij.openapi.util.NlsSafe;
import com.intellij.openapi.util.io.FileUtil;
import com.intellij.util.xml.reflect.DomAttributeChildDescription;
import org.jetbrains.annotations.NonNls;
import org.jetbrains.annotations.Nullable;

import java.io.File;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.StringTokenizer;
import java.util.regex.Pattern;

/**
 * @author Eugene Zhuravlev
 */
public class AntDomPattern extends AntDomRecursiveVisitor {
  private static final List<Pattern> ourDefaultExcludes = new ArrayList<>(getDefaultExcludes(true));
  private static final List<Pattern> ourCaseInsensitiveDefaultExcludes = new ArrayList<>(getDefaultExcludes(false));
  private final boolean myCaseSensitive;
  private static final String ourSeparatorPattern = Pattern.quote("/");

  private static List<Pattern> getDefaultExcludes(final boolean caseSensitive) {
    return Arrays.asList(
      convertToRegexPattern("**/*~", caseSensitive),
      convertToRegexPattern("**/#*#", caseSensitive),
      convertToRegexPattern("**/.#*", caseSensitive),
      convertToRegexPattern("**/%*%", caseSensitive),
      convertToRegexPattern("**/._*", caseSensitive),
      convertToRegexPattern("**/CVS", caseSensitive),
      convertToRegexPattern("**/CVS/**", caseSensitive),
      convertToRegexPattern("**/.cvsignore", caseSensitive),
      convertToRegexPattern("**/SCCS", caseSensitive),
      convertToRegexPattern("**/SCCS/**", caseSensitive),
      convertToRegexPattern("**/vssver.scc", caseSensitive),
      convertToRegexPattern("**/.svn", caseSensitive),
      convertToRegexPattern("**/.svn/**", caseSensitive),
      convertToRegexPattern("**/_svn", caseSensitive),
      convertToRegexPattern("**/_svn/**", caseSensitive),
      convertToRegexPattern("**/.DS_Store", caseSensitive)
    );
  }

  private final List<Pattern> myIncludePatterns = new ArrayList<>();
  private final List<Pattern> myExcludePatterns = new ArrayList<>();
  private final List<PrefixItem[]> myCouldBeIncludedPatterns = new ArrayList<>();

  AntDomPattern(final boolean caseSensitive) {
    myCaseSensitive = caseSensitive;
  }

  public boolean hasIncludePatterns() {
    return !myIncludePatterns.isEmpty();
  }

  @Override
  public void visitAntDomElement(AntDomElement element) {
    // todo: add support to includefile and excludefile
    if ("include".equals(element.getXmlElementName()) && !(element instanceof AntDomInclude)) {
      if (isEnabled(element)) {
        final String value = getAttributeValue(element, "name");
        if (value != null) {
          addIncludePattern(value);
        }
      }
    }
    else if ("exclude".equals(element.getXmlElementName())) {
      if (isEnabled(element)) {
        final String value = getAttributeValue(element, "name");
        if (value != null) {
          addExcludePattern(value);
        }
      }
    }
    else {
      // todo: add support to includesfile and excludesfile
      final String includeAttribs = getAttributeValue(element, "includes");
      if (includeAttribs != null) {
        addPatterns(true, includeAttribs);
      }
      final String excludeAttribs = getAttributeValue(element, "excludes");
      if (excludeAttribs != null) {
        addPatterns(false, excludeAttribs);
      }
    }
    final AntDomElement referred = element.getRefId().getValue();
    if (referred != null) {
      referred.accept(this);
    }
    super.visitAntDomElement(element);
  }

  private static @Nullable @NlsSafe String getAttributeValue(AntDomElement element, final @NonNls String attributeName) {
    final DomAttributeChildDescription description = element.getGenericInfo().getAttributeChildDescription(attributeName);
    if (description == null) {
      return null;
    }
    return description.getDomAttributeValue(element).getStringValue();
  }

  public final void addExcludePattern(final String antPattern) {
    myExcludePatterns.add(convertToRegexPattern(antPattern, myCaseSensitive));
  }

  public final void addIncludePattern(final String antPattern) {
    myIncludePatterns.add(convertToRegexPattern(antPattern, myCaseSensitive));
    String normalizedPattern = antPattern.endsWith("/") || antPattern.endsWith(File.separator)? antPattern.replace(File.separatorChar, '/') + "**" : antPattern.replace(File.separatorChar, '/');
    if (normalizedPattern.startsWith("/") && normalizedPattern.length() > 1) {
      // cut first leading slash if any
      normalizedPattern = normalizedPattern.substring(1);
    }
    if (!normalizedPattern.startsWith("/")) {
      final String[] patDirs = normalizedPattern.split(ourSeparatorPattern);
      final PrefixItem[] items = new PrefixItem[patDirs.length];
      for (int i = 0; i < patDirs.length; i++) {
        items[i] = new PrefixItem(patDirs[i]);
      }
      myCouldBeIncludedPatterns.add(items);
    }
  }

  public boolean acceptPath(final String relativePath) {
    final String path = relativePath.replace('\\', '/');
    boolean accepted = myIncludePatterns.isEmpty();
    for (Pattern includePattern : myIncludePatterns) {
      if (includePattern.matcher(path).matches()) {
        accepted = true;
        break;
      }
    }
    if (accepted) {
      for (Pattern excludePattern : myExcludePatterns) {
        if (excludePattern.matcher(path).matches()) {
          accepted = false;
          break;
        }
      }
    }
    return accepted;
  }

  private static boolean isEnabled(AntDomElement element) {
    final String ifProperty = getAttributeValue(element, "if");
    if (ifProperty != null && PropertyResolver.resolve(element.getContextAntProject(), ifProperty, element).element() == null) {
      return false;
    }
    final String unlessProperty = getAttributeValue(element, "unless");
    if (unlessProperty != null && PropertyResolver.resolve(element.getContextAntProject(), unlessProperty, element).element() != null) {
      return false;
    }
    return true;
  }

  private void addPatterns(final boolean addToIncludes, final String patternString) {
    final StringTokenizer tokenizer = new StringTokenizer(patternString, ", \t", false);
    while (tokenizer.hasMoreTokens()) {
      final String pattern = tokenizer.nextToken();
      if (!pattern.isEmpty()) {
        if (addToIncludes) {
          addIncludePattern(pattern);
        }
        else {
          addExcludePattern(pattern);
        }
      }
    }
  }

  private static Pattern convertToRegexPattern(final @NonNls String antPattern, final boolean caseSensitive) {
    return Pattern.compile(FileUtil.convertAntToRegexp(antPattern), caseSensitive? 0 : Pattern.CASE_INSENSITIVE);
  }

  public static AntDomPattern create(AntDomElement element, final boolean honorDefaultExcludes, final boolean caseSensitive) {
    final AntDomPattern antPattern = new AntDomPattern(caseSensitive);
    element.accept(antPattern);
    if (honorDefaultExcludes) {
      antPattern.myExcludePatterns.addAll(caseSensitive? ourDefaultExcludes : ourCaseInsensitiveDefaultExcludes);
    }
    return antPattern;
  }

  // from org.apache.tools.ant.DirectoryScanner
  protected static boolean matchPatternStart(PrefixItem[] patDirs, String str) {
    final String[] strDirs = str.split(ourSeparatorPattern);

    int patIdxStart = 0;
    final int patIdxEnd   = patDirs.length-1;
    int strIdxStart = 0;
    final int strIdxEnd   = strDirs.length-1;

    // up to first '**'
    while (patIdxStart <= patIdxEnd && strIdxStart <= strIdxEnd) {
      final AntDomPattern.PrefixItem item = patDirs[patIdxStart];
      if ("**".equals(item.getStrPattern())) {
        break;
      }
      if (!item.getPattern().matcher(strDirs[strIdxStart]).matches()) {
        return false;
      }
      patIdxStart++;
      strIdxStart++;
    }

    if (strIdxStart > strIdxEnd) {
      // String is exhausted
      return true;
    }

    if (patIdxStart > patIdxEnd) {
      // String not exhausted, but pattern is. Failure.
      return false;
    }

    // pattern now holds ** while string is not exhausted
    // this will generate false positives but we can live with that.
    return true;
  }

  public boolean couldBeIncluded(String relativePath) {
    if (myIncludePatterns.isEmpty()) {
      return true;
    }
    return myCouldBeIncludedPatterns.stream().anyMatch(couldBeIncludedPattern -> matchPatternStart(couldBeIncludedPattern, relativePath));
  }

  private class PrefixItem {
    private final String myStrPattern;
    private Pattern myCompiledPattern;
    PrefixItem(String strPattern) {
      myStrPattern = strPattern;
    }

    public String getStrPattern() {
      return myStrPattern;
    }

    public Pattern getPattern() {
      if (myCompiledPattern == null) {
        myCompiledPattern = Pattern.compile(FileUtil.convertAntToRegexp(myStrPattern), myCaseSensitive ? 0 : Pattern.CASE_INSENSITIVE);
      }
      return myCompiledPattern;
    }
  }
}
