/*
 * Copyright 2003-2015 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.vfs.CachingFile;
import jetbrains.mps.vfs.CachingFileSystem;
import jetbrains.mps.vfs.FileSystemEvent;
import jetbrains.mps.vfs.FileSystemExtPoint;
import jetbrains.mps.vfs.FileSystemListener;
import jetbrains.mps.vfs.IFile;
import jetbrains.mps.vfs.DefaultCachingContext;
import jetbrains.mps.vfs.openapi.FileSystem;
import jetbrains.mps.vfs.path.Path;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.mps.openapi.persistence.DataSource;
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.util.ProgressMonitor;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.HashSet;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.locks.ReadWriteLock;
import java.util.concurrent.locks.ReentrantReadWriteLock;
import java.util.stream.Collectors;

 /**
 * 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/3/12
 */
public class FolderSetDataSource extends DataSourceBase implements DataSource, FileSystemListener, FileSystemBasedDataSource {
  private final ReadWriteLock myLock = new ReentrantReadWriteLock();
  private final List<DataSourceListener> myListeners = new ArrayList<DataSourceListener>(4);
  private final Map<String, PathListener> myPaths = new LinkedHashMap<String, PathListener>(8);

  private final Set<FileSystemListener> myListenerDependencies = new HashSet<FileSystemListener>(8);

  public FolderSetDataSource() {
  }

  /**
   * @param modelRoot (optional) containing model root, which should be notified before the source during the update
   */
  public void addPath(@NotNull IFile path, ModelRoot modelRoot) {
    myLock.writeLock().lock();
    try {

      if (myPaths.containsKey(path.getPath())) {
        return;
      }

      if (modelRoot instanceof FileSystemListener) {
        myListenerDependencies.add((FileSystemListener) modelRoot);
      } else if (modelRoot != null && modelRoot.getModule() instanceof FileSystemListener) {
        myListenerDependencies.add((FileSystemListener) modelRoot.getModule());
      }

      PathListener listener = new PathListener(path, this);

      myPaths.put(path.getPath(), listener);
      if (!(myListeners.isEmpty())) {
        path.getFileSystem().addListener(listener);
      }
    } finally {
      myLock.writeLock().unlock();
    }
  }

  public Collection<String> getPaths() {
    myLock.readLock().lock();
    try {
      return new ArrayList<>(myPaths.keySet());
    } finally {
      myLock.readLock().unlock();
    }
  }

  private Collection<IFile> getFiles() {
    myLock.readLock().lock();
    try {
      Collection<IFile> rv = new ArrayList<IFile>(myPaths.size());
      for (PathListener l : myPaths.values()) {
        rv.add(l.myFile);
      }
      return rv;
    } finally {
      myLock.readLock().unlock();
    }
  }

  @Override
  public void refresh() {
    FileSystem fs = getFS();
    if (fs instanceof CachingFileSystem) {
      Set<CachingFile> collect = getFiles().stream().filter(file -> file instanceof CachingFile).map(file -> (CachingFile) file).collect(Collectors.toSet());
      ((CachingFileSystem) fs).refresh(new DefaultCachingContext(true, false), collect);
    }
  }

  @Override
  public long getTimestamp() {
    long max = -1;
    Collection<IFile> paths = getFiles();
    for (IFile path : paths) {
      String fsPath = path.getPath();
      //at least some programs don't change timestamp of a directory inside jar file after deleting a file in it
      if (fsPath.contains(Path.ARCHIVE_SEPARATOR)){
        IFile jarFile = path.getFileSystem().getFile(fsPath.substring(0, fsPath.lastIndexOf(Path.ARCHIVE_SEPARATOR)));
        if (jarFile != null){
          max = Math.max(max, jarFile.lastModified());
          continue; // no need to go deep into jar contents
        }
      }
      long ts = getTimestampRecursive(path);
      max = Math.max(max, ts);
    }
    return max;
  }

  @Override
  public boolean isReadOnly() {
    return false;
  }

  @Override
  public void delete() {
    Collection<IFile> toDelete = getFiles();
    for (IFile f : toDelete) {
      f.delete();
    }
  }

  private FileSystem getFS() {
    List<IFile> toRefresh = new ArrayList<>(getFiles());
    if (toRefresh.isEmpty()) return FileSystemExtPoint.getFS();
    return toRefresh.get(0).getFileSystem();
  }

  @NotNull
  @Override
  public String getLocation() {
    return "Folders(" + Arrays.toString(getPaths().toArray()) + ")";
  }

  @Override
  public void addListener(@NotNull DataSourceListener listener) {
    myLock.writeLock().lock();
    try {
      if (myListeners.isEmpty()) {
        for (PathListener pathListener : myPaths.values()) {
          getFS().addListener(pathListener);
        }
      }
      myListeners.add(listener);
    } finally {
      myLock.writeLock().unlock();
    }
  }

  @Override
  public void removeListener(@NotNull DataSourceListener listener) {
    myLock.writeLock().lock();
    try {
      myListeners.remove(listener);
      if (myListeners.isEmpty()) {
        for (PathListener pathListener : myPaths.values()) {
          getFS().removeListener(pathListener);
        }
      }
    } finally {
      myLock.writeLock().unlock();
    }
  }

  private List<DataSourceListener> getDataSourceListeners() {
    List<DataSourceListener> listeners;
    myLock.readLock().lock();
    try {
      listeners = new ArrayList<DataSourceListener>(myListeners);
    } finally {
      myLock.readLock().unlock();
    }
    return listeners;
  }

  @Override
  public IFile getFileToListen() {
    throw new UnsupportedOperationException();
  }

  @Override
  public Iterable<FileSystemListener> getListenerDependencies() {
    myLock.readLock().lock();
    try {
      return new ArrayList<FileSystemListener>(myListenerDependencies);
    } finally {
      myLock.readLock().unlock();
    }
  }

  @Override
  public void update(ProgressMonitor monitor, @NotNull FileSystemEvent event) {
    fireChanged(monitor);
  }

  private void fireChanged(ProgressMonitor monitor) {
    List<DataSourceListener> listeners = getDataSourceListeners();
    monitor.start("Reloading", listeners.size());
    try {
      for (DataSourceListener l : listeners) {
        l.changed(this);
        monitor.advance(1);
      }
    } finally {
      monitor.done();
    }
  }

  private static long getTimestampRecursive(IFile path) {
    long max = path.lastModified();
    if (path.isDirectory()) {
      for (IFile child : path.getChildren()) {
        long timestamp = getTimestampRecursive(child);
        if (timestamp > max) {
          max = timestamp;
        }
      }
    }
    return max;
  }

  @NotNull
  @Override
  public Collection<IFile> getAffectedFiles() {
    return getFiles();
  }

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

  private static class PathListener implements FileSystemListener {
    private final IFile myFile;
    private final FileSystemListener myDelegate;

    private PathListener(@NotNull IFile path, FileSystemListener delegate) {
      myFile = path;
      myDelegate = delegate;
    }

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

    @Override
    public Iterable<FileSystemListener> getListenerDependencies() {
      return myDelegate.getListenerDependencies();
    }

    @Override
    public void update(ProgressMonitor monitor, @NotNull FileSystemEvent event) {
      event.notify(myDelegate);
    }
  }
}
