/*
 * Licensed to the Apache Software Foundation (ASF) under one
 * or more contributor license agreements.  See the NOTICE file
 * distributed with this work for additional information
 * regarding copyright ownership.  The ASF licenses this file
 * to you 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
 *
 *   https://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.apache.accumulo.access.examples;

import static org.apache.accumulo.access.ParsedAccessExpression.ExpressionType.AND;
import static org.apache.accumulo.access.ParsedAccessExpression.ExpressionType.AUTHORIZATION;

import java.util.Map;
import java.util.SortedSet;
import java.util.TreeSet;

import org.apache.accumulo.access.Access;
import org.apache.accumulo.access.ParsedAccessExpression;
import org.apache.accumulo.access.ParsedAccessExpression.ExpressionType;

/**
 * Examples of using the parse tree in {@link ParsedAccessExpression} to inspect and transform
 * access expressions.
 */
public class ParseExamples {

  public static final Access ACCESS = Access.builder().build();

  private ParseExamples() {}

  /**
   * This example will replace authorizations in an access expression.
   */
  public static void replaceAuthorizations(ParsedAccessExpression parsed,
      StringBuilder expressionBuilder, Map<String,String> replacements) {
    if (parsed.getType() == AUTHORIZATION) {
      // If the term is quoted in the expression, the quotes will be preserved. Calling unquote()
      // will only unescape and unquote if the string is quoted, otherwise it returns the string as
      // is.
      String auth = ACCESS.unquote(parsed.getExpression());
      // Must quote any authorization that needs it. Calling quote() will only quote and escape if
      // needed, otherwise it returns the string as is.
      expressionBuilder.append(ACCESS.quote(replacements.getOrDefault(auth, auth)));
    } else {
      String operator = parsed.getType() == AND ? "&" : "|";
      String sep = "";

      for (var childExpression : parsed.getChildren()) {
        expressionBuilder.append(sep);
        if (childExpression.getType() == AUTHORIZATION) {
          replaceAuthorizations(childExpression, expressionBuilder, replacements);
        } else {
          expressionBuilder.append("(");
          replaceAuthorizations(childExpression, expressionBuilder, replacements);
          expressionBuilder.append(")");
        }
        sep = operator;
      }
    }
  }

  /**
   * As part of normalizing access expression this class is used to sort and dedupe sub-expressions
   * in a tree set.
   */
  public static class NormalizedExpression implements Comparable<NormalizedExpression> {
    public final String expression;
    public final ExpressionType type;

    NormalizedExpression(String expression, ExpressionType type) {
      this.expression = expression;
      this.type = type;
    }

    // determines the sort order of different kinds of subexpressions.
    private static int typeOrder(ExpressionType type) {
      return switch (type) {
        case AUTHORIZATION -> 1;
        case OR -> 2;
        case AND -> 3;
        case EMPTY -> throw new IllegalArgumentException("Unexpected type " + type);
      };
    }

    @Override
    public int compareTo(NormalizedExpression o) {
      // Changing this comparator would significantly change how expressions are normalized.
      int cmp = typeOrder(type) - typeOrder(o.type);
      if (cmp == 0) {
        if (type == AUTHORIZATION) {
          // sort based on the unquoted and unescaped form of the authorization
          cmp = ACCESS.unquote(expression).compareTo(ACCESS.unquote(o.expression));
        } else {
          cmp = expression.compareTo(o.expression);
        }

      }
      return cmp;
    }

    @Override
    public boolean equals(Object o) {
      return this == o || (o instanceof NormalizedExpression n && compareTo(n) == 0);
    }

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

  /**
   * This method helps with the flattening aspect of normalization by recursing down as far as
   * possible the parse tree in the case when the expression type is the same. As long as the type
   * is the same in the sub expression, keep using the same set.
   */
  public static void flatten(ExpressionType parentType, ParsedAccessExpression parsed,
      SortedSet<NormalizedExpression> normalizedExpressions) {
    if (parsed.getType() == parentType) {
      for (var child : parsed.getChildren()) {
        flatten(parentType, child, normalizedExpressions);
      }
    } else {
      // The type changed, so start again on the subexpression.
      normalizedExpressions.add(normalize(parsed));
    }
  }

  /**
   * <p>
   * For a given access expression this example will deduplicate, sort, flatten, and remove unneeded
   * parentheses or quotes in the expressions. The following list gives examples of what each
   * normalization step does.
   *
   * <ul>
   * <li>As an example of flattening, the expression {@code A&(B&C)} flattens to {@code
   * A&B&C}.</li>
   * <li>As an example of sorting, the expression {@code (Z&Y)|(C&B)} sorts to {@code
   * (B&C)|(Y&Z)}</li>
   * <li>As an example of deduplication, the expression {@code X&Y&X} normalizes to {@code X&Y}</li>
   * <li>As an example of unneeded quotes, the expression {@code "ABC"&"XYZ"} normalizes to
   * {@code ABC&XYZ}</li>
   * <li>As an example of unneeded parentheses, the expression {@code (((ABC)|(XYZ)))} normalizes to
   * {@code ABC|XYZ}</li>
   * </ul>
   *
   * <p>
   * This algorithm attempts to have the same behavior as the one in the Accumulo 2.1
   * ColumnVisibility class. However the implementation is very different.
   */
  public static NormalizedExpression normalize(ParsedAccessExpression parsed) {
    if (parsed.getType() == AUTHORIZATION) {
      // If the authorization is quoted and it does not need to be quoted then the following two
      // lines will remove the unnecessary quoting.
      String unquoted = ACCESS.unquote(parsed.getExpression());
      String quoted = ACCESS.quote(unquoted);
      return new NormalizedExpression(quoted, parsed.getType());
    } else {
      // The tree set does the work of sorting and deduplicating sub expressions.
      TreeSet<NormalizedExpression> normalizedChildren = new TreeSet<>();
      for (var child : parsed.getChildren()) {
        flatten(parsed.getType(), child, normalizedChildren);
      }

      if (normalizedChildren.size() == 1) {
        return normalizedChildren.first();
      } else {
        String operator = parsed.getType() == AND ? "&" : "|";
        String sep = "";

        StringBuilder builder = new StringBuilder();

        for (var child : normalizedChildren) {
          builder.append(sep);
          if (child.type == AUTHORIZATION) {
            builder.append(child.expression);
          } else {
            builder.append("(");
            builder.append(child.expression);
            builder.append(")");
          }
          sep = operator;
        }

        return new NormalizedExpression(builder.toString(), parsed.getType());
      }
    }
  }

  /**
   * This example recursively prints a parse tree.
   */
  public static void walk(String indent, ParsedAccessExpression parsed) {
    System.out.println(indent + parsed + " type:" + parsed.getType());
    for (var child : parsed.getChildren()) {
      walk(indent + "  ", child);
    }
  }

  public static void main(String[] args) {

    var parsed = ACCESS.newParsedExpression("((RED&GREEN)|(PINK&BLUE))");

    System.out.printf("Operating on %s%n", parsed);

    System.out.printf("%n  Normalized to %s%n", normalize(parsed).expression);
    StringBuilder expressionBuilder = new StringBuilder();
    replaceAuthorizations(parsed, expressionBuilder, Map.of("GREEN", "GREY"));
    System.out.printf("%n  Replaced GREEN with GREY : %s%n", expressionBuilder);
    System.out.println("\n  Walking :");
    walk("    ", parsed);
  }
}
