/*
 * Copyright (c) 2019 the Eclipse Milo Authors
 *
 * This program and the accompanying materials are made
 * available under the terms of the Eclipse Public License 2.0
 * which is available at https://www.eclipse.org/legal/epl-2.0/
 *
 * SPDX-License-Identifier: EPL-2.0
 */

package org.eclipse.milo.opcua.stack.core.types.builtin;

import java.time.Instant;
import java.util.Date;

import com.google.common.base.MoreObjects;

public final class DateTime {

    /**
     * When {@code true}, {@link #now()} will use nanosecond precision.
     */
    public static volatile boolean USE_NANOS = false;

    public static final DateTime MIN_VALUE = new DateTime(0L);

    public static final DateTime NULL_VALUE = MIN_VALUE;

    /**
     * The delta in 100 nanosecond intervals between Java epoch (January 1, 1970) and UTC epoch (Jan 1, 1601).
     */
    private static final long EPOCH_DELTA = 116444736000000000L;

    private final long utcTime;

    public DateTime() {
        this(javaToUtc(System.currentTimeMillis()));
    }

    public DateTime(long utcTime) {
        this.utcTime = utcTime;
    }

    public DateTime(Date date) {
        this(javaToUtc(date.getTime()));
    }

    public DateTime(Instant instant) {
        this(javaToUtc(instant.toEpochMilli()) + (instant.getNano() % 1_000_000) / 100);
    }

    /**
     * @return this time as 100 nanosecond intervals since UTC epoch.
     */
    public long getUtcTime() {
        return utcTime;
    }

    /**
     * @return this time as milliseconds since Java epoch.
     */
    public long getJavaTime() {
        return utcToJava(utcTime);
    }

    /**
     * @return this time as a {@link Date}.
     */
    public Date getJavaDate() {
        return new Date(utcToJava(utcTime));
    }

    /**
     * @return this time as an {@link Instant}.
     */
    public Instant getJavaInstant() {
        return Instant.ofEpochSecond(utcToJava(utcTime) / 1_000, (utcTime % 10_000_000) * 100);
    }

    public boolean isNull() {
        return utcTime == 0;
    }

    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (o == null || getClass() != o.getClass()) return false;

        DateTime dateTime = (DateTime) o;

        return utcTime == dateTime.utcTime;
    }

    @Override
    public int hashCode() {
        return (int) (utcTime ^ (utcTime >>> 32));
    }

    @Override
    public String toString() {
        return MoreObjects.toStringHelper(this)
            .add("utcTime", utcTime)
            .add("javaDate", getJavaDate())
            .toString();
    }

    /**
     * Get a {@link DateTime} initialized to now.
     * <p>
     * The precision depends on the value of {@link #USE_NANOS}.
     *
     * @return a {@link DateTime} initialized to now.
     */
    public static DateTime now() {
        if (USE_NANOS) {
            return nowNanos();
        } else {
            return nowMillis();
        }
    }

    /**
     * @return a {@link DateTime} initialized to now with millisecond precision.
     */
    public static DateTime nowMillis() {
        return new DateTime();
    }

    /**
     * @return a {@link DateTime} initialized to now with nanosecond precision.
     */
    public static DateTime nowNanos() {
        return new DateTime(Instant.now());
    }

    private static long javaToUtc(long javaTime) {
        return (javaTime * 10_000) + EPOCH_DELTA;
    }

    private static long utcToJava(long utcTime) {
        return (utcTime - EPOCH_DELTA) / 10_000;
    }

}
