package org.jivesoftware.spark.otrplug.impl;

import java.io.BufferedInputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.security.KeyFactory;
import java.security.KeyPair;
import java.security.KeyPairGenerator;
import java.security.NoSuchAlgorithmException;
import java.security.PrivateKey;
import java.security.PublicKey;
import java.security.spec.InvalidKeySpecException;
import java.security.spec.PKCS8EncodedKeySpec;
import java.security.spec.X509EncodedKeySpec;
import java.util.List;
import java.util.Properties;
import java.util.Vector;

import org.bouncycastle.util.encoders.Base64;

import net.java.otr4j.OtrKeyManager;
import net.java.otr4j.OtrKeyManagerListener;
import net.java.otr4j.OtrKeyManagerStore;
import net.java.otr4j.crypto.OtrCryptoEngineImpl;
import net.java.otr4j.crypto.OtrCryptoException;
import net.java.otr4j.session.SessionID;

/**
 * An implementation of the OtrKeyManager provided by otr4j. It stores the local
 * key chain
 * 
 * @author Bergunde Holger
 * 
 */
public class MyOtrKeyManager implements OtrKeyManager {

    private OtrKeyManagerStore store;

    /**
     * Use this consturctor if you want to use your own implementation for key
     * storage
     * 
     * @param store
     *            Imlementation of OtrKeyManagerStore
     */
    public MyOtrKeyManager(OtrKeyManagerStore store) {
        this.store = store;
    }

    /**
     * Inner class, own implemented key sotrage
     * 
     * @author Bergunde Holger
     * 
     */
    class DefaultPropertiesStore implements OtrKeyManagerStore {
        private final Properties properties = new Properties();
        private String filepath;

        public DefaultPropertiesStore(String filepath) throws IOException {
            if (filepath == null || filepath.length() < 1)
                throw new IllegalArgumentException();
            this.filepath = filepath;
            properties.clear();

            InputStream in = new BufferedInputStream(new FileInputStream(getConfigurationFile()));
            try {
                properties.load(in);
            } finally {
                in.close();
            }
        }

        private File getConfigurationFile() throws IOException {
            File configFile = new File(filepath);
            if (!configFile.exists())
                configFile.createNewFile();
            return configFile;
        }

        public void setProperty(String id, boolean value) {
            properties.setProperty(id, "true");
            try {
                this.store();
            } catch (Exception e) {
                e.printStackTrace();
            }
        }

        private void store() throws FileNotFoundException, IOException {
            OutputStream out = new FileOutputStream(getConfigurationFile());
            properties.store(out, null);
            out.close();
        }

        public void setProperty(String id, byte[] value) {
            properties.setProperty(id, new String(Base64.encode(value)));
            try {
                this.store();
            } catch (Exception e) {
                e.printStackTrace();
            }
        }

        public void removeProperty(String id) {
            properties.remove(id);
            try {
                this.store();
            } catch (Exception e) {
                e.printStackTrace();
            }
        }

        public byte[] getPropertyBytes(String id) {
            String value = properties.getProperty(id);
            if (value == null)
                return null;
            return Base64.decode(value);
        }

        public boolean getPropertyBoolean(String id, boolean defaultValue) {
            try {
                return Boolean.valueOf(properties.get(id).toString());
            } catch (Exception e) {
                return defaultValue;
            }
        }
    }

    /**
     * Use this consturctor if key manager should use his own implementation of
     * key storage
     * 
     * @param filepath
     *            file where the keys should be stored
     * @throws IOException
     */
    public MyOtrKeyManager(String filepath) throws IOException {
        this.store = new DefaultPropertiesStore(filepath);
    }

    private List<OtrKeyManagerListener> listeners = new Vector<OtrKeyManagerListener>();

    /**
     * Adds listener to key manager
     */
    public void addListener(OtrKeyManagerListener l) {
        synchronized (listeners) {
            if (!listeners.contains(l))
                listeners.add(l);
        }
    }

    /**
     * Remove listener from key manager
     */
    public void removeListener(OtrKeyManagerListener l) {
        synchronized (listeners) {
            listeners.remove(l);
        }
    }

    /**
     * Generate a local key pair. Be careful. If there is already an key pair,
     * it will override it
     * 
     * @param sessionID
     *            the sessionID that is identified with the local machine
     */
    public void generateLocalKeyPair(SessionID sessionID) {
        if (sessionID == null)
            return;

        String accountID = sessionID.getAccountID();
        KeyPair keyPair;
        try {
            keyPair = KeyPairGenerator.getInstance("DSA").genKeyPair();
        } catch (NoSuchAlgorithmException e) {
            e.printStackTrace();
            return;
        }

        // Store Public Key.
        PublicKey pubKey = keyPair.getPublic();
        X509EncodedKeySpec x509EncodedKeySpec = new X509EncodedKeySpec(pubKey.getEncoded());

        this.store.setProperty(accountID + ".publicKey", x509EncodedKeySpec.getEncoded());

        // Store Private Key.
        PrivateKey privKey = keyPair.getPrivate();
        PKCS8EncodedKeySpec pkcs8EncodedKeySpec = new PKCS8EncodedKeySpec(privKey.getEncoded());

        this.store.setProperty(accountID + ".privateKey", pkcs8EncodedKeySpec.getEncoded());
    }

    /**
     * Returns the local finger print for specified session. If there is no
     * finger print you might generate one.
     * 
     * @return the local finger print for this sessionID
     */
    public String getLocalFingerprint(SessionID sessionID) {
        KeyPair keyPair = loadLocalKeyPair(sessionID);

        if (keyPair == null)
            return null;

        PublicKey pubKey = keyPair.getPublic();

        try {
            return new OtrCryptoEngineImpl().getFingerprint(pubKey);
        } catch (OtrCryptoException e) {
            e.printStackTrace();
            return null;
        }
    }

    /**
     * Return remote finger print for specified sessionID.
     * 
     * @return finger print for remote contact
     */
    public String getRemoteFingerprint(SessionID sessionID) {
        PublicKey remotePublicKey = loadRemotePublicKey(sessionID);
        if (remotePublicKey == null)
            return null;
        try {
            return new OtrCryptoEngineImpl().getFingerprint(remotePublicKey);
        } catch (OtrCryptoException e) {
            e.printStackTrace();
            return null;
        }
    }

    /**
     * check if the specified sessionID is verified for this machine
     * 
     */
    public boolean isVerified(SessionID sessionID) {
        if (sessionID == null)
            return false;

        return this.store.getPropertyBoolean(sessionID.getUserID() + ".publicKey.verified", false);
    }

    /**
     * Returns the key pair (private and public key) for the local machine
     * 
     * @param sessionID
     *            sessionID for currect machine
     */
    public KeyPair loadLocalKeyPair(SessionID sessionID) {
        if (sessionID == null)
            return null;

        String accountID = sessionID.getAccountID();
        // Load Private Key.
        byte[] b64PrivKey = this.store.getPropertyBytes(accountID + ".privateKey");
        if (b64PrivKey == null)
            return null;

        PKCS8EncodedKeySpec privateKeySpec = new PKCS8EncodedKeySpec(b64PrivKey);

        // Load Public Key.
        byte[] b64PubKey = this.store.getPropertyBytes(accountID + ".publicKey");
        if (b64PubKey == null)
            return null;

        X509EncodedKeySpec publicKeySpec = new X509EncodedKeySpec(b64PubKey);

        PublicKey publicKey;
        PrivateKey privateKey;

        // Generate KeyPair.
        KeyFactory keyFactory;
        try {
            keyFactory = KeyFactory.getInstance("DSA");
            publicKey = keyFactory.generatePublic(publicKeySpec);
            privateKey = keyFactory.generatePrivate(privateKeySpec);
        } catch (NoSuchAlgorithmException e) {
            e.printStackTrace();
            return null;
        } catch (InvalidKeySpecException e) {
            e.printStackTrace();
            return null;
        }

        return new KeyPair(publicKey, privateKey);
    }

    /**
     * Loads the public key for the specified sessionID. If there is no key
     * stored, you will get 'null'
     */
    public PublicKey loadRemotePublicKey(SessionID sessionID) {
        if (sessionID == null)
            return null;

        String userID = sessionID.getUserID();

        byte[] b64PubKey = this.store.getPropertyBytes(userID + ".publicKey");
        if (b64PubKey == null)
            return null;

        X509EncodedKeySpec publicKeySpec = new X509EncodedKeySpec(b64PubKey);

        // Generate KeyPair.
        KeyFactory keyFactory;
        try {
            keyFactory = KeyFactory.getInstance("DSA");
            return keyFactory.generatePublic(publicKeySpec);
        } catch (NoSuchAlgorithmException e) {
            e.printStackTrace();
            return null;
        } catch (InvalidKeySpecException e) {
            e.printStackTrace();
            return null;
        }
    }

    /**
     * Stores the public key for a specified user from sessionID
     * 
     * @param sessionID
     *            sessionID to identifiy the owner of the key
     * @param pubKey
     *            the key which should be stored
     */
    public void savePublicKey(SessionID sessionID, PublicKey pubKey) {
        if (sessionID == null)
            return;

        X509EncodedKeySpec x509EncodedKeySpec = new X509EncodedKeySpec(pubKey.getEncoded());

        String userID = sessionID.getUserID();
        this.store.setProperty(userID + ".publicKey", x509EncodedKeySpec.getEncoded());

        this.store.removeProperty(userID + ".publicKey.verified");
    }

    /**
     * Removes the verification for the specified sessionID
     */
    public void unverify(SessionID sessionID) {
        if (sessionID == null)
            return;

        if (!isVerified(sessionID))
            return;

        this.store.removeProperty(sessionID.getUserID() + ".publicKey.verified");
        for (OtrKeyManagerListener l : listeners)
            l.verificationStatusChanged(sessionID);

    }

    /**
     * Verify the specified sessionID
     */
    public void verify(SessionID sessionID) {
        if (sessionID == null)
            return;

        if (this.isVerified(sessionID))
            return;

        this.store.setProperty(sessionID.getUserID() + ".publicKey.verified", true);

        for (OtrKeyManagerListener l : listeners)
            l.verificationStatusChanged(sessionID);
    }

}
