/*
 * 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-2010 Sun Microsystems, Inc.
 *      Portions Copyright 2013-2015 ForgeRock AS.
 */

package org.opends.quicksetup;

import org.opends.quicksetup.util.Utils;

import java.io.File;
import java.io.FileReader;
import java.io.BufferedReader;
import java.io.IOException;
import java.util.Set;
import java.util.HashSet;

/**
 * Represents the contents of an OpenDS configuration file.
 */
public class Configuration {

  private String contents;
  private String lowerCaseContents;
  private Installation install;
  private File file;

  /**
   * Create a Configuration from a file.
   * @param install of which this configuration is part
   * @param file config.ldif file
   */
  public Configuration(Installation install, File file) {
    if (install == null) {
      throw new NullPointerException("config file cannot be null");
    }
    if (file == null) {
      throw new NullPointerException("config file cannot be null");
    } else if (
            // Leave open the possibility that the file might be
            // config.ldif.<svn rev>
            !file.getName().startsWith("config.ldif")) {
      throw new IllegalArgumentException("file must be a config.ldif file");
    }
    this.install = install;
    this.file = file;
  }

  /**
   * Returns the list of directory manager dns as they appear in the
   * configuration file.
   *
   * @return the list of directory manager dns as they appear in the
   *         configuration file.
   * @throws IOException if there were problems reading the information from
   * the configuration file.
   */
  public Set<String> getDirectoryManagerDns() throws IOException {
    return getConfigurationValues("ds-cfg-alternate-bind-dn");
  }

  /**
   * Provides the LDAP port as is specified in the config.ldif file.
   *
   * @return the LDAP port specified in the config.ldif file.
   * @throws IOException if there were problems reading the information from
   * the configuration file.
   */
  public int getPort() throws IOException {
    return getLDAPPort("ds-cfg-listen-port");
  }

  /**
   * Provides the administration port as is specified in the config.ldif file.
   *
   * @return the administration port specified in the config.ldif file.
   * @throws IOException if there were problems reading the information from
   * the configuration file.
   */
  public int getAdminConnectorPort() throws IOException
  {
    return getAdminConnectorPort("ds-cfg-listen-port");
  }

  /**
   * Tells whether this server is configured as a replication server or not.
   * @return <CODE>true</CODE> if the server is configured as a Replication
   * Server and <CODE>false</CODE> otherwise.
   * @throws IOException if there were problems reading the information from
   * the configuration file.
   */
  public boolean isReplicationServer() throws IOException
  {
    return getReplicationPort() != -1;
  }

  /**
   * Provides the Replication port as is specified in the config.ldif file.
   * Returns -1 if this server is not a Replication Server.
   *
   * @return the Replication port specified in the config.ldif file.
   * @throws IOException if there were problems reading the information from
   * the configuration file.
   */
  public int getReplicationPort() throws IOException {
    int port = -1;
    String contents = getLowerCaseContents();
    int index = contents.indexOf("cn=replication server");

    if (index != -1) {
      String attrWithPoints = "ds-cfg-replication-port:";
      int index1 = contents.indexOf(attrWithPoints, index);
      if (index1 != -1) {
        int index2 =
                contents.indexOf(Constants.LINE_SEPARATOR, index1);
        if (index2 != -1) {
          String sPort =
                  contents.substring(attrWithPoints.length() +
                          index1,
                          index2).trim();
          try {
            port = Integer.parseInt(sPort);
          } catch (NumberFormatException nfe) {
            // do nothing;
          }
        }
      }
    }
    return port;
  }

  /**
   * Returns the list of paths where the logs files are located as they appear
   * in the configuration file.
   *
   * @return the list of paths where the logs files are located as they appear
   *         in the configuration file.
   * @throws IOException if there were problems reading the information from
   * the configuration file.
   */
  public Set<String> getLogPaths() throws IOException {
    return getConfigurationValues("ds-cfg-log-file");
  }

  private int extractPort(String portAttr, int index)
  {
    int port = -1;
    String attrWithPoints = portAttr + ":";
    int index1 = contents.indexOf(attrWithPoints, index);
    if (index1 != -1) {
      int index2 =
        contents.indexOf(Constants.LINE_SEPARATOR, index1);
      if (index2 != -1) {
        String sPort =
          contents.substring(attrWithPoints.length() +
              index1, index2).trim();
        try {
          port = Integer.parseInt(sPort);
        } catch (NumberFormatException nfe) {
          // do nothing;
        }
      }
    }
    return port;
  }


  private int getLDAPPort(String portAttr) throws IOException {
    String contents = getLowerCaseContents();
    int index = contents.indexOf("cn=ldap connection handler");
    if (index != -1) {
      return extractPort (portAttr, index);
    }
    return -1;
  }

  private int getAdminConnectorPort(String portAttr) throws IOException {
    String contents = getLowerCaseContents();
    int index = contents.indexOf("cn=administration connector");
    if (index != -1) {
      return extractPort(portAttr, index);
    }
    return -1;
  }

  /**
   * Indicates whether the config.ldif file has been modified (compared to what
   * we had in the zip file). This is used to know if we have configured the
   * current binaries or not.
   *
   * @return <CODE>true</CODE> if the config.ldif file has been modified, or
   *         <CODE>false</CODE> if not.
   * @throws IOException if there were problems reading the information from
   * the configuration file.
   */
  public boolean hasBeenModified() throws IOException {
    boolean isConfigFileModified = getPort() != 389;

    if (!isConfigFileModified) {
      // TODO: this is not really stable
      // Note: a better way might be to diff this file with
      // /config/ldif/upgrade/config.ldif.<svn rev>
      isConfigFileModified =
          !getLowerCaseContents().contains("# cddl header start");
    }

    return isConfigFileModified;
  }

  /**
   * Returns a Set of relative paths containing the log paths outside the
   * installation.
   * @return a Set of relative paths containing the log paths outside the
   * installation.
   * @throws IOException if there is trouble reading the config file
   */
  public Set<String> getOutsideLogs()
          throws IOException
  {
    return getOutsidePaths(getLogPaths());
  }

  /**
   * Returns a Set of relative paths containing the db paths outside the
   * installation.
   * @return a Set of relative paths containing the db paths outside the
   * installation.
   * @throws IOException if there is trouble reading the config file
   */
  public Set<String> getOutsideDbs()
          throws IOException
  {
    return getOutsidePaths(getDatabasePaths());
  }

  private Set<String> getOutsidePaths(Set<String> paths) {
    Set<String> outsidePaths = new HashSet<>();
    for (String path : paths) {
      File fullDbPath;
      File pathFile = new File(path);
      if (pathFile.isAbsolute()) {
        fullDbPath = pathFile;
      } else {
        fullDbPath = new File(install.getInstanceDirectory(), path);
      }

      if (!Utils.isDescendant(fullDbPath, install.getInstanceDirectory())) {
        outsidePaths.add(Utils.getPath(fullDbPath));
      }
    }
    return outsidePaths;
  }

  /**
   * Provides the contents of the config.ldif file in a String.
   *
   * @return a String representing the contents of the config.ldif file.
   * @throws IOException if there was a problem reading the file
   */
  public String getContents() throws IOException {
    if (contents == null) {
      load();
    }
    return contents;
  }

  /**
   * Provides the contents of the config.ldif file in a lower case String.
   *
   * @return a lower case String representing the contents of the config.ldif
   * file.
   * @throws IOException if there was a problem reading the file
   */
  public String getLowerCaseContents() throws IOException {
    if (lowerCaseContents == null) {
      load();
    }
    return lowerCaseContents;
  }

  /**
   * Returns the list of paths where the databases are installed as they appear
   * in the configuration file.
   *
   * @return the list of paths where the databases are installed as they appear
   * in the configuration file.
   * @throws IOException if there is a problem reading the config file.
   */
  public Set<String> getDatabasePaths() throws IOException {
    return getConfigurationValues("ds-cfg-db-directory");
  }

  /**
   * Returns the list of base dns as they appear in the configuration file.
   *
   * @return the list of base dns as they appear in the configuration file.
   * @throws IOException if there is a problem reading the config file.
   */
  public Set<String> getBaseDNs() throws IOException {
    return getConfigurationValues("ds-cfg-base-dn");
  }

  /**
   * Loads the contents of the configuration file into memory.
   * @throws IOException if there were problems loading the file
   */
  public void load() throws IOException {
    StringBuilder buf = new StringBuilder();
    FileReader reader = new FileReader(file);
    BufferedReader in = new BufferedReader(reader);
    String line;
    // We do not care about encoding: we are just interested in the ports
    while ((line = in.readLine()) != null) {
      buf.append(line).append(Constants.LINE_SEPARATOR);
    }
    reader.close();
    contents = buf.toString();
    lowerCaseContents = contents.toLowerCase();
  }

  private Set<String> getConfigurationValues(String attrName)
          throws IOException
  {
    Set<String> set = new HashSet<>();
    attrName += ":";
    String lowerCaseContents = getLowerCaseContents();
    String contents = getContents();
    int index1 = lowerCaseContents.indexOf(attrName);
    while (index1 != -1) {
      int index2 = lowerCaseContents.indexOf(Constants.LINE_SEPARATOR, index1);
      String value;
      if (index2 > index1 + attrName.length()) {
        value = contents.substring(attrName.length() + index1, index2).trim();
      } else if (lowerCaseContents.length() > index1 + attrName.length()) {
        // Assume end of file
        value = contents.substring(attrName.length() + index1).trim();
      } else {
        value = null;
      }

      if (value != null && value.length() > 0) {
        set.add(value);
      }

      index1 = lowerCaseContents.indexOf(attrName,
              index1 + attrName.length());
    }
    return set;
  }
}
