/*
 * Copyright (C) 2006 - 2025 ScalAgent Distributed Technologies
 *
 * This library is free software; you can redistribute it and/or
 * modify it under the terms of the GNU Lesser General Public
 * License as published by the Free Software Foundation; either
 * version 2.1 of the License, or any later version.
 * 
 * This library is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
 * Lesser General Public License for more details.
 * 
 * You should have received a copy of the GNU Lesser General Public
 * License along with this library; if not, write to the Free Software
 * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307
 * USA.
 *
 * Initial developer(s): ScalAgent Distributed Technologies
 * Contributor(s): 
 */
package fr.dyade.aaa.util;

import java.io.EOFException;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.RandomAccessFile;

import fr.dyade.aaa.util.backup.BackupFile;
import fr.dyade.aaa.util.backup.BackupRecord;

/**
 *  This class allows to use a filesystem directory as repository with the
 * NTransaction module.
 *
 * @see NTransaction
 * @see Repository
 */
public final class FileRepository implements Repository {
  File dir = null;

  /** The number of save operation to repository. */
  private int nbsaved = 0;

  /**
   * Returns the number of save operation to repository.
   *
   * @return The number of save operation to repository.
   */
  @Override
  public int getNbSavedObjects() {
    return nbsaved;
  }

  /** The number of delete operation on repository. */
  private int nbdeleted = 0;

  /**
   * Returns the number of delete operation on repository.
   *
   * @return The number of delete operation on repository.
   */
  @Override
  public int getNbDeletedObjects() {
    return nbdeleted;
  }

  /** The number of useless delete operation on repository. */
  private int baddeleted = 0;

  /**
   * Returns the number of useless delete operation on repository.
   *
   * @return The number of useless delete operation on repository.
   */
  @Override
  public int getNbBadDeletedObjects() {
    return baddeleted;
  }

  /** The number of load operation from repository. */
  private int nbloaded = 0;

  /**
   * Returns the number of load operation from repository.
   *
   * @return The number of load operation from repository.
   */
  @Override
  public int getNbLoadedObjects() {
    return nbloaded;
  }

  /**
   *  Boolean value to force the use of FileOutputStream rather than
   * RandomAccessFile. By default this value is false, it can be set to 
   * true using the FileRepository.useRandomAccessFile java property.
   * <p>
   *  This property can be fixed only from <code>java</code> launching
   * command, or through System.property method.
   */
  private boolean useFileOutputStream;

  private boolean syncOnWrite = false;
  private String mode = "rw";

  // Be careful the constructor must be public to allow newInstance from another package.
  public FileRepository() {}

  /**
   * Initializes the repository.
   * 
   * @param transaction the transaction object.
   * @param dir the directory.
   * @throws IOException an error occurs.
   */
  @Override
  public void init(Transaction transaction, File dir)  throws IOException {
    this.dir = dir;
    useFileOutputStream = transaction.getBoolean("FileRepository.useRandomAccessFile");
    syncOnWrite = transaction.getBoolean("Transaction.SyncOnWrite");
    if (syncOnWrite) mode = "rwd";
  }

  /**
   * Gets a list of persistent objects that name corresponds to prefix.
   *
   * @param prefix the prefix.
   * @return The list of corresponding names.
   * @throws IOException an error occurs.
   */
  @Override
  public String[] list(String prefix) throws IOException {
    return dir.list(new StartWithFilter(prefix));
  }

  /**
   * Save the corresponding bytes array.
   * 
   * @param dirName the directory.
   * @param name the object name.
   * @param content the serialized object view.
   * @throws IOException an error occurs.
   */
  @Override
  public void save(String dirName, String name, byte[] content) throws IOException {
    File file;
    if (dirName == null) {
      file = new File(dir, name);
    } else {
      File parentDir = new File(dir, dirName);
      if (!parentDir.exists()) {
        parentDir.mkdirs();
      }
      file = new File(parentDir, name);
    }

    if (useFileOutputStream ) {
      try (FileOutputStream fos = new FileOutputStream(file)) {
        fos.write(content);
        if (syncOnWrite) fos.getFD().sync();
      }
    } else {
      try (RandomAccessFile raf = new RandomAccessFile(file, mode)) {
        raf.write(content);
      }
    }

    nbsaved += 1;
  }

  /**
   * Loads the byte array.
   * 
   * @param dirName the directory.
   * @param name the object name.
   * @return the serialized object view.
   * @throws IOException an error occurs.
   */
  @Override
  public byte[] load(String dirName, String name) throws IOException {
      // Gets it from disk.      
      File file;
      if (dirName == null) {
        file = new File(dir, name);
      } else {
        File parentDir = new File(dir, dirName);
        file = new File(parentDir, name);
      }
      byte[] buf = null;
      try (FileInputStream fis = new FileInputStream(file)) {
        buf = new byte[(int) file.length()];
        for (int nb=0; nb<buf.length; ) {
          int ret = fis.read(buf, nb, buf.length-nb);
          if (ret == -1) throw new EOFException();
          nb += ret;
        }
      }

      nbloaded += 1;    
      return buf;
  }

  /**
   * Deletes the corresponding objects in repository.
   * 
   * @param dirName the directory.
   * @param name the object name.
   * @throws IOException an error occurs.
   */
  @Override
  public void delete(String dirName, String name) throws IOException {
    if (dirName == null) {
      if (! new File(dir, name).delete()) baddeleted += 1;
    } else {
      File parentDir = new File(dir, dirName);
      if (! new File(parentDir, name).delete()) baddeleted += 1;
      deleteDir(parentDir);
    }
    nbdeleted += 1;
  }

  /**
   * Delete the specified directory if it is empty.
   * Also recursively delete the parent directories if they are empty.
   * 
   * @param dir the directory to delete.
   */
  private final void deleteDir(File dir) {
    // Check the disk state. It may be false according to the transaction
    // log but it doesn't matter because directories are lazily created.
    String[] children = dir.list();
    // children may be null if dir doesn't exist any more.
    if (children != null && children.length == 0) {
      dir.delete();
      if (dir.getAbsolutePath().length() > 
          this.dir.getAbsolutePath().length()) {
        deleteDir(dir.getParentFile());
      }
    }
  }

  /**
   * Commits all changes to the repository.
   * @throws IOException an error occurs.
   */
  @Override
  public void commit() throws IOException {}

  @Override
  public void backup(BackupFile backup) throws IOException {
    backupDir(backup, dir, null);
  }
  
  private void backupDir(BackupFile backup, File currentDir, String path) throws IOException {
    File[] files = currentDir.listFiles();
    for (File file : files) {
      if (file.isDirectory()) {
        if (path == null) {
          if (file.getName().equals("felix-cache"))
            continue;
          backupDir(backup, file, file.getName());
        } else {
          backupDir(backup, file, path + '/' + file.getName());
        }
      } else {
        if ((path == null) && file.getName().startsWith("log#"))
          continue;
        backupFile(backup, file, path);
      }
    }
  }

  private void backupFile(BackupFile backup, File file, String path) throws IOException {
    RandomAccessFile raf = null;
    try {
      raf = new RandomAccessFile(file, "r");
      byte buf[]  = new byte[(int) raf.length()];
      raf.readFully(buf);
      backup.backup(new BackupRecord(path, file.getName(), buf));
    } finally {
      if (raf != null)
        raf.close();
    }
  }
  
  /**
   * Closes the repository.
   * @throws IOException an error occurs.
   */
  @Override
  public void close() throws IOException {}
}
