/*
 * MIT License
 *
 * Copyright (c) 2020-2023 Fabio Lima
 *
 * Permission is hereby granted, free of charge, to any person obtaining a copy
 * of this software and associated documentation files (the "Software"), to deal
 * in the Software without restriction, including without limitation the rights
 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 * copies of the Software, and to permit persons to whom the Software is
 * furnished to do so, subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included in all
 * copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
 * SOFTWARE.
 */

package com.andrewlalis.perfin.data.ulid;

import java.nio.charset.StandardCharsets;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.Arrays;

/**
 * A class that generates ULIDs.
 * <p>
 * Both types of ULID can be easily created by this generator, i.e. monotonic
 * and non-monotonic.
 * <p>
 * In addition, a "non-standard" hash-based ULID can also be generated, in which
 * the random component is replaced with the first 10 bytes of an SHA-256 hash.
 */
public final class UlidCreator {

    private UlidCreator() {
    }

    /**
     * Returns a ULID.
     * <p>
     * The random component is reset for each new ULID generated.
     *
     * @return a ULID
     */
    public static Ulid getUlid() {
        return UlidFactoryHolder.INSTANCE.create();
    }

    /**
     * Returns a ULID.
     * <p>
     * The random component is reset for each new ULID generated.
     *
     * @param time the current time in milliseconds, measured from the UNIX epoch of
     *             1970-01-01T00:00Z (UTC)
     * @return a ULID
     */
    public static Ulid getUlid(final long time) {
        return UlidFactoryHolder.INSTANCE.create(time);
    }

    /**
     * Returns a Monotonic ULID.
     * <p>
     * The random component is incremented for each new ULID generated in the same
     * millisecond.
     *
     * @return a ULID
     */
    public static Ulid getMonotonicUlid() {
        return MonotonicFactoryHolder.INSTANCE.create();
    }

    /**
     * Returns a Monotonic ULID.
     * <p>
     * The random component is incremented for each new ULID generated in the same
     * millisecond.
     *
     * @param time the current time in milliseconds, measured from the UNIX epoch of
     *             1970-01-01T00:00Z (UTC)
     * @return a ULID
     */
    public static Ulid getMonotonicUlid(final long time) {
        return MonotonicFactoryHolder.INSTANCE.create(time);
    }

    /**
     * Returns a Hash ULID.
     * <p>
     * The random component is replaced with the first 10 bytes of an SHA-256 hash.
     * <p>
     * It always returns the same ULID for a specific pair of {@code time} and
     * {@code string}.
     * <p>
     * Usage example:
     *
     * <pre>{@code
     * long time = file.getCreatedAt();
     * String name = file.getFileName();
     * Ulid ulid = UlidCreator.getHashUlid(time, name);
     * }</pre>
     *
     * @param time   the time in milliseconds, measured from the UNIX epoch of
     *               1970-01-01T00:00Z (UTC)
     * @param string a string to be hashed using SHA-256 algorithm.
     * @return a ULID
     * @since 5.2.0
     */
    public static Ulid getHashUlid(final long time, String string) {
        byte[] bytes = string.getBytes(StandardCharsets.UTF_8);
        return getHashUlid(time, bytes);
    }

    /**
     * Returns a Hash ULID.
     * <p>
     * The random component is replaced with the first 10 bytes of an SHA-256 hash.
     * <p>
     * It always returns the same ULID for a specific pair of {@code time} and
     * {@code bytes}.
     * <p>
     * Usage example:
     *
     * <pre>{@code
     * long time = file.getCreatedAt();
     * byte[] bytes = file.getFileBinary();
     * Ulid ulid = UlidCreator.getHashUlid(time, bytes);
     * }</pre>
     *
     * @param time  the time in milliseconds, measured from the UNIX epoch of
     *              1970-01-01T00:00Z (UTC)
     * @param bytes a byte array to be hashed using SHA-256 algorithm.
     * @return a ULID
     * @since 5.2.0
     */
    public static Ulid getHashUlid(final long time, byte[] bytes) {
        // Calculate the hash and take the first 10 bytes
        byte[] hash = hasher("SHA-256").digest(bytes);
        byte[] rand = Arrays.copyOf(hash, 10);
        return new Ulid(time, rand);
    }

    private static MessageDigest hasher(final String algorithm) {
        try {
            return MessageDigest.getInstance(algorithm);
        } catch (NoSuchAlgorithmException e) {
            throw new RuntimeException(String.format("%s not supported", algorithm));
        }
    }

    private static class UlidFactoryHolder {
        static final UlidFactory INSTANCE = UlidFactory.newInstance();
    }

    private static class MonotonicFactoryHolder {
        static final UlidFactory INSTANCE = UlidFactory.newMonotonicInstance();
    }
}
