/*
 * 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.openpgp;

import java.nio.file.Path;
import java.util.Collection;
import java.util.Collections;
import java.util.EnumSet;
import java.util.NavigableMap;
import java.util.Set;
import java.util.TreeMap;
import java.util.concurrent.atomic.AtomicReference;
import java.util.function.Supplier;

import org.apache.sshd.common.PropertyResolverUtils;
import org.apache.sshd.common.util.GenericUtils;
import org.apache.sshd.common.util.OsUtils;
import org.apache.sshd.common.util.ValidateUtils;
import org.apache.sshd.common.util.io.PathUtils;
import org.c02e.jpgpj.CompressionAlgorithm;
import org.c02e.jpgpj.EncryptionAlgorithm;
import org.c02e.jpgpj.Key;
import org.c02e.jpgpj.Subkey;

/**
 * TODO Add javadoc
 *
 * @author <a href="mailto:dev@mina.apache.org">Apache MINA SSHD Project</a>
 */
public final class PGPUtils {
    public static final String DEFAULT_PGP_FILE_SUFFIX = ".gpg";

    public static final String STD_LINUX_PGP_FOLDER_NAME = ".gnupg";
    public static final String STD_WINDOWS_PGP_FOLDER_NAME = "gnupg";

    /** Default MIME type for PGP encrypted files */
    public static final String PGP_ENCRYPTED_FILE = "application/pgp-encrypted";

    /** Alias for {@link EncryptionAlgorithm#Unencrypted Unencrypted} */
    public static final String NO_CIPHER_PLACEHOLDER = PropertyResolverUtils.NONE_VALUE;

    public static final Set<EncryptionAlgorithm> CIPHERS
            = Collections.unmodifiableSet(EnumSet.allOf(EncryptionAlgorithm.class));

    /** Alias for {@link CompressionAlgorithm#Uncompressed Uncompressed} */
    public static final String NO_COMPRESSION_PLACEHOLDER = PropertyResolverUtils.NONE_VALUE;

    public static final Set<CompressionAlgorithm> COMPRESSIONS
            = Collections.unmodifiableSet(EnumSet.allOf(CompressionAlgorithm.class));

    private static final AtomicReference<Supplier<? extends Path>> DEFAULT_PGP_PATH_RESOLVER_HOLDER = new AtomicReference<>();

    private PGPUtils() {
        throw new UnsupportedOperationException("No instance");
    }

    public static EncryptionAlgorithm fromCipherName(String name) {
        if (GenericUtils.isEmpty(name)) {
            return null;
        }

        if (NO_CIPHER_PLACEHOLDER.equalsIgnoreCase(name)) {
            return EncryptionAlgorithm.Unencrypted;
        }

        return CIPHERS.stream()
                .filter(c -> name.equalsIgnoreCase(c.name()))
                .findFirst()
                .orElse(null);
    }

    public static CompressionAlgorithm fromCompressionName(String name) {
        if (GenericUtils.isEmpty(name)) {
            return null;
        }

        if (NO_COMPRESSION_PLACEHOLDER.equalsIgnoreCase(name)) {
            return CompressionAlgorithm.Uncompressed;
        } else {
            return COMPRESSIONS.stream()
                    .filter(c -> name.equalsIgnoreCase(c.name()))
                    .findFirst()
                    .orElse(null);
        }
    }

    /**
     * @param  key                      The {@link Key} whose sub-keys to map - ignored if {@code null} or no sub-keys
     *                                  available
     * @return                          A {@link NavigableMap} where key=the (case <U>insensitive</U>) fingerprint
     *                                  value, value=the matching {@link Subkey}
     * @throws NullPointerException     If key with {@code null} fingerprint encountered
     * @throws IllegalArgumentException If key with empty fingerprint encountered
     * @throws IllegalStateException    If more than one key with same fingerprint found
     * @see                             #mapSubKeysByFingerprint(Collection)
     */
    public static NavigableMap<String, Subkey> mapSubKeysByFingerprint(Key key) {
        return mapSubKeysByFingerprint((key == null) ? Collections.emptyList() : key.getSubkeys());
    }

    /**
     * @param  subKeys                  The {@link Subkey}-s to map - ignored if {@code null}/empty
     * @return                          A {@link NavigableMap} where key=the (case <U>insensitive</U>) fingerprint
     *                                  value, value=the matching {@link Subkey}
     * @throws NullPointerException     If key with {@code null} fingerprint encountered
     * @throws IllegalArgumentException If key with empty fingerprint encountered
     * @throws IllegalStateException    If more than one key with same fingerprint found
     */
    public static NavigableMap<String, Subkey> mapSubKeysByFingerprint(Collection<? extends Subkey> subKeys) {
        if (GenericUtils.isEmpty(subKeys)) {
            return Collections.emptyNavigableMap();
        }

        NavigableMap<String, Subkey> keysMap = new TreeMap<>(String.CASE_INSENSITIVE_ORDER);
        for (Subkey sk : subKeys) {
            String fp = ValidateUtils.checkNotNullAndNotEmpty(sk.getFingerprint(), "No fingerprint for %s", sk);
            Subkey prev = keysMap.put(fp, sk);
            ValidateUtils.checkState(prev == null, "Multiple sub-keys with fingerprint=%s: %s / %s", fp, sk, prev);
        }

        return keysMap;
    }

    /**
     * @param  key         The {@link Key} whose sub-keys to scan - ignored if {@code null} or has no sub-keys
     * @param  fingerprint The fingerprint to match (case <U>insensitive</U>) - ignored if {@code null}/empty
     * @return             The first matching {@link Subkey} - {@code null} if no match found
     * @see                #findSubkeyByFingerprint(Collection, String)
     */
    public static Subkey findSubkeyByFingerprint(Key key, String fingerprint) {
        return findSubkeyByFingerprint((key == null) ? Collections.emptyList() : key.getSubkeys(), fingerprint);
    }

    /**
     * @param  subKeys     The {@link Subkey}-s to scan - ignored if {@code null}/empty
     * @param  fingerprint The fingerprint to match (case <U>insensitive</U>) - ignored if {@code null}/empty
     * @return             The first matching sub-key - {@code null} if no match found
     */
    public static Subkey findSubkeyByFingerprint(Collection<? extends Subkey> subKeys, String fingerprint) {
        if (GenericUtils.isEmpty(subKeys) || GenericUtils.isEmpty(fingerprint)) {
            return null;
        }

        return subKeys.stream()
                .filter(k -> fingerprint.equalsIgnoreCase(k.getFingerprint()))
                .findFirst()
                .orElse(null);
    }

    private static final class LazyDefaultPgpKeysFolderHolder {
        private static final Path PATH = PathUtils.getUserHomeFolder()
                .resolve(OsUtils.isWin32() ? STD_WINDOWS_PGP_FOLDER_NAME : STD_LINUX_PGP_FOLDER_NAME);

        private LazyDefaultPgpKeysFolderHolder() {
            throw new UnsupportedOperationException("No instance allowed");
        }
    }

    /**
     * @return The default <A HREF="https://www.gnupg.org/">Gnu Privacy Guard</A> folder used to hold key files.
     */
    @SuppressWarnings("synthetic-access")
    public static Path getDefaultPgpFolderPath() {
        Supplier<? extends Path> resolver;
        synchronized (DEFAULT_PGP_PATH_RESOLVER_HOLDER) {
            resolver = DEFAULT_PGP_PATH_RESOLVER_HOLDER.get();
        }

        return (resolver == null) ? LazyDefaultPgpKeysFolderHolder.PATH : resolver.get();
    }

    /**
     * Set the reported value from {@link #getDefaultPgpFolderPath()}
     *
     * @param resolver The {@link Path} provider to report - if {@code null} then O/S default value will be used
     */
    public static void setDefaultPgpFolderPathResolver(Supplier<? extends Path> resolver) {
        synchronized (DEFAULT_PGP_PATH_RESOLVER_HOLDER) {
            DEFAULT_PGP_PATH_RESOLVER_HOLDER.set(resolver);
        }
    }
}
