/*
 * 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.parquet.thrift;
import java.util.*;

import org.apache.parquet.hadoop.BadConfigurationException;
import org.apache.parquet.thrift.struct.ThriftType;
import org.apache.parquet.thrift.struct.ThriftType.StructType;
import org.apache.thrift.TBase;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
 * Metadata for thrift stored in the file footer
 */
public class ThriftMetaData {
  private static final Logger LOG = LoggerFactory.getLogger(ThriftMetaData.class);

  private static final String THRIFT_CLASS = "thrift.class";
  private static final String THRIFT_DESCRIPTOR = "thrift.descriptor";
  private Class<?> thriftClass;
  private final String thriftClassName;
  private final StructType descriptor;

  /**
   * @param thriftClassName the class used to serialize
   * @param descriptor the json representation of the thrift structure
   */
  public ThriftMetaData(String thriftClassName, StructType descriptor) {
    this.thriftClassName = thriftClassName;
    this.descriptor = descriptor;
  }

  /**
   * Get the Thrift Class encoded in the metadata.
   * @return Thrift Class encoded in the metadata.
   * @throws BadConfigurationException if the encoded class does not
   * extend TBase or is not available in the current classloader.
   */
  public Class<?> getThriftClass() {
    if (thriftClass == null) {
      thriftClass = getThriftClass(thriftClassName);
    }
    return thriftClass;
  }

  /**
   * @param thriftClassName the name of the thrift class
   * @return the class
   */
  public static Class<?> getThriftClass(String thriftClassName) {
    try {
      Class<?> thriftClass = Class.forName(thriftClassName);
      return thriftClass;
    } catch (ClassNotFoundException e) {
      throw new BadConfigurationException("Could not instantiate thrift class " + thriftClassName, e);
    }
  }

  /**
   * @return the thrift descriptor
   */
  public StructType getDescriptor() {
    return descriptor;
  }

  /**
   * Reads ThriftMetadata from the parquet file footer.
   *
   * @param extraMetaData  extraMetaData field of the parquet footer
   * @return the ThriftMetaData used to write a data file
   */
  public static ThriftMetaData fromExtraMetaData(
      Map<String, String> extraMetaData) {
    final String thriftClassName = extraMetaData.get(THRIFT_CLASS);
    final String thriftDescriptorString = extraMetaData.get(THRIFT_DESCRIPTOR);
    if (thriftClassName == null || thriftDescriptorString == null) {
      return null;
    }
    final StructType descriptor = parseDescriptor(thriftDescriptorString);
    return new ThriftMetaData(thriftClassName, descriptor);
  }

  /**
   * Creates ThriftMetaData from a Thrift-generated class.
   *
   * @param thriftClass a Thrift-generated class
   * @return ThriftMetaData for the given class
   */
  @SuppressWarnings("unchecked")
  public static ThriftMetaData fromThriftClass(Class<?> thriftClass) {
    if (thriftClass != null && TBase.class.isAssignableFrom(thriftClass)) {
      Class<? extends TBase<?, ?>> tClass = (Class<? extends TBase<?, ?>>) thriftClass;
      StructType descriptor = new ThriftSchemaConverter().toStructType(tClass);
      return new ThriftMetaData(thriftClass.getName(), descriptor);
    }
    return null;
  }

  private static StructType parseDescriptor(String json) {
    try {
      return (StructType)ThriftType.fromJSON(json);
    } catch (RuntimeException e) {
      throw new BadConfigurationException("Could not read the thrift descriptor " + json, e);
    }
  }

  /**
   * generates a map of key values to store in the footer
   * @return the key values
   */
  public Map<String, String> toExtraMetaData() {
    final Map<String, String> map = new HashMap<String, String>();
    map.put(THRIFT_CLASS, getThriftClass().getName());
    map.put(THRIFT_DESCRIPTOR, descriptor.toJSON());
    return map;
  }

  /**
   * @param fileMetadata the merged metadata from ultiple files
   * @return the list of thrift classes used to write them
   */
  public static Set<String> getThriftClassNames(Map<String, Set<String>> fileMetadata) {
    return fileMetadata.get(THRIFT_CLASS);
  }

  @Override
  public String toString() {
    return String.format("ThriftMetaData(thriftClassName: %s, descriptor: %s)", thriftClassName, descriptor);
  }
}


/*
 * 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.parquet.column.impl;

import java.io.IOException;

import org.apache.parquet.column.ColumnDescriptor;
import org.apache.parquet.column.ColumnWriter;
import org.apache.parquet.column.ParquetProperties;
import org.apache.parquet.column.page.DictionaryPage;
import org.apache.parquet.column.page.PageWriter;
import org.apache.parquet.column.statistics.Statistics;
import org.apache.parquet.column.values.ValuesWriter;
import org.apache.parquet.io.ParquetEncodingException;
import org.apache.parquet.io.api.Binary;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
 * Base implementation for {@link ColumnWriter} to be extended to specialize for V1 and V2 pages.
 */
abstract class ColumnWriterBase implements ColumnWriter {
  private static final Logger LOG = LoggerFactory.getLogger(ColumnWriterBase.class);

  // By default: Debugging disabled this way (using the "if (DEBUG)" IN the methods) to allow
  // the java compiler (not the JIT) to remove the unused statements during build time.
  private static final boolean DEBUG = false;

  final ColumnDescriptor path;
  final PageWriter pageWriter;
  private ValuesWriter repetitionLevelColumn;
  private ValuesWriter definitionLevelColumn;
  private ValuesWriter dataColumn;
  private int valueCount;

  private Statistics<?> statistics;
  private long rowsWrittenSoFar = 0;
  private int pageRowCount;

  ColumnWriterBase(
      ColumnDescriptor path,
      PageWriter pageWriter,
      ParquetProperties props) {
    this.path = path;
    this.pageWriter = pageWriter;
    resetStatistics();

    this.repetitionLevelColumn = createRLWriter(props, path);
    this.definitionLevelColumn = createDLWriter(props, path);
    this.dataColumn = props.newValuesWriter(path);
  }

  abstract ValuesWriter createRLWriter(ParquetProperties props, ColumnDescriptor path);

  abstract ValuesWriter createDLWriter(ParquetProperties props, ColumnDescriptor path);

  private void log(Object value, int r, int d) {
    LOG.debug("{} {} r:{} d:{}", path, value, r, d);
  }

  private void resetStatistics() {
    this.statistics = Statistics.createStats(path.getPrimitiveType());
  }

  private void definitionLevel(int definitionLevel) {
    definitionLevelColumn.writeInteger(definitionLevel);
  }

  private void repetitionLevel(int repetitionLevel) {
    repetitionLevelColumn.writeInteger(repetitionLevel);
    assert pageRowCount == 0 ? repetitionLevel == 0 : true : "Every page shall start on record boundaries";
    if (repetitionLevel == 0) {
      ++pageRowCount;
    }
  }

  /**
   * Writes the current null value
   *
   * @param repetitionLevel
   * @param definitionLevel
   */
  @Override
  public void writeNull(int repetitionLevel, int definitionLevel) {
    if (DEBUG)
      log(null, repetitionLevel, definitionLevel);
    repetitionLevel(repetitionLevel);
    definitionLevel(definitionLevel);
    statistics.incrementNumNulls();
    ++valueCount;
  }

  @Override
  public void close() {
    // Close the Values writers.
    repetitionLevelColumn.close();
    definitionLevelColumn.close();
    dataColumn.close();
  }

  @Override
  public long getBufferedSizeInMemory() {
    return repetitionLevelColumn.getBufferedSize()
        + definitionLevelColumn.getBufferedSize()
        + dataColumn.getBufferedSize()
        + pageWriter.getMemSize();
  }

  /**
   * Writes the current value
   *
   * @param value
   * @param repetitionLevel
   * @param definitionLevel
   */
  @Override
  public void write(double value, int repetitionLevel, int definitionLevel) {
    if (DEBUG)
      log(value, repetitionLevel, definitionLevel);
    repetitionLevel(repetitionLevel);
    definitionLevel(definitionLevel);
    dataColumn.writeDouble(value);
    statistics.updateStats(value);
    ++valueCount;
  }

  /**
   * Writes the current value
   *
   * @param value
   * @param repetitionLevel
   * @param definitionLevel
   */
  @Override
  public void write(float value, int repetitionLevel, int definitionLevel) {
    if (DEBUG)
      log(value, repetitionLevel, definitionLevel);
    repetitionLevel(repetitionLevel);
    definitionLevel(definitionLevel);
    dataColumn.writeFloat(value);
    statistics.updateStats(value);
    ++valueCount;
  }

  /**
   * Writes the current value
   *
   * @param value
   * @param repetitionLevel
   * @param definitionLevel
   */
  @Override
  public void write(Binary value, int repetitionLevel, int definitionLevel) {
    if (DEBUG)
      log(value, repetitionLevel, definitionLevel);
    repetitionLevel(repetitionLevel);
    definitionLevel(definitionLevel);
    dataColumn.writeBytes(value);
    statistics.updateStats(value);
    ++valueCount;
  }

  /**
   * Writes the current value
   *
   * @param value
   * @param repetitionLevel
   * @param definitionLevel
   */
  @Override
  public void write(boolean value, int repetitionLevel, int definitionLevel) {
    if (DEBUG)
      log(value, repetitionLevel, definitionLevel);
    repetitionLevel(repetitionLevel);
    definitionLevel(definitionLevel);
    dataColumn.writeBoolean(value);
    statistics.updateStats(value);
    ++valueCount;
  }

  /**
   * Writes the current value
   *
   * @param value
   * @param repetitionLevel
   * @param definitionLevel
   */
  @Override
  public void write(int value, int repetitionLevel, int definitionLevel) {
    if (DEBUG)
      log(value, repetitionLevel, definitionLevel);
    repetitionLevel(repetitionLevel);
    definitionLevel(definitionLevel);
    dataColumn.writeInteger(value);
    statistics.updateStats(value);
    ++valueCount;
  }

  /**
   * Writes the current value
   *
   * @param value
   * @param repetitionLevel
   * @param definitionLevel
   */
  @Override
  public void write(long value, int repetitionLevel, int definitionLevel) {
    if (DEBUG)
      log(value, repetitionLevel, definitionLevel);
    repetitionLevel(repetitionLevel);
    definitionLevel(definitionLevel);
    dataColumn.writeLong(value);
    statistics.updateStats(value);
    ++valueCount;
  }

  /**
   * Finalizes the Column chunk. Possibly adding extra pages if needed (dictionary, ...)
   * Is called right after writePage
   */
  void finalizeColumnChunk() {
    final DictionaryPage dictionaryPage = dataColumn.toDictPageAndClose();
    if (dictionaryPage != null) {
      if (DEBUG)
        LOG.debug("write dictionary");
      try {
        pageWriter.writeDictionaryPage(dictionaryPage);
      } catch (IOException e) {
        throw new ParquetEncodingException("could not write dictionary page for " + path, e);
      }
      dataColumn.resetDictionary();
    }
  }

  /**
   * Used to decide when to write a page
   *
   * @return the number of bytes of memory used to buffer the current data
   */
  long getCurrentPageBufferedSize() {
    return repetitionLevelColumn.getBufferedSize()
        + definitionLevelColumn.getBufferedSize()
        + dataColumn.getBufferedSize();
  }

  /**
   * Used to decide when to write a page or row group
   *
   * @return the number of bytes of memory used to buffer the current data and the previously written pages
   */
  long getTotalBufferedSize() {
    return repetitionLevelColumn.getBufferedSize()
        + definitionLevelColumn.getBufferedSize()
        + dataColumn.getBufferedSize()
        + pageWriter.getMemSize();
  }

  /**
   * @return actual memory used
   */
  long allocatedSize() {
    return repetitionLevelColumn.getAllocatedSize()
        + definitionLevelColumn.getAllocatedSize()
        + dataColumn.getAllocatedSize()
        + pageWriter.allocatedSize();
  }

  /**
   * @param indent
   *          a prefix to format lines
   * @return a formatted string showing how memory is used
   */
  String memUsageString(String indent) {
    StringBuilder b = new StringBuilder(indent).append(path).append(" {\n");
    b.append(indent).append(" r:").append(repetitionLevelColumn.getAllocatedSize()).append(" bytes\n");
    b.append(indent).append(" d:").append(definitionLevelColumn.getAllocatedSize()).append(" bytes\n");
    b.append(dataColumn.memUsageString(indent + "  data:")).append("\n");
    b.append(pageWriter.memUsageString(indent + "  pages:")).append("\n");
    b.append(indent).append(String.format("  total: %,d/%,d", getTotalBufferedSize(), allocatedSize())).append("\n");
    b.append(indent).append("}\n");
    return b.toString();
  }

  long getRowsWrittenSoFar() {
    return this.rowsWrittenSoFar;
  }

  /**
   * Writes the current data to a new page in the page store
   */
  void writePage() {
    if (valueCount == 0) {
      throw new ParquetEncodingException("writing empty page");
    }
    this.rowsWrittenSoFar += pageRowCount;
    if (DEBUG)
      LOG.debug("write page");
    try {
      writePage(pageRowCount, valueCount, statistics, repetitionLevelColumn, definitionLevelColumn, dataColumn);
    } catch (IOException e) {
      throw new ParquetEncodingException("could not write page for " + path, e);
    }
    repetitionLevelColumn.reset();
    definitionLevelColumn.reset();
    dataColumn.reset();
    valueCount = 0;
    resetStatistics();
    pageRowCount = 0;
  }

  abstract void writePage(int rowCount, int valueCount, Statistics<?> statistics, ValuesWriter repetitionLevels,
      ValuesWriter definitionLevels, ValuesWriter values) throws IOException;
}


/*
 * 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.parquet.thrift.projection;

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

import org.apache.parquet.Strings;
import org.apache.parquet.glob.WildcardPath;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
 * Stricter Implementation of {@link FieldProjectionFilter}.
 *
 * See {@link org.apache.parquet.thrift.projection.deprecated.DeprecatedFieldProjectionFilter} for the previous
 * syntax that allows for more powerful glob patterns, but has less error reporting and less strict requirements.
 *
 * This filter requires that every *possible* expansion of glob expressions (like '{x,y,z}') must match at least one
 * column. Each expansion may match more than one if it contains wildcards ('*').
 *
 * Note that this class is stateful -- it keeps track of which expanded glob paths have matched a column, so that it can
 * throw when {@link #assertNoUnmatchedPatterns()} is called.
 */
public class StrictFieldProjectionFilter implements FieldProjectionFilter {
  private static final Logger LOG = LoggerFactory.getLogger(FieldProjectionFilter.class);
  private static final String GLOB_SEPARATOR = ";";

  // use a list instead of a Set, so we can detect overlapping patterns and
  // warn about it.
  private final List<WildcardPathStatus> columnsToKeep;

  // visible for testing
  static List<String> parseSemicolonDelimitedString(String columnsToKeepGlobs) {
    String[] splits = columnsToKeepGlobs.split(GLOB_SEPARATOR);
    List<String> globs = new ArrayList<String>();
    for (String s : splits) {
      if (!s.isEmpty()) {
        globs.add(s);
      }
    }

    if (globs.isEmpty()) {
      throw new ThriftProjectionException(String.format("Semicolon delimited string '%s' contains 0 glob strings",
          columnsToKeepGlobs));
    }

    return globs;
  }

  /**
   * Construct a StrictFieldProjectionFilter from a single string.
   *
   * columnsToKeepGlobs should be a list of Strings in the format expected by
   * {@link Strings#expandGlobToWildCardPaths(String, char)}, separated by ';'
   * Should only be used for parsing values out of the hadoop config -- for APIs
   * and programmatic access, use {@link #StrictFieldProjectionFilter(List)}.
   *
   * @param columnsToKeepGlobs glob pattern for columns to keep
   * @return a field projection filter
   */
  public static StrictFieldProjectionFilter fromSemicolonDelimitedString(String columnsToKeepGlobs) {
    return new StrictFieldProjectionFilter(parseSemicolonDelimitedString(columnsToKeepGlobs));
  }

  /**
   * Construct a StrictFieldProjectionFilter from a list of Strings in the format expected by
   * {@link Strings#expandGlobToWildCardPaths(String, char)}
   * @param columnsToKeepGlobs glob patterns for columns to keep
   */
  public StrictFieldProjectionFilter(List<String> columnsToKeepGlobs) {
    this.columnsToKeep = new ArrayList<WildcardPathStatus>();
    for (String glob : columnsToKeepGlobs) {
      for (WildcardPath wp : Strings.expandGlobToWildCardPaths(glob, '.')) {
        columnsToKeep.add(new WildcardPathStatus(wp));
      }
    }
  }

  @Override
  public boolean keep(FieldsPath path) {
    return keep(path.toDelimitedString("."));
  }

  // visible for testing
  boolean keep(String path) {
    WildcardPath match = null;

    // since we have a rule of every path must match at least one column,
    // we visit every single wildcard path, instead of short circuiting,
    // for the case where more than one pattern matches a column. Otherwise
    // we'd get a misleading exception saying a path didn't match a column,
    // even though it looks like it should have (but didn't because of short circuiting).
    // This also allows us log a warning when more than one glob path matches.
    for (WildcardPathStatus wp : columnsToKeep) {
      if (wp.matches(path)) {
        if (match != null && !match.getParentGlobPath().equals(wp.getWildcardPath().getParentGlobPath())) {
          String message = "Field path: '%s' matched more than one glob path pattern. First match: " +
              "'%s' (when expanded to '%s') second match:'%s' (when expanded to '%s')";

          warn(String.format(message,
              path, match.getParentGlobPath(), match.getOriginalPattern(),
              wp.getWildcardPath().getParentGlobPath(), wp.getWildcardPath().getOriginalPattern()));
        } else {
          match = wp.getWildcardPath();
        }
      }
    }

    return match != null;
  }

  // visible for testing
  protected void warn(String warning) {
    LOG.warn(warning);
  }

  private List<WildcardPath> getUnmatchedPatterns() {
    List<WildcardPath> unmatched = new ArrayList<WildcardPath>();
    for (WildcardPathStatus wp : columnsToKeep) {
      if (!wp.hasMatched()) {
        unmatched.add(wp.getWildcardPath());
      }
    }
    return unmatched;
  }

  @Override
  public void assertNoUnmatchedPatterns() throws ThriftProjectionException{
    List<WildcardPath> unmatched = getUnmatchedPatterns();
    if (!unmatched.isEmpty()) {
      StringBuilder message =
          new StringBuilder("The following projection patterns did not match any columns in this schema:\n");
      for (WildcardPath wp : unmatched) {
        message.append(String.format("Pattern: '%s' (when expanded to '%s')",
            wp.getParentGlobPath(), wp.getOriginalPattern()));
        message.append('\n');
      }
      throw new ThriftProjectionException(message.toString());
    }
  }

  /**
   * Holds a WildcardPath and a boolean, used to track whether
   * this path has ever matched anything.
   */
  public static final class WildcardPathStatus {
    private final WildcardPath wildcardPath;
    private boolean hasMatched;

    public WildcardPathStatus(WildcardPath wildcardPath) {
      this.wildcardPath = wildcardPath;
      this.hasMatched = false;
    }

    public boolean matches(String path) {
      boolean matches = wildcardPath.matches(path);
      this.hasMatched = hasMatched || matches;
      return matches;
    }

    public WildcardPath getWildcardPath() {
      return wildcardPath;
    }

    public boolean hasMatched() {
      return hasMatched;
    }
  }

}


/*
 * 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.parquet.io;

import java.util.ArrayDeque;
import java.util.Arrays;
import java.util.Deque;

import org.apache.parquet.io.api.Binary;
import org.apache.parquet.io.api.RecordConsumer;
import org.apache.parquet.schema.MessageType;
import org.apache.parquet.schema.Type;
import org.apache.parquet.schema.PrimitiveType.PrimitiveTypeName;
import org.apache.parquet.schema.Type.Repetition;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import static org.apache.parquet.schema.PrimitiveType.PrimitiveTypeName.*;

/**
 * Wraps a record consumer
 * Validates the record written against the schema and pass down the event to the wrapped consumer
 */
public class ValidatingRecordConsumer extends RecordConsumer {
  private static final Logger LOG = LoggerFactory.getLogger(ValidatingRecordConsumer.class);

  private final RecordConsumer delegate;

  private Deque<Type> types = new ArrayDeque<Type>();
  private Deque<Integer> fields = new ArrayDeque<Integer>();
  private Deque<Integer> previousField = new ArrayDeque<Integer>();
  private Deque<Integer> fieldValueCount = new ArrayDeque<Integer>();

  /**
   *
   * @param delegate the consumer to pass down the event to
   * @param schema the schema to validate against
   */
  public ValidatingRecordConsumer(RecordConsumer delegate, MessageType schema) {
    this.delegate = delegate;
    this.types.push(schema);
  }

  /**
   * {@inheritDoc}
   */
  public void startMessage() {
    previousField.push(-1);
    delegate.startMessage();
  }

  /**
   * {@inheritDoc}
   */
  public void endMessage() {
    delegate.endMessage();
    validateMissingFields(types.peek().asGroupType().getFieldCount());
    previousField.pop();
  }

  /**
   * {@inheritDoc}
   */
  public void startField(String field, int index) {
    if (index <= previousField.peek()) {
      throw new InvalidRecordException("fields must be added in order " + field + " index " + index + " is before previous field " + previousField.peek());
    }
    validateMissingFields(index);
    fields.push(index);
    fieldValueCount.push(0);
    delegate.startField(field, index);
  }

  private void validateMissingFields(int index) {
    for (int i = previousField.peek() + 1; i < index; i++) {
      Type type = types.peek().asGroupType().getType(i);
      if (type.isRepetition(Repetition.REQUIRED)) {
        throw new InvalidRecordException("required field is missing " + type);
      }
    }
  }

  /**
   * {@inheritDoc}
   */
  public void endField(String field, int index) {
    delegate.endField(field, index);
    fieldValueCount.pop();
    previousField.push(fields.pop());
  }

  /**
   * {@inheritDoc}
   */
  public void startGroup() {
    previousField.push(-1);
    types.push(types.peek().asGroupType().getType(fields.peek()));
    delegate.startGroup();
  }

  /**
   * {@inheritDoc}
   */
  public void endGroup() {
    delegate.endGroup();
    validateMissingFields(types.peek().asGroupType().getFieldCount());
    types.pop();
    previousField.pop();
  }

  /**
   * {@inheritDoc}
   */
  @Override
  public void flush(){
    delegate.flush();
  }

  private void validate(PrimitiveTypeName p) {
    Type currentType = types.peek().asGroupType().getType(fields.peek());
    int c = fieldValueCount.pop() + 1;
    fieldValueCount.push(c);
    LOG.debug("validate {} for {}",p ,currentType.getName());
    switch (currentType.getRepetition()) {
      case OPTIONAL:
      case REQUIRED:
        if (c > 1) {
          throw new InvalidRecordException("repeated value when the type is not repeated in " + currentType);
        }
        break;
      case REPEATED:
        break;
      default:
        throw new InvalidRecordException("unknown repetition " + currentType.getRepetition() + " in " + currentType);
    }
    if (!currentType.isPrimitive() || currentType.asPrimitiveType().getPrimitiveTypeName() != p) {
      throw new InvalidRecordException("expected type " + p + " but got "+ currentType);
    }
  }

  private void validate(PrimitiveTypeName... ptypes) {
    Type currentType = types.peek().asGroupType().getType(fields.peek());
    int c = fieldValueCount.pop() + 1;
    fieldValueCount.push(c);
    if (LOG.isDebugEnabled()) LOG.debug("validate " + Arrays.toString(ptypes) + " for " + currentType.getName());
    switch (currentType.getRepetition()) {
      case OPTIONAL:
      case REQUIRED:
        if (c > 1) {
          throw new InvalidRecordException("repeated value when the type is not repeated in " + currentType);
        }
        break;
      case REPEATED:
        break;
      default:
        throw new InvalidRecordException("unknown repetition " + currentType.getRepetition() + " in " + currentType);
    }
    if (!currentType.isPrimitive()) {
      throw new InvalidRecordException(
          "expected type in " + Arrays.toString(ptypes) + " but got " + currentType);
    }
    for (PrimitiveTypeName p : ptypes) {
      if (currentType.asPrimitiveType().getPrimitiveTypeName() == p) {
        return; // type is valid
      }
    }
    throw new InvalidRecordException(
        "expected type in " + Arrays.toString(ptypes) + " but got " + currentType);
  }

  /**
   * {@inheritDoc}
   */
  public void addInteger(int value) {
    validate(INT32);
    delegate.addInteger(value);
  }

  /**
   * {@inheritDoc}
   */
  public void addLong(long value) {
    validate(INT64);
    delegate.addLong(value);
  }

  /**
   * {@inheritDoc}
   */
  public void addBoolean(boolean value) {
    validate(BOOLEAN);
    delegate.addBoolean(value);
  }

  /**
   * {@inheritDoc}
   */
  public void addBinary(Binary value) {
    validate(BINARY, INT96, FIXED_LEN_BYTE_ARRAY);
    delegate.addBinary(value);
  }

  /**
   * {@inheritDoc}
   */
  public void addFloat(float value) {
    validate(FLOAT);
    delegate.addFloat(value);
  }

  /**
   * {@inheritDoc}
   */
  public void addDouble(double value) {
    validate(DOUBLE);
    delegate.addDouble(value);
  }

}


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

import java.util.Arrays;
import java.util.List;
import java.util.Locale;
import java.util.Objects;
import java.util.Optional;

import org.apache.parquet.Preconditions;
import org.apache.parquet.ShouldNeverHappenException;
import org.apache.parquet.column.ColumnReader;
import org.apache.parquet.io.InvalidRecordException;
import org.apache.parquet.io.api.Binary;
import org.apache.parquet.io.api.PrimitiveConverter;
import org.apache.parquet.io.api.RecordConsumer;
import org.apache.parquet.schema.ColumnOrder.ColumnOrderName;

import static java.util.Optional.empty;
import static java.util.Optional.of;
import static org.apache.parquet.schema.LogicalTypeAnnotation.TimeUnit.MICROS;
import static org.apache.parquet.schema.LogicalTypeAnnotation.TimeUnit.MILLIS;

/**
 * Representation of a Primitive type
 */
public final class PrimitiveType extends Type {

  public static interface PrimitiveTypeNameConverter<T, E extends Exception> {

    T convertFLOAT(PrimitiveTypeName primitiveTypeName) throws E;

    T convertDOUBLE(PrimitiveTypeName primitiveTypeName) throws E;

    T convertINT32(PrimitiveTypeName primitiveTypeName) throws E;

    T convertINT64(PrimitiveTypeName primitiveTypeName) throws E;

    T convertINT96(PrimitiveTypeName primitiveTypeName) throws E;

    T convertFIXED_LEN_BYTE_ARRAY(PrimitiveTypeName primitiveTypeName) throws E;

    T convertBOOLEAN(PrimitiveTypeName primitiveTypeName) throws E;

    T convertBINARY(PrimitiveTypeName primitiveTypeName) throws E;

  }

  /**
   * Supported Primitive types
   */
  public static enum PrimitiveTypeName {
    INT64("getLong", Long.TYPE) {
      @Override
      public String toString(ColumnReader columnReader) {
        return String.valueOf(columnReader.getLong());
      }

      @Override
      public void addValueToRecordConsumer(RecordConsumer recordConsumer,
          ColumnReader columnReader) {
        recordConsumer.addLong(columnReader.getLong());
      }

      @Override
      public void addValueToPrimitiveConverter(
          PrimitiveConverter primitiveConverter, ColumnReader columnReader) {
        primitiveConverter.addLong(columnReader.getLong());
      }

      @Override
      public <T, E extends Exception> T convert(PrimitiveTypeNameConverter<T, E> converter) throws E {
        return converter.convertINT64(this);
      }

      @Override
      PrimitiveComparator<?> comparator(LogicalTypeAnnotation logicalType) {
        if (logicalType == null) {
          return PrimitiveComparator.SIGNED_INT64_COMPARATOR;
        }
        return logicalType.accept(new LogicalTypeAnnotation.LogicalTypeAnnotationVisitor<PrimitiveComparator>() {
          @Override
          public Optional<PrimitiveComparator> visit(LogicalTypeAnnotation.IntLogicalTypeAnnotation intLogicalType) {
            return intLogicalType.isSigned() ?
              of(PrimitiveComparator.SIGNED_INT64_COMPARATOR) : of(PrimitiveComparator.UNSIGNED_INT64_COMPARATOR);
          }

          @Override
          public Optional<PrimitiveComparator> visit(LogicalTypeAnnotation.DecimalLogicalTypeAnnotation decimalLogicalType) {
            return of(PrimitiveComparator.SIGNED_INT64_COMPARATOR);
          }

          @Override
          public Optional<PrimitiveComparator> visit(LogicalTypeAnnotation.TimeLogicalTypeAnnotation timeLogicalType) {
            return of(PrimitiveComparator.SIGNED_INT64_COMPARATOR);
          }

          @Override
          public Optional<PrimitiveComparator> visit(LogicalTypeAnnotation.TimestampLogicalTypeAnnotation timestampLogicalType) {
            return of(PrimitiveComparator.SIGNED_INT64_COMPARATOR);
          }
        }).orElseThrow(() -> new ShouldNeverHappenException("No comparator logic implemented for INT64 logical type: " + logicalType));
      }
    },
    INT32("getInteger", Integer.TYPE) {
      @Override
      public String toString(ColumnReader columnReader) {
        return String.valueOf(columnReader.getInteger());
      }

      @Override
      public void addValueToRecordConsumer(RecordConsumer recordConsumer,
          ColumnReader columnReader) {
        recordConsumer.addInteger(columnReader.getInteger());
      }

      @Override
      public void addValueToPrimitiveConverter(
          PrimitiveConverter primitiveConverter, ColumnReader columnReader) {
        primitiveConverter.addInt(columnReader.getInteger());
      }

      @Override
      public <T, E extends Exception> T convert(PrimitiveTypeNameConverter<T, E> converter) throws E {
        return converter.convertINT32(this);
      }

      @Override
      PrimitiveComparator<?> comparator(LogicalTypeAnnotation logicalType) {
        if (logicalType == null) {
          return PrimitiveComparator.SIGNED_INT32_COMPARATOR;
        }
        return logicalType.accept(new LogicalTypeAnnotation.LogicalTypeAnnotationVisitor<PrimitiveComparator>() {
          @Override
          public Optional<PrimitiveComparator> visit(LogicalTypeAnnotation.IntLogicalTypeAnnotation intLogicalType) {
            if (intLogicalType.getBitWidth() == 64) {
              return empty();
            }
            return intLogicalType.isSigned() ?
              of(PrimitiveComparator.SIGNED_INT32_COMPARATOR) : of(PrimitiveComparator.UNSIGNED_INT32_COMPARATOR);
          }

          @Override
          public Optional<PrimitiveComparator> visit(LogicalTypeAnnotation.DecimalLogicalTypeAnnotation decimalLogicalType) {
            return of(PrimitiveComparator.SIGNED_INT32_COMPARATOR);
          }

          @Override
          public Optional<PrimitiveComparator> visit(LogicalTypeAnnotation.DateLogicalTypeAnnotation dateLogicalType) {
            return of(PrimitiveComparator.SIGNED_INT32_COMPARATOR);
          }

          @Override
          public Optional<PrimitiveComparator> visit(LogicalTypeAnnotation.TimeLogicalTypeAnnotation timeLogicalType) {
            if (timeLogicalType.getUnit() == MILLIS) {
              return of(PrimitiveComparator.SIGNED_INT32_COMPARATOR);
            }
            return empty();
          }
        }).orElseThrow(
          () -> new ShouldNeverHappenException("No comparator logic implemented for INT32 logical type: " + logicalType));
      }
    },
    BOOLEAN("getBoolean", Boolean.TYPE) {
      @Override
      public String toString(ColumnReader columnReader) {
        return String.valueOf(columnReader.getBoolean());
      }

      @Override
      public void addValueToRecordConsumer(RecordConsumer recordConsumer,
          ColumnReader columnReader) {
        recordConsumer.addBoolean(columnReader.getBoolean());
      }

      @Override
      public void addValueToPrimitiveConverter(
          PrimitiveConverter primitiveConverter, ColumnReader columnReader) {
        primitiveConverter.addBoolean(columnReader.getBoolean());
      }

      @Override
      public <T, E extends Exception> T convert(PrimitiveTypeNameConverter<T, E> converter) throws E {
        return converter.convertBOOLEAN(this);
      }

      @Override
      PrimitiveComparator<?> comparator(LogicalTypeAnnotation logicalType) {
        return PrimitiveComparator.BOOLEAN_COMPARATOR;
      }
    },
    BINARY("getBinary", Binary.class) {
      @Override
      public String toString(ColumnReader columnReader) {
        return String.valueOf(columnReader.getBinary());
      }

      @Override
      public void addValueToRecordConsumer(RecordConsumer recordConsumer,
          ColumnReader columnReader) {
        recordConsumer.addBinary(columnReader.getBinary());
      }

      @Override
      public void addValueToPrimitiveConverter(
          PrimitiveConverter primitiveConverter, ColumnReader columnReader) {
        primitiveConverter.addBinary(columnReader.getBinary());
      }

      @Override
      public <T, E extends Exception> T convert(PrimitiveTypeNameConverter<T, E> converter) throws E {
        return converter.convertBINARY(this);
      }

      @Override
      PrimitiveComparator<?> comparator(LogicalTypeAnnotation logicalType) {
        if (logicalType == null) {
          return PrimitiveComparator.UNSIGNED_LEXICOGRAPHICAL_BINARY_COMPARATOR;
        }
        return logicalType.accept(new LogicalTypeAnnotation.LogicalTypeAnnotationVisitor<PrimitiveComparator>() {
          @Override
          public Optional<PrimitiveComparator> visit(LogicalTypeAnnotation.DecimalLogicalTypeAnnotation decimalLogicalType) {
            return of(PrimitiveComparator.BINARY_AS_SIGNED_INTEGER_COMPARATOR);
          }

          @Override
          public Optional<PrimitiveComparator> visit(LogicalTypeAnnotation.StringLogicalTypeAnnotation stringLogicalType) {
            return of(PrimitiveComparator.UNSIGNED_LEXICOGRAPHICAL_BINARY_COMPARATOR);
          }

          @Override
          public Optional<PrimitiveComparator> visit(LogicalTypeAnnotation.EnumLogicalTypeAnnotation enumLogicalType) {
            return of(PrimitiveComparator.UNSIGNED_LEXICOGRAPHICAL_BINARY_COMPARATOR);
          }

          @Override
          public Optional<PrimitiveComparator> visit(LogicalTypeAnnotation.JsonLogicalTypeAnnotation jsonLogicalType) {
            return of(PrimitiveComparator.UNSIGNED_LEXICOGRAPHICAL_BINARY_COMPARATOR);
          }

          @Override
          public Optional<PrimitiveComparator> visit(LogicalTypeAnnotation.BsonLogicalTypeAnnotation bsonLogicalType) {
            return of(PrimitiveComparator.UNSIGNED_LEXICOGRAPHICAL_BINARY_COMPARATOR);
          }
        }).orElseThrow(() -> new ShouldNeverHappenException("No comparator logic implemented for BINARY logical type: " + logicalType));
      }
    },
    FLOAT("getFloat", Float.TYPE) {
      @Override
      public String toString(ColumnReader columnReader) {
        return String.valueOf(columnReader.getFloat());
      }

      @Override
      public void addValueToRecordConsumer(RecordConsumer recordConsumer,
          ColumnReader columnReader) {
        recordConsumer.addFloat(columnReader.getFloat());
      }

      @Override
      public void addValueToPrimitiveConverter(
          PrimitiveConverter primitiveConverter, ColumnReader columnReader) {
        primitiveConverter.addFloat(columnReader.getFloat());
      }

      @Override
      public <T, E extends Exception> T convert(PrimitiveTypeNameConverter<T, E> converter) throws E {
        return converter.convertFLOAT(this);
      }

      @Override
      PrimitiveComparator<?> comparator(LogicalTypeAnnotation logicalType) {
        return PrimitiveComparator.FLOAT_COMPARATOR;
      }
    },
    DOUBLE("getDouble", Double.TYPE) {
      @Override
      public String toString(ColumnReader columnReader) {
        return String.valueOf(columnReader.getDouble());
      }

      @Override
      public void addValueToRecordConsumer(RecordConsumer recordConsumer,
          ColumnReader columnReader) {
        recordConsumer.addDouble(columnReader.getDouble());
      }

      @Override
      public void addValueToPrimitiveConverter(
          PrimitiveConverter primitiveConverter, ColumnReader columnReader) {
        primitiveConverter.addDouble(columnReader.getDouble());
      }

      @Override
      public <T, E extends Exception> T convert(PrimitiveTypeNameConverter<T, E> converter) throws E {
        return converter.convertDOUBLE(this);
      }

      @Override
      PrimitiveComparator<?> comparator(LogicalTypeAnnotation logicalType) {
        return PrimitiveComparator.DOUBLE_COMPARATOR;
      }
    },
    INT96("getBinary", Binary.class) {
      @Override
      public String toString(ColumnReader columnReader) {
        return Arrays.toString(columnReader.getBinary().getBytesUnsafe());
      }
      @Override
      public void addValueToRecordConsumer(RecordConsumer recordConsumer,
          ColumnReader columnReader) {
        recordConsumer.addBinary(columnReader.getBinary());
      }
      @Override
      public void addValueToPrimitiveConverter(
          PrimitiveConverter primitiveConverter, ColumnReader columnReader) {
        primitiveConverter.addBinary(columnReader.getBinary());
      }

      @Override
      public <T, E extends Exception> T convert(PrimitiveTypeNameConverter<T, E> converter) throws E {
        return converter.convertINT96(this);
      }

      @Override
      PrimitiveComparator<?> comparator(LogicalTypeAnnotation logicalType) {
        return PrimitiveComparator.BINARY_AS_SIGNED_INTEGER_COMPARATOR;
      }
    },
    FIXED_LEN_BYTE_ARRAY("getBinary", Binary.class) {
      @Override
      public String toString(ColumnReader columnReader) {
        return String.valueOf(columnReader.getBinary());
      }

      @Override
      public void addValueToRecordConsumer(RecordConsumer recordConsumer,
          ColumnReader columnReader) {
        recordConsumer.addBinary(columnReader.getBinary());
      }

      @Override
      public void addValueToPrimitiveConverter(
          PrimitiveConverter primitiveConverter, ColumnReader columnReader) {
        primitiveConverter.addBinary(columnReader.getBinary());
      }

      @Override
      public <T, E extends Exception> T convert(PrimitiveTypeNameConverter<T, E> converter) throws E {
        return converter.convertFIXED_LEN_BYTE_ARRAY(this);
      }

      @Override
      PrimitiveComparator<?> comparator(LogicalTypeAnnotation logicalType) {
        if (logicalType == null) {
          return PrimitiveComparator.UNSIGNED_LEXICOGRAPHICAL_BINARY_COMPARATOR;
        }

        return logicalType.accept(new LogicalTypeAnnotation.LogicalTypeAnnotationVisitor<PrimitiveComparator>() {
          @Override
          public Optional<PrimitiveComparator> visit(LogicalTypeAnnotation.DecimalLogicalTypeAnnotation decimalLogicalType) {
            return of(PrimitiveComparator.BINARY_AS_SIGNED_INTEGER_COMPARATOR);
          }

          @Override
          public Optional<PrimitiveComparator> visit(LogicalTypeAnnotation.IntervalLogicalTypeAnnotation intervalLogicalType) {
            return of(PrimitiveComparator.UNSIGNED_LEXICOGRAPHICAL_BINARY_COMPARATOR);
          }
        }).orElseThrow(() -> new ShouldNeverHappenException(
          "No comparator logic implemented for FIXED_LEN_BYTE_ARRAY logical type: " + logicalType));
      }
    };

    public final String getMethod;
    public final Class<?> javaType;

    private PrimitiveTypeName(String getMethod, Class<?> javaType) {
      this.getMethod = getMethod;
      this.javaType = javaType;
    }

    /**
     * reads the value from the columnReader with the appropriate accessor and returns a String representation
     * @param columnReader where to read
     * @return a string
     */
    abstract public String toString(ColumnReader columnReader);

    /**
     * reads the value from the columnReader with the appropriate accessor and writes it to the recordConsumer
     * @param recordConsumer where to write
     * @param columnReader where to read from
     */
    abstract public void addValueToRecordConsumer(RecordConsumer recordConsumer,
        ColumnReader columnReader);

    abstract public void addValueToPrimitiveConverter(
        PrimitiveConverter primitiveConverter, ColumnReader columnReader);

    abstract public <T, E extends Exception> T convert(PrimitiveTypeNameConverter<T, E> converter) throws E;

    abstract PrimitiveComparator<?> comparator(LogicalTypeAnnotation logicalType);
  }

  private final PrimitiveTypeName primitive;
  private final int length;
  private final DecimalMetadata decimalMeta;
  private final ColumnOrder columnOrder;

  /**
   * @param repetition OPTIONAL, REPEATED, REQUIRED
   * @param primitive STRING, INT64, ...
   * @param name the name of the type
   */
  public PrimitiveType(Repetition repetition, PrimitiveTypeName primitive, String name) {
    this(repetition, primitive, 0, name, (LogicalTypeAnnotation) null, null, null);
  }

  /**
   * @param repetition OPTIONAL, REPEATED, REQUIRED
   * @param primitive STRING, INT64, ...
   * @param length the length if the type is FIXED_LEN_BYTE_ARRAY, 0 otherwise (XXX)
   * @param name the name of the type
   */
  public PrimitiveType(Repetition repetition, PrimitiveTypeName primitive, int length, String name) {
    this(repetition, primitive, length, name, (LogicalTypeAnnotation) null, null, null);
  }

  /**
   * @param repetition OPTIONAL, REPEATED, REQUIRED
   * @param primitive STRING, INT64, ...
   * @param name the name of the type
   * @param originalType (optional) the original type to help with cross schema convertion (LIST, MAP, ...)
   *
   * @deprecated will be removed in 2.0.0; use builders in {@link Types} instead
   */
  @Deprecated
  public PrimitiveType(Repetition repetition, PrimitiveTypeName primitive,
                       String name, OriginalType originalType) {
    this(repetition, primitive, 0, name, originalType, null, null);
  }

  /**
   * @param repetition OPTIONAL, REPEATED, REQUIRED
   * @param primitive STRING, INT64, ...
   * @param name the name of the type
   * @param length the length if the type is FIXED_LEN_BYTE_ARRAY, 0 otherwise (XXX)
   * @param originalType (optional) the original type to help with cross schema conversion (LIST, MAP, ...)
   */
  @Deprecated
  public PrimitiveType(Repetition repetition, PrimitiveTypeName primitive,
                       int length, String name, OriginalType originalType) {
    this(repetition, primitive, length, name, originalType, null, null);
  }

  /**
   * @param repetition OPTIONAL, REPEATED, REQUIRED
   * @param primitive STRING, INT64, ...
   * @param name the name of the type
   * @param length the length if the type is FIXED_LEN_BYTE_ARRAY, 0 otherwise
   * @param originalType (optional) the original type (MAP, DECIMAL, UTF8, ...)
   * @param decimalMeta (optional) metadata about the decimal type
   * @param id the id of the field
   *
   * @deprecated will be removed in 2.0.0; use builders in {@link Types} instead
   */
  @Deprecated
  public PrimitiveType(Repetition repetition, PrimitiveTypeName primitive,
                       int length, String name, OriginalType originalType,
                       DecimalMetadata decimalMeta, ID id) {
    this(repetition, primitive, length, name, originalType, decimalMeta, id, null);
  }

  PrimitiveType(Repetition repetition, PrimitiveTypeName primitive,
      int length, String name, OriginalType originalType,
      DecimalMetadata decimalMeta, ID id, ColumnOrder columnOrder) {
    super(name, repetition, originalType, decimalMeta, id);
    this.primitive = primitive;
    this.length = length;
    this.decimalMeta = decimalMeta;

    if (columnOrder == null) {
      columnOrder = primitive == PrimitiveTypeName.INT96 || originalType == OriginalType.INTERVAL
          ? ColumnOrder.undefined()
          : ColumnOrder.typeDefined();
    }
    this.columnOrder = requireValidColumnOrder(columnOrder);
  }

  PrimitiveType(Repetition repetition, PrimitiveTypeName primitive,
                       String name, LogicalTypeAnnotation logicalTypeAnnotation) {
    this(repetition, primitive, 0, name, logicalTypeAnnotation, null, null);
  }

  PrimitiveType(Repetition repetition, PrimitiveTypeName primitive,
                       int length, String name, LogicalTypeAnnotation logicalTypeAnnotation, ID id) {
    this(repetition, primitive, length, name, logicalTypeAnnotation, id, null);
  }

  PrimitiveType(Repetition repetition, PrimitiveTypeName primitive,
                int length, String name, LogicalTypeAnnotation logicalTypeAnnotation,
                ID id, ColumnOrder columnOrder) {
    super(name, repetition, logicalTypeAnnotation, id);
    this.primitive = primitive;
    this.length = length;
    if (logicalTypeAnnotation instanceof LogicalTypeAnnotation.DecimalLogicalTypeAnnotation) {
      LogicalTypeAnnotation.DecimalLogicalTypeAnnotation decimal = (LogicalTypeAnnotation.DecimalLogicalTypeAnnotation) logicalTypeAnnotation;
      this.decimalMeta = new DecimalMetadata(decimal.getPrecision(), decimal.getScale());
    } else {
      this.decimalMeta = null;
    }

    if (columnOrder == null) {
      columnOrder = primitive == PrimitiveTypeName.INT96 || logicalTypeAnnotation instanceof LogicalTypeAnnotation.IntervalLogicalTypeAnnotation
        ? ColumnOrder.undefined()
        : ColumnOrder.typeDefined();
    }
    this.columnOrder = requireValidColumnOrder(columnOrder);
  }

  private ColumnOrder requireValidColumnOrder(ColumnOrder columnOrder) {
    if (primitive == PrimitiveTypeName.INT96) {
      Preconditions.checkArgument(columnOrder.getColumnOrderName() == ColumnOrderName.UNDEFINED,
          "The column order {} is not supported by INT96", columnOrder);
    }
    if (getLogicalTypeAnnotation() != null) {
      Preconditions.checkArgument(getLogicalTypeAnnotation().isValidColumnOrder(columnOrder),
        "The column order {} is not supported by {} ({})", columnOrder, primitive, getLogicalTypeAnnotation());
    }
    return columnOrder;
  }

  /**
   * @param id the field id
   * @return a new PrimitiveType with the same fields and a new id
   */
  @Override
  public PrimitiveType withId(int id) {
    return new PrimitiveType(getRepetition(), primitive, length, getName(), getLogicalTypeAnnotation(), new ID(id),
        columnOrder);
  }

  /**
   * @return the primitive type
   */
  public PrimitiveTypeName getPrimitiveTypeName() {
    return primitive;
  }

  /**
   * @return the type length
   */
  public int getTypeLength() {
    return length;
  }

  /**
   * @return the decimal type metadata
   */
  @Deprecated
  public DecimalMetadata getDecimalMetadata() {
    return decimalMeta;
  }

  /**
   * @return true
   */
  @Override
  public boolean isPrimitive() {
    return true;
  }

  /**
   * {@inheritDoc}
   */
  @Override
  public void accept(TypeVisitor visitor) {
    visitor.visit(this);
  }

  /**
   * {@inheritDoc}
   */
  @Override
  public void writeToStringBuilder(StringBuilder sb, String indent) {
    sb.append(indent)
        .append(getRepetition().name().toLowerCase(Locale.ENGLISH))
        .append(" ")
        .append(primitive.name().toLowerCase());
    if (primitive == PrimitiveTypeName.FIXED_LEN_BYTE_ARRAY) {
      sb.append("(" + length + ")");
    }
    sb.append(" ").append(getName());
    if (getLogicalTypeAnnotation() != null) {
      // TODO: should we print decimal metadata too?
      sb.append(" (").append(getLogicalTypeAnnotation().toString()).append(")");
    }
    if (getId() != null) {
      sb.append(" = ").append(getId());
    }
  }

  @Override @Deprecated
  protected int typeHashCode() {
    return hashCode();
  }

  @Override @Deprecated
  protected boolean typeEquals(Type other) {
    return equals(other);
  }

  /**
   * {@inheritDoc}
   */
  @Override
  protected boolean equals(Type other) {
    if (!other.isPrimitive()) {
      return false;
    }
    PrimitiveType otherPrimitive = other.asPrimitiveType();
    return super.equals(other)
        && primitive == otherPrimitive.getPrimitiveTypeName()
        && length == otherPrimitive.length
        && columnOrder.equals(otherPrimitive.columnOrder)
        && eqOrBothNull(decimalMeta, otherPrimitive.decimalMeta);
  }

  /**
   * {@inheritDoc}
   */
  @Override
  public int hashCode() {
    int hash = super.hashCode();
    hash = hash * 31 + primitive.hashCode();
    hash = hash * 31 + length;
    hash = hash * 31 + columnOrder.hashCode();
    if (decimalMeta != null) {
      hash = hash * 31 + decimalMeta.hashCode();
    }
    return hash;
  }

  @Override
  public int getMaxRepetitionLevel(String[] path, int i) {
    if (path.length != i) {
      throw new InvalidRecordException("Arrived at primitive node, path invalid");
    }
    return isRepetition(Repetition.REPEATED)? 1 : 0;
  }

  @Override
  public int getMaxDefinitionLevel(String[] path, int i) {
    if (path.length != i) {
      throw new InvalidRecordException("Arrived at primitive node, path invalid");
    }
    return isRepetition(Repetition.REQUIRED) ? 0 : 1;
  }

  @Override
  public Type getType(String[] path, int i) {
    if (path.length != i) {
      throw new InvalidRecordException("Arrived at primitive node at index " + i + " , path invalid: " + Arrays.toString(path));
    }
    return this;
  }

  @Override
  protected List<String[]> getPaths(int depth) {
    return Arrays.<String[]>asList(new String[depth]);
  }

  @Override
  void checkContains(Type subType) {
    super.checkContains(subType);
    if (!subType.isPrimitive()) {
      throw new InvalidRecordException(subType + " found: expected " + this);
    }
    PrimitiveType primitiveType = subType.asPrimitiveType();
    if (this.primitive != primitiveType.primitive) {
      throw new InvalidRecordException(subType + " found: expected " + this);
    }

  }

  @Override
  public <T> T convert(List<GroupType> path, TypeConverter<T> converter) {
    return converter.convertPrimitiveType(path, this);
  }

  @Override
  protected boolean containsPath(String[] path, int depth) {
    return path.length == depth;
  }

  @Override
  protected Type union(Type toMerge) {
    return union(toMerge, true);
  }

  private void reportSchemaMergeError(Type toMerge) {
    throw new IncompatibleSchemaModificationException("can not merge type " + toMerge + " into " + this);
  }

  private void reportSchemaMergeErrorWithColumnOrder(Type toMerge) {
    throw new IncompatibleSchemaModificationException("can not merge type " + toMerge + " with column order "
        + toMerge.asPrimitiveType().columnOrder() + " into " + this + " with column order " + columnOrder());
  }

  @Override
  protected Type union(Type toMerge, boolean strict) {
    if (!toMerge.isPrimitive()) {
      reportSchemaMergeError(toMerge);
    }

    if (strict) {
      // Can't merge primitive fields of different type names or different original types
      if (!primitive.equals(toMerge.asPrimitiveType().getPrimitiveTypeName()) ||
        !Objects.equals(getLogicalTypeAnnotation(), toMerge.getLogicalTypeAnnotation())) {
        reportSchemaMergeError(toMerge);
      }

      // Can't merge FIXED_LEN_BYTE_ARRAY fields of different lengths
      int toMergeLength = toMerge.asPrimitiveType().getTypeLength();
      if (primitive == PrimitiveTypeName.FIXED_LEN_BYTE_ARRAY && length != toMergeLength) {
        reportSchemaMergeError(toMerge);
      }

      // Can't merge primitive fields with different column orders
      if (!columnOrder().equals(toMerge.asPrimitiveType().columnOrder())) {
        reportSchemaMergeErrorWithColumnOrder(toMerge);
      }
    }

    Repetition repetition = Repetition.leastRestrictive(this.getRepetition(), toMerge.getRepetition());
    Types.PrimitiveBuilder<PrimitiveType> builder = Types.primitive(primitive, repetition);

    if (PrimitiveTypeName.FIXED_LEN_BYTE_ARRAY == primitive) {
      builder.length(length);
    }

    return builder.as(getLogicalTypeAnnotation()).named(getName());
  }

  /**
   * Returns the {@link Type} specific comparator for properly comparing values. The natural ordering of the values
   * might not proper in certain cases (e.g. {@code UINT_32} requires unsigned comparison of {@code int} values while
   * the natural ordering is signed.)
   *
   * @param <T> the type of values compared by the returned PrimitiveComparator
   * @return a PrimitiveComparator for values of this type
   */
  @SuppressWarnings("unchecked")
  public <T> PrimitiveComparator<T> comparator() {
    return (PrimitiveComparator<T>) getPrimitiveTypeName().comparator(getLogicalTypeAnnotation());
  }

  /**
   * @return the column order for this type
   */
  public ColumnOrder columnOrder() {
    return columnOrder;
  }

  /**
   * @return the {@link Type} specific stringifier for generating the proper string representation of the values.
   */
  @SuppressWarnings("unchecked")
  public PrimitiveStringifier stringifier() {
    LogicalTypeAnnotation logicalTypeAnnotation = getLogicalTypeAnnotation();
    return logicalTypeAnnotation == null ? PrimitiveStringifier.DEFAULT_STRINGIFIER : logicalTypeAnnotation.valueStringifier(this);
  }
}


/*
 * 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.parquet.column.values.delta;

import java.io.IOException;

import org.apache.parquet.bytes.ByteBufferInputStream;
import org.apache.parquet.bytes.BytesUtils;
import org.apache.parquet.column.values.ValuesReader;
import org.apache.parquet.column.values.bitpacking.BytePackerForLong;
import org.apache.parquet.column.values.bitpacking.Packer;
import org.apache.parquet.io.ParquetDecodingException;

import java.nio.ByteBuffer;

/**
 * Read values written by {@link DeltaBinaryPackingValuesWriter}
 */
public class DeltaBinaryPackingValuesReader extends ValuesReader {
  private int totalValueCount;
  /**
   * values read by the caller
   */
  private int valuesRead;
  private long minDeltaInCurrentBlock;

  /**
   * stores the decoded values including the first value which is written to the header
   */
  private long[] valuesBuffer;
  /**
   * values loaded to the buffer, it could be bigger than the totalValueCount
   * when data is not aligned to mini block, which means padding 0s are in the buffer
   */
  private int valuesBuffered;
  private ByteBufferInputStream in;
  private DeltaBinaryPackingConfig config;
  private int[] bitWidths;

  /**
   * eagerly loads all the data into memory
   */
  @Override
  public void initFromPage(int valueCount, ByteBufferInputStream stream) throws IOException {
    this.in = stream;
    long startPos = in.position();
    this.config = DeltaBinaryPackingConfig.readConfig(in);
    this.totalValueCount = BytesUtils.readUnsignedVarInt(in);
    allocateValuesBuffer();
    bitWidths = new int[config.miniBlockNumInABlock];

    //read first value from header
    valuesBuffer[valuesBuffered++] = BytesUtils.readZigZagVarLong(in);

    while (valuesBuffered < totalValueCount) { //values Buffered could be more than totalValueCount, since we flush on a mini block basis
      loadNewBlockToBuffer();
    }
    updateNextOffset((int) (in.position() - startPos));
  }

  /**
   * the value buffer is allocated so that the size of it is multiple of mini block
   * because when writing, data is flushed on a mini block basis
   */
  private void allocateValuesBuffer() {
    int totalMiniBlockCount = (int) Math.ceil((double) totalValueCount / config.miniBlockSizeInValues);
    //+ 1 because first value written to header is also stored in values buffer
    valuesBuffer = new long[totalMiniBlockCount * config.miniBlockSizeInValues + 1];
  }

  @Override
  public void skip() {
    checkRead();
    valuesRead++;
  }

  @Override
  public void skip(int n) {
    // checkRead() is invoked before incrementing valuesRead so increase valuesRead size in 2 steps
    valuesRead += n - 1;
    checkRead();
    ++valuesRead;
  }

  @Override
  public int readInteger() {
    // TODO: probably implement it separately
    return (int) readLong();
  }

  @Override
  public long readLong() {
    checkRead();
    return valuesBuffer[valuesRead++];
  }

  private void checkRead() {
    if (valuesRead >= totalValueCount) {
      throw new ParquetDecodingException("no more value to read, total value count is " + totalValueCount);
    }
  }

  private void loadNewBlockToBuffer() throws IOException {
    try {
      minDeltaInCurrentBlock = BytesUtils.readZigZagVarLong(in);
    } catch (IOException e) {
      throw new ParquetDecodingException("can not read min delta in current block", e);
    }

    readBitWidthsForMiniBlocks();

    // mini block is atomic for reading, we read a mini block when there are more values left
    int i;
    for (i = 0; i < config.miniBlockNumInABlock && valuesBuffered < totalValueCount; i++) {
      BytePackerForLong packer = Packer.LITTLE_ENDIAN.newBytePackerForLong(bitWidths[i]);
      unpackMiniBlock(packer);
    }

    //calculate values from deltas unpacked for current block
    int valueUnpacked=i*config.miniBlockSizeInValues;
    for (int j = valuesBuffered-valueUnpacked; j < valuesBuffered; j++) {
      int index = j;
      valuesBuffer[index] += minDeltaInCurrentBlock + valuesBuffer[index - 1];
    }
  }

  /**
   * mini block has a size of 8*n, unpack 8 value each time
   *
   * @param packer the packer created from bitwidth of current mini block
   */
  private void unpackMiniBlock(BytePackerForLong packer) throws IOException {
    for (int j = 0; j < config.miniBlockSizeInValues; j += 8) {
      unpack8Values(packer);
    }
  }

  private void unpack8Values(BytePackerForLong packer) throws IOException {
    // get a single buffer of 8 values. most of the time, this won't require a copy
    // TODO: update the packer to consume from an InputStream
    ByteBuffer buffer = in.slice(packer.getBitWidth());
    packer.unpack8Values(buffer, buffer.position(), valuesBuffer, valuesBuffered);
    this.valuesBuffered += 8;
  }

  private void readBitWidthsForMiniBlocks() {
    for (int i = 0; i < config.miniBlockNumInABlock; i++) {
      try {
        bitWidths[i] = BytesUtils.readIntLittleEndianOnOneByte(in);
      } catch (IOException e) {
        throw new ParquetDecodingException("Can not decode bitwidth in block header", e);
      }
    }
  }
}


/*
 * 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.parquet.hadoop.metadata;

import static java.util.Collections.unmodifiableMap;
import static org.apache.parquet.Preconditions.checkNotNull;

import java.io.Serializable;
import java.util.HashMap;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Set;

import org.apache.parquet.schema.MessageType;

/**
 * Merged metadata when reading from multiple files.
 * THis is to allow schema evolution
 */
public class GlobalMetaData implements Serializable {
  private static final long serialVersionUID = 1L;

  private final MessageType schema;

  private final Map<String, Set<String>> keyValueMetaData;

  private final Set<String> createdBy;

  /**
   * @param schema the union of the schemas for all the files
   * @param keyValueMetaData the merged app specific metadata
   * @param createdBy the description of the library that created the file
   */
  public GlobalMetaData(MessageType schema, Map<String, Set<String>> keyValueMetaData, Set<String> createdBy) {
    super();
    this.schema = checkNotNull(schema, "schema");
    this.keyValueMetaData = unmodifiableMap(checkNotNull(keyValueMetaData, "keyValueMetaData"));
    this.createdBy = createdBy;
  }

  /**
   * @return the schema for the file
   */
  public MessageType getSchema() {
    return schema;
  }

  @Override
  public String toString() {
    return "GlobalMetaData{schema: "+schema+ ", metadata: " + keyValueMetaData + "}";
  }

  /**
   * @return meta data for extensions
   */
  public Map<String, Set<String>> getKeyValueMetaData() {
    return keyValueMetaData;
  }

  /**
   * @return the description of the library that created the file
   */
  public Set<String> getCreatedBy() {
    return createdBy;
  }

  /**
   * Will merge the metadata as if it was coming from a single file.
   * (for all part files written together this will always work)
   * If there are conflicting values an exception will be thrown
   * @return the merged version of this
   */
  public FileMetaData merge() {
    String createdByString = createdBy.size() == 1 ?
      createdBy.iterator().next() :
      createdBy.toString();
    Map<String, String> mergedKeyValues = new HashMap<String, String>();
    for (Entry<String, Set<String>> entry : keyValueMetaData.entrySet()) {
      if (entry.getValue().size() > 1) {
        throw new RuntimeException("could not merge metadata: key " + entry.getKey() + " has conflicting values: " + entry.getValue());
      }
      mergedKeyValues.put(entry.getKey(), entry.getValue().iterator().next());
    }
    return new FileMetaData(schema, mergedKeyValues, createdByString);
  }

}


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

import org.apache.parquet.Preconditions;
import org.apache.yetus.audience.InterfaceAudience;

import java.util.Collections;
import java.util.HashSet;
import java.util.List;
import java.util.Objects;
import java.util.Optional;
import java.util.Set;
import java.util.function.Supplier;

import static java.util.Arrays.asList;
import static java.util.Optional.empty;
import static org.apache.parquet.schema.ColumnOrder.ColumnOrderName.TYPE_DEFINED_ORDER;
import static org.apache.parquet.schema.ColumnOrder.ColumnOrderName.UNDEFINED;
import static org.apache.parquet.schema.PrimitiveStringifier.TIMESTAMP_MICROS_STRINGIFIER;
import static org.apache.parquet.schema.PrimitiveStringifier.TIMESTAMP_MICROS_UTC_STRINGIFIER;
import static org.apache.parquet.schema.PrimitiveStringifier.TIMESTAMP_MILLIS_STRINGIFIER;
import static org.apache.parquet.schema.PrimitiveStringifier.TIMESTAMP_MILLIS_UTC_STRINGIFIER;
import static org.apache.parquet.schema.PrimitiveStringifier.TIMESTAMP_NANOS_STRINGIFIER;
import static org.apache.parquet.schema.PrimitiveStringifier.TIMESTAMP_NANOS_UTC_STRINGIFIER;
import static org.apache.parquet.schema.PrimitiveStringifier.TIME_NANOS_STRINGIFIER;
import static org.apache.parquet.schema.PrimitiveStringifier.TIME_NANOS_UTC_STRINGIFIER;
import static org.apache.parquet.schema.PrimitiveStringifier.TIME_STRINGIFIER;
import static org.apache.parquet.schema.PrimitiveStringifier.TIME_UTC_STRINGIFIER;

public abstract class LogicalTypeAnnotation {
  enum LogicalTypeToken {
    MAP {
      @Override
      protected LogicalTypeAnnotation fromString(List<String> params) {
        return mapType();
      }
    },
    LIST {
      @Override
      protected LogicalTypeAnnotation fromString(List<String> params) {
        return listType();
      }
    },
    STRING {
      @Override
      protected LogicalTypeAnnotation fromString(List<String> params) {
        return stringType();
      }
    },
    MAP_KEY_VALUE {
      @Override
      protected LogicalTypeAnnotation fromString(List<String> params) {
        return MapKeyValueTypeAnnotation.getInstance();
      }
    },
    ENUM {
      @Override
      protected LogicalTypeAnnotation fromString(List<String> params) {
        return enumType();
      }
    },
    DECIMAL {
      @Override
      protected LogicalTypeAnnotation fromString(List<String> params) {
        if (params.size() != 2) {
          throw new RuntimeException("Expecting 2 parameters for decimal logical type, got " + params.size());
        }
        return decimalType(Integer.valueOf(params.get(1)), Integer.valueOf(params.get(0)));
      }
    },
    DATE {
      @Override
      protected LogicalTypeAnnotation fromString(List<String> params) {
        return dateType();
      }
    },
    TIME {
      @Override
      protected LogicalTypeAnnotation fromString(List<String> params) {
        if (params.size() != 2) {
          throw new RuntimeException("Expecting 2 parameters for time logical type, got " + params.size());
        }
        return timeType(Boolean.parseBoolean(params.get(1)), TimeUnit.valueOf(params.get(0)));
      }
    },
    TIMESTAMP {
      @Override
      protected LogicalTypeAnnotation fromString(List<String> params) {
        if (params.size() != 2) {
          throw new RuntimeException("Expecting 2 parameters for timestamp logical type, got " + params.size());
        }
        return timestampType(Boolean.parseBoolean(params.get(1)), TimeUnit.valueOf(params.get(0)));
      }
    },
    INTEGER {
      @Override
      protected LogicalTypeAnnotation fromString(List<String> params) {
        if (params.size() != 2) {
          throw new RuntimeException("Expecting 2 parameters for integer logical type, got " + params.size());
        }
        return intType(Integer.valueOf(params.get(0)), Boolean.parseBoolean(params.get(1)));
      }
    },
    JSON {
      @Override
      protected LogicalTypeAnnotation fromString(List<String> params) {
        return jsonType();
      }
    },
    BSON {
      @Override
      protected LogicalTypeAnnotation fromString(List<String> params) {
        return bsonType();
      }
    },
    INTERVAL {
      @Override
      protected LogicalTypeAnnotation fromString(List<String> params) {
        return IntervalLogicalTypeAnnotation.getInstance();
      }
    };

    protected abstract LogicalTypeAnnotation fromString(List<String> params);
  }

  /**
   * Convert this logical type to old logical type representation in parquet-mr (if there's any).
   * Those logical type implementations, which don't have a corresponding mapping should return null.
   *
   * @return the OriginalType representation of the new logical type, or null if there's none
   */
  @InterfaceAudience.Private
  public abstract OriginalType toOriginalType();

  /**
   * Visits this logical type with the given visitor
   *
   * @param logicalTypeAnnotationVisitor the visitor to visit this type
   */
  public abstract <T> Optional<T> accept(LogicalTypeAnnotationVisitor<T> logicalTypeAnnotationVisitor);

  abstract LogicalTypeToken getType();

  String typeParametersAsString() {
    return "";
  }

  boolean isValidColumnOrder(ColumnOrder columnOrder) {
    return columnOrder.getColumnOrderName() == UNDEFINED || columnOrder.getColumnOrderName() == TYPE_DEFINED_ORDER;
  }

  @Override
  public String toString() {
    StringBuilder sb = new StringBuilder();
    sb.append(getType());
    sb.append(typeParametersAsString());
    return sb.toString();
  }

  PrimitiveStringifier valueStringifier(PrimitiveType primitiveType) {
    throw new UnsupportedOperationException("Stringifier is not supported for the logical type: " + this);
  }

  /**
   * Helper method to convert the old representation of logical types (OriginalType) to new logical type.
   */
  @InterfaceAudience.Private
  public static LogicalTypeAnnotation fromOriginalType(OriginalType originalType, DecimalMetadata decimalMetadata) {
    if (originalType == null) {
      return null;
    }
    switch (originalType) {
      case UTF8:
        return stringType();
      case MAP:
        return mapType();
      case DECIMAL:
        int scale = (decimalMetadata == null ? 0 : decimalMetadata.getScale());
        int precision = (decimalMetadata == null ? 0 : decimalMetadata.getPrecision());
        return decimalType(scale, precision);
      case LIST:
        return listType();
      case DATE:
        return dateType();
      case INTERVAL:
        return IntervalLogicalTypeAnnotation.getInstance();
      case TIMESTAMP_MILLIS:
        return timestampType(true, LogicalTypeAnnotation.TimeUnit.MILLIS);
      case TIMESTAMP_MICROS:
        return timestampType(true, LogicalTypeAnnotation.TimeUnit.MICROS);
      case TIME_MILLIS:
        return timeType(true, LogicalTypeAnnotation.TimeUnit.MILLIS);
      case TIME_MICROS:
        return timeType(true, LogicalTypeAnnotation.TimeUnit.MICROS);
      case UINT_8:
        return intType(8, false);
      case UINT_16:
        return intType(16, false);
      case UINT_32:
        return intType(32, false);
      case UINT_64:
        return intType(64, false);
      case INT_8:
        return intType(8, true);
      case INT_16:
        return intType(16, true);
      case INT_32:
        return intType(32, true);
      case INT_64:
        return intType(64, true);
      case ENUM:
        return enumType();
      case JSON:
        return jsonType();
      case BSON:
        return bsonType();
      case MAP_KEY_VALUE:
        return MapKeyValueTypeAnnotation.getInstance();
      default:
        throw new RuntimeException("Can't convert original type to logical type, unknown original type " + originalType);
    }
  }

  public static StringLogicalTypeAnnotation stringType() {
    return StringLogicalTypeAnnotation.INSTANCE;
  }

  public static MapLogicalTypeAnnotation mapType() {
    return MapLogicalTypeAnnotation.INSTANCE;
  }

  public static ListLogicalTypeAnnotation listType() {
    return ListLogicalTypeAnnotation.INSTANCE;
  }

  public static EnumLogicalTypeAnnotation enumType() {
    return EnumLogicalTypeAnnotation.INSTANCE;
  }

  public static DecimalLogicalTypeAnnotation decimalType(final int scale, final int precision) {
    return new DecimalLogicalTypeAnnotation(scale, precision);
  }

  public static DateLogicalTypeAnnotation dateType() {
    return DateLogicalTypeAnnotation.INSTANCE;
  }

  public static TimeLogicalTypeAnnotation timeType(final boolean isAdjustedToUTC, final TimeUnit unit) {
    return new TimeLogicalTypeAnnotation(isAdjustedToUTC, unit);
  }

  public static TimestampLogicalTypeAnnotation timestampType(final boolean isAdjustedToUTC, final TimeUnit unit) {
    return new TimestampLogicalTypeAnnotation(isAdjustedToUTC, unit);
  }

  public static IntLogicalTypeAnnotation intType(final int bitWidth, final boolean isSigned) {
    Preconditions.checkArgument(
      bitWidth == 8 || bitWidth == 16 || bitWidth == 32 || bitWidth == 64,
      "Invalid bit width for integer logical type, " + bitWidth + " is not allowed, " +
        "valid bit width values: 8, 16, 32, 64");
    return new IntLogicalTypeAnnotation(bitWidth, isSigned);
  }

  public static JsonLogicalTypeAnnotation jsonType() {
    return JsonLogicalTypeAnnotation.INSTANCE;
  }

  public static BsonLogicalTypeAnnotation bsonType() {
    return BsonLogicalTypeAnnotation.INSTANCE;
  }

  public static class StringLogicalTypeAnnotation extends LogicalTypeAnnotation {
    private static final StringLogicalTypeAnnotation INSTANCE = new StringLogicalTypeAnnotation();

    private StringLogicalTypeAnnotation() {
    }

    @Override
    @InterfaceAudience.Private
    public OriginalType toOriginalType() {
      return OriginalType.UTF8;
    }

    @Override
    public <T> Optional<T> accept(LogicalTypeAnnotationVisitor<T> logicalTypeAnnotationVisitor) {
      return logicalTypeAnnotationVisitor.visit(this);
    }

    @Override
    LogicalTypeToken getType() {
      return LogicalTypeToken.STRING;
    }

    @Override
    public boolean equals(Object obj) {
      return obj instanceof StringLogicalTypeAnnotation;
    }

    @Override
    public int hashCode() {
      // This type doesn't have any parameters, thus using class hashcode
      return getClass().hashCode();
    }

    @Override
    PrimitiveStringifier valueStringifier(PrimitiveType primitiveType) {
      return PrimitiveStringifier.UTF8_STRINGIFIER;
    }
  }

  public static class MapLogicalTypeAnnotation extends LogicalTypeAnnotation {
    private static final MapLogicalTypeAnnotation INSTANCE = new MapLogicalTypeAnnotation();

    private MapLogicalTypeAnnotation() {
    }

    @Override
    @InterfaceAudience.Private
    public OriginalType toOriginalType() {
      return OriginalType.MAP;
    }

    @Override
    public <T> Optional<T> accept(LogicalTypeAnnotationVisitor<T> logicalTypeAnnotationVisitor) {
      return logicalTypeAnnotationVisitor.visit(this);
    }

    @Override
    LogicalTypeToken getType() {
      return LogicalTypeToken.MAP;
    }

    @Override
    public boolean equals(Object obj) {
      return obj instanceof MapLogicalTypeAnnotation;
    }

    @Override
    public int hashCode() {
      // This type doesn't have any parameters, thus using class hashcode
      return getClass().hashCode();
    }
  }

  public static class ListLogicalTypeAnnotation extends LogicalTypeAnnotation {
    private static final ListLogicalTypeAnnotation INSTANCE = new ListLogicalTypeAnnotation();

    private ListLogicalTypeAnnotation() {
    }

    @Override
    @InterfaceAudience.Private
    public OriginalType toOriginalType() {
      return OriginalType.LIST;
    }

    @Override
    public <T> Optional<T> accept(LogicalTypeAnnotationVisitor<T> logicalTypeAnnotationVisitor) {
      return logicalTypeAnnotationVisitor.visit(this);
    }

    @Override
    LogicalTypeToken getType() {
      return LogicalTypeToken.LIST;
    }

    @Override
    public boolean equals(Object obj) {
      return obj instanceof ListLogicalTypeAnnotation;
    }

    @Override
    public int hashCode() {
      // This type doesn't have any parameters, thus using class hashcode
      return getClass().hashCode();
    }
  }

  public static class EnumLogicalTypeAnnotation extends LogicalTypeAnnotation {
    private static final EnumLogicalTypeAnnotation INSTANCE = new EnumLogicalTypeAnnotation();

    private EnumLogicalTypeAnnotation() {
    }

    @Override
    @InterfaceAudience.Private
    public OriginalType toOriginalType() {
      return OriginalType.ENUM;
    }

    @Override
    public <T> Optional<T> accept(LogicalTypeAnnotationVisitor<T> logicalTypeAnnotationVisitor) {
      return logicalTypeAnnotationVisitor.visit(this);
    }

    @Override
    LogicalTypeToken getType() {
      return LogicalTypeToken.ENUM;
    }

    @Override
    public boolean equals(Object obj) {
      return obj instanceof EnumLogicalTypeAnnotation;
    }

    @Override
    public int hashCode() {
      // This type doesn't have any parameters, thus using class hashcode
      return getClass().hashCode();
    }

    @Override
    PrimitiveStringifier valueStringifier(PrimitiveType primitiveType) {
      return PrimitiveStringifier.UTF8_STRINGIFIER;
    }
  }

  public static class DecimalLogicalTypeAnnotation extends LogicalTypeAnnotation {
    private final PrimitiveStringifier stringifier;
    private final int scale;
    private final int precision;

    private DecimalLogicalTypeAnnotation(int scale, int precision) {
      this.scale = scale;
      this.precision = precision;
      stringifier = PrimitiveStringifier.createDecimalStringifier(scale);
    }

    public int getPrecision() {
      return precision;
    }

    public int getScale() {
      return scale;
    }

    @Override
    @InterfaceAudience.Private
    public OriginalType toOriginalType() {
      return OriginalType.DECIMAL;
    }

    @Override
    public <T> Optional<T> accept(LogicalTypeAnnotationVisitor<T> logicalTypeAnnotationVisitor) {
      return logicalTypeAnnotationVisitor.visit(this);
    }

    @Override
    LogicalTypeToken getType() {
      return LogicalTypeToken.DECIMAL;
    }

    @Override
    protected String typeParametersAsString() {
      StringBuilder sb = new StringBuilder();
      sb.append("(");
      sb.append(precision);
      sb.append(",");
      sb.append(scale);
      sb.append(")");
      return sb.toString();
    }

    @Override
    public boolean equals(Object obj) {
      if (!(obj instanceof DecimalLogicalTypeAnnotation)) {
        return false;
      }
      DecimalLogicalTypeAnnotation other = (DecimalLogicalTypeAnnotation) obj;
      return scale == other.scale && precision == other.precision;
    }

    @Override
    public int hashCode() {
      return Objects.hash(scale, precision);
    }

    @Override
    PrimitiveStringifier valueStringifier(PrimitiveType primitiveType) {
      return stringifier;
    }
  }

  public static class DateLogicalTypeAnnotation extends LogicalTypeAnnotation {
    private static final DateLogicalTypeAnnotation INSTANCE = new DateLogicalTypeAnnotation();

    private DateLogicalTypeAnnotation() {
    }

    @Override
    @InterfaceAudience.Private
    public OriginalType toOriginalType() {
      return OriginalType.DATE;
    }

    @Override
    public <T> Optional<T> accept(LogicalTypeAnnotationVisitor<T> logicalTypeAnnotationVisitor) {
      return logicalTypeAnnotationVisitor.visit(this);
    }

    @Override
    LogicalTypeToken getType() {
      return LogicalTypeToken.DATE;
    }

    @Override
    public boolean equals(Object obj) {
      return obj instanceof DateLogicalTypeAnnotation;
    }

    @Override
    public int hashCode() {
      // This type doesn't have any parameters, thus using class hashcode
      return getClass().hashCode();
    }

    @Override
    PrimitiveStringifier valueStringifier(PrimitiveType primitiveType) {
      return PrimitiveStringifier.DATE_STRINGIFIER;
    }
  }

  public enum TimeUnit {
    MILLIS,
    MICROS,
    NANOS
  }

  public static class TimeLogicalTypeAnnotation extends LogicalTypeAnnotation {
    private final boolean isAdjustedToUTC;
    private final TimeUnit unit;

    private TimeLogicalTypeAnnotation(boolean isAdjustedToUTC, TimeUnit unit) {
      this.isAdjustedToUTC = isAdjustedToUTC;
      this.unit = unit;
    }

    @Override
    @InterfaceAudience.Private
    public OriginalType toOriginalType() {
      if (!isAdjustedToUTC) {
        return null;
      }
      switch (unit) {
        case MILLIS:
          return OriginalType.TIME_MILLIS;
        case MICROS:
          return OriginalType.TIME_MICROS;
        default:
          return null;
      }
    }

    @Override
    public <T> Optional<T> accept(LogicalTypeAnnotationVisitor<T> logicalTypeAnnotationVisitor) {
      return logicalTypeAnnotationVisitor.visit(this);
    }

    @Override
    LogicalTypeToken getType() {
      return LogicalTypeToken.TIME;
    }

    @Override
    protected String typeParametersAsString() {
      StringBuilder sb = new StringBuilder();
      sb.append("(");
      sb.append(unit);
      sb.append(",");
      sb.append(isAdjustedToUTC);
      sb.append(")");
      return sb.toString();
    }

    public TimeUnit getUnit() {
      return unit;
    }

    public boolean isAdjustedToUTC() {
      return isAdjustedToUTC;
    }

    @Override
    public boolean equals(Object obj) {
      if (!(obj instanceof TimeLogicalTypeAnnotation)) {
        return false;
      }
      TimeLogicalTypeAnnotation other = (TimeLogicalTypeAnnotation) obj;
      return isAdjustedToUTC == other.isAdjustedToUTC && unit == other.unit;
    }

    @Override
    public int hashCode() {
      return Objects.hash(isAdjustedToUTC, unit);
    }

    @Override
    PrimitiveStringifier valueStringifier(PrimitiveType primitiveType) {
      switch (unit) {
        case MICROS:
        case MILLIS:
          return isAdjustedToUTC ? TIME_UTC_STRINGIFIER : TIME_STRINGIFIER;
        case NANOS:
          return isAdjustedToUTC ? TIME_NANOS_UTC_STRINGIFIER : TIME_NANOS_STRINGIFIER;
        default:
          return super.valueStringifier(primitiveType);
      }
    }
  }

  public static class TimestampLogicalTypeAnnotation extends LogicalTypeAnnotation {
    private final boolean isAdjustedToUTC;
    private final TimeUnit unit;

    private TimestampLogicalTypeAnnotation(boolean isAdjustedToUTC, TimeUnit unit) {
      this.isAdjustedToUTC = isAdjustedToUTC;
      this.unit = unit;
    }

    @Override
    @InterfaceAudience.Private
    public OriginalType toOriginalType() {
      if (!isAdjustedToUTC) {
        return null;
      }
      switch (unit) {
        case MILLIS:
          return OriginalType.TIMESTAMP_MILLIS;
        case MICROS:
          return OriginalType.TIMESTAMP_MICROS;
        default:
          return null;
      }
    }

    @Override
    public <T> Optional<T> accept(LogicalTypeAnnotationVisitor<T> logicalTypeAnnotationVisitor) {
      return logicalTypeAnnotationVisitor.visit(this);
    }

    @Override
    LogicalTypeToken getType() {
      return LogicalTypeToken.TIMESTAMP;
    }

    @Override
    protected String typeParametersAsString() {
      StringBuilder sb = new StringBuilder();
      sb.append("(");
      sb.append(unit);
      sb.append(",");
      sb.append(isAdjustedToUTC);
      sb.append(")");
      return sb.toString();
    }

    public TimeUnit getUnit() {
      return unit;
    }

    public boolean isAdjustedToUTC() {
      return isAdjustedToUTC;
    }

    @Override
    public boolean equals(Object obj) {
      if (!(obj instanceof TimestampLogicalTypeAnnotation)) {
        return false;
      }
      TimestampLogicalTypeAnnotation other = (TimestampLogicalTypeAnnotation) obj;
      return (isAdjustedToUTC == other.isAdjustedToUTC) && (unit == other.unit);
    }

    @Override
    public int hashCode() {
      return Objects.hash(isAdjustedToUTC, unit);
    }

    @Override
    PrimitiveStringifier valueStringifier(PrimitiveType primitiveType) {
      switch (unit) {
        case MICROS:
          return isAdjustedToUTC ? TIMESTAMP_MICROS_UTC_STRINGIFIER : TIMESTAMP_MICROS_STRINGIFIER;
        case MILLIS:
          return isAdjustedToUTC ? TIMESTAMP_MILLIS_UTC_STRINGIFIER : TIMESTAMP_MILLIS_STRINGIFIER;
        case NANOS:
          return isAdjustedToUTC ? TIMESTAMP_NANOS_UTC_STRINGIFIER : TIMESTAMP_NANOS_STRINGIFIER;
        default:
          return super.valueStringifier(primitiveType);
      }
    }
  }

  public static class IntLogicalTypeAnnotation extends LogicalTypeAnnotation {
    private static final Set<Integer> VALID_BIT_WIDTH = Collections.unmodifiableSet(
      new HashSet<>(asList(8, 16, 32, 64)));

    private final int bitWidth;
    private final boolean isSigned;

    private IntLogicalTypeAnnotation(int bitWidth, boolean isSigned) {
      if (!VALID_BIT_WIDTH.contains(bitWidth)) {
        throw new IllegalArgumentException("Invalid integer bit width: " + bitWidth);
      }
      this.bitWidth = bitWidth;
      this.isSigned = isSigned;
    }

    @Override
    @InterfaceAudience.Private
    public OriginalType toOriginalType() {
      switch (bitWidth) {
        case 8:
          return isSigned ? OriginalType.INT_8 : OriginalType.UINT_8;
        case 16:
          return isSigned ? OriginalType.INT_16 : OriginalType.UINT_16;
        case 32:
          return isSigned ? OriginalType.INT_32 : OriginalType.UINT_32;
        case 64:
          return isSigned ? OriginalType.INT_64 : OriginalType.UINT_64;
        default:
          return null;
      }
    }

    @Override
    public <T> Optional<T> accept(LogicalTypeAnnotationVisitor<T> logicalTypeAnnotationVisitor) {
      return logicalTypeAnnotationVisitor.visit(this);
    }

    @Override
    LogicalTypeToken getType() {
      return LogicalTypeToken.INTEGER;
    }

    @Override
    protected String typeParametersAsString() {
      StringBuilder sb = new StringBuilder();
      sb.append("(");
      sb.append(bitWidth);
      sb.append(",");
      sb.append(isSigned);
      sb.append(")");
      return sb.toString();
    }

    public int getBitWidth() {
      return bitWidth;
    }

    public boolean isSigned() {
      return isSigned;
    }

    @Override
    public boolean equals(Object obj) {
      if (!(obj instanceof IntLogicalTypeAnnotation)) {
        return false;
      }
      IntLogicalTypeAnnotation other = (IntLogicalTypeAnnotation) obj;
      return (bitWidth == other.bitWidth) && (isSigned == other.isSigned);
    }

    @Override
    public int hashCode() {
      return Objects.hash(bitWidth, isSigned);
    }

    @Override
    PrimitiveStringifier valueStringifier(PrimitiveType primitiveType) {
      return isSigned ? PrimitiveStringifier.DEFAULT_STRINGIFIER : PrimitiveStringifier.UNSIGNED_STRINGIFIER;
    }
  }

  public static class JsonLogicalTypeAnnotation extends LogicalTypeAnnotation {
    private static final JsonLogicalTypeAnnotation INSTANCE = new JsonLogicalTypeAnnotation();

    private JsonLogicalTypeAnnotation() {
    }

    @Override
    @InterfaceAudience.Private
    public OriginalType toOriginalType() {
      return OriginalType.JSON;
    }

    @Override
    public <T> Optional<T> accept(LogicalTypeAnnotationVisitor<T> logicalTypeAnnotationVisitor) {
      return logicalTypeAnnotationVisitor.visit(this);
    }

    @Override
    LogicalTypeToken getType() {
      return LogicalTypeToken.JSON;
    }

    @Override
    public boolean equals(Object obj) {
      return obj instanceof JsonLogicalTypeAnnotation;
    }

    @Override
    public int hashCode() {
      // This type doesn't have any parameters, thus using class hashcode
      return getClass().hashCode();
    }

    @Override
    PrimitiveStringifier valueStringifier(PrimitiveType primitiveType) {
      return PrimitiveStringifier.UTF8_STRINGIFIER;
    }
  }

  public static class BsonLogicalTypeAnnotation extends LogicalTypeAnnotation {
    private static final BsonLogicalTypeAnnotation INSTANCE = new BsonLogicalTypeAnnotation();

    private BsonLogicalTypeAnnotation() {
    }

    @Override
    @InterfaceAudience.Private
    public OriginalType toOriginalType() {
      return OriginalType.BSON;
    }

    @Override
    public <T> Optional<T> accept(LogicalTypeAnnotationVisitor<T> logicalTypeAnnotationVisitor) {
      return logicalTypeAnnotationVisitor.visit(this);
    }

    @Override
    LogicalTypeToken getType() {
      return LogicalTypeToken.BSON;
    }

    @Override
    public boolean equals(Object obj) {
      return obj instanceof BsonLogicalTypeAnnotation;
    }

    @Override
    public int hashCode() {
      // This type doesn't have any parameters, thus using class hashcode
      return getClass().hashCode();
    }

    @Override
    PrimitiveStringifier valueStringifier(PrimitiveType primitiveType) {
      return PrimitiveStringifier.DEFAULT_STRINGIFIER;
    }
  }

  // This logical type annotation is implemented to support backward compatibility with ConvertedType.
  // The new logical type representation in parquet-format doesn't have any interval type,
  // thus this annotation is mapped to UNKNOWN.
  public static class IntervalLogicalTypeAnnotation extends LogicalTypeAnnotation {
    private static IntervalLogicalTypeAnnotation INSTANCE = new IntervalLogicalTypeAnnotation();

    public static LogicalTypeAnnotation getInstance() {
      return INSTANCE;
    }

    private IntervalLogicalTypeAnnotation() {
    }

    @Override
    @InterfaceAudience.Private
    public OriginalType toOriginalType() {
      return OriginalType.INTERVAL;
    }

    @Override
    public <T> Optional<T> accept(LogicalTypeAnnotationVisitor<T> logicalTypeAnnotationVisitor) {
      return logicalTypeAnnotationVisitor.visit(this);
    }

    @Override
    LogicalTypeToken getType() {
      return LogicalTypeToken.INTERVAL;
    }

    @Override
    public boolean equals(Object obj) {
      return obj instanceof IntervalLogicalTypeAnnotation;
    }

    @Override
    public int hashCode() {
      // This type doesn't have any parameters, thus using class hashcode
      return getClass().hashCode();
    }

    @Override
    PrimitiveStringifier valueStringifier(PrimitiveType primitiveType) {
      return PrimitiveStringifier.INTERVAL_STRINGIFIER;
    }

    @Override
    boolean isValidColumnOrder(ColumnOrder columnOrder) {
      return columnOrder.getColumnOrderName() == UNDEFINED;
    }
  }

  // This logical type annotation is implemented to support backward compatibility with ConvertedType.
  // The new logical type representation in parquet-format doesn't have any key-value type,
  // thus this annotation is mapped to UNKNOWN. This type shouldn't be used.
  public static class MapKeyValueTypeAnnotation extends LogicalTypeAnnotation {
    private static MapKeyValueTypeAnnotation INSTANCE = new MapKeyValueTypeAnnotation();

    public static MapKeyValueTypeAnnotation getInstance() {
      return INSTANCE;
    }

    private MapKeyValueTypeAnnotation() {
    }

    @Override
    @InterfaceAudience.Private
    public OriginalType toOriginalType() {
      return OriginalType.MAP_KEY_VALUE;
    }

    @Override
    public <T> Optional<T> accept(LogicalTypeAnnotationVisitor<T> logicalTypeAnnotationVisitor) {
      return logicalTypeAnnotationVisitor.visit(this);
    }

    @Override
    LogicalTypeToken getType() {
      return LogicalTypeToken.MAP_KEY_VALUE;
    }

    @Override
    public boolean equals(Object obj) {
      return obj instanceof MapKeyValueTypeAnnotation;
    }

    @Override
    public int hashCode() {
      // This type doesn't have any parameters, thus using class hashcode
      return getClass().hashCode();
    }
  }

  /**
   * Implement this interface to visit a logical type annotation in the schema.
   * The default implementation for each logical type specific visitor method is empty.
   * <p>
   * Example usage: logicalTypeAnnotation.accept(new LogicalTypeAnnotationVisitor() { ... });
   *
   * Every visit method returns {@link Optional#empty()} by default.
   * It means that for the given logical type no specific action is needed.
   * Client code can use {@link Optional#orElse(Object)} to return a default value for unhandled types,
   * or {@link Optional#orElseThrow(Supplier)} to throw exception if omitting a type is not allowed.
   */
  public interface LogicalTypeAnnotationVisitor<T> {
    default Optional<T> visit(StringLogicalTypeAnnotation stringLogicalType) {
      return empty();
    }

    default Optional<T> visit(MapLogicalTypeAnnotation mapLogicalType) {
      return empty();
    }

    default Optional<T> visit(ListLogicalTypeAnnotation listLogicalType) {
      return empty();
    }

    default Optional<T> visit(EnumLogicalTypeAnnotation enumLogicalType) {
      return empty();
    }

    default Optional<T> visit(DecimalLogicalTypeAnnotation decimalLogicalType) {
      return empty();
    }

    default Optional<T> visit(DateLogicalTypeAnnotation dateLogicalType) {
      return empty();
    }

    default Optional<T> visit(TimeLogicalTypeAnnotation timeLogicalType) {
      return empty();
    }

    default Optional<T> visit(TimestampLogicalTypeAnnotation timestampLogicalType) {
      return empty();
    }

    default Optional<T> visit(IntLogicalTypeAnnotation intLogicalType) {
      return empty();
    }

    default Optional<T> visit(JsonLogicalTypeAnnotation jsonLogicalType) {
      return empty();
    }

    default Optional<T> visit(BsonLogicalTypeAnnotation bsonLogicalType) {
      return empty();
    }

    default Optional<T> visit(IntervalLogicalTypeAnnotation intervalLogicalType) {
      return empty();
    }

    default Optional<T> visit(MapKeyValueTypeAnnotation mapKeyValueLogicalType) {
      return empty();
    }
  }
}