/*
 * Copyright 2018-2024 the original author or authors.
 *
 * 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
 *
 *      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.springframework.data.redis.connection.stream;

import org.springframework.lang.Nullable;
import org.springframework.util.Assert;
import org.springframework.util.NumberUtils;
import org.springframework.util.ObjectUtils;
import org.springframework.util.StringUtils;

/**
 * The id of a single {@link Record} within a stream. Composed of two parts:
 * {@literal <millisecondsTime>-<sequenceNumber>}.
 *
 * @author Christoph Strobl
 * @since 2.2
 * @see <a href="https://redis.io/topics/streams-intro#entry-ids">Redis Documentation - Entriy ID</a>
 */
public class RecordId {

	private static final String GENERATE_ID = "*";
	private static final String DELIMITER = "-";

	/**
	 * Auto-generation of IDs by the server is almost always what you want so we've got this instance here shortcutting
	 * computation.
	 */
	private static final RecordId AUTOGENERATED = new RecordId(GENERATE_ID) {

		@Override
		public Long getSequence() {
			return null;
		}

		@Override
		public Long getTimestamp() {
			return null;
		}

		@Override
		public boolean shouldBeAutoGenerated() {
			return true;
		}
	};

	private final String raw;

	/**
	 * Private constructor - validate input in static initializer blocks.
	 *
	 * @param raw
	 */
	private RecordId(String raw) {
		this.raw = raw;
	}

	/**
	 * Obtain an instance of {@link RecordId} using the provided String formatted as
	 * {@literal <millisecondsTime>-<sequenceNumber>}. <br />
	 * For server auto generated {@literal entry-id} on insert pass in {@literal null} or {@literal *}. Event better, just
	 * use {@link #autoGenerate()}.
	 *
	 * @param value can be {@literal null}.
	 * @return new instance of {@link RecordId} if no autogenerated one requested.
	 */
	public static RecordId of(@Nullable String value) {

		if (value == null || GENERATE_ID.equals(value)) {
			return autoGenerate();
		}

		Assert.isTrue(value.contains(DELIMITER),
				"Invalid id format; Please use the 'millisecondsTime-sequenceNumber' format");
		return new RecordId(value);
	}

	/**
	 * Create a new instance of {@link RecordId} using the provided String formatted as
	 * {@literal <millisecondsTime>-<sequenceNumber>}. <br />
	 * For server auto generated {@literal entry-id} on insert use {@link #autoGenerate()}.
	 *
	 * @param millisecondsTime
	 * @param sequenceNumber
	 * @return new instance of {@link RecordId}.
	 */
	public static RecordId of(long millisecondsTime, long sequenceNumber) {
		return of(millisecondsTime + DELIMITER + sequenceNumber);
	}

	/**
	 * Obtain the {@link RecordId} signalling the server to auto generate an {@literal entry-id} on insert ({@code XADD}).
	 *
	 * @return {@link RecordId} instance signalling {@link #shouldBeAutoGenerated()}.
	 */
	public static RecordId autoGenerate() {
		return AUTOGENERATED;
	}

	/**
	 * Get the {@literal entry-id millisecondsTime} part or {@literal null} if it {@link #shouldBeAutoGenerated()}.
	 *
	 * @return millisecondsTime of the {@literal entry-id}. Can be {@literal null}.
	 */
	@Nullable
	public Long getTimestamp() {
		return value(0);
	}

	/**
	 * Get the {@literal entry-id sequenceNumber} part or {@literal null} if it {@link #shouldBeAutoGenerated()}.
	 *
	 * @return sequenceNumber of the {@literal entry-id}. Can be {@literal null}.
	 */
	@Nullable
	public Long getSequence() {
		return value(1);
	}

	/**
	 * @return {@literal true} if a new {@literal entry-id} shall be generated on server side when calling {@code XADD}.
	 */
	public boolean shouldBeAutoGenerated() {
		return false;
	}

	/**
	 * @return get the string representation of the {@literal entry-id} in {@literal <millisecondsTime>-<sequenceNumber>}
	 *         format or {@literal *} if it {@link #shouldBeAutoGenerated()}. Never {@literal null}.
	 */
	public String getValue() {
		return raw;
	}

	@Override
	public String toString() {
		return raw;
	}

	private Long value(int index) {
		return NumberUtils.parseNumber(StringUtils.split(raw, DELIMITER)[index], Long.class);
	}

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

		RecordId recordId = (RecordId) o;

		return ObjectUtils.nullSafeEquals(raw, recordId.raw);
	}

	@Override
	public int hashCode() {
		return ObjectUtils.nullSafeHashCode(raw);
	}
}
