/*
 * 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.internal.cache;

import java.util.Properties;

import org.apache.geode.annotations.Immutable;
import org.apache.geode.cache.AttributesFactory;
import org.apache.geode.cache.DiskStoreFactory;
import org.apache.geode.cache.DiskWriteAttributes;
import org.apache.geode.cache.Region;
import org.apache.geode.cache.RegionAttributes;
import org.apache.geode.internal.cache.xmlcache.CacheXml;
import org.apache.geode.util.internal.GeodeGlossary;

/**
 * Implementation of DiskWriteAttributes
 *
 * @see AttributesFactory#setDiskWriteAttributes
 * @see RegionAttributes#getDiskWriteAttributes
 * @see Region#writeToDisk
 *
 *
 * @since GemFire 5.1
 */
@SuppressWarnings({"deprecation", "unused"})
public class DiskWriteAttributesImpl implements DiskWriteAttributes {
  private static final long serialVersionUID = -4269181954992768424L;

  /** Are writes synchronous? */
  private final boolean isSynchronous;

  /**
   * The the number of milliseconds that can elapse before unwritten data is written to disk.
   */
  private final long timeInterval;

  /**
   * The number of bytes of region entry data to queue up before writing to disk.
   */
  private final long bytesThreshold;

  /** Whether compaction is to be permitted or not. Defaults to true * */
  private final boolean compactOplogs;

  /** stored in bytes as a long but specified in megabytes by client applications **/
  private final long maxOplogSize;

  /** default max in bytes **/
  private static final long DEFAULT_MAX_OPLOG_SIZE =
      Long.getLong(GeodeGlossary.GEMFIRE_PREFIX + "DEFAULT_MAX_OPLOG_SIZE", 1024L)
          * (1024 * 1024); // 1 GB

  /** default max limit in bytes **/
  private static final long DEFAULT_MAX_OPLOG_SIZE_LIMIT = (long) Integer.MAX_VALUE * (1024 * 1024);

  private static final boolean DEFAULT_ROLL_OPLOGS = true;

  private static final boolean DEFAULT_ALLOW_FORCE_COMPACTION = false;

  private static final boolean DEFAULT_IS_SYNCHRONOUS = false; // the pre 6.5 default

  // private static final long DEFAULT_BYTES_THRESHOLD = 0;

  static final long DEFAULT_TIME_INTERVAL = 1000; // 1 sec

  private static final int DEFAULT_COMPACTION_THRESHOLD = 50;

  public static final String SYNCHRONOUS_PROPERTY = "synchronous";

  /**
   * Default disk directory size in megabytes
   *
   * @since GemFire 5.1
   */
  public static final int DEFAULT_DISK_DIR_SIZE = DiskStoreFactory.DEFAULT_DISK_DIR_SIZE;

  @Immutable
  private static final DiskWriteAttributes DEFAULT_ASYNC_DWA;
  static {
    Properties props = new Properties();
    props.setProperty(SYNCHRONOUS_PROPERTY, "false");
    DEFAULT_ASYNC_DWA = new DiskWriteAttributesImpl(props);
  }

  @Immutable
  private static final DiskWriteAttributes DEFAULT_SYNC_DWA;
  static {
    Properties props = new Properties();
    props.setProperty(SYNCHRONOUS_PROPERTY, "true");
    DEFAULT_SYNC_DWA = new DiskWriteAttributesImpl(props);
  }

  /////////////////////// Constructors ///////////////////////

  /**
   *
   * Creates a new <code>DiskWriteAttributes</code> object using the properties specified. The
   * properties that can be specified are:
   * <ul>
   * <li>synchronous : boolean to specify whether DiskWrites will be synchronous (true)/asynchronous
   * (false)
   * <li>auto-compact : boolean to specify whether to automatically compact disk files so they use
   * less disk space (true)
   * <li>allow-force-compaction : boolean to specify whether to manual compaction of disk files is
   * allowed (false)
   * <li>compaction-threshold: The threshold at which an oplog becomes compactable. Must be in the
   * range 0..100 inclusive. (50)
   * <li>max-oplog-size: The maximum size of an oplog. 0 would mean infinity
   * <li>time-interval: The number of milliseconds that can elapse before unwritten data is written
   * to disk.
   * <li>bytes-threshold: The number of unwritten bytes of data that can be enqueued before being
   * written to disk
   * </ul>
   * The above properties are case sensitive and if a propery which is not in the list above is
   * passed no action is taken. If a property which is present above is not specified then the
   * following default values will be uses
   *
   * <ul>
   * <li>synchronous = false
   * <li>auto-compact = true
   * <li>allow-force-compaction = false
   * <li>compaction-threshold = 50 %
   * <li>max-oplog-size = 1 GB
   * <li>time-interval = 1000 milliseconds
   * <li>byte-threshold = 0 bytes
   * </ul>
   *
   *
   * @throws IllegalArgumentException If any of the properties specified are not in the expected
   *         format.
   * @throws IllegalStateException if max-oplog-size is set to infinity(0) and compaction is set to
   *         true
   */

  public DiskWriteAttributesImpl(Properties properties) {

    String isSynchronousString = properties.getProperty(SYNCHRONOUS_PROPERTY);
    if (isSynchronousString == null) {
      isSynchronous = DEFAULT_IS_SYNCHRONOUS;
    } else {
      verifyBooleanString(isSynchronousString, SYNCHRONOUS_PROPERTY);
      isSynchronous = Boolean.parseBoolean(isSynchronousString);
    }

    String compactOplogsString = properties.getProperty(CacheXml.ROLL_OPLOG);
    if (compactOplogsString == null) {
      compactOplogs = DEFAULT_ROLL_OPLOGS;
    } else {
      verifyBooleanString(compactOplogsString, CacheXml.ROLL_OPLOG);
      compactOplogs = Boolean.parseBoolean(compactOplogsString);
    }

    String bytesThresholdString = properties.getProperty(CacheXml.BYTES_THRESHOLD);
    if (bytesThresholdString != null) {
      if (isSynchronous) {
        // log warning, no use setting time if is synchronous
      }
      bytesThreshold = verifyLongInString(bytesThresholdString, CacheXml.BYTES_THRESHOLD);
    } else {
      bytesThreshold = 0L;
    }

    String timeIntervalString = properties.getProperty(CacheXml.TIME_INTERVAL);
    if (timeIntervalString != null) {
      if (isSynchronous) {
        // log warning, no use setting time if is synchronous
      }
      timeInterval = verifyLongInString(timeIntervalString, CacheXml.TIME_INTERVAL);
    } else {
      if (!isSynchronous && bytesThreshold == 0) {
        timeInterval = DiskWriteAttributesImpl.DEFAULT_TIME_INTERVAL;
      } else {
        timeInterval = 0;
      }
    }



    String maxOplogSizeString = properties.getProperty(CacheXml.MAX_OPLOG_SIZE);

    if (maxOplogSizeString != null) {
      long opSize = verifyLongInString(maxOplogSizeString, CacheXml.MAX_OPLOG_SIZE);
      if (opSize == 0 && compactOplogs == true) {
        throw new IllegalStateException(
            "Compaction cannot be set to true if max-oplog-size is set to infinite (infinite is represented by size zero : 0)");

      }
      if (opSize == 0 || opSize == DEFAULT_MAX_OPLOG_SIZE_LIMIT) {
        if (compactOplogs) {
          throw new IllegalArgumentException(
              "Cannot set maxOplogs size to infinity (0) if compaction is set to true");
        } else {
          maxOplogSize = DEFAULT_MAX_OPLOG_SIZE_LIMIT; // infinity
        }
      } else {
        maxOplogSize = opSize;
      }
    } else {
      maxOplogSize = DEFAULT_MAX_OPLOG_SIZE;
    }
  }

  /**
   * Verifys if the propertyString passed is a valid boolean value or null else throws an
   * IllegalArgument exception
   *
   * @throws IllegalArgumentException if the property string passed does not represent a boolean or
   *         null
   *
   */
  private void verifyBooleanString(String propertyString, String property) {
    if (!(propertyString.equalsIgnoreCase("true") || propertyString.equalsIgnoreCase("false"))) {
      throw new IllegalArgumentException(
          String.format("%s property has to be true or false or null and cannot be %s",
              property, propertyString));
    }
  }

  /**
   * Verifys if the string passed, is in a number format which is acceptable and returns the long
   * value of the string.
   *
   * @return the long value of the string
   */
  private long verifyLongInString(String propertyString, String property) {
    long returnValue;
    try {
      returnValue = Long.parseLong(propertyString);
    } catch (NumberFormatException e) {
      throw new IllegalArgumentException(
          String.format("%s has to be a valid number and not %s",
              property, propertyString));
    }

    if (returnValue < 0) {
      throw new IllegalArgumentException(
          String.format("%s has to be positive number and the value given %s is not acceptable",
              property, returnValue));
    }

    return returnValue;
  }

  /**
   * Verifys if the string passed, is in a number format which is acceptable and returns the int
   * value of the string.
   *
   * @return the int value of the string
   */
  private int verifyPercentInString(String propertyString, String property) {
    int returnValue;
    try {
      returnValue = Integer.parseInt(propertyString);
    } catch (NumberFormatException e) {
      throw new IllegalArgumentException(
          String.format("%s has to be a valid number and not %s",
              property, propertyString));
    }

    if (returnValue < 0) {
      throw new IllegalArgumentException(
          String.format("%s has to be positive number and the value given %s is not acceptable",
              property, returnValue));
    } else if (returnValue > 100) {
      throw new IllegalArgumentException(
          String.format(
              "%s has to be a number that does not exceed %s so the value given %s is not acceptable",

              property, returnValue, 100));
    }

    return returnValue;
  }

  // //////////////////// Instance Methods //////////////////////

  /**
   * Returns whether or not this <code>DiskWriteAttributes</code> configures synchronous writes.
   */
  @Override
  public boolean isSynchronous() {
    return isSynchronous;
  }

  /**
   * Returns true if the oplogs is to be rolled
   */
  @Override
  public boolean isRollOplogs() {
    return compactOplogs;
  }

  /** Get the max Oplog Size in megabytes. The value is stored in bytes so division is necessary **/
  @Override
  public int getMaxOplogSize() {
    return (int) (maxOplogSize / (1024 * 1024));
  }


  /** Get the max Oplog Size in bytes **/
  long getMaxOplogSizeInBytes() {
    return maxOplogSize;
  }

  /**
   * Returns the number of milliseconds that can elapse before unwritten data is written to disk. If
   * this <code>DiskWriteAttributes</code> configures synchronous writing, then
   * <code>timeInterval</code> has no meaning.
   */
  @Override
  public long getTimeInterval() {
    return timeInterval;
  }

  /**
   * Returns the number of unwritten bytes of data that can be enqueued before being written to
   * disk. If this <code>DiskWriteAttributes</code> configures synchronous writing, then
   * <code>bytesThreshold</code> has no meaning.
   */
  @Override
  public long getBytesThreshold() {
    return bytesThreshold;
  }


  /**
   * Two <code>DiskWriteAttributes</code> are equal if the both specify the synchronous writes, or
   * they both specify asynchronous writes with the same time interval and bytes threshold.
   */
  @Override
  public boolean equals(Object o) {
    if (!(o instanceof DiskWriteAttributesImpl)) {
      return false;
    }

    DiskWriteAttributesImpl other = (DiskWriteAttributesImpl) o;
    if (other.isSynchronous() != isSynchronous()) {
      return false;
    }
    boolean result =
        other.isRollOplogs() == isRollOplogs() && other.getMaxOplogSize() == getMaxOplogSize();
    if (!isSynchronous()) {
      result = result && other.getTimeInterval() == getTimeInterval()
          && other.getBytesThreshold() == getBytesThreshold();
    }
    return result;
  }

  /*
   * (non-Javadoc)
   *
   * @see java.lang.Object#hashCode()
   *
   * Note that we just need to make sure that equal objects return equal hashcodes; nothing really
   * elaborate is done here.
   */
  @Override
  public int hashCode() {
    long result = 0;

    if (isSynchronous()) {
      if (isRollOplogs()) {
        result += 2;
      }
    } else {
      result += 1; // asynchronous
      result += getTimeInterval();
      result += getBytesThreshold();
    }
    result += getMaxOplogSize();
    return (int) (result & 0xFFFFFFFF);
  }

  @Override
  public String toString() {
    StringBuilder sb = new StringBuilder();
    if (isSynchronous()) {
      sb.append("Synchronous writes to disk");

    } else {
      sb.append("Asynchronous writes to disk after a threshold of ");
      sb.append(getTimeInterval());
      sb.append("ms or ");
      sb.append(getBytesThreshold());
      sb.append(" bytes");
    }
    sb.append(". MaxOplog size is        : " + maxOplogSize);
    sb.append(". RollOplogs is          : " + compactOplogs);
    return sb.toString();
  }

  /**
   * Get the default max oplog size limit in megabytes
   *
   * @return the default max oplog size limit in megabytes
   */
  public static int getDefaultMaxOplogSizeLimit() {
    return (int) (DEFAULT_MAX_OPLOG_SIZE_LIMIT / (1024 * 1024));
  }

  /**
   *
   * Returns the default compaction oplog value
   *
   * @return the default compaction oplog value
   */
  public static boolean getDefaultRollOplogsValue() {
    return DEFAULT_ROLL_OPLOGS;
  }

  /**
   *
   * Gets the default max oplog size in megabytes
   *
   * @return the default max oplog size in megabytes
   */
  public static int getDefaultMaxOplogSize() {
    return (int) (DEFAULT_MAX_OPLOG_SIZE / (1024 * 1024));
  }

  /**
   * Returns a default instance of DiskWriteAttributes
   *
   * @return the default DiskWriteAttributes instance
   */
  public static DiskWriteAttributes getDefaultAsyncInstance() {
    return DEFAULT_ASYNC_DWA;
  }

  public static DiskWriteAttributes getDefaultSyncInstance() {
    return DEFAULT_SYNC_DWA;
  }
}
