/*
 * 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
 *
 * http://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.sshd.common.digest;

import java.nio.charset.Charset;
import java.nio.charset.StandardCharsets;
import java.security.MessageDigest;
import java.util.Base64;
import java.util.Collection;
import java.util.Comparator;
import java.util.Objects;

import org.apache.sshd.common.Factory;
import org.apache.sshd.common.util.GenericUtils;
import org.apache.sshd.common.util.NumberUtils;
import org.apache.sshd.common.util.ValidateUtils;
import org.apache.sshd.common.util.buffer.BufferUtils;
import org.apache.sshd.common.util.security.SecurityUtils;

/**
 * @author <a href="mailto:dev@mina.apache.org">Apache MINA SSHD Project</a>
 */
public final class DigestUtils {
    private DigestUtils() {
        throw new UnsupportedOperationException("No instance");
    }

    /**
     * @param algorithm The digest algorithm - never {@code null}/empty
     * @return {@code true} if this digest algorithm is supported
     * @see SecurityUtils#getMessageDigest(String)
     */
    public static boolean checkSupported(String algorithm) {
        ValidateUtils.checkNotNullAndNotEmpty(algorithm, "No algorithm");
        try {
            MessageDigest digest = SecurityUtils.getMessageDigest(algorithm);
            return digest != null;  // just in case
        } catch (Exception e) {
            return false;
        }
    }

    /**
     * @param <D> The generic type of digest factory
     * @param algo The required algorithm name - ignored if {@code null}/empty
     * @param comp The {@link Comparator} to use to compare algorithm names
     * @param digests The factories to check - ignored if {@code null}/empty
     * @return The first {@link DigestFactory} whose algorithm matches the required one
     * according to the comparator - {@code null} if no match found
     */
    public static <D extends Digest> D findDigestByAlgorithm(String algo, Comparator<? super String> comp, Collection<? extends D> digests) {
        if (GenericUtils.isEmpty(algo) || GenericUtils.isEmpty(digests)) {
            return null;
        }

        for (D d : digests) {
            if (comp.compare(algo, d.getAlgorithm()) == 0) {
                return d;
            }
        }

        return null;
    }

    /**
     * @param <F> The generic type of digest factory
     * @param algo The required algorithm name - ignored if {@code null}/empty
     * @param comp The {@link Comparator} to use to compare algorithm names
     * @param factories The factories to check - ignored if {@code null}/empty
     * @return The first {@link DigestFactory} whose algorithm matches the required one
     * according to the comparator - {@code null} if no match found
     */
    public static <F extends DigestFactory> F findFactoryByAlgorithm(String algo, Comparator<? super String> comp, Collection<? extends F> factories) {
        if (GenericUtils.isEmpty(algo) || GenericUtils.isEmpty(factories)) {
            return null;
        }

        for (F f : factories) {
            if (comp.compare(algo, f.getAlgorithm()) == 0) {
                return f;
            }
        }

        return null;
    }

    /**
     * @param f The {@link Factory} to create the {@link Digest} to use
     * @param s The {@link String} to digest - ignored if {@code null}/empty,
     *          otherwise its UTF-8 representation is used as input for the fingerprint
     * @return The fingerprint - {@code null} if {@code null}/empty input
     * @throws Exception If failed to calculate the digest
     * @see #getFingerPrint(Digest, String, Charset)
     */
    public static String getFingerPrint(Factory<? extends Digest> f, String s) throws Exception {
        return getFingerPrint(f, s, StandardCharsets.UTF_8);
    }

    /**
     * @param f       The {@link Factory} to create the {@link Digest} to use
     * @param s       The {@link String} to digest - ignored if {@code null}/empty
     * @param charset The {@link Charset} to use in order to convert the
     *                string to its byte representation to use as input for the fingerprint
     * @return The fingerprint - {@code null} if {@code null}/empty input
     * @throws Exception If failed to calculate the digest
     */
    public static String getFingerPrint(Factory<? extends Digest> f, String s, Charset charset) throws Exception {
        return getFingerPrint(Objects.requireNonNull(f, "No factory").create(), s, charset);
    }

    /**
     * @param d The {@link Digest} to use
     * @param s The {@link String} to digest - ignored if {@code null}/empty,
     *          otherwise its UTF-8 representation is used as input for the fingerprint
     * @return The fingerprint - {@code null} if {@code null}/empty input
     * @throws Exception If failed to calculate the digest
     * @see #getFingerPrint(Digest, String, Charset)
     */
    public static String getFingerPrint(Digest d, String s) throws Exception {
        return getFingerPrint(d, s, StandardCharsets.UTF_8);
    }

    /**
     * @param d       The {@link Digest} to use
     * @param s       The {@link String} to digest - ignored if {@code null}/empty
     * @param charset The {@link Charset} to use in order to convert the
     *                string to its byte representation to use as input for the fingerprint
     * @return The fingerprint - {@code null} if {@code null}/empty input
     * @throws Exception If failed to calculate the digest
     */
    public static String getFingerPrint(Digest d, String s, Charset charset) throws Exception {
        if (GenericUtils.isEmpty(s)) {
            return null;
        } else {
            return DigestUtils.getFingerPrint(d, s.getBytes(charset));
        }
    }

    /**
     * @param f   The {@link Factory} to create the {@link Digest} to use
     * @param buf The data buffer to be fingerprint-ed
     * @return The fingerprint - {@code null} if empty data buffer
     * @throws Exception If failed to calculate the fingerprint
     * @see #getFingerPrint(Factory, byte[], int, int)
     */
    public static String getFingerPrint(Factory<? extends Digest> f, byte... buf) throws Exception {
        return getFingerPrint(f, buf, 0, NumberUtils.length(buf));
    }

    /**
     * @param f      The {@link Factory} to create the {@link Digest} to use
     * @param buf    The data buffer to be fingerprint-ed
     * @param offset The offset of the data in the buffer
     * @param len    The length of data - ignored if non-positive
     * @return The fingerprint - {@code null} if non-positive length
     * @throws Exception If failed to calculate the fingerprint
     */
    public static String getFingerPrint(Factory<? extends Digest> f, byte[] buf, int offset, int len) throws Exception {
        return getFingerPrint(Objects.requireNonNull(f, "No factory").create(), buf, offset, len);
    }

    /**
     * @param d   The {@link Digest} to use
     * @param buf The data buffer to be fingerprint-ed
     * @return The fingerprint - {@code null} if empty data buffer
     * @throws Exception If failed to calculate the fingerprint
     * @see #getFingerPrint(Digest, byte[], int, int)
     */
    public static String getFingerPrint(Digest d, byte... buf) throws Exception {
        return getFingerPrint(d, buf, 0, NumberUtils.length(buf));
    }

    /**
     * @param d      The {@link Digest} to use
     * @param buf    The data buffer to be fingerprint-ed
     * @param offset The offset of the data in the buffer
     * @param len    The length of data - ignored if non-positive
     * @return The fingerprint - {@code null} if non-positive length
     * @throws Exception If failed to calculate the fingerprint
     * @see #getRawFingerprint(Digest, byte[], int, int)
     */
    public static String getFingerPrint(Digest d, byte[] buf, int offset, int len) throws Exception {
        if (len <= 0) {
            return null;
        }

        byte[] data = getRawFingerprint(d, buf, offset, len);
        String algo = d.getAlgorithm();
        if (BuiltinDigests.md5.getAlgorithm().equals(algo)) {
            return algo + ":" + BufferUtils.toHex(':', data).toLowerCase();
        }

        Base64.Encoder encoder = Base64.getEncoder();
        return algo.replace("-", "").toUpperCase() + ":" + encoder.encodeToString(data).replaceAll("=", "");
    }

    public static byte[] getRawFingerprint(Digest d, byte... buf) throws Exception {
        return getRawFingerprint(d, buf, 0, NumberUtils.length(buf));
    }

    public static byte[] getRawFingerprint(Digest d, byte[] buf, int offset, int len) throws Exception {
        if (len <= 0) {
            return null;
        }

        Objects.requireNonNull(d, "No digest").init();
        d.update(buf, offset, len);

        return d.digest();
    }
}
