/*
 * 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 2015 ForgeRock AS
 */
package org.opends.server.backends.pluggable;

import java.util.NoSuchElementException;
import java.util.Set;

import org.forgerock.i18n.slf4j.LocalizedLogger;
import org.forgerock.opendj.config.server.ConfigException;
import org.forgerock.opendj.ldap.ByteSequence;
import org.forgerock.opendj.ldap.ByteString;
import org.opends.server.backends.pluggable.spi.AccessMode;
import org.opends.server.backends.pluggable.spi.Cursor;
import org.opends.server.backends.pluggable.spi.Importer;
import org.opends.server.backends.pluggable.spi.ReadOperation;
import org.opends.server.backends.pluggable.spi.ReadableTransaction;
import org.opends.server.backends.pluggable.spi.SequentialCursor;
import org.opends.server.backends.pluggable.spi.Storage;
import org.opends.server.backends.pluggable.spi.StorageRuntimeException;
import org.opends.server.backends.pluggable.spi.StorageStatus;
import org.opends.server.backends.pluggable.spi.TreeName;
import org.opends.server.backends.pluggable.spi.UpdateFunction;
import org.opends.server.backends.pluggable.spi.WriteOperation;
import org.opends.server.backends.pluggable.spi.WriteableTransaction;
import org.opends.server.types.BackupConfig;
import org.opends.server.types.BackupDirectory;
import org.opends.server.types.DirectoryException;
import org.opends.server.types.RestoreConfig;

/** Decorates a {@link Storage} with additional trace logging. */
final class TracedStorage implements Storage
{
  private void appendKeyValue(final StringBuilder builder, final Object key, final Object value)
  {
    builder.append("\"").append(key).append("\":\"").append(value).append("\"");
  }

  /**
   * Decorates {@link Cursor} with trace logging. Navigational methods will perform enter/leave logging in order to
   * help detect deadlocks.
   */
  private class TracedCursor implements Cursor<ByteString, ByteString>
  {
    private final Cursor<ByteString, ByteString> cursor;

    private TracedCursor(final Cursor<ByteString, ByteString> cursor)
    {
      this.cursor = cursor;
    }

    private void traceEnter(String method, Object... args)
    {
      trace("Cursor", id(), method + "#enter", args);
    }

    private void traceLeave(String method, Object... args)
    {
      trace("Cursor", id(), method + "#leave", args);
    }

    @Override
    public boolean positionToKey(final ByteSequence key)
    {
      traceEnter("positionToKey", "key", hex(key));
      boolean found = cursor.positionToKey(key);
      traceLeave("positionToKey", "key", hex(key), "found", found);
      return found;
    }

    @Override
    public boolean positionToKeyOrNext(final ByteSequence key)
    {
      traceEnter("positionToKeyOrNext", "key", hex(key));
      boolean found = cursor.positionToKeyOrNext(key);
      traceLeave("positionToKeyOrNext", "key", hex(key), "found", found);
      return found;
    }

    @Override
    public boolean positionToLastKey()
    {
      traceEnter("positionToLastKey");
      boolean found = cursor.positionToLastKey();
      traceLeave("positionToLastKey", "found", found);
      return found;
    }

    @Override
    public boolean positionToIndex(final int index)
    {
      traceEnter("positionToIndex", "index", index);
      boolean found = cursor.positionToIndex(index);
      traceLeave("positionToIndex", "index", index, "found", found);
      return found;
    }

    @Override
    public boolean next()
    {
      traceEnter("next");
      boolean found = cursor.next();
      traceLeave("next", "found", found);
      return found;
    }

    @Override
    public void delete()
    {
      traceEnter("delete");
      cursor.delete();
      traceLeave("delete");
    }

    @Override
    public boolean isDefined()
    {
      traceEnter("isDefined");
      boolean isDefined = cursor.isDefined();
      traceLeave("isDefined", "isDefined", isDefined);
      return isDefined;
    }

    @Override
    public ByteString getKey() throws NoSuchElementException
    {
      traceEnter("getKey");
      try
      {
        ByteString key = cursor.getKey();
        traceLeave("getKey", "key", hex(key));
        return key;
      }
      catch (NoSuchElementException e)
      {
        traceLeave("getKey", "NoSuchElementException", e.getMessage());
        throw e;
      }
    }

    @Override
    public ByteString getValue() throws NoSuchElementException
    {
      traceEnter("getValue");
      try
      {
        ByteString value = cursor.getValue();
        traceLeave("getValue", "value", hex(value));
        return value;
      }
      catch (NoSuchElementException e)
      {
        traceLeave("getValue", "NoSuchElementException", e.getMessage());
        throw e;
      }
    }

    @Override
    public void close()
    {
      traceEnter("close");
      cursor.close();
      traceLeave("close");
    }

    private int id()
    {
      return System.identityHashCode(this);
    }
  }

  /** Decorates an {@link Importer} with additional trace logging. */
  private final class TracedImporter implements Importer
  {
    private final Importer importer;

    private TracedImporter(final Importer importer)
    {
      this.importer = importer;
    }

    private void traceEnter(String method, Object... args)
    {
      trace("Importer", id(), method + "#enter", args);
    }

    private void traceLeave(String method, Object... args)
    {
      trace("Importer", id(), method + "#leave", args);
    }

    @Override
    public void clearTree(final TreeName name)
    {
      traceEnter("clearTree", "name", name);
      importer.clearTree(name);
      traceLeave("clearTree", "name", name);
    }

    @Override
    public void put(final TreeName name, final ByteSequence key, final ByteSequence value)
    {
      traceEnter("put", "name", name, "key", hex(key), "value", hex(value));
      importer.put(name, key, value);
      traceLeave("put", "name", name, "key", hex(key), "value", hex(value));
    }

    @Override
    public ByteString read(TreeName name, ByteSequence key)
    {
      traceEnter("read", "name", name, "key", hex(key));
      final ByteString value = importer.read(name, key);
      traceLeave("read", "name", name, "key", hex(key), "value", hex(value));
      return value;
    }

    @Override
    public void close()
    {
      traceEnter("close");
      importer.close();
      traceLeave("close");
    }

    private int id()
    {
      return System.identityHashCode(this);
    }

    @Override
    public SequentialCursor<ByteString, ByteString> openCursor(TreeName name)
    {
      traceEnter("openCursor", "name", name);
      SequentialCursor<ByteString, ByteString> cursor = importer.openCursor(name);
      traceLeave("openCursor", "name", name);
      return cursor;
    }
  }

  /** Decorates an {@link ReadableTransaction} with additional trace logging. */
  private final class TracedReadableTransaction implements ReadableTransaction
  {
    private final ReadableTransaction txn;

    private TracedReadableTransaction(final ReadableTransaction txn)
    {
      this.txn = txn;
    }

    private void traceEnter(String method, Object... args)
    {
      trace("ReadableTransaction", id(), method + "#enter", args);
    }

    private void traceLeave(String method, Object... args)
    {
      trace("ReadableTransaction", id(), method + "#leave", args);
    }

    @Override
    public long getRecordCount(TreeName name)
    {
      traceEnter("getRecordCount", "name", name);
      final long count = txn.getRecordCount(name);
      traceLeave("getRecordCount", "name", name, "count", count);
      return count;
    }

    @Override
    public Cursor<ByteString, ByteString> openCursor(final TreeName name)
    {
      traceEnter("openCursor", "name", name);
      final Cursor<ByteString, ByteString> cursor = txn.openCursor(name);
      traceLeave("openCursor", "name", name);
      return new TracedCursor(cursor);
    }

    @Override
    public ByteString read(final TreeName name, final ByteSequence key)
    {
      traceEnter("read", "name", name, "key", hex(key));
      final ByteString value = txn.read(name, key);
      traceLeave("read", "name", name, "key", hex(key), "value", hex(value));
      return value;
    }

    private int id()
    {
      return System.identityHashCode(this);
    }
  }

  /** Decorates an {@link WriteableTransaction} with additional trace logging. */
  private final class TracedWriteableTransaction implements WriteableTransaction
  {
    private final WriteableTransaction txn;

    private TracedWriteableTransaction(final WriteableTransaction txn)
    {
      this.txn = txn;
    }

    private void traceEnter(String method, Object... args)
    {
      trace("WriteableTransaction", id(), method + "#enter", args);
    }

    private void traceLeave(String method, Object... args)
    {
      trace("WriteableTransaction", id(), method + "#leave", args);
    }

    @Override
    public void put(final TreeName name, final ByteSequence key, final ByteSequence value)
    {
      traceEnter("put", "name", name, "key", hex(key), "value", hex(value));
      txn.put(name, key, value);
      traceLeave("put", "name", name, "key", hex(key), "value", hex(value));
    }

    @Override
    public boolean delete(final TreeName name, final ByteSequence key)
    {
      traceEnter("delete", "name", name, "key", hex(key));
      final boolean isDeleted = txn.delete(name, key);
      traceLeave("delete", "name", name, "key", hex(key), "isDeleted", isDeleted);
      return isDeleted;
    }

    @Override
    public void deleteTree(final TreeName name)
    {
      traceEnter("deleteTree", "name", name);
      txn.deleteTree(name);
      traceLeave("deleteTree", "name", name);
    }

    @Override
    public long getRecordCount(TreeName name)
    {
      traceEnter("getRecordCount", "name", name);
      final long count = txn.getRecordCount(name);
      traceLeave("getRecordCount", "name", name, "count", count);
      return count;
    }

    @Override
    public Cursor<ByteString, ByteString> openCursor(final TreeName name)
    {
      traceEnter("openCursor", "name", name);
      final Cursor<ByteString, ByteString> cursor = txn.openCursor(name);
      traceLeave("openCursor", "name", name);
      return new TracedCursor(cursor);
    }

    @Override
    public void openTree(final TreeName name, boolean createOnDemand)
    {
      traceEnter("openTree", "name", name, "createOnDemand", createOnDemand);
      txn.openTree(name, createOnDemand);
      traceLeave("openTree", "name", name, "createOnDemand", createOnDemand);
    }

    @Override
    public ByteString read(final TreeName name, final ByteSequence key)
    {
      traceEnter("read", "name", name, "key", hex(key));
      final ByteString value = txn.read(name, key);
      traceLeave("read", "name", name, "key", hex(key), "value", hex(value));
      return value;
    }

    @Override
    public boolean update(final TreeName name, final ByteSequence key, final UpdateFunction f)
    {
      traceEnter("update", "name", name, "key", hex(key));
      final boolean isUpdated = txn.update(name, key, new UpdateFunction() {
        @Override
        public ByteSequence computeNewValue(final ByteSequence oldValue) {
          traceEnter("updateFunction", "oldValue", hex(oldValue));
          ByteSequence newValue = f.computeNewValue(oldValue);
          traceLeave("updateFunction", "oldValue", hex(oldValue), "newValue", hex(newValue));
          return newValue;
        }
      });
      traceLeave("update", "name", name, "key", hex(key), "isUpdated", isUpdated);
      return isUpdated;
    }

    private int id()
    {
      return System.identityHashCode(this);
    }
  }

  private static final LocalizedLogger logger = LocalizedLogger.getLoggerForThisClass();

  private final String backendId;
  private final Storage storage;

  TracedStorage(final Storage storage, final String backendId)
  {
    this.storage = storage;
    this.backendId = backendId;
  }

  private void trace(String type, int id, String method, Object... args)
  {
    StringBuilder builder = new StringBuilder();
    // Include a "!" in order to facilitate filtering using commands like "cut".
    builder.append("!{");
    appendKeyValue(builder, "backend", backendId);
    builder.append(",");
    appendKeyValue(builder, "storage", storageId());
    builder.append(",");
    appendKeyValue(builder, "type", type);
    builder.append(",");
    appendKeyValue(builder, "id", id);
    builder.append(",");
    appendKeyValue(builder, "method", method);
    for (int i = 0; i < args.length; i += 2)
    {
      builder.append(",");
      appendKeyValue(builder, args[i], args[i + 1]);
    }
    builder.append("}");
    logger.trace(builder.toString());
  }

  private void traceEnter(String method, Object... args)
  {
    if (logger.isTraceEnabled())
    {
      trace("Storage", storageId(), method + "#enter", args);
    }
  }

  private void traceLeave(String method, Object... args)
  {
    if (logger.isTraceEnabled())
    {
      trace("Storage", storageId(), method + "#leave", args);
    }
  }


  @Override
  public void close()
  {
    traceEnter("close");
    storage.close();
    traceLeave("close");
  }

  @Override
  public StorageStatus getStorageStatus()
  {
    return storage.getStorageStatus();
  }

  @Override
  public void open(AccessMode accessMode) throws Exception
  {
    traceEnter("open", "accessMode", accessMode);
    storage.open(accessMode);
    traceLeave("open", "accessMode", accessMode);
  }

  @Override
  public <T> T read(final ReadOperation<T> readOperation) throws Exception
  {
    ReadOperation<T> op = readOperation;
    if (logger.isTraceEnabled())
    {
      op = new ReadOperation<T>()
      {
        @Override
        public T run(final ReadableTransaction txn) throws Exception
        {
          return readOperation.run(new TracedReadableTransaction(txn));
        }
      };
    }
    return storage.read(op);
  }

  @Override
  public void removeStorageFiles() throws StorageRuntimeException
  {
    traceEnter("removeStorageFiles");
    storage.removeStorageFiles();
    traceLeave("removeStorageFiles");
  }

  @Override
  public Importer startImport() throws ConfigException, StorageRuntimeException
  {
    traceEnter("startImport");
    final Importer importer = storage.startImport();
    traceLeave("startImport");
    return logger.isTraceEnabled() ? new TracedImporter(importer) : importer;
  }

  @Override
  public boolean supportsBackupAndRestore()
  {
    return storage.supportsBackupAndRestore();
  }

  @Override
  public void write(final WriteOperation writeOperation) throws Exception
  {
    WriteOperation op = writeOperation;
    if (logger.isTraceEnabled())
    {
      op = new WriteOperation()
      {
        @Override
        public void run(final WriteableTransaction txn) throws Exception
        {
          writeOperation.run(new TracedWriteableTransaction(txn));
        }
      };
    }
    storage.write(op);
  }

  @Override
  public void createBackup(BackupConfig backupConfig) throws DirectoryException
  {
    traceEnter("createBackup");
    storage.createBackup(backupConfig);
    traceLeave("createBackup");
  }

  @Override
  public void removeBackup(BackupDirectory backupDirectory, String backupID) throws DirectoryException
  {
    traceEnter("removeBackup", "backupID", backupID);
    storage.removeBackup(backupDirectory, backupID);
    traceLeave("removeBackup", "backupID", backupID);
  }

  @Override
  public void restoreBackup(RestoreConfig restoreConfig) throws DirectoryException
  {
    traceEnter("restoreBackup");
    storage.restoreBackup(restoreConfig);
    traceLeave("restoreBackup");
  }

  @Override
  public Set<TreeName> listTrees()
  {
    traceEnter("listTrees");
    final Set<TreeName> results = storage.listTrees();
    traceLeave("listTrees", "trees", results);
    return results;
  }

  private static String hex(final ByteSequence bytes)
  {
    return bytes != null ? bytes.toByteString().toASCIIString() : null;
  }

  private int storageId()
  {
    return System.identityHashCode(this);
  }
}
