/*
 * Copyright 2003-2012 JetBrains s.r.o.
 *
 * Licensed 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 jetbrains.mps.extapi.persistence;

import jetbrains.mps.extapi.persistence.datasource.PreinstalledDataSourceTypes;
import org.jetbrains.mps.openapi.persistence.datasource.DataSourceType;
import jetbrains.mps.util.annotation.ToRemove;
import jetbrains.mps.vfs.FileSystemEvent;
import jetbrains.mps.vfs.FileSystemListener;
import jetbrains.mps.vfs.IFile;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import org.jetbrains.mps.openapi.persistence.DataSourceListener;
import org.jetbrains.mps.openapi.persistence.ModelFactory;
import org.jetbrains.mps.openapi.persistence.ModelRoot;
import org.jetbrains.mps.openapi.persistence.MultiStreamDataSource;
import org.jetbrains.mps.openapi.persistence.MultiStreamDataSourceListener;
import org.jetbrains.mps.openapi.util.ProgressMonitor;

import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.Date;
import java.util.HashSet;
import java.util.List;
import java.util.Set;

/**
 * Must be replaced with the FileDataSource everywhere.
 * Additional functionality (like #isIncluded) must be extracted or removed.
 * Remember: it is supposed to be just a simple notion of location with file system for {@link ModelFactory}
 * to load/save/create models there.
 *
 * @author apyshkin
 * evgeny, 11/4/12
 */
@ToRemove(version = 4.0)
public class FolderDataSource extends DataSourceBase implements MultiStreamDataSource, FileSystemListener, FileSystemBasedDataSource {
  private final Object LOCK = new Object();
  private List<DataSourceListener> myListeners = new ArrayList<DataSourceListener>();

  @NotNull
  private final IFile myFolder;
  private final ModelRoot myModelRoot;

  private long myLastAddRemove = -1;

  public FolderDataSource(@NotNull IFile folder) {
    this(folder, null);
  }

  /**
   * @param modelRoot (optional) containing model root, which should be notified before the source during the update
   */
  @ToRemove(version = 3.5)
  @Deprecated
  protected FolderDataSource(@NotNull IFile folder, @Nullable ModelRoot modelRoot) {
    if (folder.exists() && !folder.isDirectory()) {
      throw new IllegalArgumentException("Could not create FolderDataSource with regular file: " + folder);
    }
    this.myFolder = folder;
    this.myModelRoot = modelRoot;
  }

  /**
   * Returns true iff the file potentially contains some model data
   *
   * @return whether file is an actual source file
   */
  public boolean isIncluded(@NotNull IFile file) {
    return myFolder.equals(file.getParent());
  }

  protected Iterable<IFile> getStreams() {
    return myFolder.getChildren();
  }

  protected String getStreamName(IFile file) {
    return file.getName();
  }

  public IFile getFile(String streamName) {
    return myFolder.getDescendant(streamName);
  }

  @NotNull
  public IFile getFolder() {
    return myFolder;
  }

  @Override
  public boolean isReadOnly() {
    return myFolder.isPackaged(); // !!! legacy
  }

  @NotNull
  @Override
  public String getLocation() {
    return myFolder.toString();
  }

  @NotNull
  @Override
  public Iterable<String> getAvailableStreams() {
    Set<String> names = new HashSet<String>();
    for (IFile file : getStreams()) {
      if (isIncluded(file)) {
        names.add(getStreamName(file));
      }
    }
    return names;
  }

  @NotNull
  @Override
  public InputStream openInputStream(String name) throws IOException {
    IFile file = getFile(name);
    if (file == null) {
      throw new IOException("stream is not available");
    }
    return file.openInputStream();
  }

  @NotNull
  @Override
  public OutputStream openOutputStream(String name) throws IOException {
    IFile file = getFile(name);
    if (file == null) {
      throw new IOException("stream is not available");
    }
    return file.openOutputStream();
  }

  @Override
  public boolean delete(String name) {
    IFile file = getFile(name);
    return file != null && file.delete();
  }

  @Override
  public long getTimestamp() {
    long max = myLastAddRemove;
    for (IFile file : getStreams()) {
      if (!isIncluded(file)) {
        continue;
      }

      long timestamp = file.lastModified();
      if (timestamp > max) {
        max = timestamp;
      }
    }
    return max;
  }

  @Override
  public void addListener(@NotNull DataSourceListener listener) {
    synchronized (LOCK) {
      if (myListeners.isEmpty()) {
        myFolder.getFileSystem().addListener(this);
      }
      myListeners.add(listener);
    }
  }

  @Override
  public void removeListener(@NotNull DataSourceListener listener) {
    synchronized (LOCK) {
      myListeners.remove(listener);
      if (myListeners.isEmpty()) {
        myFolder.getFileSystem().removeListener(this);
      }
    }
  }

  @NotNull
  @Override
  public IFile getFileToListen() {
    return myFolder;
  }

  @Override
  public void delete() {
    if (isReadOnly()) {
      return;
    }
    ArrayList<IFile> toDelete = new ArrayList<IFile>();
    for (IFile file : getStreams()) {
      if (isIncluded(file)) {
        toDelete.add(file);
      }
    }
    for (IFile file : toDelete) {
      file.delete();
    }
    myLastAddRemove = -1;
  }

  @Override
  public Iterable<FileSystemListener> getListenerDependencies() {
    if (myModelRoot instanceof FileSystemListener) {
      return Collections.singleton((FileSystemListener) myModelRoot);
    }
    if (myModelRoot != null && myModelRoot.getModule() instanceof FileSystemListener) {
      return Collections.singleton((FileSystemListener) myModelRoot.getModule());
    }
    return null;
  }

  @Override
  public void update(ProgressMonitor monitor, @NotNull FileSystemEvent event) {
    Set<String> affectedStreams = new HashSet<String>();
    for (IFile file : event.getChanged()) {
      if (isIncluded(file)) {
        affectedStreams.add(getStreamName(file));
        break;
      }
    }
    for (IFile file : event.getCreated()) {
      if (isIncluded(file)) {
        affectedStreams.add(getStreamName(file));
        myLastAddRemove = new Date().getTime();
        break;
      }
    }
    for (IFile file : event.getRemoved()) {
      if (isIncluded(file)) {
        affectedStreams.add(getStreamName(file));
        myLastAddRemove = new Date().getTime();
        break;
      }
    }
    fireChanged(monitor, affectedStreams);
  }

  private void fireChanged(ProgressMonitor monitor, Iterable<String> streams) {
    List<DataSourceListener> listeners;
    synchronized (LOCK) {
      listeners = new ArrayList<DataSourceListener>(myListeners);
    }
    monitor.start("Reloading", listeners.size());
    try {
      for (DataSourceListener l : listeners) {
        if (l instanceof MultiStreamDataSourceListener) {
          ((MultiStreamDataSourceListener) l).changed(this, streams);
        } else {
          l.changed(this);
        }
        monitor.advance(1);
      }
    } finally {
      monitor.done();
    }
  }

  @NotNull
  @Override
  public Collection<IFile> getAffectedFiles() {
    return Collections.singleton(myFolder);
  }

  @NotNull
  @Override
  public DataSourceType getType() {
    return PreinstalledDataSourceTypes.FOLDER;
  }
}
