/*
 * CDDL HEADER START
 *
 * The contents of this file are subject to the terms of the
 * Common Development and Distribution License, Version 1.0 only
 * (the "License").  You may not use this file except in compliance
 * with the License.
 *
 * You can obtain a copy of the license at legal-notices/CDDLv1_0.txt
 * or http://forgerock.org/license/CDDLv1.0.html.
 * See the License for the specific language governing permissions
 * and limitations under the License.
 *
 * When distributing Covered Code, include this CDDL HEADER in each
 * file and include the License file at legal-notices/CDDLv1_0.txt.
 * If applicable, add the following below this CDDL HEADER, with the
 * fields enclosed by brackets "[]" replaced with your own identifying
 * information:
 *      Portions Copyright [yyyy] [name of copyright owner]
 *
 * CDDL HEADER END
 *
 *
 *      Copyright 2006-2008 Sun Microsystems, Inc.
 *      Portions Copyright 2014-2015 ForgeRock AS
 */
package org.opends.server.extensions;

import static org.opends.messages.ExtensionMessages.*;
import static org.opends.server.util.StaticUtils.*;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.SortedSet;
import java.util.StringTokenizer;

import org.forgerock.i18n.LocalizableMessage;
import org.forgerock.i18n.slf4j.LocalizedLogger;
import org.forgerock.opendj.config.server.ConfigChangeResult;
import org.forgerock.opendj.config.server.ConfigException;
import org.forgerock.opendj.ldap.ByteString;
import org.forgerock.opendj.ldap.ResultCode;
import org.opends.server.admin.server.ConfigurationChangeListener;
import org.opends.server.admin.std.server.PasswordGeneratorCfg;
import org.opends.server.admin.std.server.RandomPasswordGeneratorCfg;
import org.opends.server.api.PasswordGenerator;
import org.opends.server.core.DirectoryServer;
import org.opends.server.types.*;

/**
 * This class provides an implementation of a Directory Server password
 * generator that will create random passwords based on fixed-length strings
 * built from one or more character sets.
 */
public class RandomPasswordGenerator
       extends PasswordGenerator<RandomPasswordGeneratorCfg>
       implements ConfigurationChangeListener<RandomPasswordGeneratorCfg>
{
  private static final LocalizedLogger logger = LocalizedLogger.getLoggerForThisClass();


  /** The current configuration for this password validator. */
  private RandomPasswordGeneratorCfg currentConfig;

  /** The encoded list of character sets defined for this password generator. */
  private SortedSet<String> encodedCharacterSets;

  /** The DN of the configuration entry for this password generator. */
  private DN configEntryDN;

  /** The total length of the password that will be generated. */
  private int totalLength;

  /**
   * The numbers of characters of each type that should be used to generate the
   * passwords.
   */
  private int[] characterCounts;

  /** The character sets that should be used to generate the passwords. */
  private NamedCharacterSet[] characterSets;

  /**
   * The lock to use to ensure that the character sets and counts are not
   * altered while a password is being generated.
   */
  private Object generatorLock;

  /** The character set format string for this password generator. */
  private String formatString;



  /** {@inheritDoc} */
  @Override
  public void initializePasswordGenerator(
      RandomPasswordGeneratorCfg configuration)
         throws ConfigException, InitializationException
  {
    this.configEntryDN = configuration.dn();
    generatorLock = new Object();

    // Get the character sets for use in generating the password.  At least one
    // must have been provided.
    HashMap<String,NamedCharacterSet> charsets = new HashMap<>();

    try
    {
      encodedCharacterSets = configuration.getPasswordCharacterSet();

      if (encodedCharacterSets.isEmpty())
      {
        LocalizableMessage message = ERR_RANDOMPWGEN_NO_CHARSETS.get(configEntryDN);
        throw new ConfigException(message);
      }
      for (NamedCharacterSet s : NamedCharacterSet
          .decodeCharacterSets(encodedCharacterSets))
      {
        if (charsets.containsKey(s.getName()))
        {
          LocalizableMessage message = ERR_RANDOMPWGEN_CHARSET_NAME_CONFLICT.get(configEntryDN, s.getName());
          throw new ConfigException(message);
        }
        else
        {
          charsets.put(s.getName(), s);
        }
      }
    }
    catch (ConfigException ce)
    {
      throw ce;
    }
    catch (Exception e)
    {
      logger.traceException(e);

      LocalizableMessage message =
          ERR_RANDOMPWGEN_CANNOT_DETERMINE_CHARSETS.get(getExceptionMessage(e));
      throw new InitializationException(message, e);
    }


    // Get the value that describes which character set(s) and how many
    // characters from each should be used.

    try
    {
      formatString = configuration.getPasswordFormat();
      StringTokenizer tokenizer = new StringTokenizer(formatString, ", ");

      ArrayList<NamedCharacterSet> setList = new ArrayList<>();
      ArrayList<Integer> countList = new ArrayList<>();

      while (tokenizer.hasMoreTokens())
      {
        String token = tokenizer.nextToken();

        try
        {
          int colonPos = token.indexOf(':');
          String name = token.substring(0, colonPos);
          int count = Integer.parseInt(token.substring(colonPos + 1));

          NamedCharacterSet charset = charsets.get(name);
          if (charset == null)
          {
            throw new ConfigException(ERR_RANDOMPWGEN_UNKNOWN_CHARSET.get(formatString, name));
          }
          else
          {
            setList.add(charset);
            countList.add(count);
          }
        }
        catch (ConfigException ce)
        {
          throw ce;
        }
        catch (Exception e)
        {
          logger.traceException(e);

          LocalizableMessage message = ERR_RANDOMPWGEN_INVALID_PWFORMAT.get(formatString);
          throw new ConfigException(message, e);
        }
      }

      characterSets = new NamedCharacterSet[setList.size()];
      characterCounts = new int[characterSets.length];

      totalLength = 0;
      for (int i = 0; i < characterSets.length; i++)
      {
        characterSets[i] = setList.get(i);
        characterCounts[i] = countList.get(i);
        totalLength += characterCounts[i];
      }
    }
    catch (ConfigException ce)
    {
      throw ce;
    }
    catch (Exception e)
    {
      logger.traceException(e);

      LocalizableMessage message =
          ERR_RANDOMPWGEN_CANNOT_DETERMINE_PWFORMAT.get(getExceptionMessage(e));
      throw new InitializationException(message, e);
    }

    configuration.addRandomChangeListener(this) ;
    currentConfig = configuration;
  }



  /** {@inheritDoc} */
  @Override
  public void finalizePasswordGenerator()
  {
    currentConfig.removeRandomChangeListener(this);
  }



  /**
   * Generates a password for the user whose account is contained in the
   * specified entry.
   *
   * @param  userEntry  The entry for the user for whom the password is to be
   *                    generated.
   *
   * @return  The password that has been generated for the user.
   *
   * @throws  DirectoryException  If a problem occurs while attempting to
   *                              generate the password.
   */
  @Override
  public ByteString generatePassword(Entry userEntry)
         throws DirectoryException
  {
    StringBuilder buffer = new StringBuilder(totalLength);

    synchronized (generatorLock)
    {
      for (int i=0; i < characterSets.length; i++)
      {
        characterSets[i].getRandomCharacters(buffer, characterCounts[i]);
      }
    }

    return ByteString.valueOfUtf8(buffer);
  }



  /** {@inheritDoc} */
  @Override
  public boolean isConfigurationAcceptable(PasswordGeneratorCfg configuration,
                                           List<LocalizableMessage> unacceptableReasons)
  {
    RandomPasswordGeneratorCfg config =
         (RandomPasswordGeneratorCfg) configuration;
    return isConfigurationChangeAcceptable(config, unacceptableReasons);
  }



  /** {@inheritDoc} */
  @Override
  public boolean isConfigurationChangeAcceptable(
      RandomPasswordGeneratorCfg configuration,
      List<LocalizableMessage> unacceptableReasons)
  {
    DN cfgEntryDN = configuration.dn();

    // Get the character sets for use in generating the password.
    // At least one must have been provided.
    HashMap<String,NamedCharacterSet> charsets = new HashMap<>();
    try
    {
      SortedSet<String> currentPasSet = configuration.getPasswordCharacterSet();
      if (currentPasSet.isEmpty())
      {
        throw new ConfigException(ERR_RANDOMPWGEN_NO_CHARSETS.get(cfgEntryDN));
      }

      for (NamedCharacterSet s : NamedCharacterSet
          .decodeCharacterSets(currentPasSet))
      {
        if (charsets.containsKey(s.getName()))
        {
          unacceptableReasons.add(ERR_RANDOMPWGEN_CHARSET_NAME_CONFLICT.get(cfgEntryDN, s.getName()));
          return false;
        }
        else
        {
          charsets.put(s.getName(), s);
        }
      }
    }
    catch (ConfigException ce)
    {
      unacceptableReasons.add(ce.getMessageObject());
      return false;
    }
    catch (Exception e)
    {
      logger.traceException(e);

      LocalizableMessage message = ERR_RANDOMPWGEN_CANNOT_DETERMINE_CHARSETS.get(
              getExceptionMessage(e));
      unacceptableReasons.add(message);
      return false;
    }


    // Get the value that describes which character set(s) and how many
    // characters from each should be used.
    try
    {
        String formatString = configuration.getPasswordFormat() ;
        StringTokenizer tokenizer = new StringTokenizer(formatString, ", ");

        while (tokenizer.hasMoreTokens())
        {
          String token = tokenizer.nextToken();

          try
          {
            int    colonPos = token.indexOf(':');
            String name     = token.substring(0, colonPos);

            NamedCharacterSet charset = charsets.get(name);
            if (charset == null)
            {
              unacceptableReasons.add(ERR_RANDOMPWGEN_UNKNOWN_CHARSET.get(formatString, name));
              return false;
            }
          }
          catch (Exception e)
          {
            logger.traceException(e);

            unacceptableReasons.add(ERR_RANDOMPWGEN_INVALID_PWFORMAT.get(formatString));
            return false;
          }
        }
    }
    catch (Exception e)
    {
      logger.traceException(e);

      LocalizableMessage message = ERR_RANDOMPWGEN_CANNOT_DETERMINE_PWFORMAT.get(
              getExceptionMessage(e));
      unacceptableReasons.add(message);
      return false;
    }


    // If we've gotten here, then everything looks OK.
    return true;
  }



  /** {@inheritDoc} */
  @Override
  public ConfigChangeResult applyConfigurationChange(
      RandomPasswordGeneratorCfg configuration)
  {
    final ConfigChangeResult ccr = new ConfigChangeResult();


    // Get the character sets for use in generating the password.  At least one
    // must have been provided.
    SortedSet<String> newEncodedCharacterSets = null;
    HashMap<String,NamedCharacterSet> charsets = new HashMap<>();
    try
    {
      newEncodedCharacterSets = configuration.getPasswordCharacterSet();
      if (newEncodedCharacterSets.isEmpty())
      {
        ccr.addMessage(ERR_RANDOMPWGEN_NO_CHARSETS.get(configEntryDN));
        ccr.setResultCodeIfSuccess(ResultCode.OBJECTCLASS_VIOLATION);
      }
      else
      {
        for (NamedCharacterSet s :
             NamedCharacterSet.decodeCharacterSets(newEncodedCharacterSets))
        {
          if (charsets.containsKey(s.getName()))
          {
            ccr.addMessage(ERR_RANDOMPWGEN_CHARSET_NAME_CONFLICT.get(configEntryDN, s.getName()));
            ccr.setResultCodeIfSuccess(ResultCode.CONSTRAINT_VIOLATION);
          }
          else
          {
            charsets.put(s.getName(), s);
          }
        }
      }
    }
    catch (ConfigException ce)
    {
      ccr.addMessage(ce.getMessageObject());
      ccr.setResultCodeIfSuccess(ResultCode.INVALID_ATTRIBUTE_SYNTAX);
    }
    catch (Exception e)
    {
      logger.traceException(e);

      ccr.addMessage(ERR_RANDOMPWGEN_CANNOT_DETERMINE_CHARSETS.get(getExceptionMessage(e)));
      ccr.setResultCodeIfSuccess(DirectoryServer.getServerErrorResultCode());
    }


    // Get the value that describes which character set(s) and how many
    // characters from each should be used.
    ArrayList<NamedCharacterSet> newSetList = new ArrayList<>();
    ArrayList<Integer> newCountList = new ArrayList<>();
    String newFormatString = null;

    try
    {
      newFormatString = configuration.getPasswordFormat();
      StringTokenizer tokenizer = new StringTokenizer(newFormatString, ", ");

      while (tokenizer.hasMoreTokens())
      {
        String token = tokenizer.nextToken();

        try
        {
          int colonPos = token.indexOf(':');
          String name = token.substring(0, colonPos);
          int count = Integer.parseInt(token.substring(colonPos + 1));

          NamedCharacterSet charset = charsets.get(name);
          if (charset == null)
          {
            ccr.addMessage(ERR_RANDOMPWGEN_UNKNOWN_CHARSET.get(newFormatString, name));
            ccr.setResultCodeIfSuccess(ResultCode.CONSTRAINT_VIOLATION);
          }
          else
          {
            newSetList.add(charset);
            newCountList.add(count);
          }
        }
        catch (Exception e)
        {
          logger.traceException(e);

          ccr.addMessage(ERR_RANDOMPWGEN_INVALID_PWFORMAT.get(newFormatString));
          ccr.setResultCodeIfSuccess(DirectoryServer.getServerErrorResultCode());
        }
      }
    }
    catch (Exception e)
    {
      logger.traceException(e);

      ccr.addMessage(ERR_RANDOMPWGEN_CANNOT_DETERMINE_PWFORMAT.get(getExceptionMessage(e)));
      ccr.setResultCodeIfSuccess(DirectoryServer.getServerErrorResultCode());
    }


    // If everything looks OK, then apply the changes.
    if (ccr.getResultCode() == ResultCode.SUCCESS)
    {
      synchronized (generatorLock)
      {
        encodedCharacterSets = newEncodedCharacterSets;
        formatString         = newFormatString;

        characterSets   = new NamedCharacterSet[newSetList.size()];
        characterCounts = new int[characterSets.length];

        totalLength = 0;
        for (int i=0; i < characterCounts.length; i++)
        {
          characterSets[i]    = newSetList.get(i);
          characterCounts[i]  = newCountList.get(i);
          totalLength        += characterCounts[i];
        }
      }
    }

    return ccr;
  }
}
