/**
 * Copyright 2018 LinkedIn Corporation. All rights reserved.
 * Licensed under the BSD-2 Clause license.
 * See LICENSE in the project root for license information.
 */
package com.linkedin.transport.hive.types.objectinspector;

import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import org.apache.commons.lang3.tuple.Pair;
import org.apache.hadoop.hive.serde2.objectinspector.ListObjectInspector;
import org.apache.hadoop.hive.serde2.objectinspector.MapObjectInspector;
import org.apache.hadoop.hive.serde2.objectinspector.ObjectInspector;
import org.apache.hadoop.hive.serde2.objectinspector.ObjectInspectorConverters;
import org.apache.hadoop.hive.serde2.objectinspector.ObjectInspectorConverters.Converter;
import org.apache.hadoop.hive.serde2.objectinspector.PrimitiveObjectInspector;
import org.apache.hadoop.hive.serde2.objectinspector.SettableListObjectInspector;
import org.apache.hadoop.hive.serde2.objectinspector.SettableMapObjectInspector;
import org.apache.hadoop.hive.serde2.objectinspector.SettableStructObjectInspector;
import org.apache.hadoop.hive.serde2.objectinspector.StructField;
import org.apache.hadoop.hive.serde2.objectinspector.StructObjectInspector;
import org.apache.hadoop.hive.serde2.objectinspector.primitive.JavaStringObjectInspector;
import org.apache.hadoop.hive.serde2.objectinspector.primitive.SettableBinaryObjectInspector;
import org.apache.hadoop.hive.serde2.objectinspector.primitive.SettableBooleanObjectInspector;
import org.apache.hadoop.hive.serde2.objectinspector.primitive.SettableByteObjectInspector;
import org.apache.hadoop.hive.serde2.objectinspector.primitive.SettableDateObjectInspector;
import org.apache.hadoop.hive.serde2.objectinspector.primitive.SettableDoubleObjectInspector;
import org.apache.hadoop.hive.serde2.objectinspector.primitive.SettableFloatObjectInspector;
import org.apache.hadoop.hive.serde2.objectinspector.primitive.SettableHiveCharObjectInspector;
import org.apache.hadoop.hive.serde2.objectinspector.primitive.SettableHiveDecimalObjectInspector;
import org.apache.hadoop.hive.serde2.objectinspector.primitive.SettableHiveVarcharObjectInspector;
import org.apache.hadoop.hive.serde2.objectinspector.primitive.SettableIntObjectInspector;
import org.apache.hadoop.hive.serde2.objectinspector.primitive.SettableLongObjectInspector;
import org.apache.hadoop.hive.serde2.objectinspector.primitive.SettableShortObjectInspector;
import org.apache.hadoop.hive.serde2.objectinspector.primitive.SettableTimestampObjectInspector;
import org.apache.hadoop.hive.serde2.objectinspector.primitive.VoidObjectInspector;
import org.apache.hadoop.hive.serde2.objectinspector.primitive.WritableStringObjectInspector;


/**
 * CacheableObjectInspectorConverters.
 *
 */
public final class CacheableObjectInspectorConverters {

  private Map<Pair<ObjectInspector, ObjectInspector>, Converter> _cachedConverters;

  public CacheableObjectInspectorConverters() {
    _cachedConverters = new ConcurrentHashMap<>();
  }

  private Converter getConverterFromCache(ObjectInspector inputOI, ObjectInspector outputOI) {
    return _cachedConverters.get(Pair.of(inputOI, outputOI));
  }

  private void cacheConverter(ObjectInspector inputOI, ObjectInspector outputOI, Converter c) {
    _cachedConverters.putIfAbsent(Pair.of(inputOI, outputOI), c);
  }

  private Converter getConverter(PrimitiveObjectInspector inputOI, PrimitiveObjectInspector outputOI) {
    Converter c = getConverterFromCache(inputOI, outputOI);
    if (c != null) {
      return c;
    }
    switch (outputOI.getPrimitiveCategory()) {
      case BOOLEAN:
        c = new CacheablePrimitiveObjectInspectorConverter.BooleanConverter(inputOI,
            (SettableBooleanObjectInspector) outputOI);
        break;
      case BYTE:
        c = new CacheablePrimitiveObjectInspectorConverter.ByteConverter(inputOI,
            (SettableByteObjectInspector) outputOI);
        break;
      case SHORT:
        c = new CacheablePrimitiveObjectInspectorConverter.ShortConverter(inputOI,
            (SettableShortObjectInspector) outputOI);
        break;
      case INT:
        c = new CacheablePrimitiveObjectInspectorConverter.IntConverter(inputOI, (SettableIntObjectInspector) outputOI);
        break;
      case LONG:
        c = new CacheablePrimitiveObjectInspectorConverter.LongConverter(inputOI,
            (SettableLongObjectInspector) outputOI);
        break;
      case FLOAT:
        c = new CacheablePrimitiveObjectInspectorConverter.FloatConverter(inputOI,
            (SettableFloatObjectInspector) outputOI);
        break;
      case DOUBLE:
        c = new CacheablePrimitiveObjectInspectorConverter.DoubleConverter(inputOI,
            (SettableDoubleObjectInspector) outputOI);
        break;
      case STRING:
        if (outputOI instanceof WritableStringObjectInspector) {
          c = new CacheablePrimitiveObjectInspectorConverter.TextConverter(inputOI);
        } else if (outputOI instanceof JavaStringObjectInspector) {
          c = new CacheablePrimitiveObjectInspectorConverter.StringConverter(inputOI);
        }
        break;
      case CHAR:
        c = new CacheablePrimitiveObjectInspectorConverter.HiveCharConverter(inputOI,
            (SettableHiveCharObjectInspector) outputOI);
        break;
      case VARCHAR:
        c = new CacheablePrimitiveObjectInspectorConverter.HiveVarcharConverter(inputOI,
            (SettableHiveVarcharObjectInspector) outputOI);
        break;
      case DATE:
        c = new CacheablePrimitiveObjectInspectorConverter.DateConverter(inputOI,
            (SettableDateObjectInspector) outputOI);
        break;
      case TIMESTAMP:
        c = new CacheablePrimitiveObjectInspectorConverter.TimestampConverter(inputOI,
            (SettableTimestampObjectInspector) outputOI);
        break;
      case BINARY:
        c = new CacheablePrimitiveObjectInspectorConverter.BinaryConverter(inputOI,
            (SettableBinaryObjectInspector) outputOI);
        break;
      case DECIMAL:
        c = new CacheablePrimitiveObjectInspectorConverter.HiveDecimalConverter(inputOI,
            (SettableHiveDecimalObjectInspector) outputOI);
        break;
      default:
        throw new UnsupportedOperationException(
            "Hive internal error: conversion of " + inputOI.getTypeName() + " to " + outputOI.getTypeName()
                + " not supported yet.");
    }
    cacheConverter(inputOI, outputOI, c);
    return c;
  }

  /**
   * Returns a converter that converts objects from one OI to another OI. The
   * returned (converted) object does not belong to the converter. Hence once convertor can be used
   * multiple times within one eval invocation.
   */
  public Converter getConverter(ObjectInspector inputOI, ObjectInspector outputOI) {
    // If the inputOI is the same as the outputOI, just return an
    // IdentityConverter.
    if (inputOI.equals(outputOI)) {
      return new ObjectInspectorConverters.IdentityConverter();
    }
    Converter c = getConverterFromCache(inputOI, outputOI);
    if (c != null) {
      return c;
    }
    switch (outputOI.getCategory()) {
      case PRIMITIVE:
        return getConverter((PrimitiveObjectInspector) inputOI, (PrimitiveObjectInspector) outputOI);
      case STRUCT:
        c = new StructConverter(inputOI, (SettableStructObjectInspector) outputOI);
        break;
      case LIST:
        c = new ListConverter(inputOI, (SettableListObjectInspector) outputOI);
        break;
      case MAP:
        c = new MapConverter(inputOI, (SettableMapObjectInspector) outputOI);
        break;
      default:
        throw new UnsupportedOperationException(
            "Hive internal error: conversion of " + inputOI.getTypeName() + " to " + outputOI.getTypeName()
                + " not supported yet.");
    }
    cacheConverter(inputOI, outputOI, c);
    return c;
  }

  /**
   * A converter class for List.
   */
  public class ListConverter implements Converter {

    ListObjectInspector inputOI;
    SettableListObjectInspector outputOI;

    ObjectInspector inputElementOI;
    ObjectInspector outputElementOI;

    Converter elementConverter;

    public ListConverter(ObjectInspector inputOI, SettableListObjectInspector outputOI) {
      if (inputOI instanceof ListObjectInspector) {
        this.inputOI = (ListObjectInspector) inputOI;
        this.outputOI = outputOI;
        inputElementOI = this.inputOI.getListElementObjectInspector();
        outputElementOI = outputOI.getListElementObjectInspector();
        elementConverter = getConverter(inputElementOI, outputElementOI);
      } else if (!(inputOI instanceof VoidObjectInspector)) {
        throw new UnsupportedOperationException(
            "Hive internal error: conversion of " + inputOI.getTypeName() + " to " + outputOI.getTypeName()
                + "not supported yet.");
      }
    }

    @Override
    public Object convert(Object input) {
      if (input == null) {
        return null;
      }
      Object output = outputOI.create(0);
      int size = inputOI.getListLength(input);

      // Convert the elements
      outputOI.resize(output, size);
      for (int index = 0; index < size; index++) {
        Object inputElement = inputOI.getListElement(input, index);
        Object outputElement = elementConverter.convert(inputElement);
        outputOI.set(output, index, outputElement);
      }
      return output;
    }
  }

  /**
   * A converter class for Struct.
   */
  public class StructConverter implements Converter {

    StructObjectInspector inputOI;
    SettableStructObjectInspector outputOI;

    List<? extends StructField> inputFields;
    List<? extends StructField> outputFields;

    ArrayList<Converter> fieldConverters;

    public StructConverter(ObjectInspector inputOI, SettableStructObjectInspector outputOI) {
      if (inputOI instanceof StructObjectInspector) {
        this.inputOI = (StructObjectInspector) inputOI;
        this.outputOI = outputOI;
        inputFields = this.inputOI.getAllStructFieldRefs();
        outputFields = outputOI.getAllStructFieldRefs();

        // If the output has some extra fields, set them to NULL.
        int minFields = Math.min(inputFields.size(), outputFields.size());
        fieldConverters = new ArrayList<Converter>(minFields);
        for (int f = 0; f < minFields; f++) {
          fieldConverters.add(getConverter(inputFields.get(f).getFieldObjectInspector(),
              outputFields.get(f).getFieldObjectInspector()));
        }
      } else if (!(inputOI instanceof VoidObjectInspector)) {
        throw new UnsupportedOperationException(
            "Hive internal error: conversion of " + inputOI.getTypeName() + " to " + outputOI.getTypeName()
                + "not supported yet.");
      }
    }

    @Override
    public Object convert(Object input) {
      if (input == null) {
        return null;
      }
      Object output = outputOI.create();
      int minFields = Math.min(inputFields.size(), outputFields.size());
      // Convert the fields
      for (int f = 0; f < minFields; f++) {
        Object inputFieldValue = inputOI.getStructFieldData(input, inputFields.get(f));
        Object outputFieldValue = fieldConverters.get(f).convert(inputFieldValue);
        outputOI.setStructFieldData(output, outputFields.get(f), outputFieldValue);
      }

      // set the extra fields to null
      for (int f = minFields; f < outputFields.size(); f++) {
        outputOI.setStructFieldData(output, outputFields.get(f), null);
      }

      return output;
    }
  }

  /**
   * A converter class for Map.
   */
  public class MapConverter implements Converter {

    MapObjectInspector inputOI;
    SettableMapObjectInspector outputOI;

    ObjectInspector inputKeyOI;
    ObjectInspector outputKeyOI;

    ObjectInspector inputValueOI;
    ObjectInspector outputValueOI;

    Converter keyConverter;
    Converter valueConverter;

    public MapConverter(ObjectInspector inputOI, SettableMapObjectInspector outputOI) {
      if (inputOI instanceof MapObjectInspector) {
        this.inputOI = (MapObjectInspector) inputOI;
        this.outputOI = outputOI;
        inputKeyOI = this.inputOI.getMapKeyObjectInspector();
        outputKeyOI = outputOI.getMapKeyObjectInspector();
        inputValueOI = this.inputOI.getMapValueObjectInspector();
        outputValueOI = outputOI.getMapValueObjectInspector();
        keyConverter = getConverter(inputKeyOI, outputKeyOI);
        valueConverter = getConverter(inputValueOI, outputValueOI);
      } else if (!(inputOI instanceof VoidObjectInspector)) {
        throw new UnsupportedOperationException(
            "Hive internal error: conversion of " + inputOI.getTypeName() + " to " + outputOI.getTypeName()
                + "not supported yet.");
      }
    }

    @Override
    public Object convert(Object input) {
      if (input == null) {
        return null;
      }
      Object output = outputOI.create();

      // NOTE: This code tries to get all key-value pairs out of the map.
      // It's not very efficient. The more efficient way should be to let MapOI
      // return an Iterator. This is currently not supported by MapOI yet.

      Map<?, ?> map = inputOI.getMap(input);

      // Convert the key/value pairs
      for (Map.Entry<?, ?> entry : map.entrySet()) {
        Object inputKey = entry.getKey();
        Object inputValue = entry.getValue();
        Object outputKey = keyConverter.convert(inputKey);
        Object outputValue = valueConverter.convert(inputValue);
        outputOI.put(output, outputKey, outputValue);
      }
      return output;
    }
  }
}