/**
 * Copyright © 2017 Jeremy Custenborder ([email protected])
 *
 * Licensed 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 com.github.jcustenborder.kafka.connect.transform.common;

import com.google.common.base.MoreObjects;
import com.google.common.base.Preconditions;
import com.google.common.collect.ComparisonChain;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.Ordering;
import org.apache.kafka.connect.connector.ConnectRecord;
import org.apache.kafka.connect.data.Date;
import org.apache.kafka.connect.data.Decimal;
import org.apache.kafka.connect.data.Schema;
import org.apache.kafka.connect.data.Struct;
import org.apache.kafka.connect.data.Time;
import org.apache.kafka.connect.data.Timestamp;
import org.apache.kafka.connect.data.Values;
import org.apache.kafka.connect.header.Header;

import java.util.HashMap;
import java.util.Map;
import java.util.Objects;

abstract class ConversionHandler {
  final Schema headerSchema;
  final String header;
  final String field;

  protected ConversionHandler(Schema headerSchema, String header, String field) {
    this.headerSchema = headerSchema;
    this.header = header;
    this.field = field;
  }

  abstract Object convert(Header header);

  public void convert(ConnectRecord record, Struct struct) {
    final Header header = record.headers().lastWithName(this.header);
    Object fieldValue;
    if (null != header) {
      fieldValue = convert(header);
    } else {
      fieldValue = null;
    }

    struct.put(this.field, fieldValue);
  }

  static class StringConversionHandler extends ConversionHandler {
    public StringConversionHandler(Schema headerSchema, String header, String field) {
      super(headerSchema, header, field);
    }

    @Override
    Object convert(Header header) {
      return Values.convertToString(header.schema(), header.value());
    }
  }

  static class BooleanConversionHandler extends ConversionHandler {
    public BooleanConversionHandler(Schema headerSchema, String header, String field) {
      super(headerSchema, header, field);
    }

    @Override
    Object convert(Header header) {
      return Values.convertToBoolean(header.schema(), header.value());
    }
  }

  static class Float32ConversionHandler extends ConversionHandler {
    public Float32ConversionHandler(Schema headerSchema, String header, String field) {
      super(headerSchema, header, field);
    }

    @Override
    Object convert(Header header) {
      return Values.convertToFloat(header.schema(), header.value());
    }
  }

  static class Float64ConversionHandler extends ConversionHandler {
    public Float64ConversionHandler(Schema headerSchema, String header, String field) {
      super(headerSchema, header, field);
    }

    @Override
    Object convert(Header header) {
      return Values.convertToDouble(header.schema(), header.value());
    }
  }

  static class Int8ConversionHandler extends ConversionHandler {
    public Int8ConversionHandler(Schema headerSchema, String header, String field) {
      super(headerSchema, header, field);
    }

    @Override
    Object convert(Header header) {
      return Values.convertToByte(header.schema(), header.value());
    }
  }

  static class Int16ConversionHandler extends ConversionHandler {
    public Int16ConversionHandler(Schema headerSchema, String header, String field) {
      super(headerSchema, header, field);
    }

    @Override
    Object convert(Header header) {
      return Values.convertToShort(header.schema(), header.value());
    }
  }

  static class Int32ConversionHandler extends ConversionHandler {
    public Int32ConversionHandler(Schema headerSchema, String header, String field) {
      super(headerSchema, header, field);
    }

    @Override
    Object convert(Header header) {
      return Values.convertToInteger(header.schema(), header.value());
    }
  }

  static class Int64ConversionHandler extends ConversionHandler {
    public Int64ConversionHandler(Schema headerSchema, String header, String field) {
      super(headerSchema, header, field);
    }

    @Override
    Object convert(Header header) {
      return Values.convertToLong(header.schema(), header.value());
    }
  }

  static class DecimalConversionHandler extends ConversionHandler {
    private final int scale;

    public DecimalConversionHandler(Schema headerSchema, String header, String field) {
      super(headerSchema, header, field);
      String scaleText = null != headerSchema.parameters() ? headerSchema.parameters().get(Decimal.SCALE_FIELD) : null;
      Preconditions.checkNotNull(scaleText, "schema parameters must contain a '%s' parameter.", Decimal.SCALE_FIELD);
      scale = Integer.parseInt(scaleText);
    }

    @Override
    Object convert(Header header) {
      return Values.convertToDecimal(header.schema(), header.value(), scale);
    }
  }

  static class TimestampConversionHandler extends ConversionHandler {

    public TimestampConversionHandler(Schema headerSchema, String header, String field) {
      super(headerSchema, header, field);
    }

    @Override
    Object convert(Header header) {
      return Values.convertToTimestamp(header.schema(), header.value());
    }
  }

  static class TimeConversionHandler extends ConversionHandler {

    public TimeConversionHandler(Schema headerSchema, String header, String field) {
      super(headerSchema, header, field);
    }

    @Override
    Object convert(Header header) {
      return Values.convertToTime(header.schema(), header.value());
    }
  }

  static class DateConversionHandler extends ConversionHandler {

    public DateConversionHandler(Schema headerSchema, String header, String field) {
      super(headerSchema, header, field);
    }

    @Override
    Object convert(Header header) {
      return Values.convertToDate(header.schema(), header.value());
    }
  }


  static class SchemaKey implements Comparable<SchemaKey> {
    final String name;
    final Schema.Type type;


    SchemaKey(String name, Schema.Type type) {
      this.name = name;
      this.type = type;
    }

    public static SchemaKey of(Schema schema) {
      return new SchemaKey(schema.name(), schema.type());
    }

    @Override
    public int hashCode() {
      return Objects.hash(this.type, this.name);
    }

    @Override
    public int compareTo(SchemaKey that) {
      return ComparisonChain.start()
          .compare(this.type, that.type)
          .compare(this.name, that.name, Ordering.natural().nullsLast())
          .result();
    }

    @Override
    public boolean equals(Object obj) {
      if (obj instanceof SchemaKey) {
        return 0 == compareTo((SchemaKey) obj);
      } else {
        return false;
      }
    }

    @Override
    public String toString() {
      return MoreObjects.toStringHelper(this)
          .omitNullValues()
          .add("type", this.type)
          .add("name", this.name)
          .toString();
    }
  }

  interface ConversionHandlerFactory {
    ConversionHandler create(Schema schema, String header, String field);
  }

  static final Map<SchemaKey, ConversionHandlerFactory> CONVERSION_HANDLER_FACTORIES;

  static {
    Map<SchemaKey, ConversionHandlerFactory> handlerFactories = new HashMap<>();
    handlerFactories.put(SchemaKey.of(Schema.STRING_SCHEMA), StringConversionHandler::new);
    handlerFactories.put(SchemaKey.of(Schema.BOOLEAN_SCHEMA), BooleanConversionHandler::new);
    handlerFactories.put(SchemaKey.of(Schema.FLOAT32_SCHEMA), Float32ConversionHandler::new);
    handlerFactories.put(SchemaKey.of(Schema.FLOAT64_SCHEMA), Float64ConversionHandler::new);
    handlerFactories.put(SchemaKey.of(Schema.INT8_SCHEMA), Int8ConversionHandler::new);
    handlerFactories.put(SchemaKey.of(Schema.INT16_SCHEMA), Int16ConversionHandler::new);
    handlerFactories.put(SchemaKey.of(Schema.INT32_SCHEMA), Int32ConversionHandler::new);
    handlerFactories.put(SchemaKey.of(Schema.INT64_SCHEMA), Int64ConversionHandler::new);
    handlerFactories.put(SchemaKey.of(Decimal.schema(1)), DecimalConversionHandler::new);
    handlerFactories.put(SchemaKey.of(Timestamp.SCHEMA), TimestampConversionHandler::new);
    handlerFactories.put(SchemaKey.of(Time.SCHEMA), TimeConversionHandler::new);
    handlerFactories.put(SchemaKey.of(Date.SCHEMA), DateConversionHandler::new);


    CONVERSION_HANDLER_FACTORIES = ImmutableMap.copyOf(handlerFactories);
  }


  public static ConversionHandler of(Schema headerSchema, String header, String field) {
    SchemaKey key = SchemaKey.of(headerSchema);
    ConversionHandlerFactory factory = CONVERSION_HANDLER_FACTORIES.get(key);
    if (null == factory) {
      throw new UnsupportedOperationException(
          String.format("%s is not supported", key)
      );
    }
    return factory.create(headerSchema, header, field);
  }
}