/*
 * Copyright 2006 Sascha Weinreuter
 *
 * 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 org.intellij.plugins.intelliLang.util;

import com.intellij.openapi.util.text.StringUtil;
import com.intellij.patterns.StringPattern;
import org.jetbrains.annotations.NotNull;

import java.util.*;

/**
 * Simple abstraction of a String matcher that can be based on simple and vastly more
 * efficient String comparisons than doing a full pattern match.
 */
public abstract class StringMatcher<T> {
  protected final T myTarget;

  public static final StringMatcher NONE = new Any("<none>", false);
  public static final StringMatcher ANY = new Any("", true);
  public static final StringMatcher ANY_PATTERN = new Any(".*", true);

  protected StringMatcher(T target) {
    myTarget = target;
  }

  private abstract static class Simple extends StringMatcher<String> {
    Simple(String target) {
      super(target);
    }

    @Override
    public String getPattern() {
      return myTarget;
    }
  }

  private static final class Pattern extends StringMatcher<java.util.regex.Pattern> {
    Pattern(String target) {
      super(java.util.regex.Pattern.compile(target));
    }

    @Override
    public boolean matches(@NotNull String what) {
      return myTarget.matcher(StringPattern.newBombedCharSequence(what)).matches();
    }

    @Override
    public String getPattern() {
      return myTarget.pattern();
    }
  }

  private static final class Equals extends Simple {
    Equals(String target) {
      super(target);
    }

    @Override
    public boolean matches(@NotNull String what) {
      return myTarget.equals(what);
    }
  }

  private static final class StartsWith extends Simple {
    StartsWith(String target) {
      super(target);
    }

    @Override
    public boolean matches(@NotNull String what) {
      return what.startsWith(myTarget);
    }

    @Override
    public String getPattern() {
      return super.getPattern() + ".*";
    }
  }

  private static final class EndsWith extends Simple {
    EndsWith(String target) {
      super(target);
    }

    @Override
    public boolean matches(@NotNull String what) {
      return what.endsWith(myTarget);
    }

    @Override
    public String getPattern() {
      return ".*" + super.getPattern();
    }
  }

  private static final class Contains extends Simple {
    Contains(String target) {
      super(target);
    }

    @Override
    public boolean matches(@NotNull String what) {
      return what.contains(myTarget);
    }

    @Override
    public String getPattern() {
      return ".*" + super.getPattern() + ".*";
    }
  }

  private static final class Any extends Simple {
    private final boolean myMatches;

    Any(String target, boolean matches) {
      super(target);
      myMatches = matches;
    }

    @Override
    public boolean matches(@NotNull String what) {
      return myMatches;
    }
  }

  private static final class IgnoreCase extends StringMatcher<StringMatcher> {
    IgnoreCase(StringMatcher target) {
      super(target);
    }

    @Override
    public boolean matches(@NotNull String what) {
      return myTarget.matches(StringUtil.toLowerCase(what));
    }

    @Override
    public String getPattern() {
      return "(?i)" + myTarget.getPattern();
    }
  }

  private static final class Cache extends StringMatcher<StringMatcher> {
    private final Map<String, Boolean> myCache = new WeakHashMap<>();

    Cache(StringMatcher target) {
      super(target);
    }

    @Override
    public String getPattern() {
      return myTarget.getPattern();
    }

    @Override
    public synchronized boolean matches(@NotNull String what) {
      final Boolean o = myCache.get(what);
      if (o != null) {
        return o;
      }
      final boolean b = myTarget.matches(what);
      myCache.put(what, b);
      return b;
    }
  }

  public static final class MatcherSet extends StringMatcher<Set<StringMatcher>> {
    private final String myPattern;

    private MatcherSet(Set<StringMatcher> target) {
      super(target);
      myPattern = StringUtil.join(target, s -> s.getPattern(), "|");
    }

    public static StringMatcher create(Set<StringMatcher> matchers) {
      final MatcherSet m = new MatcherSet(matchers);
      return matchers.size() > 3 ? new Cache(m) : m;
    }

    @Override
    public boolean matches(@NotNull String what) {
      for (StringMatcher matcher : myTarget) {
        if (matcher.matches(what)) {
          return true;
        }
      }
      return false;
    }

    @Override
    public String getPattern() {
      return myPattern;
    }
  }

  public abstract boolean matches(@NotNull String what);

  public abstract String getPattern();

  public static StringMatcher create(String target) {
    if (target.isEmpty()) return ANY;
    if (target.equals(".*")) return ANY_PATTERN;
    if (target.equals(NONE.getPattern())) return NONE;

    final List<String> branches = StringUtil.split(target,"|");
    final Set<StringMatcher> matchers = new LinkedHashSet<>();

    for (String branch : branches) {
      boolean startsWith = false;
      boolean endsWith = false;
      boolean ignoreCase = false;

      // this assumes the regex is syntactically correct
      if (branch.startsWith("(?i)")) {
        ignoreCase = true;
        branch = StringUtil.toLowerCase(branch.substring(2));
      }
      if (branch.endsWith(".*")) {
        startsWith = true;
        branch = branch.substring(0, branch.length() - 2);
      }
      if (branch.startsWith(".*")) {
        endsWith = true;
        branch = branch.substring(2);
      }

      final boolean m = analyseBranch(branch);
      if (!m) {
        try {
          return new Cache(new Pattern(target));
        }
        catch (Exception e) {
          return new Any(target, false);
        }
      }

      final StringMatcher matcher;
      if (startsWith && endsWith) {
        matcher = new Contains(branch);
      }
      else if (startsWith) {
        matcher = new StartsWith(branch);
      }
      else if (endsWith) {
        matcher = new EndsWith(branch);
      }
      else {
        matcher = new Equals(branch);
      }

      matchers.add(ignoreCase ? new IgnoreCase(matcher) : matcher);
    }

    return matchers.size() == 1 ? matchers.iterator().next() : MatcherSet.create(matchers);
  }

  private static boolean analyseBranch(String target) {
    for (int i = 0; i < target.length(); i++) {
      final char c = target.charAt(i);
      if (c != '_' && c != '-' && !Character.isLetterOrDigit(c)) {
        return false;
      }
    }
    return true;
  }

  @Override
  public int hashCode() {
    return myTarget.hashCode();
  }

  @Override
  @SuppressWarnings({"SimplifiableIfStatement"})
  public boolean equals(Object obj) {
    if (obj == null) return false;
    if (obj.getClass() != getClass()) return false;
    return ((StringMatcher<?>)obj).myTarget.equals(myTarget);
  }
}
