/*
 * 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
 *
 *     https://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.avro.protobuf;

import com.google.protobuf.Timestamp;
import org.apache.avro.Conversion;
import org.apache.avro.LogicalType;
import org.apache.avro.LogicalTypes;
import org.apache.avro.Schema;

public class ProtoConversions {

  private static final int THOUSAND = 1_000;
  private static final int MILLION = 1_000_000;
  private static final int BILLION = 1_000_000_000;

  // second value must be from 0001-01-01T00:00:00Z to 9999-12-31T23:59:59Z
  // inclusive.
  static final long SECONDS_LOWERLIMIT = -62135596800L;
  static final long SECONDS_UPPERLIMIT = 253402300799L;

  // nano value Must be from 0 to 999,999,999 inclusive.
  private static final int NANOSECONDS_LOWERLIMIT = 0;
  private static final int NANOSECONDS_UPPERLIMIT = 999999999;

  // timestamp precise of conversion from long
  private enum TimestampPrecise {
    Millis, Micros, Nanos
  };

  public static class TimestampMillisConversion extends Conversion<Timestamp> {
    @Override
    public Class<Timestamp> getConvertedType() {
      return Timestamp.class;
    }

    @Override
    public String getLogicalTypeName() {
      return "timestamp-millis";
    }

    @Override
    public Timestamp fromLong(Long millisFromEpoch, Schema schema, LogicalType type) throws IllegalArgumentException {
      return ProtoConversions.fromLong(millisFromEpoch, TimestampPrecise.Millis);
    }

    @Override
    public Long toLong(Timestamp value, Schema schema, LogicalType type) {
      return ProtoConversions.toLong(value, TimestampPrecise.Millis);
    }

    @Override
    public Schema getRecommendedSchema() {
      return LogicalTypes.timestampMillis().addToSchema(Schema.create(Schema.Type.LONG));
    }
  }

  public static class TimestampMicrosConversion extends Conversion<Timestamp> {
    @Override
    public Class<Timestamp> getConvertedType() {
      return Timestamp.class;
    }

    @Override
    public String getLogicalTypeName() {
      return "timestamp-micros";
    }

    @Override
    public Timestamp fromLong(Long microsFromEpoch, Schema schema, LogicalType type) throws IllegalArgumentException {
      return ProtoConversions.fromLong(microsFromEpoch, TimestampPrecise.Micros);
    }

    @Override
    public Long toLong(Timestamp value, Schema schema, LogicalType type) {
      return ProtoConversions.toLong(value, TimestampPrecise.Micros);
    }

    @Override
    public Schema getRecommendedSchema() {
      return LogicalTypes.timestampMicros().addToSchema(Schema.create(Schema.Type.LONG));
    }
  }

  public static class TimestampNanosConversion extends Conversion<Timestamp> {
    @Override
    public Class<Timestamp> getConvertedType() {
      return Timestamp.class;
    }

    @Override
    public String getLogicalTypeName() {
      return "timestamp-nanos";
    }

    @Override
    public Timestamp fromLong(Long nanosFromEpoch, Schema schema, LogicalType type) throws IllegalArgumentException {
      return ProtoConversions.fromLong(nanosFromEpoch, TimestampPrecise.Nanos);
    }

    @Override
    public Long toLong(Timestamp value, Schema schema, LogicalType type) {
      return ProtoConversions.toLong(value, TimestampPrecise.Nanos);
    }

    @Override
    public Schema getRecommendedSchema() {
      return LogicalTypes.timestampNanos().addToSchema(Schema.create(Schema.Type.LONG));
    }
  }

  private static long toLong(Timestamp value, TimestampPrecise precise) {
    long rv = 0L;

    switch (precise) {
    case Millis:
      rv = value.getSeconds() * THOUSAND + value.getNanos() / MILLION;
      break;
    case Micros:
      rv = value.getSeconds() * MILLION + value.getNanos() / THOUSAND;
      break;
    }

    return rv;
  }

  private static Timestamp fromLong(Long elapsedSinceEpoch, TimestampPrecise precise) throws IllegalArgumentException {
    final long seconds;
    final int nanos;

    switch (precise) {
    case Millis:
      seconds = Math.floorDiv(elapsedSinceEpoch, (long) THOUSAND);
      nanos = (int) Math.floorMod(elapsedSinceEpoch, (long) THOUSAND) * MILLION;
      break;
    case Micros:
      seconds = Math.floorDiv(elapsedSinceEpoch, (long) MILLION);
      nanos = (int) Math.floorMod(elapsedSinceEpoch, (long) MILLION) * THOUSAND;
      break;
    case Nanos:
      seconds = Math.floorDiv(elapsedSinceEpoch, (long) BILLION);
      nanos = (int) Math.floorMod(elapsedSinceEpoch, (long) BILLION);
      break;
    default:
      throw new IllegalArgumentException("Unknown precision: " + precise);
    }

    if (seconds < SECONDS_LOWERLIMIT || seconds > SECONDS_UPPERLIMIT) {
      throw new IllegalArgumentException("given seconds is out of range");
    }

    if (nanos < NANOSECONDS_LOWERLIMIT || nanos > NANOSECONDS_UPPERLIMIT) {
      // NOTE here is unexpected cases because exceeded part is
      // moved to seconds by floor methods
      throw new IllegalArgumentException("given nanos is out of range");
    }

    return Timestamp.newBuilder().setSeconds(seconds).setNanos(nanos).build();
  }
}
