/**
 * 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.hadoop.hive.hbase;

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

import javax.annotation.Nullable;

import org.apache.avro.Schema;
import org.apache.avro.reflect.ReflectData;
import org.apache.hadoop.conf.Configuration;
import org.apache.hadoop.hive.common.JavaUtils;
import org.apache.hadoop.hive.hbase.ColumnMappings.ColumnMapping;
import org.apache.hadoop.hive.hbase.struct.AvroHBaseValueFactory;
import org.apache.hadoop.hive.hbase.struct.DefaultHBaseValueFactory;
import org.apache.hadoop.hive.hbase.struct.HBaseValueFactory;
import org.apache.hadoop.hive.hbase.struct.StructHBaseValueFactory;
import org.apache.hadoop.hive.serde.serdeConstants;
import org.apache.hadoop.hive.serde2.SerDeException;
import org.apache.hadoop.hive.serde2.avro.AvroSerdeUtils.AvroTableProperties;
import org.apache.hadoop.hive.serde2.lazy.LazySerDeParameters;
import org.apache.hadoop.hive.serde2.typeinfo.TypeInfo;
import org.apache.hadoop.util.ReflectionUtils;

/**
 * HBaseSerDeParameters encapsulates SerDeParameters and additional configurations that are specific for
 * HBaseSerDe.
 *
 */
public class HBaseSerDeParameters {

  public static final String AVRO_SERIALIZATION_TYPE = "avro";
  public static final String STRUCT_SERIALIZATION_TYPE = "struct";

  private final LazySerDeParameters serdeParams;

  private final Configuration job;

  private final String columnMappingString;
  private final ColumnMappings columnMappings;
  private final boolean doColumnRegexMatching;
  private final boolean doColumnPrefixCut;

  private final long putTimestamp;
  private final HBaseKeyFactory keyFactory;
  private final List<HBaseValueFactory> valueFactories;

  HBaseSerDeParameters(Configuration job, Properties tbl, String serdeName) throws SerDeException {
    this.job = job;

    // Read configuration parameters
    columnMappingString = tbl.getProperty(HBaseSerDe.HBASE_COLUMNS_MAPPING);
    doColumnRegexMatching =
        Boolean.parseBoolean(tbl.getProperty(HBaseSerDe.HBASE_COLUMNS_REGEX_MATCHING, "true"));
    doColumnPrefixCut = Boolean.parseBoolean(tbl.getProperty(HBaseSerDe.HBASE_COLUMNS_PREFIX_HIDE, "false"));
    // Parse and initialize the HBase columns mapping
    columnMappings = HBaseSerDe.parseColumnsMapping(columnMappingString, doColumnRegexMatching, doColumnPrefixCut);

    // Build the type property string if not supplied
    String columnTypeProperty = tbl.getProperty(serdeConstants.LIST_COLUMN_TYPES);
    String autogenerate = tbl.getProperty(HBaseSerDe.HBASE_AUTOGENERATE_STRUCT);

    if (columnTypeProperty == null || columnTypeProperty.isEmpty()) {
      String columnNameProperty = tbl.getProperty(serdeConstants.LIST_COLUMNS);
      if (columnNameProperty == null || columnNameProperty.isEmpty()) {
        if (autogenerate == null || autogenerate.isEmpty()) {
          throw new IllegalArgumentException("Either the columns must be specified or the "
              + HBaseSerDe.HBASE_AUTOGENERATE_STRUCT + " property must be set to true.");
        }

        tbl.setProperty(serdeConstants.LIST_COLUMNS,
            columnMappings.toNamesString(tbl, autogenerate));
      }

      tbl.setProperty(serdeConstants.LIST_COLUMN_TYPES,
          columnMappings.toTypesString(tbl, job, autogenerate));
    }

    this.serdeParams = new LazySerDeParameters(job, tbl, serdeName);
    this.putTimestamp = Long.parseLong(tbl.getProperty(HBaseSerDe.HBASE_PUT_TIMESTAMP, "-1"));

    columnMappings.setHiveColumnDescription(serdeName, serdeParams.getColumnNames(),
        serdeParams.getColumnTypes());

    // Precondition: make sure this is done after the rest of the SerDe initialization is done.
    String hbaseTableStorageType = tbl.getProperty(HBaseSerDe.HBASE_TABLE_DEFAULT_STORAGE_TYPE);
    columnMappings.parseColumnStorageTypes(hbaseTableStorageType);

    this.keyFactory = initKeyFactory(job, tbl);
    this.valueFactories = initValueFactories(job, tbl);
  }

  public List<String> getColumnNames() {
    return serdeParams.getColumnNames();
  }

  public List<TypeInfo> getColumnTypes() {
    return serdeParams.getColumnTypes();
  }

  public LazySerDeParameters getSerdeParams() {
    return serdeParams;
  }

  public long getPutTimestamp() {
    return putTimestamp;
  }

  public int getKeyIndex() {
    return columnMappings.getKeyIndex();
  }

  public ColumnMapping getKeyColumnMapping() {
    return columnMappings.getKeyMapping();
  }

  public int getTimestampIndex() {
    return columnMappings.getTimestampIndex();
  }

  public ColumnMapping getTimestampColumnMapping() {
    return columnMappings.getTimestampMapping();
  }

  public ColumnMappings getColumnMappings() {
    return columnMappings;
  }

  public HBaseKeyFactory getKeyFactory() {
    return keyFactory;
  }

  public List<HBaseValueFactory> getValueFactories() {
    return valueFactories;
  }

  public Configuration getBaseConfiguration() {
    return job;
  }

  public TypeInfo getTypeForName(String columnName) {
    List<String> columnNames = serdeParams.getColumnNames();
    List<TypeInfo> columnTypes = serdeParams.getColumnTypes();
    for (int i = 0; i < columnNames.size(); i++) {
      if (columnName.equals(columnNames.get(i))) {
        return columnTypes.get(i);
      }
    }
    throw new IllegalArgumentException("Invalid column name " + columnName);
  }

  public String toString() {
    return "[" + columnMappingString + ":" + getColumnNames() + ":" + getColumnTypes() + "]";
  }

  private HBaseKeyFactory initKeyFactory(Configuration conf, Properties tbl) throws SerDeException {
    try {
      HBaseKeyFactory keyFactory = createKeyFactory(conf, tbl);
      if (keyFactory != null) {
        keyFactory.init(this, tbl);
      }
      return keyFactory;
    } catch (Exception e) {
      throw new SerDeException(e);
    }
  }

  private static HBaseKeyFactory createKeyFactory(Configuration job, Properties tbl)
      throws Exception {
    String factoryClassName = tbl.getProperty(HBaseSerDe.HBASE_COMPOSITE_KEY_FACTORY);
    if (factoryClassName != null) {
      Class<?> factoryClazz = loadClass(factoryClassName, job);
      return (HBaseKeyFactory) ReflectionUtils.newInstance(factoryClazz, job);
    }
    String keyClassName = tbl.getProperty(HBaseSerDe.HBASE_COMPOSITE_KEY_CLASS);
    if (keyClassName != null) {
      Class<?> keyClass = loadClass(keyClassName, job);
      return new CompositeHBaseKeyFactory(keyClass);
    }
    return new DefaultHBaseKeyFactory();
  }

  private static Class<?> loadClass(String className, @Nullable Configuration configuration)
      throws Exception {
    if (configuration != null) {
      return configuration.getClassByName(className);
    }
    return JavaUtils.loadClass(className);
  }

  private List<HBaseValueFactory> initValueFactories(Configuration conf, Properties tbl)
      throws SerDeException {
    List<HBaseValueFactory> valueFactories = createValueFactories(conf, tbl);

    for (HBaseValueFactory valueFactory : valueFactories) {
      valueFactory.init(this, conf, tbl);
    }

    return valueFactories;
  }

  private List<HBaseValueFactory> createValueFactories(Configuration conf, Properties tbl)
      throws SerDeException {
    List<HBaseValueFactory> valueFactories = new ArrayList<HBaseValueFactory>();

    try {
      for (int i = 0; i < columnMappings.size(); i++) {
        String serType = getSerializationType(conf, tbl, columnMappings.getColumnsMapping()[i]);

        if (AVRO_SERIALIZATION_TYPE.equals(serType)) {
          Schema schema = getSchema(conf, tbl, columnMappings.getColumnsMapping()[i]);
          valueFactories.add(new AvroHBaseValueFactory(i, schema));
        } else if (STRUCT_SERIALIZATION_TYPE.equals(serType)) {
          String structValueClassName = tbl.getProperty(HBaseSerDe.HBASE_STRUCT_SERIALIZER_CLASS);

          if (structValueClassName == null) {
            throw new IllegalArgumentException(HBaseSerDe.HBASE_STRUCT_SERIALIZER_CLASS
                + " must be set for hbase columns of type [" + STRUCT_SERIALIZATION_TYPE + "]");
          }

          Class<?> structValueClass = loadClass(structValueClassName, job);
          valueFactories.add(new StructHBaseValueFactory(i, structValueClass));
        } else {
          valueFactories.add(new DefaultHBaseValueFactory(i));
        }
      }
    } catch (Exception e) {
      throw new SerDeException(e);
    }

    return valueFactories;
  }

  /**
   * Get the type for the given {@link ColumnMapping colMap}
   * */
  private String getSerializationType(Configuration conf, Properties tbl,
      ColumnMapping colMap) throws Exception {
    String serType = null;

    if (colMap.qualifierName == null) {
      // only a column family

      if (colMap.qualifierPrefix != null) {
        serType = tbl.getProperty(colMap.familyName + "." + colMap.qualifierPrefix + "."
            + HBaseSerDe.SERIALIZATION_TYPE);
      } else {
        serType = tbl.getProperty(colMap.familyName + "." + HBaseSerDe.SERIALIZATION_TYPE);
      }
    } else if (!colMap.hbaseRowKey) {
      // not an hbase row key. This should either be a prefix or an individual qualifier
      String qualifierName = colMap.qualifierName;

      if (colMap.qualifierName.endsWith("*")) {
        qualifierName = colMap.qualifierName.substring(0, colMap.qualifierName.length() - 1);
      }

      serType =
          tbl.getProperty(colMap.familyName + "." + qualifierName + "."
              + HBaseSerDe.SERIALIZATION_TYPE);
    }

    return serType;
  }

  private Schema getSchema(Configuration conf, Properties tbl, ColumnMapping colMap)
      throws Exception {
    String serType = null;
    String serClassName = null;
    String schemaLiteral = null;
    String schemaUrl = null;

    if (colMap.qualifierName == null) {
      // only a column family

      if (colMap.qualifierPrefix != null) {
        serType =
            tbl.getProperty(colMap.familyName + "." + colMap.qualifierPrefix + "."
                + HBaseSerDe.SERIALIZATION_TYPE);

        serClassName =
            tbl.getProperty(colMap.familyName + "." + colMap.qualifierPrefix + "."
                + serdeConstants.SERIALIZATION_CLASS);

        schemaLiteral =
            tbl.getProperty(colMap.familyName + "." + colMap.qualifierPrefix + "."
                + AvroTableProperties.SCHEMA_LITERAL.getPropName());

        schemaUrl =
            tbl.getProperty(colMap.familyName + "." + colMap.qualifierPrefix + "."
                + AvroTableProperties.SCHEMA_URL.getPropName());
      } else {
        serType = tbl.getProperty(colMap.familyName + "." + HBaseSerDe.SERIALIZATION_TYPE);

        serClassName =
            tbl.getProperty(colMap.familyName + "." + serdeConstants.SERIALIZATION_CLASS);

        schemaLiteral = tbl.getProperty(colMap.familyName + "." + AvroTableProperties.SCHEMA_LITERAL.getPropName());

        schemaUrl = tbl.getProperty(colMap.familyName + "." + AvroTableProperties.SCHEMA_URL.getPropName());
      }
    } else if (!colMap.hbaseRowKey) {
      // not an hbase row key. This should either be a prefix or an individual qualifier
      String qualifierName = colMap.qualifierName;

      if (colMap.qualifierName.endsWith("*")) {
        qualifierName = colMap.qualifierName.substring(0, colMap.qualifierName.length() - 1);
      }

      serType =
          tbl.getProperty(colMap.familyName + "." + qualifierName + "."
              + HBaseSerDe.SERIALIZATION_TYPE);

      serClassName =
          tbl.getProperty(colMap.familyName + "." + qualifierName + "."
              + serdeConstants.SERIALIZATION_CLASS);

      schemaLiteral =
          tbl.getProperty(colMap.familyName + "." + qualifierName + "."
              + AvroTableProperties.SCHEMA_LITERAL.getPropName());

      schemaUrl =
          tbl.getProperty(colMap.familyName + "." + qualifierName + "." + AvroTableProperties.SCHEMA_URL.getPropName());
    }

    if (serType == null) {
      throw new IllegalArgumentException("serialization.type property is missing");
    }

    String avroSchemaRetClass = tbl.getProperty(AvroTableProperties.SCHEMA_RETRIEVER.getPropName());

    if (schemaLiteral == null && serClassName == null && schemaUrl == null
        && avroSchemaRetClass == null) {
      throw new IllegalArgumentException("serialization.type was set to [" + serType
          + "] but neither " + AvroTableProperties.SCHEMA_LITERAL.getPropName() + ", " + AvroTableProperties.SCHEMA_URL.getPropName()
          + ", serialization.class or " + AvroTableProperties.SCHEMA_RETRIEVER.getPropName() + " property was set");
    }

    Class<?> deserializerClass = null;

    if (serClassName != null) {
      deserializerClass = loadClass(serClassName, conf);
    }

    Schema schema = null;

    // only worry about getting schema if we are dealing with Avro
    if (serType.equalsIgnoreCase(AVRO_SERIALIZATION_TYPE)) {
      if (avroSchemaRetClass == null) {
        // bother about generating a schema only if a schema retriever class wasn't provided
        if (schemaLiteral != null) {
          schema = Schema.parse(schemaLiteral);
        } else if (schemaUrl != null) {
          schema = HBaseSerDeHelper.getSchemaFromFS(schemaUrl, conf);
        } else if (deserializerClass != null) {
          schema = ReflectData.get().getSchema(deserializerClass);
        }
      }
    }

    return schema;
  }
}
