/*
 * 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.geode.security.generator;

import java.security.Principal;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Properties;

import org.apache.logging.log4j.Logger;

import org.apache.geode.logging.internal.log4j.api.LogService;
import org.apache.geode.security.AuthInitialize;
import org.apache.geode.security.Authenticator;
import org.apache.geode.security.templates.DummyAuthenticator;
import org.apache.geode.security.templates.LdapUserAuthenticator;
import org.apache.geode.security.templates.PKCSAuthenticator;

/**
 * Encapsulates obtaining valid and invalid credentials. Implementations will be for different kinds
 * of authentication schemes.
 *
 * @since GemFire 5.5
 */
public abstract class CredentialGenerator {

  private static final Logger logger = LogService.getLogger();

  /**
   * A set of properties that should be added to the Gemfire system properties before using the
   * authentication module.
   */
  private Properties systemProperties = null;

  /**
   * A set of properties that should be added to the java system properties before using the
   * authentication module.
   */
  protected Properties javaProperties = null;

  /**
   * A factory method to create a new instance of an {@link CredentialGenerator} for the given
   * {@link ClassCode}. Caller is supposed to invoke {@link CredentialGenerator#init} immediately
   * after obtaining the instance.
   *
   * @param classCode the {@code ClassCode} of the {@code CredentialGenerator} implementation
   *
   * @return an instance of {@code CredentialGenerator} for the given class code
   */
  public static CredentialGenerator create(final ClassCode classCode) {
    switch (classCode.classType) {
      // Removing dummy one to reduce test run times
      // case ClassCode.ID_DUMMY:
      // return new DummyCredentialGenerator();
      case ClassCode.ID_LDAP:
        return new LdapUserCredentialGenerator();
      // case ClassCode.ID_SSL:ø
      // return new SSLCredentialGenerator();
      case ClassCode.ID_PKCS:
        return new PKCSCredentialGenerator();
      default:
        return null;
    }
  }

  /**
   * Initialize the credential generator.
   *
   * @throws IllegalArgumentException when there is a problem during initialization
   */
  public void init() throws IllegalArgumentException {
    systemProperties = initialize();
    logger.info("Generating CredentialGenerator with {}", systemProperties);
  }

  /**
   * @return A set of extra properties that should be added to Gemfire system properties when not
   *         null.
   */
  public Properties getSystemProperties() {
    return systemProperties;
  }

  /**
   * @return A set of extra properties that should be added to Gemfire system properties when not
   *         null.
   */
  public Properties getJavaProperties() {
    return javaProperties;
  }

  /**
   * The {@link ClassCode} of this particular implementation.
   *
   * @return the {@code ClassCode}
   */
  public abstract ClassCode classCode();

  /**
   * The name of the {@link AuthInitialize} factory function that should be used in conjunction with
   * the credentials generated by this generator.
   *
   * @return name of the {@code AuthInitialize} factory function
   */
  public abstract String getAuthInit();

  /**
   * The name of the {@link Authenticator} factory function that should be used in conjunction with
   * the credentials generated by this generator.
   *
   * @return name of the {@code Authenticator} factory function
   */
  public abstract String getAuthenticator();

  /*
   * Get a set of valid credentials generated using the given index.
   */
  public abstract Properties getValidCredentials(final int index);

  /**
   * Get a set of valid credentials for the given {@link Principal}.
   *
   * @param principal the {@link Principal} for which to get credentials
   * @return credentials for the given {@code Principal} or null if none possible.
   */
  public abstract Properties getValidCredentials(final Principal principal);

  /*
   * Get a set of invalid credentials generated using the given index.
   */
  public abstract Properties getInvalidCredentials(final int index);

  /**
   * Initialize the credential generator. This is provided separately from the {@link #init()}
   * method for convenience of implementations so that they do not need to store in
   * {@link #systemProperties}. The latter is convenient for the users who do not need to store
   * these properties rather can obtain it later by invoking {@link #getSystemProperties()}
   *
   * <p>
   * Required to be implemented by concrete classes that implement this abstract class.
   *
   * @return A set of extra properties that should be added to Gemfire system properties when not
   *         null.
   *
   * @throws IllegalArgumentException when there is a problem during initialization
   */
  protected abstract Properties initialize() throws IllegalArgumentException;

  /**
   * Enumeration for various {@link CredentialGenerator} implementations.
   *
   * <p>
   * The following schemes are supported as of now: {@code DummyAuthenticator},
   * {@code LdapUserAuthenticator}, {@code PKCSAuthenticator}. In addition SSL socket mode with
   * mutual authentication is also supported.
   *
   * <p>
   * To add a new authentication scheme the following needs to be done:
   * <ul>
   * <li>Add implementations for {@link AuthInitialize} and {@link Authenticator} classes for
   * clients/peers.</li>
   * <li>Add a new enumeration value for the scheme in this class. Notice the size of {@code VALUES}
   * array and increase that if it is getting overflowed. Note the methods and fields for existing
   * schemes and add for the new one in a similar manner.</li>
   * <li>Add an implementation for {@link CredentialGenerator}.</li>
   * <li>Modify the CredentialGenerator.Factory#create [no such Factory exists] method to add
   * creation of an instance of the new implementation for the {@code ClassCode} enumeration
   * value.</li>
   * </ul>
   *
   * <p>
   * All security dunit tests will automagically start testing the new implementation after this.
   *
   * @since GemFire 5.5
   */
  public static class ClassCode {

    private static byte nextOrdinal = 0;

    private static final byte ID_DUMMY = 1;
    private static final byte ID_LDAP = 2;
    private static final byte ID_PKCS = 3;
    private static final byte ID_SSL = 4;

    private static final ClassCode[] VALUES = new ClassCode[10];
    private static final Map CODE_NAME_MAP = new HashMap();

    public static final ClassCode DUMMY =
        new ClassCode(DummyAuthenticator.class.getName() + ".create", ID_DUMMY);
    public static final ClassCode LDAP =
        new ClassCode(LdapUserAuthenticator.class.getName() + ".create", ID_LDAP);
    public static final ClassCode PKCS =
        new ClassCode(PKCSAuthenticator.class.getName() + ".create", ID_PKCS);
    public static final ClassCode SSL = new ClassCode("SSL", ID_SSL);

    /** The name of this class. */
    private final String name;

    /** byte used as ordinal to represent this class */
    private final byte ordinal;

    /**
     * One of the following: ID_DUMMY, ID_LDAP, ID_PKCS
     */
    private final byte classType;

    /** Creates a new instance of class code. */
    private ClassCode(final String name, final byte classType) {
      this.name = name;
      this.classType = classType;
      ordinal = nextOrdinal++;
      VALUES[ordinal] = this;
      CODE_NAME_MAP.put(name, this);
    }

    public boolean isDummy() {
      return classType == ID_DUMMY;
    }

    public boolean isLDAP() {
      return classType == ID_LDAP;
    }

    public boolean isPKCS() {
      return classType == ID_PKCS;
    }

    public boolean isSSL() {
      return classType == ID_SSL;
    }

    /*
     * Returns the ClassCode represented by specified ordinal.
     */
    public static ClassCode fromOrdinal(final byte ordinal) {
      return VALUES[ordinal];
    }

    /*
     * Returns the ClassCode represented by specified string.
     */
    public static ClassCode parse(final String operationName) {
      return (ClassCode) CODE_NAME_MAP.get(operationName);
    }

    /*
     * Returns all the possible values.
     */
    public static List getAll() {
      final List codes = new ArrayList();
      for (final Object o : CODE_NAME_MAP.values()) {
        codes.add(o);
      }
      return codes;
    }

    /**
     * Returns the ordinal for this operation code.
     *
     * @return the ordinal of this operation.
     */
    public byte toOrdinal() {
      return ordinal;
    }

    /**
     * Returns a string representation for this operation.
     *
     * @return the name of this operation.
     */
    @Override
    public String toString() {
      return name;
    }

    /**
     * Indicates whether other object is same as this one.
     *
     * @return true if other object is same as this one.
     */
    @Override
    public boolean equals(final Object obj) {
      if (obj == this) {
        return true;
      }
      if (!(obj instanceof ClassCode)) {
        return false;
      }
      final ClassCode other = (ClassCode) obj;
      return other.ordinal == ordinal;
    }

    /**
     * Indicates whether other {@code ClassCode} is same as this one.
     *
     * @param opCode the {@code ClassCode} to check
     * @return true if other {@code ClassCode} is same as this one.
     */
    public boolean equals(final ClassCode opCode) {
      return opCode != null && opCode.ordinal == ordinal;
    }

    /**
     * Returns a hash code value for this {@code ClassCode} which is the same as its ordinal.
     *
     * @return the ordinal of this operation.
     */
    @Override
    public int hashCode() {
      return ordinal;
    }
  }
}
