/**
 * 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
 *
 *     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.apache.avro.generic;

import java.io.IOException;

import org.apache.avro.AvroRuntimeException;
import org.apache.avro.Schema;
import org.apache.avro.Schema.Field;
import org.apache.avro.data.RecordBuilderBase;
import org.apache.avro.generic.GenericData.Record;

/** A RecordBuilder for generic records. GenericRecordBuilder fills in default values
 * for fields if they are not specified.  */
public class GenericRecordBuilder extends RecordBuilderBase<Record> {
  private final GenericData.Record record;

  /**
   * Creates a GenericRecordBuilder for building Record instances.
   * @param schema the schema associated with the record class.
   */
  public GenericRecordBuilder(Schema schema) {
    super(schema, GenericData.get());
    record = new GenericData.Record(schema);
  }

  /**
   * Creates a GenericRecordBuilder by copying an existing GenericRecordBuilder.
   * @param other the GenericRecordBuilder to copy.
   */
  public GenericRecordBuilder(GenericRecordBuilder other) {
    super(other, GenericData.get());
    record = new GenericData.Record(other.record, /* deepCopy = */ true);
  }

  /**
   * Creates a GenericRecordBuilder by copying an existing record instance.
   * @param other the record instance to copy.
   */
  public GenericRecordBuilder(Record other) {
    super(other.getSchema(), GenericData.get());
    record = new GenericData.Record(other, /* deepCopy = */ true);

    // Set all fields in the RecordBuilder that are set in the record
    for (Field f : schema().getFields()) {
      Object value = other.get(f.pos());
      // Only set the value if it is not null, if the schema type is null,
      // or if the schema type is a union that accepts nulls.
      if (isValidValue(f, value)) {
        set(f, data().deepCopy(f.schema(), value));
      }
    }
  }

  /**
   * Gets the value of a field.
   * @param fieldName the name of the field to get.
   * @return the value of the field with the given name, or null if not set.
   */
  public Object get(String fieldName) {
    return get(schema().getField(fieldName));
  }

  /**
   * Gets the value of a field.
   * @param field the field to get.
   * @return the value of the given field, or null if not set.
   */
  public Object get(Field field) {
    return get(field.pos());
  }

  /**
   * Gets the value of a field.
   * @param pos the position of the field to get.
   * @return the value of the field with the given position, or null if not set.
   */
  protected Object get(int pos) {
    return record.get(pos);
  }

  /**
   * Sets the value of a field.
   * @param fieldName the name of the field to set.
   * @param value the value to set.
   * @return a reference to the RecordBuilder.
   */
  public GenericRecordBuilder set(String fieldName, Object value) {
    return set(schema().getField(fieldName), value);
  }

  /**
   * Sets the value of a field.
   * @param field the field to set.
   * @param value the value to set.
   * @return a reference to the RecordBuilder.
   */
  public GenericRecordBuilder set(Field field, Object value) {
    return set(field, field.pos(), value);
  }

  /**
   * Sets the value of a field.
   * @param pos the field to set.
   * @param value the value to set.
   * @return a reference to the RecordBuilder.
   */
  protected GenericRecordBuilder set(int pos, Object value) {
    return set(fields()[pos], pos, value);
  }

  /**
   * Sets the value of a field.
   * @param field the field to set.
   * @param pos the position of the field.
   * @param value the value to set.
   * @return a reference to the RecordBuilder.
   */
  private GenericRecordBuilder set(Field field, int pos, Object value) {
    validate(field, value);
    record.put(pos, value);
    fieldSetFlags()[pos] = true;
    return this;
  }

  /**
   * Checks whether a field has been set.
   * @param fieldName the name of the field to check.
   * @return true if the given field is non-null; false otherwise.
   */
  public boolean has(String fieldName) {
    return has(schema().getField(fieldName));
  }

  /**
   * Checks whether a field has been set.
   * @param field the field to check.
   * @return true if the given field is non-null; false otherwise.
   */
  public boolean has(Field field) {
    return has(field.pos());
  }

  /**
   * Checks whether a field has been set.
   * @param pos the position of the field to check.
   * @return true if the given field is non-null; false otherwise.
   */
  protected boolean has(int pos) {
    return fieldSetFlags()[pos];
  }

  /**
   * Clears the value of the given field.
   * @param fieldName the name of the field to clear.
   * @return a reference to the RecordBuilder.
   */
  public GenericRecordBuilder clear(String fieldName) {
    return clear(schema().getField(fieldName));
  }

  /**
   * Clears the value of the given field.
   * @param field the field to clear.
   * @return a reference to the RecordBuilder.
   */
  public GenericRecordBuilder clear(Field field) {
    return clear(field.pos());
  }

  /**
   * Clears the value of the given field.
   * @param pos the position of the field to clear.
   * @return a reference to the RecordBuilder.
   */
  protected GenericRecordBuilder clear(int pos) {
    record.put(pos, null);
    fieldSetFlags()[pos] = false;
    return this;
  }

  @Override
  public Record build() {
    Record record;
    try {
      record = new GenericData.Record(schema());
    } catch (Exception e) {
      throw new AvroRuntimeException(e);
    }

    for (Field field : fields()) {
      Object value;
      try {
        value = getWithDefault(field);
      } catch(IOException e) {
        throw new AvroRuntimeException(e);
      }
      if (value != null) {
        record.put(field.pos(), value);
      }
    }

    return record;
  }

  /**
   * Gets the value of the given field.
   * If the field has been set, the set value is returned (even if it's null).
   * If the field hasn't been set and has a default value, the default value
   * is returned.
   * @param field the field whose value should be retrieved.
   * @return the value set for the given field, the field's default value,
   * or null.
   * @throws IOException
   */
  private Object getWithDefault(Field field) throws IOException {
    return fieldSetFlags()[field.pos()] ?
        record.get(field.pos()) : defaultValue(field);
  }

  @Override
  public int hashCode() {
    final int prime = 31;
    int result = super.hashCode();
    result = prime * result + ((record == null) ? 0 : record.hashCode());
    return result;
  }

  @Override
  public boolean equals(Object obj) {
    if (this == obj)
      return true;
    if (!super.equals(obj))
      return false;
    if (getClass() != obj.getClass())
      return false;
    GenericRecordBuilder other = (GenericRecordBuilder) obj;
    if (record == null) {
      if (other.record != null)
        return false;
    } else if (!record.equals(other.record))
      return false;
    return true;
  }
}
