package org.apache.avro.avsc;


import java.util.ArrayDeque;
import java.util.Collections;
import java.util.Deque;
import java.util.IdentityHashMap;
import java.util.Map;
import java.util.Set;
import java.util.function.Supplier;

import org.apache.avro.JsonProperties;
import org.apache.avro.LogicalType;
import org.apache.avro.Schema;
import java.util.Iterator;
import java.util.List;
import java.util.ListIterator;
import java.util.stream.Stream;

/**
 * Avro Schema utilities, to traverse...
 */
public final class Schemas {

  private Schemas() {
  }

  @SuppressWarnings("checkstyle:MissingSwitchDefault")
  public static void copyAliases(final Schema from, final Schema to) {
    switch (from.getType()) { // only named types.
      case RECORD:
      case ENUM:
      case FIXED:
        Set<String> aliases = from.getAliases();
        for (String alias : aliases) {
          to.addAlias(alias);
        }
    }
  }

  public static void copyAliases(final Schema.Field from, final Schema.Field to) {
    Set<String> aliases = from.aliases();
    for (String alias : aliases) {
      to.addAlias(alias);
    }
  }

  public static void copyLogicalTypes(final Schema from, final Schema to) {
    LogicalType logicalType = from.getLogicalType();
    if (logicalType != null) {
      logicalType.addToSchema(to);
    }
  }

  public static void copyProperties(final JsonProperties from, final JsonProperties to) {
    Map<String, Object> objectProps = from.getObjectProps();
    for (Map.Entry<String, Object> entry : objectProps.entrySet()) {
      to.addProp(entry.getKey(), entry.getValue());
    }
  }

  public static boolean hasGeneratedJavaClass(final Schema schema) {
    Schema.Type type = schema.getType();
    switch (type) {
      case ENUM:
      case RECORD:
      case FIXED:
        return true;
      default:
        return false;
    }
  }

  public static String getJavaClassName(final Schema schema) {
    String namespace = schema.getNamespace();
    if (namespace == null) {
      return mangle(schema.getName());
    } else {
      return namespace + '.' + mangle(schema.getName());
    }
  }

  /**
   * depth first visit.
   *
   * @param start
   * @param visitor
   */
  public static <T> T visit(final Schema start, final SchemaVisitor<T> visitor) {
    // Set of Visited Schemas
    IdentityHashMap<Schema, Schema> visited = new IdentityHashMap<Schema, Schema>();
    // Stack that contains the Schams to process and afterVisitNonTerminal functions.
    // Deque<Either<Schema, Supplier<SchemaVisitorAction>>>
    // Using either has a cost which we want to avoid...
    Deque<Object> dq = new ArrayDeque<Object>();
    dq.addLast(start);
    Object current;
    while ((current = dq.pollLast()) != null) {
      if (current instanceof Supplier) {
        // we are executing a non terminal post visit.
        SchemaVisitorAction action = ((Supplier<SchemaVisitorAction>) current).get();
        switch (action) {
          case CONTINUE:
            break;
          case SKIP_SUBTREE:
            throw new UnsupportedOperationException();
          case SKIP_SIBLINGS:
            while (dq.getLast() instanceof Schema) {
              dq.removeLast();
            }
            break;
          case TERMINATE:
            return visitor.get();
          default:
            throw new UnsupportedOperationException("Invalid action " + action);
        }
      } else {
        Schema schema = (Schema) current;
        boolean terminate;
        if (!visited.containsKey(schema)) {
          Schema.Type type = schema.getType();
          switch (type) {
            case ARRAY:
              terminate = visitNonTerminal(visitor, schema, dq, Collections.singletonList(schema.getElementType()));
              visited.put(schema, schema);
              break;
            case RECORD:
              List<Schema.Field> fields = schema.getFields();
              int fsize = fields.size();
              ListIterator<Schema.Field> it = fields.listIterator(fsize);
              terminate = visitNonTerminal(visitor, schema, dq,
                     () -> Stream.generate(it::previous).limit(fsize).map(Schema.Field::schema).iterator());
              visited.put(schema, schema);
              break;


            case UNION:
              terminate = visitNonTerminal(visitor, schema, dq, schema.getTypes());
              visited.put(schema, schema);
              break;
            case MAP:
              terminate = visitNonTerminal(visitor, schema, dq, Collections.singletonList(schema.getValueType()));
              visited.put(schema, schema);
              break;
            case NULL:
            case BOOLEAN:
            case BYTES:
            case DOUBLE:
            case ENUM:
            case FIXED:
            case FLOAT:
            case INT:
            case LONG:
            case STRING:
              terminate = visitTerminal(visitor, schema, dq);
              break;
            default:
              throw new UnsupportedOperationException("Invalid type " + type);
          }

        } else {
          terminate = visitTerminal(visitor, schema, dq);
        }
        if (terminate) {
          return visitor.get();
        }
      }
    }
    return visitor.get();
  }

  private static boolean visitNonTerminal(final SchemaVisitor visitor,
                                          final Schema schema, final Deque<Object> dq,
                                          final Iterable<Schema> itSupp) {
    SchemaVisitorAction action = visitor.visitNonTerminal(schema);
    switch (action) {
      case CONTINUE:
        dq.addLast(new Supplier<SchemaVisitorAction>() {
          @Override
          public SchemaVisitorAction get() {
            return visitor.afterVisitNonTerminal(schema);
          }
        });
        Iterator<Schema> it = itSupp.iterator();
        while (it.hasNext()) {
          Schema child = it.next();
          dq.addLast(child);
        }
        break;
      case SKIP_SUBTREE:
        break;
      case SKIP_SIBLINGS:
        while (!dq.isEmpty() && dq.getLast() instanceof Schema) {
          dq.removeLast();
        }
        break;
      case TERMINATE:
        return true;
      default:
        throw new UnsupportedOperationException("Invalid action " + action + " for " + schema);
    }
    return false;
  }

  private static boolean visitTerminal(final SchemaVisitor visitor, final Schema schema,
                                       final Deque<Object> dq) {
    SchemaVisitorAction action = visitor.visitTerminal(schema);
    switch (action) {
      case CONTINUE:
        break;
      case SKIP_SUBTREE:
        throw new UnsupportedOperationException("Invalid action " + action + " for " + schema);
      case SKIP_SIBLINGS:
        while (!dq.isEmpty() && dq.getLast() instanceof Schema) {
          dq.removeLast();
        }
        break;
      case TERMINATE:
        return true;
      default:
        throw new UnsupportedOperationException("Invalid action " + action + " for " + schema);
    }
    return false;
  }

  /** Utility for template use.  Adds a dollar sign to reserved words. */
  public static String mangle(final String word) {
    return mangle(word, Collections.EMPTY_SET);
  }


  /** Utility for template use.  Adds a dollar sign to reserved words. */
  public static String mangle(final String word, final Set<String> reservedWords) {
    return mangle(word, reservedWords, false);
  }

  /** Utility for template use.  Adds a dollar sign to reserved words. */
  public static String mangle(final String word, final Set<String> reservedWords,
      final boolean isMethod) {
    if (word.contains(".")) {
      // If the 'word' is really a full path of a class we must mangle just the classname
      int lastDot = word.lastIndexOf('.');
      String packageName = word.substring(0, lastDot + 1);
      String className = word.substring(lastDot + 1);
      return packageName + mangle(className, reservedWords, isMethod);
    }
    if (reservedWords.contains(word)
            || (isMethod && reservedWords.contains(
            Character.toLowerCase(word.charAt(0))
                    + ((word.length() > 1) ? word.substring(1) : "")))) {
      return word + "$";
    }
    return word;
  }

}
