/*
 * Copyright 2003-2011 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.vfs.impl;

import org.apache.log4j.Logger;
import org.apache.log4j.LogManager;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.mps.openapi.util.Consumer;

import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.util.Collections;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;
import java.util.zip.ZipEntry;
import java.util.zip.ZipFile;

/**
 * This class represents a jar file abstraction
 * It stores cache with all subdirectories and all entries
 */
class JarFileData extends AbstractJarFileData {
  private static Logger LOG = LogManager.getLogger(JarFileData.class);

  private final Object myLock = new Object();
  private boolean isInitialized = false;
  private final ZipFileContainer myZipFileContainer = new ZipFileContainer(); // cleared up in the JarFileDataCache#removeGCedReferences
  private Map<String, Set<String>> myFiles = new HashMap<>();
  private Map<String, Set<String>> mySubDirectories = new HashMap<>();
  private Map<String, ZipEntry> myEntries = new HashMap<>();

  JarFileData(File file) {
    super(file);
  }

  ZipFileContainer getZipFileContainer() {
    return myZipFileContainer;
  }

  @Override
  Set<String> getFiles(String dir) {
    ensureInitialized();

    return Collections.unmodifiableSet(myFiles.get(dir));
  }

  @Override
  Set<String> getSubdirectories(String dir) {
    ensureInitialized();

    return Collections.unmodifiableSet(mySubDirectories.get(dir));
  }

  @Override
  boolean exists(String path) {
    ensureInitialized();

    return (myEntries.get(path) != null) || (mySubDirectories.get(path) != null);
  }

  @Override
  boolean isDirectory(String path) {
    ensureInitialized();

    if (myEntries.get(path) != null) {
      return myEntries.get(path).isDirectory();
    }

    return myFiles.get(path) != null || mySubDirectories.get(path) != null;
  }

  @Override
  String getParentDirectory(String dir) {
    int lastSlash = dir.lastIndexOf("/");
    if (lastSlash == -1) {
      return "";
    }
    return dir.substring(0, lastSlash);
  }

  private Set<String> getDirectoriesFor(String dir) {
    mySubDirectories.putIfAbsent(dir, new HashSet<String>());
    return mySubDirectories.get(dir);
  }

  private Set<String> getFilesFor(String dir) {
    myFiles.putIfAbsent(dir, new HashSet<String>());
    return myFiles.get(dir);
  }

  @Override
  InputStream openStream(String path) throws IOException {
    ensureInitialized();

    ZipEntry entry = myEntries.get(path);
    return new MyInputStream(entry);
  }

  @Override
  long getLength(String path) {
    ensureInitialized();

    return myEntries.get(path).getSize();
  }

  private void ensureInitialized() {
    synchronized (myLock) {
      if (isInitialized) {
        return;
      }

      isInitialized = true;
      myZipFileContainer.zipFile = null;
      try {
        myZipFileContainer.zipFile = new ZipFile(getFile());
        Enumeration<? extends ZipEntry> entries = myZipFileContainer.zipFile.entries();

        while (entries.hasMoreElements()) {
          ZipEntry entry = entries.nextElement();
          if (entry.isDirectory()) {
            String name = entry.getName();
            while (name.endsWith("/")) {
              name = name.substring(0, name.length() - 1);
            }

            buildDirectoryCaches(name);
          } else {
            String name = entry.getName();

            int packEnd = name.lastIndexOf('/');
            String dir;
            String fileName;
            if (packEnd == -1) {
              dir = "";
              fileName = name;
            } else {
              dir = packEnd > 0 ? name.substring(0, packEnd) : name;
              fileName = name.substring(packEnd + 1);
            }

            buildDirectoryCaches(dir);
            getFilesFor(dir).add(fileName);

            if (dir.length() > 0) {
              myEntries.put(dir + "/" + fileName, entry);
            } else {
              myEntries.put(fileName, entry);
            }
          }
        }
      } catch (IOException e) {
        LOG.error(null, e);
      }
    }
  }

  private void buildDirectoryCaches(String dir) {
    String parent = getParentDirectory(dir);

    getDirectoriesFor(dir);
    getFilesFor(dir);

    if (parent.equals(dir)) {
      return;
    }
    getDirectoriesFor(parent).add(dir);
    buildDirectoryCaches(parent);
  }

  // Let's be paranoid and have it non-static, because when the enclosing JarFileData is garbage collected
  // its ZipFile will be closed (see JarFileDataCache)
  // And this way the instance of this class will retain the enclosing instance from becoming garbage
  private class MyInputStream extends InputStream {
    private InputStream stream;

    public MyInputStream(ZipEntry entry) throws IOException {
      stream = myZipFileContainer.zipFile.getInputStream(entry);
    }

    @Override
    public int read() throws IOException {
      return stream.read();
    }

    @Override
    public int read(@NotNull byte[] b) throws IOException {
      return stream.read(b);
    }

    @Override
    public int read(@NotNull byte[] b, int off, int len) throws IOException {
      return stream.read(b, off, len);
    }

    @Override
    public long skip(long n) throws IOException {
      return stream.skip(n);
    }

    @Override
    public int available() throws IOException {
      return stream.available();
    }

    @Override
    public void close() throws IOException {
      try {
        super.close();
      } finally {
        stream.close();
      }
    }

    @Override
    public void mark(int readLimit) {
      stream.mark(readLimit);
    }

    @Override
    public void reset() throws IOException {
      stream.reset();
    }

    @Override
    public boolean markSupported() {
      return stream.markSupported();
    }
  }
}
