/*
 * Licensed to the Apache Software Foundation (ASF) under one or more
 * contributor license agreements.  See the NOTICE file distributed with
 * this work for additional information regarding copyright ownership.
 * The ASF licenses this file to You 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.apache.gobblin.util.filesystem;

import com.google.common.base.Optional;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.attribute.FileTime;
import java.nio.file.Paths;

import org.apache.hadoop.conf.Configuration;
import org.apache.hadoop.fs.FileStatus;
import org.apache.hadoop.fs.FileSystem;
import org.apache.hadoop.fs.Path;

import lombok.extern.slf4j.Slf4j;


@Slf4j
public class FileStatusEntry extends FileStatus {

  static final FileStatusEntry[] EMPTY_ENTRIES = new FileStatusEntry[0];

  private final FileStatusEntry parent;
  private FileStatusEntry[] children;

  private boolean exists;
  private final FileSystem fs;

  private FileTime changeTime;

  public Optional<FileStatus> _fileStatus;

  public FileStatusEntry(final Path path)
      throws IOException {
    this(null, path, path.getFileSystem(new Configuration()));
  }

  private FileStatusEntry(final FileStatusEntry parent, final Path path, FileSystem fs)
      throws IOException {
    if (path == null) {
      throw new IllegalArgumentException("Path is missing");
    }
    this.parent = parent;
    this.fs = fs;
    this._fileStatus = Optional.fromNullable(this.fs.getFileStatus(path));
    this.changeTime = getChangeTime(path);
  }

  public boolean refresh(final Path path)
      throws IOException {
    if (_fileStatus.isPresent()) {
      Optional<FileStatus> oldStatus = this._fileStatus;
      FileTime oldChangeTime = this.changeTime;
      try {
        this._fileStatus = Optional.of(this.fs.getFileStatus(path));
        this.changeTime = getChangeTime(path);
        this.exists = this._fileStatus.isPresent();

        // using ctime instead of modificationTime since modificationTime is set to start of epoch for a new file
        return (oldStatus.isPresent() != this._fileStatus.isPresent()
            || (oldChangeTime != null && !oldChangeTime.equals(this.changeTime))
            || oldStatus.get().isDirectory() != this._fileStatus.get().isDirectory()
            || oldStatus.get().getLen() != this._fileStatus.get().getLen());
      } catch (FileNotFoundException e) {
        _fileStatus = Optional.absent();
        this.changeTime = null;
        this.exists = false;
        return true;
      }
    } else {
      if (path.getFileSystem(new Configuration()).exists(path)) {
        _fileStatus = Optional.of(this.fs.getFileStatus(path));
        this.changeTime = getChangeTime(path);
        return true;
      } else {
        return false;
      }
    }
  }

  /**
   * Create a new child instance.
   * <p>
   * Custom implementations should override this method to return
   * a new instance of the appropriate type.
   *
   * @param path The child file
   * @return a new child instance
   */
  public FileStatusEntry newChildInstance(final Path path)
      throws IOException {
    return new FileStatusEntry(this, path, this.fs);
  }

  /**
   * Return the parent entry.
   *
   * @return the parent entry
   */
  public FileStatusEntry getParent() {
    return parent;
  }

  /**
   * Return the level
   *
   * @return the level
   */
  public int getLevel() {
    return parent == null ? 0 : parent.getLevel() + 1;
  }

  /**
   * Return the directory's files.
   *
   * @return This directory's files or an empty
   * array if the file is not a directory or the
   * directory is empty
   */
  public FileStatusEntry[] getChildren() {
    return children != null ? children : EMPTY_ENTRIES;
  }

  /**
   * Set the directory's files.
   *
   * @param children This directory's files, may be null
   */
  public void setChildren(final FileStatusEntry[] children) {
    this.children = children;
  }

  /**
   * Indicate whether the file existed the last time it
   * was checked.
   *
   * @return whether the file existed
   */
  public boolean isExists() {
    return exists;
  }

  /**
   * Return the path from the instance FileStatus variable
   * @return
   */
  public Path getPath() {
    return _fileStatus.get().getPath();
  }

  /**
   * Return whether the path is a directory from the instance FileStatus variable.
   * @return
   */
  public boolean isDirectory() {
    return _fileStatus.get().isDirectory();
  }

  /** Compare if this object is equal to another object
   * @param  o the object to be compared.
   * @return true if two file status has the same path name; false if not.
   */
  @Override
  public boolean equals(Object o) {
    if (o == null || this.getClass() != o.getClass()) {
      return false;
    }
    FileStatusEntry other = (FileStatusEntry) o;
    return this._fileStatus.get().equals(other._fileStatus.get());
  }

  /**
   * Returns a hash code value for the object, which is defined as
   * the hash code of the path name.
   *
   * @return a hash code value for the path name.
   */
  @Override
  public int hashCode() {
    return getPath().hashCode();
  }

  private FileTime getChangeTime(Path path) {
    try{
      java.nio.file.Path filePath = Paths.get(path.toUri().getPath());
      return (FileTime) Files.getAttribute(filePath, "unix:ctime");
    }
    catch (Exception e) {
      log.info("Exception occurred while getting ctime for {}", path.toUri().getPath(), e);
      return null;
    }
  }
}
