/*
 * 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.util.Objects;
import java.util.function.Consumer;

import com.google.common.base.MoreObjects;
import com.google.common.base.MoreObjects.ToStringHelper;
import org.eclipse.milo.opcua.stack.core.AttributeId;
import org.eclipse.milo.opcua.stack.core.types.builtin.unsigned.UShort;
import org.eclipse.milo.opcua.stack.core.types.enumerated.TimestampsToReturn;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;

public final class DataValue {

    private final Variant value;
    private final StatusCode status;
    private final DateTime sourceTime;
    private final UShort sourcePicoseconds;
    private final DateTime serverTime;
    private final UShort serverPicoseconds;

    public DataValue(long statusCode) {
        this(new StatusCode(statusCode));
    }

    public DataValue(StatusCode statusCode) {
        this(Variant.NULL_VALUE, statusCode, DateTime.MIN_VALUE);
    }

    public DataValue(Variant value) {
        this(value, StatusCode.GOOD);
    }

    public DataValue(Variant value, StatusCode status) {
        this(value, status, DateTime.now());
    }

    public DataValue(Variant value, StatusCode status, @Nullable DateTime time) {
        this(value, status, time, time);
    }

    public DataValue(Variant value, StatusCode status, @Nullable DateTime sourceTime, @Nullable DateTime serverTime) {
        this(value, status, sourceTime, null, serverTime, null);
    }

    public DataValue(@NotNull Variant value,
                     @Nullable StatusCode status,
                     @Nullable DateTime sourceTime,
                     @Nullable UShort sourcePicoseconds,
                     @Nullable DateTime serverTime,
                     @Nullable UShort serverPicoseconds) {

        this.value = value;
        this.status = status;
        this.sourceTime = sourceTime;
        this.sourcePicoseconds = sourcePicoseconds;
        this.serverTime = serverTime;
        this.serverPicoseconds = serverPicoseconds;
    }

    public Variant getValue() {
        return value;
    }

    @Nullable
    public StatusCode getStatusCode() {
        return status;
    }

    @Nullable
    public DateTime getSourceTime() {
        return sourceTime;
    }

    @Nullable
    public UShort getSourcePicoseconds() {
        return sourcePicoseconds;
    }

    @Nullable
    public DateTime getServerTime() {
        return serverTime;
    }

    @Nullable
    public UShort getServerPicoseconds() {
        return serverPicoseconds;
    }

    public DataValue withStatus(StatusCode status) {
        return new DataValue(value, status, sourceTime, serverTime);
    }

    public DataValue withSourceTime(@Nullable DateTime sourceTime) {
        return new DataValue(value, status, sourceTime, serverTime);
    }

    public DataValue withServerTime(@Nullable DateTime serverTime) {
        return new DataValue(value, status, sourceTime, serverTime);
    }

    @Override
    public String toString() {
        ToStringHelper helper = MoreObjects.toStringHelper(this);

        helper.add("value", value);
        helper.add("status", status);

        if (sourceTime != null) {
            helper.add("sourceTime", sourceTime);
        }
        if (sourcePicoseconds != null) {
            helper.add("sourcePicoseconds", sourcePicoseconds);
        }
        if (serverTime != null) {
            helper.add("serverTime", serverTime);
        }
        if (serverPicoseconds != null) {
            helper.add("serverPicoseconds", serverPicoseconds);
        }

        return helper.toString();
    }

    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (o == null || getClass() != o.getClass()) return false;
        DataValue dataValue = (DataValue) o;
        return Objects.equals(value, dataValue.value) &&
            Objects.equals(status, dataValue.status) &&
            Objects.equals(sourceTime, dataValue.sourceTime) &&
            Objects.equals(sourcePicoseconds, dataValue.sourcePicoseconds) &&
            Objects.equals(serverTime, dataValue.serverTime) &&
            Objects.equals(serverPicoseconds, dataValue.serverPicoseconds);
    }

    @Override
    public int hashCode() {
        return Objects.hash(value, status, sourceTime, sourcePicoseconds, serverTime, serverPicoseconds);
    }

    public DataValue.Builder copy() {
        return new Builder(this);
    }

    public DataValue copy(Consumer<DataValue.Builder> c) {
        Builder builder = new Builder(this);

        c.accept(builder);

        return builder.build();
    }

    public static DataValue.Builder newValue() {
        return new Builder();
    }

    /**
     * Derive a new {@link DataValue} from a given {@link DataValue} with a current server timestamp, if applicable.
     *
     * @param from       the {@link DataValue} to derive from.
     * @param timestamps the timestamps to return in the derived value.
     * @return a derived {@link DataValue}.
     */
    public static DataValue derivedValue(DataValue from, TimestampsToReturn timestamps) {
        boolean includeSource = timestamps == TimestampsToReturn.Source || timestamps == TimestampsToReturn.Both;
        boolean includeServer = timestamps == TimestampsToReturn.Server || timestamps == TimestampsToReturn.Both;

        return new DataValue(
            from.value,
            from.status,
            includeSource ? from.sourceTime : null,
            includeServer ? DateTime.now() : null
        );
    }

    /**
     * Derive a new {@link DataValue} from a given {@link DataValue} with a current server timestamp, if applicable.
     * <p>
     * The value is assumed to be for a non-value Node attribute, and therefore the source timestamp is not returned.
     *
     * @param from       the {@link DataValue} to derive from.
     * @param timestamps the timestamps to return in the derived value.
     * @return a derived {@link DataValue}.
     */
    public static DataValue derivedNonValue(DataValue from, TimestampsToReturn timestamps) {
        boolean includeServer = timestamps == TimestampsToReturn.Server || timestamps == TimestampsToReturn.Both;

        return new DataValue(
            from.value,
            from.status,
            null,
            includeServer ? DateTime.now() : null
        );
    }

    /**
     * Create a {@link DataValue} containing *only* the Variant. All other fields will be null.
     *
     * @param v the value {@link Variant}.
     * @return a {@link DataValue} containing only the value.
     */
    public static DataValue valueOnly(Variant v) {
        return new DataValue(v, null, null, null);
    }

    public static class Builder {

        public Variant value = Variant.NULL_VALUE;
        public StatusCode status;
        public DateTime sourceTime;
        public UShort sourcePicoseconds;
        public DateTime serverTime;
        public UShort serverPicoseconds;

        public Builder() {}

        public Builder(DataValue other) {
            this.value = other.value;
            this.status = other.status;
            this.sourceTime = other.sourceTime;
            this.sourcePicoseconds = other.sourcePicoseconds;
            this.serverTime = other.serverTime;
            this.serverPicoseconds = other.serverPicoseconds;
        }

        public Builder setValue(Variant value) {
            this.value = value;
            return this;
        }

        public Builder setStatus(StatusCode status) {
            this.status = status;
            return this;
        }

        public Builder setSourceTime(DateTime sourceTime) {
            this.sourceTime = sourceTime;
            return this;
        }

        public Builder setSourcePicoseconds(UShort sourcePicoseconds) {
            this.sourcePicoseconds = sourcePicoseconds;
            return this;
        }

        public Builder setServerTime(DateTime serverTime) {
            this.serverTime = serverTime;
            return this;
        }

        public Builder setServerPicoseconds(UShort serverPicoseconds) {
            this.serverPicoseconds = serverPicoseconds;
            return this;
        }

        public Builder applyTimestamps(AttributeId attributeId, TimestampsToReturn timestamps) {
            boolean includeSource = attributeId == AttributeId.Value &&
                (timestamps == TimestampsToReturn.Source || timestamps == TimestampsToReturn.Both);

            boolean includeServer = timestamps == TimestampsToReturn.Server || timestamps == TimestampsToReturn.Both;

            // Source timestamps can only be removed; they
            // can't be added because we aren't the source.
            if (!includeSource) {
                setSourceTime(null);
                setSourcePicoseconds(null);
            }

            if (!includeServer) {
                setServerTime(null);
                setServerPicoseconds(null);
            } else {
                if (serverTime == null) {
                    setServerTime(DateTime.now());
                }
            }

            return this;
        }

        public DataValue build() {
            return new DataValue(
                value,
                status,
                sourceTime,
                sourcePicoseconds,
                serverTime,
                serverPicoseconds
            );
        }

    }

}
