/*
 * Copyright 2000-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 org.jetbrains.jps.javac;

import com.sun.tools.javac.file.BaseFileObject;
import com.sun.tools.javac.file.JavacFileManager;
import com.sun.tools.javac.file.RelativePath;
import com.sun.tools.javac.util.Context;
import com.sun.tools.javac.util.List;
import com.sun.tools.javac.util.ListBuffer;

import javax.lang.model.SourceVersion;
import javax.tools.*;
import java.io.*;
import java.lang.ref.Reference;
import java.lang.ref.SoftReference;
import java.lang.reflect.Field;
import java.net.URI;
import java.net.URISyntaxException;
import java.nio.ByteBuffer;
import java.nio.CharBuffer;
import java.nio.charset.CharsetDecoder;
import java.nio.file.LinkOption;
import java.util.*;

/**
 * WARNING: Loaded via reflection, do not delete
 *
 * @noinspection UnusedDeclaration
 */
class OptimizedFileManager17 extends com.sun.tools.javac.file.JavacFileManager {
  private boolean myUseZipFileIndex;
  private final Map<File, Archive> myArchives;
  private final Map<File, Boolean> myIsFile = new HashMap<File, Boolean>();
  private final Map<File, File[]> myDirectoryCache = new HashMap<File, File[]>();
  public static final File[] NULL_FILE_ARRAY = new File[0];

  private static final String _OS_NAME = System.getProperty("os.name").toLowerCase(Locale.US);
  private static final boolean isWindows = _OS_NAME.startsWith("windows");
  private static final boolean isOS2 = _OS_NAME.startsWith("os/2") || _OS_NAME.startsWith("os2");
  private static final boolean isMac = _OS_NAME.startsWith("mac");
  private static final boolean isFileSystemCaseSensitive = !isWindows && !isOS2 && !isMac;
  private static final boolean ourUseContentCache = Boolean.valueOf(System.getProperty("javac.use.content.cache", "false"));

  OptimizedFileManager17() throws Throwable {
    super(new Context(), true, null);
    final Field archivesField = com.sun.tools.javac.file.JavacFileManager.class.getDeclaredField("archives");
    archivesField.setAccessible(true);
    myArchives = (Map<File, Archive>) archivesField.get(this);
  }

  @Override
  public Iterable<? extends JavaFileObject> getJavaFileObjectsFromFiles(Iterable<? extends File> files) {
    java.util.List<InputFileObject> result;
    if (files instanceof Collection) {
      result = new ArrayList<InputFileObject>(((Collection)files).size());
    }
    else {
      result = new ArrayList<InputFileObject>();
    }
    for (File f: files) {
      result.add(new InputFileObject(this, f));
    }
    return result;
  }

  @Override
  public Iterable<JavaFileObject> list(Location location, String packageName, Set<JavaFileObject.Kind> kinds, boolean recurse) throws IOException {
    Iterable<? extends File> locationRoots = getLocation(location);
    if (locationRoots == null) {
      return List.nil();
    }

    RelativePath.RelativeDirectory subdirectory = new RelativePath.RelativeDirectory(packageName.replace('.', '/'));

    ListBuffer<JavaFileObject> results = new ListBuffer<JavaFileObject>();

    for (File root : locationRoots) {
      Archive archive = myArchives.get(root);

      final boolean isFile;
      if (archive != null) {
        isFile = true;
      }
      else {
        isFile = isFile(root);
      }

      if (isFile) {
        // Not a directory; either a file or non-existant, create the archive
        try {
          if (archive == null) {
            archive = openArchive(root);
          }
          listArchive(archive, subdirectory, kinds, recurse, results);
        }
        catch (IOException ex) {
          log.error("error.reading.file", root, getMessage(ex));
        }
      }
      else {
        final File dir = subdirectory.getFile(root);
        if (recurse) {
          listDirectoryRecursively(dir, kinds, results, true);
        }
        else {
          listDirectory(dir, kinds, results);
        }
      }

    }
    return results.toList();
  }

  private static void listArchive(Archive archive, RelativePath.RelativeDirectory subdirectory, Set<JavaFileObject.Kind> fileKinds, boolean recurse, ListBuffer<JavaFileObject> resultList) {
    // Get the files directly in the subdir
    List<String> files = archive.getFiles(subdirectory);
    if (files != null) {
      for (; !files.isEmpty(); files = files.tail) {
        String file = files.head;
        if (isValidFile(file, fileKinds)) {
          resultList.append(archive.getFileObject(subdirectory, file));
        }
      }
    }
    if (recurse) {
      for (RelativePath.RelativeDirectory s: archive.getSubdirectories()) {
        if (contains(subdirectory, s)) {
          // Because the archive map is a flat list of directories,
          // the enclosing loop will pick up all child subdirectories.
          // Therefore, there is no need to recurse deeper.
          listArchive(archive, s, fileKinds, false, resultList);
        }
      }
    }
  }

  private void listDirectory(File directory, Set<JavaFileObject.Kind> fileKinds, ListBuffer<JavaFileObject> resultList) {
    final File[] files = listChildren(directory);
    if (files != null) {
      if (sortFiles != null) {
        Arrays.sort(files, sortFiles);
      }
      final boolean acceptUnknownFiles = fileKinds.contains(JavaFileObject.Kind.OTHER);
      for (File f: files) {
        final String fileName = f.getName();
        if (isValidFile(fileName, fileKinds)) {
          if (acceptUnknownFiles && !isFile(f)) {
            continue;
          }
          final JavaFileObject fe = new InputFileObject(this, f);
          resultList.append(fe);
        }
      }
    }
  }

  private void listDirectoryRecursively(File file, Set<JavaFileObject.Kind> fileKinds, ListBuffer<JavaFileObject> resultList, boolean isRootCall) {
    final File[] children = listChildren(file);
    final String fileName = file.getName();
    if (children != null) { // is directory
      if (isRootCall || SourceVersion.isIdentifier(fileName)) {
        if (sortFiles != null) {
          Arrays.sort(children, sortFiles);
        }
        for (File child : children) {
          listDirectoryRecursively(child, fileKinds, resultList, false);
        }
      }
    }
    else {
      if (isValidFile(fileName, fileKinds)) {
        JavaFileObject fe = new InputFileObject(this, file);
        resultList.append(fe);
      }
    }
  }

  private File[] listChildren(File file) {
    File[] cached = myDirectoryCache.get(file);
    if (cached == null) {
      cached = file.listFiles();
      myDirectoryCache.put(file, cached != null? cached : NULL_FILE_ARRAY);
    }
    return cached == NULL_FILE_ARRAY ? null : cached;
  }

  // important! called via reflection, so avoid renaming or signature changing or rename carefully
  public void fileGenerated(File file) {
    final File parent = file.getParentFile();
    if (parent != null) {
      myDirectoryCache.remove(parent);
    }
  }

  private boolean isFile(File root) {
    Boolean cachedIsFile = myIsFile.get(root);
    if (cachedIsFile == null) {
      cachedIsFile = Boolean.valueOf(root.isFile());
      myIsFile.put(root, cachedIsFile);
    }
    return cachedIsFile.booleanValue();
  }

  private static boolean contains(RelativePath.RelativeDirectory subdirectory, RelativePath.RelativeDirectory other) {
    final String subdirPath = subdirectory.getPath();
    final String otherPath = other.getPath();
    return otherPath.length() > subdirPath.length() && otherPath.startsWith(subdirPath);
  }

  private static boolean isValidFile(String name, Set<JavaFileObject.Kind> fileKinds) {
    return fileKinds.contains(getKind(name));
  }

  private static class InputFileObject extends BaseFileObject {
    private static final Kind[] ourAvailableKinds = Kind.values();
    private final String name;
    private final File file;
    private final Kind kind;
    private Reference<File> absFileRef;

    InputFileObject(JavacFileManager fileManager, File f) {
      this(fileManager, f.getName(), f);
    }

    InputFileObject(JavacFileManager fileManager, String name, File f) {
      super(fileManager);
      this.name = name;
      this.file = f;
      kind = findKind(name);
    }

    @Override
    public URI toUri() {
      try {
        return convertToURI(file.getPath());
      }
      catch (Throwable e) {
        return file.toURI().normalize(); // fallback
      }
    }

    private static URI convertToURI(String localPath) throws URISyntaxException {
      String p = localPath.replace('\\', '/');
      if (!p.startsWith("/")) {
        p = "/" + p;
      }
      if (!p.startsWith("//")) {
        p = "//" + p;
      }
      return new URI("file", null, p, null);
    }

    @Override
    public String getName() {
      return file.getPath();
    }

    @Override
    public String getShortName() {
      return name;
    }

    @Override
    public JavaFileObject.Kind getKind() {
      return kind;
    }

    private static JavaFileObject.Kind findKind(String name) {
      for (Kind kind : ourAvailableKinds) {
        if (kind != Kind.OTHER && name.endsWith(kind.extension)) {
          return kind;
        }
      }
      return Kind.OTHER;
    }

    @Override
    public InputStream openInputStream() throws IOException {
      return new FileInputStream(file);
    }

    @Override
    public OutputStream openOutputStream() throws IOException {
      throw new UnsupportedOperationException();
    }

    @Override
    public Writer openWriter() throws IOException {
      throw new UnsupportedOperationException();
    }

    @Override
    public long getLastModified() {
      return file.lastModified();
    }

    @Override
    public boolean delete() {
      return file.delete();
    }

    @Override
    protected CharsetDecoder getDecoder(boolean ignoreEncodingErrors) {
      return fileManager.getDecoder(fileManager.getEncodingName(), ignoreEncodingErrors);
    }

    @Override
    protected String inferBinaryName(Iterable<? extends File> path) {
      final String fPath = file.getPath();
      for (File dir: path) {
        String dirPath = dir.getPath();
        if (dirPath.length() == 0) {
          dirPath = System.getProperty("user.dir");
        }
        if (!fPath.regionMatches(!isFileSystemCaseSensitive, 0, dirPath, 0, dirPath.length())) {
          continue;
        }
        final int pathLength = fPath.length();
        final boolean endsWithSeparator = dirPath.endsWith(File.separator);
        if (!endsWithSeparator) {
          // need to check if the next char in fPath is file separator
          final int separatorIdx = dirPath.length();
          if (pathLength <= separatorIdx || fPath.charAt(separatorIdx) != File.separatorChar) {
            continue;
          }
        }
        // fPath starts with dirPath
        final int startIndex = endsWithSeparator ? dirPath.length() : dirPath.length() + 1;
        int endIndex = fPath.lastIndexOf('.');
        if (endIndex <= startIndex) {
          endIndex = fPath.length();
        }
        final int length = endIndex - startIndex;
        final StringBuilder buf = new StringBuilder(length).append(fPath, startIndex, endIndex);
        for (int idx = 0; idx < length; idx++) {
          if (buf.charAt(idx) == File.separatorChar) {
            buf.setCharAt(idx, '.');
          }
        }
        return buf.toString();
      }
      return null;
    }

    @Override
    public boolean isNameCompatible(String cn, JavaFileObject.Kind kind) {
      cn.getClass();
      // null check
      if (kind == Kind.OTHER && getKind() != kind) {
        return false;
      }
      final String n = cn + kind.extension;
      if (name.equals(n)) {
        return true;
      }
      if (name.equalsIgnoreCase(n)) {
        // if we are on a case-insensitive file system,
        // try to compare against the real (exactly as on the disk) file name
        //
        try {
          //noinspection Since15
          return n.equals(file.toPath().toRealPath(LinkOption.NOFOLLOW_LINKS).getFileName().toString());
        }
        catch (IOException ignored) {
        }
      }
      return false;
    }

    /**
     * Check if two file objects are equal.
     * Two RegularFileObjects are equal if the absolute paths of the underlying
     * files are equal.
     */
    @Override
    public boolean equals(Object other) {
      if (this == other)
        return true;

      if (!(other instanceof InputFileObject))
        return false;

      InputFileObject o = (InputFileObject) other;
      return getAbsoluteFile().equals(o.getAbsoluteFile());
    }

    @Override
    public int hashCode() {
      return getAbsoluteFile().hashCode();
    }

    private File getAbsoluteFile() {
      File absFile = (absFileRef == null ? null : absFileRef.get());
      if (absFile == null) {
        absFile = file.getAbsoluteFile();
        absFileRef = new SoftReference<File>(absFile);
      }
      return absFile;
    }

    @Override
    public CharBuffer getCharContent(boolean ignoreEncodingErrors) throws IOException {
      CharBuffer cb = ourUseContentCache? fileManager.getCachedContent(this) : null;
      if (cb == null) {
        InputStream in = new FileInputStream(file);
        try {
          final ByteBuffer bb = fileManager.makeByteBuffer(in);
          final JavaFileObject prev = fileManager.log.useSource(this);
          try {
            cb = fileManager.decode(bb, ignoreEncodingErrors);
          }
          finally {
            fileManager.log.useSource(prev);
          }
          fileManager.recycleByteBuffer(bb);
          if (ourUseContentCache && !ignoreEncodingErrors) {
            fileManager.cache(this, cb);
          }
        }
        finally {
          in.close();
        }
      }
      return cb;
    }
  }

  @Override
  public void close() {
    try {
      super.close();
    }
    finally {
      // archives are cleared in super.close()
      myDirectoryCache.clear();
      myIsFile.clear();
    }
  }
}
