/*
 *  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.directory.studio.connection.core;


import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.security.GeneralSecurityException;
import java.security.KeyStore;
import java.security.KeyStore.SecretKeyEntry;
import java.security.KeyStoreException;
import java.util.Collections;
import java.util.HashMap;
import java.util.Map;
import java.util.Set;

import javax.crypto.SecretKey;
import javax.crypto.SecretKeyFactory;
import javax.crypto.spec.PBEKeySpec;

import org.apache.directory.api.util.FileUtils;


/**
 * A wrapper around {@link KeyStore} for storing passwords.
 *
 * @author <a href="mailto:dev@directory.apache.org">Apache Directory Project</a>
 */
public class PasswordsKeyStoreManager
{
    /** The default filename */
    private static final String KEYSTORE_DEFAULT_FILENAME = "passwords.jks";

    /** The keystore file name */
    private String filename = KEYSTORE_DEFAULT_FILENAME;

    /** The master password */
    private String masterPassword;

    /** The keystore */
    private KeyStore keystore;


    /**
     * Creates a new instance of PasswordsKeyStoreManager.
     */
    public PasswordsKeyStoreManager()
    {
    }


    /**
     * Creates a new instance of PasswordsKeyStoreManager.
     *
     * @param filename the filename
     */
    public PasswordsKeyStoreManager( String filename )
    {
        this.filename = filename;
    }


    /**
     * Indicates if the keystore is loaded.
     *
     * @return <code>true</code> if the keystore is loaded,
     *         <code>false</code> if not.
     */
    public boolean isLoaded()
    {
        return keystore != null;
    }


    public void load( String masterPassword ) throws KeyStoreException
    {
        this.masterPassword = masterPassword;

        try
        {
            keystore = KeyStore.getInstance( "PKCS12" ); //$NON-NLS-1$

            // Getting the keystore file
            File keystoreFile = getKeyStoreFile();

            // Checking if the keystore file is available on disk
            if ( keystoreFile.exists() && keystoreFile.isFile() && keystoreFile.canRead() )
            {
                try ( FileInputStream fis = new FileInputStream( keystoreFile ) )
                {
                    keystore.load( fis, masterPassword.toCharArray() );
                }
            }
            else
            {
                keystore.load( null, null );
            }
        }
        // Catch for the following exceptions that may be raised while
        // handling the keystore:
        // - java.security.KeyStoreException
        // - java.security.NoSuchAlgorithmException
        // - java.security.cert.CertificateException
        catch ( GeneralSecurityException e )
        {
            this.masterPassword = null;
            this.keystore = null;

            throw new KeyStoreException( e );
        }
        // Catch for the following exceptions that may be raised while
        // handling the file:
        // - java.io.IOException
        // - java.io.FileNotFoundException
        catch ( IOException e )
        {
            this.masterPassword = null;
            this.keystore = null;

            throw new KeyStoreException( e );
        }
    }


    /**
     * Saves the keystore on disk.
     */
    public void save() throws KeyStoreException
    {
        if ( isLoaded() && ( masterPassword != null ) )
        {
            try ( FileOutputStream fos = new FileOutputStream( getKeyStoreFile() ) )
            {
                keystore.store( fos, masterPassword.toCharArray() );
            }
            // Catch for the following exceptions that may be raised while
            // handling the keystore:
            // - java.security.KeyStoreException
            // - java.security.NoSuchAlgorithmException
            // - java.security.cert.CertificateException
            catch ( GeneralSecurityException e )
            {
                throw new KeyStoreException( e );
            }
            // Catch for the following exceptions that may be raised while
            // handling the file:
            // - java.io.IOException
            // - java.io.FileNotFoundException
            catch ( IOException e )
            {
                throw new KeyStoreException( e );
            }
        }
    }


    /**
     * Checks the master password.
     *
     * @param masterPassword the master password
     * @return <code>true</code> if the master password is correct,
     *         <code>false</code> if not.
     * @throws KeyStoreException if an error occurs
     */
    public boolean checkMasterPassword( String masterPassword ) throws KeyStoreException
    {
        // If the keystore is already loaded, we compare the master password directly
        if ( isLoaded() )
        {
            return ( ( this.masterPassword != null ) && ( this.masterPassword.equals( masterPassword ) ) );
        }
        // The keystore is not loaded yet 
        else
        {
            try
            {
                // Loading the keystore
                load( masterPassword );

                // Returning the check value
                return isLoaded();
            }
            catch ( KeyStoreException e )
            {
                throw e;
            }
        }
    }


    /**
     * Sets the master password.
     *
     * @param masterPassword the master password
     */
    public void setMasterPassword( String masterPassword )
    {
        // Creating a map to store previously stored passwords
        Map<String, String> passwordsMap = new HashMap<String, String>();

        if ( isLoaded() )
        {
            // Getting the connection IDs
            String[] connectionIds = getConnectionIds();

            // Storing the password of each connection in the map
            for ( String connectionId : connectionIds )
            {
                // Getting the connection password
                String connectionPassword = getConnectionPassword( connectionId );

                // Checking if we got a password
                if ( connectionPassword != null )
                {
                    // Storing the password of the connection in the map
                    passwordsMap.put( connectionId, connectionPassword );
                }

                // Removing the password from the keystore
                storeConnectionPassword( connectionId, null, false );
            }
        }

        // Assigning the new master password
        this.masterPassword = masterPassword;

        // Storing the previous passwords back in the keystore
        if ( passwordsMap.size() > 0 )
        {
            Set<String> connectionIds = passwordsMap.keySet();

            // Storing the password of each connection in the keystore
            if ( connectionIds != null )
            {
                for ( String connectionId : connectionIds )
                {
                    String connectionPassword = passwordsMap.get( connectionId );

                    if ( connectionPassword != null )
                    {
                        // Storing the password of the connection in the keystore
                        storeConnectionPassword( connectionId, connectionPassword, false );
                    }
                }
            }
        }
    }


    /**
     * Gets the keystore file.
     *
     * @return the keystore file
     */
    public File getKeyStoreFile()
    {
        return ConnectionCorePlugin.getDefault().getStateLocation().append( filename ).toFile();
    }


    /**
     * Deletes the keystore.
     */
    public void deleteKeystoreFile()
    {
        // Getting the keystore file
        File keystoreFile = getKeyStoreFile();

        // Checking if the keystore file is available on disk
        if ( keystoreFile.exists() && keystoreFile.isFile() && keystoreFile.canRead() && keystoreFile.canWrite() )
        {
            keystoreFile.delete();
        }
    }


    /**
     * Gets the connections IDs contained in the keystore.
     *
     * @return the connection IDs contained in the keystore
     */
    public String[] getConnectionIds()
    {
        if ( keystore != null )
        {
            try
            {
                return Collections.list( keystore.aliases() ).toArray( new String[0] );
            }
            catch ( KeyStoreException e )
            {
                // Silent
            }
        }

        return new String[0];
    }


    /**
     * Stores a connection password.
     *
     * @param connection the connection
     * @param password the password
     */
    public void storeConnectionPassword( Connection connection, String password )
    {
        if ( connection != null )
        {
            storeConnectionPassword( connection.getId(), password );
        }
    }


    /**
     * Stores a connection password.
     *
     * @param connection the connection
     * @param password the password
     * @param saveKeystore if the keystore needs to be saved
     */
    public void storeConnectionPassword( Connection connection, String password, boolean saveKeystore )
    {
        if ( connection != null )
        {
            storeConnectionPassword( connection.getId(), password, true );
        }
    }


    /**
     * Stores a connection password.
     *
     * @param connectionId the connection id
     * @param password the password
     */
    public void storeConnectionPassword( String connectionId, String password )
    {
        storeConnectionPassword( connectionId, password, true );
    }


    /**
     * Stores a connection password.
     *
     * @param connectionId the connection id
     * @param password the password
     * @param saveKeystore if the keystore needs to be saved
     */
    public void storeConnectionPassword( String connectionId, String password, boolean saveKeystore )
    {
        if ( isLoaded() && ( connectionId != null ) )
        {
            try
            {
                // Checking if the password is null
                if ( password == null )
                {
                    // We need to remove the corresponding entry in the keystore
                    if ( keystore.containsAlias( connectionId ) )
                    {
                        keystore.deleteEntry( connectionId );
                    }
                }
                else
                {
                    // Generating a secret key from the password
                    SecretKeyFactory factory = SecretKeyFactory.getInstance( "PBE" );
                    SecretKey generatedSecret = factory.generateSecret( new PBEKeySpec( password.toCharArray() ) );

                    // Setting the entry in the keystore
                    keystore.setEntry( connectionId, new KeyStore.SecretKeyEntry( generatedSecret ),
                        new KeyStore.PasswordProtection( masterPassword.toCharArray() ) );
                }

                // Saving
                if ( saveKeystore )
                {
                    save();
                }
            }
            catch ( Exception e )
            {
                // Silent
            }
        }
    }


    /**
     * Gets a connection password.
     *
     * @param connection the connection
     * @return the password for the connection or <code>null</code>.
     */
    public String getConnectionPassword( Connection connection )
    {
        if ( connection != null )
        {
            return getConnectionPassword( connection.getId() );
        }

        return null;
    }


    /**
     * Gets a connection password.
     *
     * @param connectionId the connection id
     * @return the password for the connection id or <code>null</code>.
     */
    public String getConnectionPassword( String connectionId )
    {
        if ( isLoaded() && ( connectionId != null ) )
        {
            try
            {
                SecretKeyFactory factory = SecretKeyFactory.getInstance( "PBE" );
                SecretKeyEntry ske = ( SecretKeyEntry ) keystore.getEntry( connectionId,
                    new KeyStore.PasswordProtection( masterPassword.toCharArray() ) );

                if ( ske != null )
                {
                    PBEKeySpec keySpec = ( PBEKeySpec ) factory.getKeySpec( ske.getSecretKey(), PBEKeySpec.class );

                    if ( keySpec != null )
                    {
                        char[] password = keySpec.getPassword();

                        if ( password != null )
                        {
                            return new String( password );
                        }
                    }
                }
            }
            catch ( Exception e )
            {
                return null;
            }
        }

        return null;
    }


    /**
     * Resets the keystore manager.
     */
    public void reset()
    {
        // Reseting the fields
        this.keystore = null;
        this.masterPassword = null;

        // Getting the keystore file
        File keystoreFile = getKeyStoreFile();

        // If the keystore file exists, we need to remove it
        if ( keystoreFile.exists() )
        {
            // Deleting the file
            FileUtils.deleteQuietly( keystoreFile );
        }
    }


    public void unload()
    {
        // Reseting the fields
        this.keystore = null;
        this.masterPassword = null;
    }


    public void reload( String masterPassword ) throws KeyStoreException
    {
        unload();

        load( masterPassword );
    }


    /**
     * Gets the master password.
     *
     * @return the master password
     */
    public String getMasterPassword()
    {
        return masterPassword;
    }
}