package com.jscape.inet.ssh.protocol.v2.marshaling.algorithms;

import java.security.Provider;
import java.util.ArrayList;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;

import static com.jscape.util.Parameters.assertGreater;
import static com.jscape.util.Parameters.assertNotNull;

/**
 * JCE-based MAC algorithm factory.
 *
 * @author Alex Usun
 */
public class JceMacFactory
        implements MacFactory {

    private final Map<String, Entry> entries;

    /**
     * Creates a new factory instance.
     *
     * @param entries entries
     */
    public JceMacFactory(Entry... entries) {
        this.entries = new LinkedHashMap<>();
        set(entries);
    }

    /**
     * Sets algorithm entries.
     *
     * @param entries entries
     *
     * @return current factory instance
     */
    public JceMacFactory set(Entry... entries) {
        for (Entry entry : entries) {
            this.entries.put(entry.name, entry);
        }

        return this;
    }

    @Override
    public List<String> macs() {
        return new ArrayList<String>(this.entries.keySet());
    }

    @Override
    public Mac clientToServerMacFor(String name, KeyFactory keyFactory)
            throws OperationException {
        Entry entry = entryFor(name);
        return entry.clientToServer(keyFactory);
    }

    @Override
    public Mac serverToClientMacFor(String name, KeyFactory keyFactory)
            throws OperationException {
        Entry entry = entryFor(name);
        return entry.serverToClient(keyFactory);
    }

    @Override
    public String toString() {
        return "JceMacFactory {"
                + "entries=" + this.entries
                + '}';
    }

    private Entry entryFor(String name) {
        Entry entry = this.entries.get(name);

        if (entry == null) {
            throw new IllegalArgumentException("Unsupported algorithm: " + name);
        }

        return entry;
    }

    /**
     * An algorithm entry.
     */
    public static class Entry {

        public static final Entry NONE = new Entry("none", "", null, 1, 1) {

            @Override
            public Mac clientToServer(KeyFactory keyFactory) {
                return Mac.NULL;
            }

            @Override
            public Mac serverToClient(KeyFactory keyFactory) {
                return Mac.NULL;
            }
        };

        public final String name;
        public final String algorithm;
        public final Object provider;
        public final int digestLength;
        public final int keyLength;

        public static Entry entryFor(String name, String algorithm, String provider, int digestLength, int keyLength) {
            return new Entry(name, algorithm, provider, digestLength, keyLength);
        }

        public static Entry entryFor(String name, String algorithm, Provider provider, int digestLength, int keyLength) {
            return new Entry(name, algorithm, provider, digestLength, keyLength);
        }

        /**
         * Creates a new algorithm entry.
         *
         * @param name a SSH algorithm name
         * @param algorithm a JCE algorithm name
         * @param provider a JCE provider name
         * @param digestLength the resulting digest length in bytes
         * @param keyLength the key length in bytes
         */
        protected Entry(String name, String algorithm, Object provider, int digestLength, int keyLength) {
            assertNotNull(name);
            this.name = name;

            assertNotNull(algorithm);
            this.algorithm = algorithm;

            this.provider = provider;

            assertGreater(digestLength, 0, "Bad digest length.");
            this.digestLength = digestLength;

            assertGreater(keyLength, 0, "Bad key length.");
            this.keyLength = keyLength;
        }

        public Mac clientToServer(KeyFactory keyFactory)
                throws OperationException {
            try {
                byte[] keyData = keyFactory.integrityKeyClientToServer(this.keyLength);

                return macFor(keyData);
            } catch (Exception e) {
                throw new OperationException(e);
            }
        }

        public Mac serverToClient(KeyFactory keyFactory)
                throws OperationException {
            try {
                byte[] keyData = keyFactory.integrityKeyServerToClient(this.keyLength);

                return macFor(keyData);
            } catch (Exception e) {
                throw new OperationException(e);
            }
        }

        @Override
        public String toString() {
            return "Entry {"
                    + "name='" + this.name + '\''
                    + ", algorithm='" + this.algorithm + '\''
                    + ", provider='" + this.provider + '\''
                    + ", digestLength=" + this.digestLength
                    + ", keyLength=" + this.keyLength
                    + '}';
        }

        private Mac macFor(byte[] keyData)
                throws Exception {
            return this.provider instanceof String
                   ? JceMac.macFor(this.algorithm, (String) this.provider, keyData, this.digestLength)
                   : this.provider instanceof Provider ? JceMac.macFor(this.algorithm, (Provider) this.provider, keyData, this.digestLength)
                                                       : JceMac.macFor(this.algorithm, (String) null, keyData, this.digestLength);
        }
    }
}
