/*
 * Copyright 2000-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 com.intellij.util.lang;

import com.intellij.openapi.util.io.FileUtil;
import com.intellij.openapi.util.text.StringUtil;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;

import java.io.*;
import java.net.URL;
import java.util.List;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.atomic.AtomicLong;

class FileLoader extends Loader {
  private final File myRootDir;
  private final String myRootDirAbsolutePath;
  private final boolean myCanHavePersistentIndex;

  FileLoader(URL url, int index, boolean canHavePersistentIndex) throws IOException {
    super(url, index);
    myRootDir = new File(FileUtil.unquote(url.getFile()));
    myRootDirAbsolutePath = myRootDir.getAbsolutePath();
    myCanHavePersistentIndex = canHavePersistentIndex;
  }

  private void buildPackageCache(final File dir, ClasspathCache.LoaderData loaderData) {
    loaderData.addResourceEntry(getRelativeResourcePath(dir));

    final File[] files = dir.listFiles();
    if (files == null) {
      return;
    }

    boolean containsClasses = false;
    for (File file : files) {
      final boolean isClass = file.getPath().endsWith(UrlClassLoader.CLASS_EXTENSION);
      if (isClass) {
        if (!containsClasses) {
          loaderData.addResourceEntry(getRelativeResourcePath(file));
          containsClasses = true;
        }
        loaderData.addNameEntry(file.getName());
      }
      else {
        loaderData.addNameEntry(file.getName());
        buildPackageCache(file, loaderData);
      }
    }
  }

  private String getRelativeResourcePath(final File file) {
    return getRelativeResourcePath(file.getAbsolutePath());
  }

  private String getRelativeResourcePath(final String absFilePath) {
    String relativePath = absFilePath.substring(myRootDirAbsolutePath.length());
    relativePath = relativePath.replace(File.separatorChar, '/');
    relativePath = StringUtil.trimStart(relativePath, "/");
    return relativePath;
  }

  @Override
  @Nullable
  Resource getResource(final String name, boolean check) {
    URL url = null;
    File file = null;

    try {
      url = new URL(getBaseURL(), name);
      if (!url.getFile().startsWith(getBaseURL().getFile())) return null;

      file = new File(myRootDir, name.replace('/', File.separatorChar));
      if (!check || file.exists()) {     // check means we load or process resource so we check its existence via old way
        return new MyResource(url, file, !check);
      }
    }
    catch (Exception exception) {
      if (!check && file != null && file.exists()) {
        try {   // we can not open the file if it is directory, Resource still can be created
          return new MyResource(url, file, false);
        }
        catch (IOException ex) {}
      }
    }
    return null;
  }

  private static final AtomicInteger totalLoaders = new AtomicInteger();
  private static final AtomicLong totalScanning = new AtomicLong();
  private static final AtomicLong totalSaving = new AtomicLong();
  private static final AtomicLong totalReading = new AtomicLong();

  private static final Boolean doFsActivityLogging = false;

  private ClasspathCache.LoaderData tryReadFromIndex() {
    if (!myCanHavePersistentIndex) return null;
    long started = System.nanoTime();
    ClasspathCache.LoaderData loaderData = new ClasspathCache.LoaderData();
    File index = getIndexFileFile();

    BufferedReader reader = null;
    try {
      reader = new BufferedReader(new FileReader(index));
      readList(reader, loaderData.getResourcePaths());
      readList(reader, loaderData.getNames());

      return loaderData;
    } catch (Exception ex) {
      if (!(ex instanceof FileNotFoundException)) index.delete();
    }
    finally {
      if (reader != null) {
        try {
          reader.close();
        } catch (IOException ignore) {}
      }
      totalReading.addAndGet(System.nanoTime() - started);
    }

    return null;
  }

  private static void readList(BufferedReader reader, List<String> paths) throws IOException {
    String line = reader.readLine();
    int numberOfElements = Integer.parseInt(line);
    for(int i = 0; i < numberOfElements; ++i) paths.add(reader.readLine());
  }

  private void trySaveToIndex(ClasspathCache.LoaderData data) {
    if (!myCanHavePersistentIndex) return;
    long started = System.nanoTime();
    File index = getIndexFileFile();
    BufferedWriter writer = null;

    try {
      writer = new BufferedWriter(new FileWriter(index));
      writeList(writer, data.getResourcePaths());
      writeList(writer, data.getNames());
    } catch (IOException ex) {
      index.delete();
    }
    finally {
      if (writer != null) {
        try {
          writer.close();
        } catch (IOException ignore) {}
      }
      totalSaving.addAndGet(System.nanoTime() - started);
    }
  }

  private static void writeList(BufferedWriter writer, List<String> paths) throws IOException {
    writer.append(Integer.toString(paths.size())).append('\n');
    for(String s: paths) writer.append(s).append('\n');
  }

  @NotNull
  private File getIndexFileFile() {
    return new File(myRootDir, "classpath.index");
  }

  @NotNull
  @Override
  public ClasspathCache.LoaderData buildData() throws IOException {
    ClasspathCache.LoaderData fromIndex = tryReadFromIndex();
    final ClasspathCache.LoaderData loaderData = fromIndex != null ? fromIndex : new ClasspathCache.LoaderData();

    final int nsMsFactor = 1000000;
    int currentLoaders = totalLoaders.incrementAndGet();
    long currentScanning;
    if (fromIndex == null) {
      long started = System.nanoTime();
/*    // todo code below uses java 7 api, uncomment once we are done with Java 6
      if (SystemInfo.isJavaVersionAtLeast("1.7") && !SystemProperties.getBooleanProperty("idea.no.nio.class.scanning", false) && false) {
        final Path start = Paths.get(myRootDir.getPath());
        Files.walkFileTree(start, new FileVisitor<Path>() {
          final Stack<Boolean> containsClasses = new Stack<Boolean>();
          @Override
          public FileVisitResult preVisitDirectory(Path dir, BasicFileAttributes attrs) throws IOException {
            containsClasses.push(Boolean.FALSE);
            if (dir != start) loaderData.addNameEntry(dir.getFileName().toString());
            loaderData.addResourceEntry(getRelativeResourcePath(dir.toAbsolutePath().toString()));
            return FileVisitResult.CONTINUE;
          }

          @Override
          public FileVisitResult visitFile(Path file, BasicFileAttributes attrs)
            throws IOException {
            String fileName = file.getFileName().toString();
            final boolean isClass = fileName.endsWith(UrlClassLoader.CLASS_EXTENSION);
            if (isClass) {
              Boolean dirContainClasses = containsClasses.peek();
              if (dirContainClasses == Boolean.FALSE) {
                loaderData.addResourceEntry(getRelativeResourcePath(file.toAbsolutePath().toString()));
                containsClasses.set(containsClasses.size() - 1, Boolean.TRUE);
              }
              loaderData.addNameEntry(fileName);
            }
            else {
              loaderData.addNameEntry(fileName);
              loaderData.addResourceEntry(getRelativeResourcePath(file.toAbsolutePath().toString()));
            }
            return FileVisitResult.CONTINUE;
          }

          @Override
          public FileVisitResult visitFileFailed(Path file, IOException exc) throws IOException {
            throw exc;
          }

          @Override
          public FileVisitResult postVisitDirectory(Path dir, IOException exc) throws IOException {
            containsClasses.pop();
            return FileVisitResult.CONTINUE;
          }
        });
      } else {  */
        buildPackageCache(myRootDir, loaderData);
      /* } */
      final long doneNanos = System.nanoTime() - started;
      currentScanning = totalScanning.addAndGet(doneNanos);
      if (doFsActivityLogging) {
        System.out.println("Scanned: " + myRootDirAbsolutePath + " for " + (doneNanos / nsMsFactor) + "ms");
      }
      trySaveToIndex(loaderData);
    } else {
      currentScanning = totalScanning.get();
    }

    loaderData.addResourceEntry("foo.class");
    loaderData.addResourceEntry("bar.properties");

    if (doFsActivityLogging) {
      System.out.println("Scanning: " + (currentScanning / nsMsFactor) + "ms, saving: " + (totalSaving.get() / nsMsFactor) +
                         "ms, loading:" + (totalReading.get() / nsMsFactor) + "ms for " + currentLoaders + " loaders");
    }

    return loaderData;
  }

  private static class MyResource extends Resource {
    private final URL myUrl;
    private final File myFile;

    public MyResource(URL url, File file, boolean willLoadBytes) throws IOException {
      myUrl = url;
      myFile = file;
      if (willLoadBytes) getInputStream().close(); // check file existence
    }

    @Override
    public URL getURL() {
      return myUrl;
    }

    @Override
    public InputStream getInputStream() throws IOException {
      return new BufferedInputStream(new FileInputStream(myFile));
    }

    @Override
    public byte[] getBytes() throws IOException {
      return FileUtil.loadFileBytes(myFile);
    }
  }

  @Override
  public String toString() {
    return "FileLoader [" + myRootDir + "]";
  }
}
