/*
 * 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.avro.compiler.schema;

import java.util.ArrayList;
import java.util.IdentityHashMap;
import java.util.List;

import org.apache.avro.Schema;

import static org.apache.avro.Schema.Type.RECORD;

/**
 * this visitor will create a clone of the original Schema with docs and other
 * nonessential fields stripped by default. what attributes are copied is
 * customizable.
 */
public final class CloningVisitor implements SchemaVisitor<Schema> {

  private final IdentityHashMap<Schema, Schema> replace = new IdentityHashMap<>();

  private final Schema root;

  private final PropertyCopier copyProperties;

  private final boolean copyDocs;

  public interface PropertyCopier {
    void copy(Schema first, Schema second);

    void copy(Schema.Field first, Schema.Field second);
  }

  /**
   * copy only serialization necessary fields.
   *
   * @param root
   */
  public CloningVisitor(final Schema root) {
    this(new PropertyCopier() {
      @Override
      public void copy(final Schema first, final Schema second) {
        Schemas.copyLogicalTypes(first, second);
        Schemas.copyAliases(first, second);
      }

      @Override
      public void copy(final Schema.Field first, final Schema.Field second) {
        Schemas.copyAliases(first, second);
      }
    }, false, root);
  }

  public CloningVisitor(final PropertyCopier copyProperties, final boolean copyDocs, final Schema root) {
    this.copyProperties = copyProperties;
    this.copyDocs = copyDocs;
    this.root = root;
  }

  @Override
  public SchemaVisitorAction visitTerminal(final Schema terminal) {
    Schema.Type type = terminal.getType();
    Schema newSchema;
    switch (type) {
    case RECORD: // recursion.
    case ARRAY:
    case MAP:
    case UNION:
      if (!replace.containsKey(terminal)) {
        throw new IllegalStateException("Schema " + terminal + " must be already processed");
      }
      return SchemaVisitorAction.CONTINUE;
    case BOOLEAN:
    case BYTES:
    case DOUBLE:
    case FLOAT:
    case INT:
    case LONG:
    case NULL:
    case STRING:
      newSchema = Schema.create(type);
      break;
    case ENUM:
      newSchema = Schema.createEnum(terminal.getName(), copyDocs ? terminal.getDoc() : null, terminal.getNamespace(),
          terminal.getEnumSymbols());
      break;
    case FIXED:
      newSchema = Schema.createFixed(terminal.getName(), copyDocs ? terminal.getDoc() : null, terminal.getNamespace(),
          terminal.getFixedSize());
      break;
    default:
      throw new IllegalStateException("Unsupported schema " + terminal);
    }
    copyProperties.copy(terminal, newSchema);
    replace.put(terminal, newSchema);
    return SchemaVisitorAction.CONTINUE;
  }

  @Override
  public SchemaVisitorAction visitNonTerminal(final Schema nt) {
    Schema.Type type = nt.getType();
    if (type == RECORD) {
      Schema newSchema = Schema.createRecord(nt.getName(), copyDocs ? nt.getDoc() : null, nt.getNamespace(),
          nt.isError());
      copyProperties.copy(nt, newSchema);
      replace.put(nt, newSchema);
    }
    return SchemaVisitorAction.CONTINUE;
  }

  @Override
  public SchemaVisitorAction afterVisitNonTerminal(final Schema nt) {
    Schema.Type type = nt.getType();
    Schema newSchema;
    switch (type) {
    case RECORD:
      newSchema = replace.get(nt);
      List<Schema.Field> fields = nt.getFields();
      List<Schema.Field> newFields = new ArrayList<>(fields.size());
      for (Schema.Field field : fields) {
        Schema.Field newField = new Schema.Field(field.name(), replace.get(field.schema()),
            copyDocs ? field.doc() : null, field.defaultVal(), field.order());
        copyProperties.copy(field, newField);
        newFields.add(newField);
      }
      newSchema.setFields(newFields);
      return SchemaVisitorAction.CONTINUE;
    case UNION:
      List<Schema> types = nt.getTypes();
      List<Schema> newTypes = new ArrayList<>(types.size());
      for (Schema sch : types) {
        newTypes.add(replace.get(sch));
      }
      newSchema = Schema.createUnion(newTypes);
      break;
    case ARRAY:
      newSchema = Schema.createArray(replace.get(nt.getElementType()));
      break;
    case MAP:
      newSchema = Schema.createMap(replace.get(nt.getValueType()));
      break;
    default:
      throw new IllegalStateException("Illegal type " + type + ", schema " + nt);
    }
    copyProperties.copy(nt, newSchema);
    replace.put(nt, newSchema);
    return SchemaVisitorAction.CONTINUE;
  }

  @Override
  public Schema get() {
    return replace.get(root);
  }

  @Override
  public String toString() {
    return "CloningVisitor{" + "replace=" + replace + ", root=" + root + '}';
  }

}
