/*
 * Copyright (C) 2003-2016 eXo Platform SAS.
 *
 * This 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 (at your option) any later version.
 *
 * This software 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 software; if not, write to the Free
 * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA
 * 02110-1301 USA, or see the FSF site: http://www.fsf.org.
 */
package org.exoplatform.services.cms.clouddrives;

import java.util.Calendar;
import java.util.Collection;
import java.util.Queue;
import java.util.concurrent.ConcurrentLinkedQueue;
import java.util.concurrent.ExecutionException;

import javax.jcr.Node;
import javax.jcr.RepositoryException;

import org.exoplatform.services.log.ExoLogger;
import org.exoplatform.services.log.Log;

/**
 * Local mirror of cloud drive. All files of this drive contain metadata such as
 * name, author, dates and link to actual drive on the cloud provider. <br>
 * <p>
 * Created by The eXo Platform SAS.
 * 
 * @author <a href="mailto:pnedonosko@exoplatform.com">Peter Nedonosko</a>
 * @version $Id: CloudDrive.java 00000 Sep 7, 2012 pnedonosko $
 */
public abstract class CloudDrive {

  /** The Constant LOG. */
  protected static final Log LOG = ExoLogger.getLogger(CloudDrive.class);

  /**
   * Cloud Drive command and its result. <br>
   */
  public interface Command {

    /**
     * Percentage of command completion.
     */
    final int COMPLETE = 100;

    /**
     * Return command completion in percents.
     * 
     * @return integer, a percent of 100.
     */
    int getProgress();

    /**
     * Answer with {@code true} if command complete. Note that done status will
     * be {@code true} only after all registered listeners will be fired about
     * the command completion.
     * 
     * @return boolean flag, {@code true} if command complete, {@code false}
     *         otherwise.
     */
    boolean isDone();

    /**
     * System time when process started to execute.
     * 
     * @return long, time in milliseconds
     */
    long getStartTime();

    /**
     * System time when process finished to execute.
     * 
     * @return long, time in milliseconds
     */
    long getFinishTime();

    /**
     * Command name.
     * 
     * @return String
     */
    String getName();

    /**
     * Return <code>true</code> when command produced changes in the drive (has
     * added/updated/removed files).
     * 
     * @return boolean will be <code>true</code> when command produced changes,
     *         <code>false</code> otherwise
     */
    boolean hasChanges();

    /**
     * Collection of files affect by the command. Call to this method will
     * return unmodifiable view on actual results and should be treated
     * accordingly until the command will not be completed.
     * 
     * @return collection of {@link CloudFile} objects
     */
    Collection<CloudFile> getFiles();

    /**
     * Collection of file paths removed by the command. Call to this method will
     * return unmodifiable view on actual results and should be treated
     * accordingly until the command will not be completed.
     * 
     * @return collection of {@link String} file paths
     */
    Collection<String> getRemoved();

    /**
     * Messages for an user optionally generated by the command.
     * 
     * @return collection of {@link CloudDriveMessage} objects, can be empty,
     *         not <code>null</code>
     */
    Collection<CloudDriveMessage> getMessages();

    /**
     * Wait for command completion.
     * 
     * @throws ExecutionException if command thrown an exception.
     * @throws InterruptedException if current thread was interrupted.
     */
    void await() throws ExecutionException, InterruptedException;
  }

  /**
   * Internal interface used for calculation of {@link Command} progress.
   */
  protected interface CommandProgress {
    /**
     * Complete work in abstract units.
     * 
     * @return long
     */
    long getComplete();

    /**
     * Available work to do in abstract units.
     * 
     * @return long
     */
    long getAvailable();

    /**
     * Number of attempts already tried to process the command. First attempt is
     * 0 and so on. This value can be greater if provider supports retry on
     * error.
     * 
     * @return number of attempts already tried (starting from 0)
     * @see CloudProvider#retryOnProviderError()
     */
    int getAttempts();
  }

  /**
   * Information about {@link CloudDrive} files update progress.
   */
  public interface FilesState {
    /**
     * Collection of file paths currently updating in the drive.
     * 
     * @return collection of {@link String} file paths
     */
    Collection<String> getUpdating();

    /**
     * Return <code>true</code> if file pointed by given ID or local drive path
     * is in process of update (synchronization) with remote cloud.
     * 
     * @param fileIdOrPath {@link String} file ID or local path
     * @return <code>true</code> if file is in process of update
     *         (synchronization)
     */
    boolean isUpdating(String fileIdOrPath);

    /**
     * Return <code>true</code> if file pointed by given ID or local drive path
     * is newly added to the drive but not yet started updating
     * (synchronization) with remote cloud.
     * 
     * @param fileIdOrPath {@link String} file ID or local path
     * @return <code>true</code> if file is a new in the drive and will be
     *         updated soon (via synchronization)
     */
    boolean isNew(String fileIdOrPath);
  }

  /**
   * Helper for listeners firing.
   */
  protected class ListenerSupport {

    /** The registry. */
    Queue<CloudDriveListener> registry = new ConcurrentLinkedQueue<CloudDriveListener>();

    /**
     * Fire on connect.
     *
     * @param event the event
     */
    public void fireOnConnect(CloudDriveEvent event) {
      for (CloudDriveListener listener : registry) {
        try {
          listener.onConnect(event);
        } catch (Throwable th) {
          // nothing should prevent the connect at this point
          LOG.warn("Error firing onConnect listener on Cloud Drive '" + title() + "': " + th.getMessage(), th);
        }
      }
    }

    /**
     * Fire on disconnect.
     *
     * @param event the event
     */
    public void fireOnDisconnect(CloudDriveEvent event) {
      for (CloudDriveListener listener : registry) {
        try {
          listener.onDisconnect(event);
        } catch (Throwable th) {
          // nothing should prevent at this point
          LOG.warn("Error firing onDisconnect listener on Cloud Drive '" + title() + "': " + th.getMessage(), th);
        }
      }
    }

    /**
     * Fire on remove.
     *
     * @param event the event
     */
    public void fireOnRemove(CloudDriveEvent event) {
      for (CloudDriveListener listener : registry) {
        try {
          listener.onRemove(event);
        } catch (Throwable th) {
          // nothing should prevent at this point
          LOG.warn("Error firing onRemove listener on Cloud Drive '" + title() + "': " + th.getMessage(), th);
        }
      }
    }

    /**
     * Fire on synchronized.
     *
     * @param event the event
     */
    public void fireOnSynchronized(CloudDriveEvent event) {
      for (CloudDriveListener listener : registry) {
        try {
          listener.onSynchronized(event);
        } catch (Throwable th) {
          // nothing should prevent at this point
          LOG.warn("Error firing onSynchronized listener on Cloud Drive '" + title() + "': " + th.getMessage(), th);
        }
      }
    }

    /**
     * Fire on error.
     *
     * @param event the event
     * @param error the error
     * @param operationName the operation name
     */
    public void fireOnError(CloudDriveEvent event, Throwable error, String operationName) {
      for (CloudDriveListener listener : registry) {
        try {
          listener.onError(event, error, operationName);
        } catch (Throwable th) {
          // nothing should prevent at this point
          LOG.warn("Error firing onError listener about '" + error.getMessage() + "' during " + operationName
              + " in cloud drive '" + title() + "': " + th.getMessage(), th);
        }
      }
    }

    /**
     * Fire on created event.
     *
     * @param event the event
     */
    public void fireOnCreated(CloudDriveEvent event) {
      for (CloudDriveListener listener : registry) {
        try {
          listener.onCreate(event);
        } catch (Throwable th) {
          // nothing should prevent at this point
          LOG.warn("Error firing onCreated listener on Cloud Drive '" + title() + "': " + th.getMessage(), th);
        }
      }
    }
  }

  // *********** class body ************

  /** The listeners. */
  protected final ListenerSupport listeners = new ListenerSupport();

  /**
   * {@inheritDoc}
   */
  @Override
  public String toString() {
    return title() + " " + super.toString();
  }

  /**
   * Adds the listener.
   *
   * @param listener {@link CloudDriveListener}
   */
  public void addListener(CloudDriveListener listener) {
    if (!listeners.registry.contains(listener)) {
      listeners.registry.add(listener);
    }
  }

  /**
   * Removes the listener.
   *
   * @param listener {@link CloudDriveListener}
   */
  public void removeListener(CloudDriveListener listener) {
    listeners.registry.remove(listener);
  }
  
  /**
   * Return cloud user related to this Cloud Drive.
   * 
   * @return {@link CloudUser}
   */
  public abstract CloudUser getUser();

  /**
   * Cloud Drive title in storage.
   * 
   * @return {@link String} with title
   * @throws DriveRemovedException when drive removed
   * @throws RepositoryException on local storage error
   */
  public abstract String getTitle() throws DriveRemovedException, RepositoryException;

  /**
   * Link to the drive home.
   *
   * @return {@link String}
   * @throws DriveRemovedException when drive removed
   * @throws NotConnectedException when drive disconnected
   * @throws RepositoryException on local storage error
   */
  public abstract String getLink() throws DriveRemovedException, NotConnectedException, RepositoryException;

  /**
   * State object is vendor specific and describe the drive's current state
   * including changes notification and other internal mechanisms. This kind of
   * service optional and may not be supported. If state not supported then this
   * object will be <code>null</code> .
   *
   * @return {@link FilesState} an object instance that can be used to monitor
   *         the drive current state or <code>null</code> if such service not
   *         supported.
   * @throws DriveRemovedException when drive removed
   * @throws RefreshAccessException if an an access to the drive should be
   *           renewed by invoking an authentication and/or authorization of
   *           actual user
   * @throws CloudProviderException an error specific to the drive provider
   *           raised (anything related to connectivity the cloud or processing
   *           response results)
   * @throws RepositoryException on local storage error
   */
  public abstract FilesState getState() throws DriveRemovedException,
                                        RefreshAccessException,
                                        CloudProviderException,
                                        RepositoryException;

  /**
   * Local user related to this Cloud Drive.
   * 
   * @return {@link String}
   * @throws DriveRemovedException when drive removed
   * @throws RepositoryException on local storage error
   */
  public abstract String getLocalUser() throws DriveRemovedException, RepositoryException;

  /**
   * Initialization date of this Cloud Drive.
   *
   * @return {@link Calendar}
   * @throws DriveRemovedException when drive removed
   * @throws RepositoryException on local storage error
   */
  public abstract Calendar getInitDate() throws DriveRemovedException, RepositoryException;

  /**
   * Date of currently established Cloud Drive connection.
   *
   * @return {@link Calendar}
   * @throws DriveRemovedException when drive removed
   * @throws NotConnectedException when drive disconnected
   * @throws RepositoryException on local storage error
   */
  public abstract Calendar getConnectDate() throws DriveRemovedException, NotConnectedException, RepositoryException;

  /**
   * Cloud Drive path in storage. It introduces storage depended identifier of
   * the drive.
   * <p>
   * For JCR storage it's a drive Node path. It can be changed if drive node
   * will be moved.
   *
   * @return String with path in the store.
   * @throws DriveRemovedException when drive removed
   * @throws RepositoryException on local storage error
   */
  public abstract String getPath() throws DriveRemovedException, RepositoryException;

  /**
   * Cloud Drive workspace in storage. It introduces storage depended identifier
   * of the drive.
   * <p>
   * For JCR storage it's a drive Node's session workspace.
   *
   * @return String with path in the store.
   * @throws DriveRemovedException when drive removed
   * @throws RepositoryException on local storage error
   */
  public abstract String getWorkspace() throws DriveRemovedException, RepositoryException;

  /**
   * Unique identifier of the drive. The Id never changes.
   *
   * @return String with id.
   * @throws DriveRemovedException when drive removed
   * @throws NotConnectedException when drive disconnected
   * @throws RepositoryException on local storage error
   */
  public abstract String getId() throws DriveRemovedException, NotConnectedException, RepositoryException;

  /**
   * Return file from local cloud drive.
   * 
   * @param path {@link String} path in JCR
   * @return local cloud file in the drive.
   * @throws DriveRemovedException when drive removed
   * @throws NotCloudDriveException if given path doesn't belong to this cloud
   *           drive
   * @throws NotCloudFileException if given path doesn't belong to this cloud
   *           drive
   * @throws NotYetCloudFileException if file not yet a cloud file in this drive
   *           (e.g. in process of creation)
   * @throws RepositoryException on local storage error
   * @see #hasFile(String)
   */
  public abstract CloudFile getFile(String path) throws DriveRemovedException,
                                                 NotCloudDriveException,
                                                 NotCloudFileException,
                                                 NotYetCloudFileException,
                                                 RepositoryException;

  /**
   * Tells if cloud file with given path exists in this cloud drive. Note that
   * local node existing in a cloud drive, and not yet added to the cloud, will
   * be treated as not a file and <code>false</code> will be returned. <br>
   * This method also works with links to files from the drive.
   * 
   * @param path {@link String} path in JCR
   * @return boolean, {@code true} if given path points to a cloud file in this
   *         drive, {@code false} otherwise
   * @throws DriveRemovedException when drive removed
   * @throws NotCloudDriveException if given paths doesn't belong to drive node
   * @throws RepositoryException on local storage error if storage error
   */
  public abstract boolean hasFile(String path) throws DriveRemovedException, NotCloudDriveException, RepositoryException;

  /**
   * List of files on local cloud drive.
   * 
   * @return collection of local cloud files in the drive.
   * @throws DriveRemovedException when drive removed
   * @throws CloudDriveException drive error
   * @throws RepositoryException on local storage error
   */
  public abstract Collection<CloudFile> listFiles() throws DriveRemovedException, CloudDriveException, RepositoryException;

  /**
   * Connects cloud drive to local JCR storage. This method fetches metadata of
   * remote files from the cloud and adds records them in the local. Optionally
   * it can fetch content of the file but this depends on the cloud drive
   * implementation. <br>
   * To check the state of the connect process use
   * {@link CloudDrive#isConnected()}. To be informed in the process register a
   * listener to the drive {@link CloudDrive#addListener(CloudDriveListener)}.
   * <br>
   * Method returns {@link Command} object what provides information about the
   * connect process such as progress in percents, timing and affected files
   * available during the processing of the command. Connect process will be
   * started asynchronously in another thread and method return immediately.
   *
   * @return {@link Command} describing the connect process
   * @throws CloudDriveException drive error
   * @throws RepositoryException on local storage error
   * @see CloudDriveListener#onConnect(CloudDriveEvent)
   */
  public abstract Command connect() throws CloudDriveException, RepositoryException;

  /**
   * Synchronize local storage with cloud drive. Refreshes metadata (and
   * optionally a content) of the cloud drive.<br>
   * Drive may not support synchronization. In such case
   * {@link SyncNotSupportedException} will be thrown.<br>
   * To check the state of the synchronization register a listener to drive
   * {@link CloudDrive#addListener(CloudDriveListener)}. <br>
   * Method returns {@link Command} object providing information about
   * sychronization progress in percents, timing and affected files available
   * during the processing of the command. Synchronization process will be
   * started asynchronously in another thread and method return immediately.
   * 
   * @return {@link Command} describing the synchronization process
   * @throws SyncNotSupportedException if synchronization not supported
   * @throws DriveRemovedException when drive removed
   * @throws CloudDriveException drive error
   * @throws RepositoryException on local storage error
   * @see CloudDriveListener#onSynchronized(CloudDriveEvent)
   */
  public abstract Command synchronize() throws SyncNotSupportedException,
                                        DriveRemovedException,
                                        CloudDriveException,
                                        RepositoryException;

  /**
   * Answers if drive is connected.
   * 
   * @return boolean, {@code true} if drive connected to local store,
   *         {@code false} otherwise.
   * @throws DriveRemovedException when drive removed
   * @throws RepositoryException on local storage error
   */
  public abstract boolean isConnected() throws DriveRemovedException, RepositoryException;

  /**
   * Tells whether given node instance represents this Cloud Drive. <br>
   * A node represents a cloud drive if it is a root node of the drive storage.
   * <br>
   * This method also works with links to files of the drive.
   * 
   * @param node {@link Node}
   * @return boolean, {@code true} if given node belongs to this Cloud Drive,
   *         {@code false} otherwise.
   * @throws DriveRemovedException when drive removed
   * @throws RepositoryException on local storage error
   */
  public abstract boolean isDrive(Node node) throws DriveRemovedException, RepositoryException;

  // ********** internal stuff **********
  
  /**
   * Initialize the drive after creation, this method should be invoked before first use of the drive.
   *
   * @throws RepositoryException if repository exception
   * @throws DriveRemovedException if the drive was removed
   */
  protected void initCreated() throws RepositoryException, DriveRemovedException {
    listeners.fireOnCreated(new CloudDriveEvent(getUser(), getLocalUser(), getWorkspace(), getPath(), title()));
  }

  /**
   * Tells whether given node instance belongs to this Cloud Drive folder. <br>
   * A node belong to a cloud drive if it represents this drive's storage root
   * node or it represents a file on path of this drive folder.<br>
   * This method also works with links to files of the drive.
   * 
   * @param node {@link Node}
   * @return boolean, {@code true} if given node belongs to this Cloud Drive,
   *         {@code false} otherwise.
   * @throws DriveRemovedException when drive removed
   * @throws RepositoryException on local storage error
   */
  protected abstract boolean isInDrive(Node node) throws DriveRemovedException, RepositoryException;

  /**
   * Tells whether given path belongs to this Cloud Drive. <br>
   * A path belong a cloud drive if it represents this drive's storage root node
   * and the drive connected. If {@code includeFiles} is {@code true} then given
   * path also will be tested whether it is a path of a file in this drive, and
   * if it is, {@code true} will be returned.<br>
   * This method also works with links to files of the drive.
   * 
   * @param workspace {@link String}
   * @param path {@link String}
   * @param includeFiles boolean, if {@code true} then given path also will be
   *          tested as a possible file of this drive and thus {@code true} will
   *          be returned for the file on this drive.
   * @return boolean, {@code true} if given path belongs to this Cloud Drive,
   *         {@code false} otherwise.
   * @throws DriveRemovedException when drive removed if drive removed
   * @throws RepositoryException on local storage error
   */
  protected abstract boolean isDrive(String workspace, String path, boolean includeFiles) throws DriveRemovedException,
                                                                                          RepositoryException;

  /**
   * Disconnects cloud drive from local storage. Clean (remove) metadata of
   * remote files from the local.
   * 
   * @throws DriveRemovedException when drive removed
   * @throws CloudDriveException drive error
   * @throws RepositoryException on local storage error
   */
  protected abstract void disconnect() throws DriveRemovedException, CloudDriveException, RepositoryException;

  /**
   * Refresh access to the cloud provider services using locally stored refresh
   * keys.
   * 
   * @throws CloudDriveException drive or provider error
   */
  protected abstract void refreshAccess() throws CloudDriveException;

  /**
   * Renew access using given user credentials.
   * 
   * @param user {@link CloudUser}
   * @throws CloudDriveException if drive node was removed or cloud provider
   *           error
   * @throws RepositoryException on local storage error if storage error
   */
  protected abstract void updateAccess(CloudUser user) throws CloudDriveException, RepositoryException;

  /**
   * Used internally for logger messages.
   * 
   * @return {@link String} with drive title.
   */
  protected abstract String title();

  /**
   * Configure environment for commands execution (optional).
   *
   * @param env {@link CloudDriveEnvironment}
   * @param synchronizers collection of {@link CloudFileSynchronizer}, it will
   *          be used for file synchronization.
   */
  protected abstract void configure(CloudDriveEnvironment env, Collection<CloudFileSynchronizer> synchronizers);

  /**
   * Initialize future cloud file removal. This operation will complete on
   * parent node save. This method created for use from JCR pre-remove action.
   * 
   * @param file {@link Node} a node representing a file in the drive.
   * @throws SyncNotSupportedException if synchronization not supported
   * @throws CloudDriveException drive error
   * @throws RepositoryException on local storage error
   */
  protected abstract void initRemove(Node file) throws SyncNotSupportedException, CloudDriveException, RepositoryException;

  /**
   * Initialize future cloud file copying.
   * 
   * @param srcNode {@link Node}
   * @param destNode {@link Node}
   * @throws SyncNotSupportedException if synchronization not supported
   * @throws CloudDriveException drive error
   * @throws RepositoryException on local storage error
   */
  protected abstract void initCopy(Node srcNode, Node destNode) throws SyncNotSupportedException,
                                                                CloudDriveException,
                                                                RepositoryException;

}
