/*
 * Copyright (C) 2003-2012 eXo Platform SAS.
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU Affero General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * This program 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 Affero General Public License for more details.
 *
 * You should have received a copy of the GNU Affero General Public License
 * along with this program. If not, see <http://www.gnu.org/licenses/>.
 */
package org.exoplatform.clouddrive;

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

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;

/**
 * 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 {

  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 {@link 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 {

    Queue<CloudDriveListener> registry = new ConcurrentLinkedQueue<CloudDriveListener>();

    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);
        }
      }
    }

    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);
        }
      }
    }

    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);
        }
      }
    }

    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);
        }
      }
    }

    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);
        }
      }
    }
  }

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

  protected final ListenerSupport listeners = new ListenerSupport();

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

  public void addListener(CloudDriveListener listener) {
    if (!listeners.registry.contains(listener)) {
      listeners.registry.add(listener);
    }
  }

  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
   */
  public abstract String getTitle() throws DriveRemovedException, RepositoryException;

  /**
   * Link to the drive home.
   * 
   * @return {@link String}
   */
  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.
   */
  public abstract FilesState getState() throws DriveRemovedException,
                                        RefreshAccessException,
                                        CloudProviderException,
                                        RepositoryException;

  /**
   * Local user related to this Cloud Drive.
   * 
   * @return {@link String}
   * @throws RepositoryException
   */
  public abstract String getLocalUser() throws DriveRemovedException, RepositoryException;

  /**
   * Initialization date of this Cloud Drive.
   * 
   * @return {@link Calendar}
   * @throws RepositoryException
   * @throws DriveRemovedException
   */
  public abstract Calendar getInitDate() throws DriveRemovedException, RepositoryException;

  /**
   * Date of currently established Cloud Drive connection.
   * 
   * @return {@link Calendar}
   * @throws RepositoryException
   * @throws DriveRemovedException
   * @throws NotConnectedException
   */
  public abstract Calendar getConnectDate() throws DriveRemovedException,
                                            NotConnectedException,
                                            RepositoryException;

  /**
   * Cloud Drive path in storage. It introduces storage depended identifier of the drive. <br/>
   * 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 RepositoryException
   * @throws DriveRemovedException
   */
  public abstract String getPath() throws DriveRemovedException, RepositoryException;

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

  /**
   * Return file from local cloud drive.
   * 
   * @see #hasFile(String)
   * @return local cloud file in the drive.
   * @throws DriveRemovedException if 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
   */
  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}
   * @return boolean, {@code true} if given path points to a cloud file in this drive, {@code false} otherwise
   * @throws DriveRemovedException if drive removed
   * @throws NotCloudDriveException if given paths doesn't belong to drive node
   * @throws RepositoryException 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 if drive removed
   * @throws CloudDriveException
   * @throws RepositoryException
   */
  public abstract Collection<CloudFile> listFiles() throws DriveRemovedException,
                                                    CloudDriveException,
                                                    RepositoryException;

  /**
   * List of local cloud files on given as parent folder.
   * 
   * @param CloudFile parent folder
   * @return collection of file is a folder, empty list otherwise.
   * @throws DriveRemovedException
   * @throws NotCloudDriveException
   * @throws CloudDriveException
   * @throws RepositoryException
   */
  @Deprecated
  public abstract Collection<CloudFile> listFiles(CloudFile parent) throws DriveRemovedException,
                                                                    NotCloudDriveException,
                                                                    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
   * @see CloudDriveListener#onConnect(CloudDriveEvent)
   * @throws CloudDriveException
   * @throws RepositoryException
   */
  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.
   * 
   * @see CloudDriveListener#onSynchronized(CloudDriveEvent)
   * @see CloudDriveListener#getFileChangeAction()
   * @return {@link Command} describing the synchronization process
   * @throws SyncNotSupportedException if synchronization not supported
   * @throws DriveRemovedException
   * @throws CloudDriveException
   * @throws RepositoryException
   */
  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
   * @throws RepositoryException
   */
  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
   * @throws RepositoryException
   */
  public abstract boolean isDrive(Node node) throws DriveRemovedException, RepositoryException;

  // ********** internal stuff **********

  /**
   * 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
   * @throws RepositoryException
   */
  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 if drive removed
   * @throws RepositoryException
   */
  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
   * @throws CloudDriveException
   * @throws RepositoryException
   */
  protected abstract void disconnect() throws DriveRemovedException, CloudDriveException, RepositoryException;

  /**
   * Refresh access to the cloud provider services using locally stored refresh keys.
   * 
   * @throws CloudDriveException if cloud 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 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
   * @throws CloudDriveException
   * @throws RepositoryException
   */
  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
   * @throws CloudDriveException
   * @throws RepositoryException
   */
  protected abstract void initCopy(Node srcNode, Node destNode) throws SyncNotSupportedException,
                                                                CloudDriveException,
                                                                RepositoryException;
}
