/*
 * 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.carbondata.core.metadata.datatype;

import java.util.ArrayList;
import java.util.LinkedList;
import java.util.List;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import org.apache.carbondata.common.annotations.InterfaceAudience;
import org.apache.carbondata.common.annotations.InterfaceStability;
import org.apache.carbondata.core.metadata.schema.table.column.ColumnSchema;

/**
 * A field represent one column
 */
@InterfaceAudience.User
@InterfaceStability.Unstable
public class Field {

  private String name;
  private DataType type;
  private List<StructField> children;
  private String parent;
  private int schemaOrdinal = -1;
  private int precision = 0;
  private int scale = 0;
  private String rawSchema = "";
  private String columnComment = "";

  /**
   * Field Constructor
   * @param name name of the field
   * @param type datatype of field, specified in strings.
   */
  public Field(String name, String type) {
    this.name = name.toLowerCase().trim();
    if (type.equalsIgnoreCase("string")) {
      this.type = DataTypes.STRING;
    } else if (type.equalsIgnoreCase("varchar")) {
      this.type = DataTypes.VARCHAR;
    } else if (type.equalsIgnoreCase("date")) {
      this.type = DataTypes.DATE;
    } else if (type.equalsIgnoreCase("timestamp")) {
      this.type = DataTypes.TIMESTAMP;
    } else if (type.equalsIgnoreCase("boolean")) {
      this.type = DataTypes.BOOLEAN;
    } else if (type.equalsIgnoreCase("byte")) {
      this.type = DataTypes.BYTE;
    } else if (type.equalsIgnoreCase("short")) {
      this.type = DataTypes.SHORT;
    } else if (type.equalsIgnoreCase("int")) {
      this.type = DataTypes.INT;
    } else if (type.equalsIgnoreCase("long")) {
      this.type = DataTypes.LONG;
    } else if (type.equalsIgnoreCase("float")) {
      this.type = DataTypes.FLOAT;
    } else if (type.equalsIgnoreCase("double")) {
      this.type = DataTypes.DOUBLE;
    } else if (type.equalsIgnoreCase("binary")) {
      this.type = DataTypes.BINARY;
    } else if (type.toLowerCase().startsWith("decimal")) {
      if ("decimal".equalsIgnoreCase(type.toLowerCase())) {
        this.type = DataTypes.createDefaultDecimalType();
      } else {
        try {
          Matcher m = Pattern.compile("^decimal\\(([^)]+)\\)").matcher(type.toLowerCase());
          m.find();
          String matchedString = m.group(1);
          String[] scaleAndPrecision = matchedString.split(",");
          precision = Integer.parseInt(scaleAndPrecision[0].trim());
          scale = Integer.parseInt(scaleAndPrecision[1].trim());
          this.type = DataTypes.createDecimalType(precision, scale);
        } catch (Exception e) {
          throw new IllegalArgumentException("unsupported data type: " + type
              + ". Please use decimal or decimal(precision,scale), " +
              "precision can be 10 and scale can be 2", e);
        }
      }
    } else if (type.equalsIgnoreCase("array")) {
      this.type = DataTypes.createDefaultArrayType();
    } else if (type.equalsIgnoreCase("struct")) {
      this.type = DataTypes.createDefaultStructType();
    } else if (type.equalsIgnoreCase("map")) {
      this.type = DataTypes.createDefaultMapType();
    } else {
      throw new IllegalArgumentException("unsupported data type: " + type);
    }
  }

  public Field(String name, String type, List<StructField> fields) {
    this.name = name.toLowerCase().trim();
    this.children = fields;
    if (type.equalsIgnoreCase("string")) {
      this.type = DataTypes.STRING;
    } else if (type.equalsIgnoreCase("varchar")) {
      this.type = DataTypes.VARCHAR;
    } else if (type.equalsIgnoreCase("date")) {
      this.type = DataTypes.DATE;
    } else if (type.equalsIgnoreCase("timestamp")) {
      this.type = DataTypes.TIMESTAMP;
    } else if (type.equalsIgnoreCase("boolean")) {
      this.type = DataTypes.BOOLEAN;
    } else if (type.equalsIgnoreCase("byte")) {
      this.type = DataTypes.BYTE;
    } else if (type.equalsIgnoreCase("short")) {
      this.type = DataTypes.SHORT;
    } else if (type.equalsIgnoreCase("int")) {
      this.type = DataTypes.INT;
    } else if (type.equalsIgnoreCase("long")) {
      this.type = DataTypes.LONG;
    } else if (type.equalsIgnoreCase("float")) {
      this.type = DataTypes.FLOAT;
    } else if (type.equalsIgnoreCase("double")) {
      this.type = DataTypes.DOUBLE;
    } else if (type.equalsIgnoreCase("binary")) {
      this.type = DataTypes.BINARY;
    } else if (type.equalsIgnoreCase("array")) {
      this.type = DataTypes.createArrayType(fields.get(0).getDataType(),
        fields.get(0).getFieldName());
    } else if (type.equalsIgnoreCase("struct")) {
      this.type = DataTypes.createStructType(fields);
    } else {
      throw new IllegalArgumentException("unsupported data type: " + type);
    }
  }

  public Field(String name, DataType type, List<StructField> fields) {
    this.name = name.toLowerCase().trim();
    this.type = type;
    this.children = fields;
  }

  public Field(String name, DataType type) {
    this.name = name.toLowerCase().trim();
    this.type = type;
    initComplexTypeChildren();
  }

  /**
   * Construct Field from ColumnSchema
   *
   * @param columnSchema ColumnSchema, Store the information about the column meta data
   */
  public Field(ColumnSchema columnSchema) {
    this.name = columnSchema.getColumnName().toLowerCase().trim();
    this.type = columnSchema.getDataType();
    children = new LinkedList<>();
    schemaOrdinal = columnSchema.getSchemaOrdinal();
    precision = columnSchema.getPrecision();
    scale = columnSchema.getScale();
  }

  public String getFieldName() {
    return name;
  }

  public DataType getDataType() {
    return type;
  }

  public List<StructField> getChildren() {
    return children;
  }

  public void setChildren(List<StructField> children) {
    this.children = children;
  }

  public String getParent() {
    return parent;
  }

  public void setParent(String parent) {
    this.parent = parent;
  }

  public int getSchemaOrdinal() {
    return schemaOrdinal;
  }

  public void setSchemaOrdinal(int schemaOrdinal) {
    this.schemaOrdinal = schemaOrdinal;
  }

  public int getPrecision() {
    return precision;
  }

  public void setPrecision(int precision) {
    this.precision = precision;
  }

  public int getScale() {
    return scale;
  }

  public void setScale(int scale) {
    this.scale = scale;
  }

  public String getRawSchema() {
    return rawSchema;
  }

  public void setRawSchema(String rawSchema) {
    this.rawSchema = rawSchema;
  }

  public String getColumnComment() {
    return columnComment;
  }

  public void setColumnComment(String columnComment) {
    this.columnComment = columnComment;
  }

  /* for SDK, change string type to varchar by default for parent columns */
  public void updateDataTypeToVarchar() {
    this.type = DataTypes.VARCHAR;
  }

  private void initComplexTypeChildren() {
    if (getDataType().isComplexType()) {
      StructField subFields = prepareSubFields(getFieldName(), getDataType());
      if (DataTypes.isArrayType(getDataType()) || DataTypes.isMapType(getDataType())) {
        children = subFields.getChildren();
      } else if (DataTypes.isStructType(getDataType())) {
        children = ((StructType) subFields.getDataType()).getFields();
      }
    }
  }

  @Override
  public String toString() {
    return "Field{" +
        "name='" + name + '\'' +
        ", type=" + type +
        ", schemaOrdinal=" + schemaOrdinal +
        '}';
  }

  /**
   * prepare sub fields for complex types
   *
   * @param fieldName column name
   * @param dataType data type of column or it's children
   * @return
   */
  private StructField prepareSubFields(String fieldName, DataType dataType) {
    if (DataTypes.isArrayType(dataType)) {
      List<StructField> arrayFields = new ArrayList<>();
      StructField arrayField = prepareSubFields(fieldName, ((ArrayType) dataType).getElementType());
      arrayFields.add(arrayField);
      return new StructField(fieldName, DataTypes.createArrayType(arrayField.getDataType()),
          arrayFields);
    } else if (DataTypes.isStructType(dataType)) {
      List<StructField> structFields = new ArrayList<>();
      List<StructField> fields = ((StructType) dataType).getFields();
      for (StructField field : fields) {
        structFields.add(prepareSubFields(field.getFieldName(), field.getDataType()));
      }
      return new StructField(fieldName, DataTypes.createStructType(structFields), structFields);
    } else if (DataTypes.isMapType(dataType)) {
      // Internally Map<key, value> is stored as Array<struct<key, value>>. So the below method
      // will convert a map type into similar field structure. The columnSchema will be formed
      // as Map<Struct<key,value>>
      List<StructField> mapFields = new ArrayList<>();
      MapType mapType = (MapType) dataType;
      // key is primitive type so type can be fetched directly
      StructField keyField = new StructField(fieldName + ".key", mapType.getKeyType());
      StructField valueField = prepareSubFields(fieldName + ".value", mapType.getValueType());
      mapFields.add(keyField);
      mapFields.add(valueField);
      StructField field =
          new StructField(fieldName + ".val", DataTypes.createStructType(mapFields), mapFields);
      MapType mapDataType = DataTypes.createMapType(keyField.getDataType(), field.getDataType());
      List<StructField> mapStructField = new ArrayList<>();
      mapStructField.add(field);
      return new StructField(fieldName, mapDataType, mapStructField);
    } else {
      return new StructField(fieldName, dataType);
    }
  }

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

  @Override
  public boolean equals(Object obj) {
    if (obj instanceof Field) {
      Field field = (Field) obj;
      return (this.getDataType().equals(field.getDataType()))
          && (this.getFieldName().equals(field.getFieldName()))
          && (this.getSchemaOrdinal() == (field.getSchemaOrdinal()));
    } else {
      return false;
    }
  }
}
