/*
 * Class: VanDykeFormat
 *
 * Author: Alex Usun
 *
 * Date: 2/9/2005
 */
package com.jscape.inet.ssh.util.keyreader.vandyke;

import com.jscape.inet.ssh.util.keyreader.FormatException;
import com.jscape.inet.ssh.util.keyreader.KeyPairFormat;
import com.jscape.inet.ssh.util.keyreader.KeyPairSpec;
import com.jscape.inet.util.Base64;
import com.jscape.util.Assert;

import java.io.BufferedReader;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.StringReader;
import java.nio.charset.StandardCharsets;
import java.security.GeneralSecurityException;
import java.security.KeyPair;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

/**
 * VanDyke key pair format.
 *
 * @author Alex Usun
 */
@SuppressWarnings("unchecked")
public abstract class VanDykeFormat
        implements KeyPairFormat {

    private static final String START_LINE = "---- BEGIN VAN DYKE SSH2 PRIVATE KEY ----";
    private static final String STOP_LINE = "---- END VAN DYKE SSH2 PRIVATE KEY ----";
    private static final String KEY_SEPARATOR = ":";

    private final Base64 base64Coder = new Base64();

    /**
     * @see KeyPairFormat#restoreKeyPair(byte[], String)
     */
    public KeyPair restoreKeyPair(byte[] data, String passphrase)
            throws FormatException {
        Assert.notNull(data);
        Assert.notNull(passphrase);

        Record record = restoreRecord(data);

        try {
            if (record.isEncrypted()) {
                throw new FormatException("cannot restore ecrypted keys");
            }

            return restoreKeyPair(record).toKeyPair();
        } catch (IOException e) {
            throw new FormatException(e.getMessage());
        } catch (GeneralSecurityException e) {
            throw new FormatException(e.getMessage());
        }
    }

    /**
     * Restores key pair.
     *
     * @param record key pair record
     *
     * @return restored key pair
     *
     * @throws IOException if key pair cannot be restored
     */
    protected abstract KeyPairSpec restoreKeyPair(Record record)
            throws IOException;

    /**
     * Returns supported algorithm name.
     *
     * @return supported algorithm name
     */
    protected abstract String getAlgorithm();

    /**
     * Restores key pair record.
     *
     * @param data record data
     *
     * @return restored key pair record
     *
     * @throws IllegalArgumentException if record cannot be restored
     */
    private Record restoreRecord(byte[] data) {
        Assert.notNull(data);

        try {
            String[] recordLines = restoreRecordData(new String(data, StandardCharsets.US_ASCII));
            Map comments = new HashMap();
            int i = restoreComments(recordLines, comments);

            StringBuffer buf = new StringBuffer();

            for (; i < recordLines.length; ++i) {
                buf.append(recordLines[i]);
            }

            byte[] recordData = this.base64Coder.decode(buf.toString().toCharArray());

            DataReader reader = new DataReader(new ByteArrayInputStream(recordData));

            reader.readInt(); // version
            byte[] publicKeyData = reader.readStringAsByteArray();
            String encryprionAlgorithm = reader.readString();
            byte[] privateKeyData = reader.readStringAsByteArray();
            byte[] mac = reader.readStringAsByteArray();

            return new Record(getAlgorithm(), encryprionAlgorithm, comments, publicKeyData, privateKeyData, mac);
        } catch (Exception e) {
            throw new IllegalArgumentException("bad format");
        }
    }

    /**
     * Restores record data.
     *
     * @param data record data
     *
     * @return record lines
     *
     * @throws IOException if an I/O error occurs
     */
    private String[] restoreRecordData(String data)
            throws IOException {
        BufferedReader reader = new BufferedReader(new StringReader(data));

        String startLine = reader.readLine();
        Assert.isTrue(startLine != null && startLine.startsWith(START_LINE), "Bad format.");

        List lines = new ArrayList();

        for (String line = reader.readLine(); line != null; line = reader.readLine()) {
            lines.add(line);
        }

        String stopLine = (String) lines.remove(lines.size() - 1);
        Assert.isTrue(stopLine.startsWith(STOP_LINE), "bad format");

        return (String[]) lines.toArray(new String[lines.size()]);
    }

    /**
     * Restores record comments.
     *
     * @param data     record lines
     * @param comments record comments
     *
     * @return key pair data starting index
     */
    private int restoreComments(String[] data, Map comments) {
        int i = 0;

        for (int index = data[i].indexOf(KEY_SEPARATOR); i < data.length && index >= 0; ++i, index = data[i].indexOf(KEY_SEPARATOR)) {
            String name = data[i].substring(0, index).trim();
            String value = (index < (data[i].length() - 1)) ? data[i].substring(index + 1).trim() : "";
            comments.put(name, value);
        }

        return i;
    }


    /**
     * @see Object#equals(Object)
     */
    public boolean equals(Object obj) {
        if (this == obj) {
            return true;
        }
        if (obj == null) {
            return false;
        }

        return getClass().equals(obj.getClass());
    }

}
