package com.hubspot.jackson.jaxrs;

import java.util.Collection;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Set;
import java.util.StringJoiner;

import com.fasterxml.jackson.core.filter.TokenFilter;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.node.ArrayNode;
import com.fasterxml.jackson.databind.node.ObjectNode;

public class PropertyFilter extends TokenFilter {
  private final NestedPropertyFilter filter = new NestedPropertyFilter();

  public PropertyFilter(Collection<String> properties) {
    for (String property : properties) {
      if (!property.isEmpty()) {
        filter.addProperty(property);
      }
    }

    applyWildcardsToNamedProperties(filter);
  }

  public boolean hasFilters() {
    return filter.hasFilters();
  }

  public void filter(JsonNode node) {
    filter.filter(node);
  }

  public boolean matches(String property) {
    return filter.matches(property);
  }

  @Override
  public TokenFilter includeProperty(String name) {
    return filter.includeProperty(name);
  }

  @Override
  public String toString() {
    return new StringJoiner(", ", "PropertyFilter[", "]")
        .add("filter=" + filter)
        .toString();
  }

  private void applyWildcardsToNamedProperties(NestedPropertyFilter root) {
    if (root.nestedProperties.containsKey("*")) {
      NestedPropertyFilter wildcardFilters = root.nestedProperties.get("*");

      for (Entry<String, NestedPropertyFilter> wildcardSibling : root.nestedProperties.entrySet()) {
        wildcardSibling.getValue().mergeFilters(wildcardFilters);
      }
    } else {
      for (NestedPropertyFilter child : root.nestedProperties.values()) {
        applyWildcardsToNamedProperties(child);
      }
    }
  }

  private static class NestedPropertyFilter extends TokenFilter {
    private final Set<String> includedProperties = new HashSet<String>();
    private final Set<String> excludedProperties = new HashSet<String>();
    private final Map<String, NestedPropertyFilter> nestedProperties = new HashMap<String, NestedPropertyFilter>();

    public void addProperty(String property) {
      boolean excluded = property.startsWith("!");
      if (excluded) {
        property = property.substring(1);
      }

      if (property.contains(".")) {
        String prefix = property.substring(0, property.indexOf('.'));
        String suffix = property.substring(property.indexOf('.') + 1);

        NestedPropertyFilter nestedFilter = nestedProperties.get(prefix);
        if (nestedFilter == null) {
          nestedFilter = new NestedPropertyFilter();
          nestedProperties.put(prefix, nestedFilter);
        }

        if (excluded) {
          nestedFilter.addProperty("!" + suffix);
        } else {
          nestedFilter.addProperty(suffix);
          includedProperties.add(prefix);
        }
      } else if (excluded) {
        excludedProperties.add(property);
      } else {
        includedProperties.add(property);
      }
    }

    public void mergeFilters(NestedPropertyFilter other) {
      includedProperties.addAll(other.includedProperties);
      excludedProperties.addAll(other.excludedProperties);
    }

    public boolean hasFilters() {
      return !(includedProperties.isEmpty() && excludedProperties.isEmpty() && nestedProperties.isEmpty());
    }

    public void filter(JsonNode node) {
      if (node.isObject()) {
        filter((ObjectNode) node);
      } else if (node.isArray()) {
        filter((ArrayNode) node);
      }
    }

    public boolean matches(String property) {
      if (!hasFilters()) {
        return true;
      }

      final String prefix;
      final String suffix;
      if (property.contains(".")) {
        prefix = property.substring(0, property.indexOf('.'));
        suffix = property.substring(property.indexOf('.') + 1);
      } else {
        prefix = property;
        suffix = null;
      }

      if (excludedProperties.contains("*") || excludedProperties.contains(prefix)) {
        return false;
      } else if (includedProperties.contains("*") || includedProperties.contains(prefix)) {
        if (suffix != null && nestedProperties.containsKey(prefix)) {
          return nestedProperties.get(prefix).matches(suffix);
        } else if (nestedProperties.containsKey("*")) {
          return suffix != null && nestedProperties.get("*").matches(suffix);
        } else {
          return true;
        }
      } else if (suffix != null) {
        if (nestedProperties.containsKey(prefix)) {
          return nestedProperties.get(prefix).matches(suffix);
        } else if (nestedProperties.containsKey("*")) {
          return nestedProperties.get("*").matches(suffix);
        } else {
          return includedProperties.isEmpty();
        }
      } else {
        return includedProperties.isEmpty();
      }
    }

    private void filter(ArrayNode array) {
      for (JsonNode node : array) {
        filter(node);
      }
    }

    @Override
    public TokenFilter includeProperty(String name) {
      if (!includedProperties.isEmpty() && !includedProperties.contains("*") && !includedProperties.contains(name)) {
        return null;
      } else if (excludedProperties.contains("*") || excludedProperties.contains(name)) {
        return null;
      } else if (nestedProperties.containsKey(name)) {
        return nestedProperties.get(name);
      } else if (nestedProperties.containsKey("*")) {
        return nestedProperties.get("*");
      } else {
        return TokenFilter.INCLUDE_ALL;
      }
    }

    @Override
    public String toString() {
      return new StringJoiner(", ", "NestedPropertyFilter[", "]")
          .add("includedProperties=" + includedProperties)
          .add("excludedProperties=" + excludedProperties)
          .add("nestedProperties=" + nestedProperties)
          .toString();
    }

    private void filter(ObjectNode object) {
      if (!includedProperties.isEmpty() && !includedProperties.contains("*")) {
        object.retain(includedProperties);
      }

      if (excludedProperties.contains("*")) {
        object.removeAll();
      } else {
        object.remove(excludedProperties);
      }

      Iterator<Entry<String, JsonNode>> fields = object.fields();
      while (fields.hasNext()) {
        Entry<String, JsonNode> field = fields.next();

        if (nestedProperties.containsKey(field.getKey())) {
          nestedProperties.get(field.getKey()).filter(field.getValue());
        } else if (nestedProperties.containsKey("*")) {
          nestedProperties.get("*").filter(field.getValue());
        }
      }
    }
  }
}