/**
 * 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.hadoop.hdfs.inotify;

import org.apache.hadoop.classification.InterfaceAudience;
import org.apache.hadoop.classification.InterfaceStability;
import org.apache.hadoop.fs.XAttr;
import org.apache.hadoop.fs.permission.AclEntry;
import org.apache.hadoop.fs.permission.FsPermission;

import java.util.List;
import java.util.Optional;

/**
 * Events sent by the inotify system. Note that no events are necessarily sent
 * when a file is opened for read (although a MetadataUpdateEvent will be sent
 * if the atime is updated).
 */
@InterfaceAudience.Public
@InterfaceStability.Unstable
public abstract class Event {
  public enum EventType {
    CREATE, CLOSE, APPEND, RENAME, METADATA, UNLINK, TRUNCATE
  }

  private EventType eventType;

  public EventType getEventType() {
    return eventType;
  }

  public Event(EventType eventType) {
    this.eventType = eventType;
  }

  /**
   * Sent when a file is closed after append or create.
   */
  @InterfaceAudience.Public
  public static class CloseEvent extends Event {
    private String path;
    private long fileSize;
    private long timestamp;

    public CloseEvent(String path, long fileSize, long timestamp) {
      super(EventType.CLOSE);
      this.path = path;
      this.fileSize = fileSize;
      this.timestamp = timestamp;
    }

    public String getPath() {
      return path;
    }

    /**
     * The size of the closed file in bytes. May be -1 if the size is not
     * available (e.g. in the case of a close generated by a concat operation).
     */
    public long getFileSize() {
      return fileSize;
    }

    /**
     * The time when this event occurred, in milliseconds since the epoch.
     */
    public long getTimestamp() {
      return timestamp;
    }

    @Override
    @InterfaceStability.Unstable
    public String toString() {
      return "CloseEvent [path=" + path + ", fileSize=" + fileSize
          + ", timestamp=" + timestamp + "]";
    }

  }

  /**
   * Sent when a new file is created (including overwrite).
   */
  @InterfaceAudience.Public
  public static class CreateEvent extends Event {

    public enum INodeType {
      FILE, DIRECTORY, SYMLINK
    }

    private INodeType iNodeType;
    private String path;
    private long ctime;
    private int replication;
    private String ownerName;
    private String groupName;
    private FsPermission perms;
    private String symlinkTarget;
    private boolean overwrite;
    private long defaultBlockSize;
    private Optional<Boolean> erasureCoded;

    public static class Builder {
      private INodeType iNodeType;
      private String path;
      private long ctime;
      private int replication;
      private String ownerName;
      private String groupName;
      private FsPermission perms;
      private String symlinkTarget;
      private boolean overwrite;
      private long defaultBlockSize = 0;
      private Optional<Boolean> erasureCoded = Optional.empty();

      public Builder iNodeType(INodeType type) {
        this.iNodeType = type;
        return this;
      }

      public Builder path(String path) {
        this.path = path;
        return this;
      }

      public Builder ctime(long ctime) {
        this.ctime = ctime;
        return this;
      }

      public Builder replication(int replication) {
        this.replication = replication;
        return this;
      }

      public Builder ownerName(String ownerName) {
        this.ownerName = ownerName;
        return this;
      }

      public Builder groupName(String groupName) {
        this.groupName = groupName;
        return this;
      }

      public Builder perms(FsPermission perms) {
        this.perms = perms;
        return this;
      }

      public Builder symlinkTarget(String symlinkTarget) {
        this.symlinkTarget = symlinkTarget;
        return this;
      }

      public Builder overwrite(boolean overwrite) {
        this.overwrite = overwrite;
        return this;
      }

      public Builder defaultBlockSize(long defaultBlockSize) {
        this.defaultBlockSize = defaultBlockSize;
        return this;
      }

      public Builder erasureCoded(boolean ecCoded) {
        this.erasureCoded = Optional.of(ecCoded);
        return this;
      }

      public CreateEvent build() {
        return new CreateEvent(this);
      }
    }

    private CreateEvent(Builder b) {
      super(EventType.CREATE);
      this.iNodeType = b.iNodeType;
      this.path = b.path;
      this.ctime = b.ctime;
      this.replication = b.replication;
      this.ownerName = b.ownerName;
      this.groupName = b.groupName;
      this.perms = b.perms;
      this.symlinkTarget = b.symlinkTarget;
      this.overwrite = b.overwrite;
      this.defaultBlockSize = b.defaultBlockSize;
      this.erasureCoded = b.erasureCoded;
    }

    public INodeType getiNodeType() {
      return iNodeType;
    }

    public String getPath() {
      return path;
    }

    /**
     * Creation time of the file, directory, or symlink.
     */
    public long getCtime() {
      return ctime;
    }

    /**
     * Replication is zero if the CreateEvent iNodeType is directory or symlink.
     */
    public int getReplication() {
      return replication;
    }

    public String getOwnerName() {
      return ownerName;
    }

    public String getGroupName() {
      return groupName;
    }

    public FsPermission getPerms() {
      return perms;
    }

    /**
     * Symlink target is null if the CreateEvent iNodeType is not symlink.
     */
    public String getSymlinkTarget() {
      return symlinkTarget;
    }

    public boolean getOverwrite() {
      return overwrite;
    }

    public long getDefaultBlockSize() {
      return defaultBlockSize;
    }

    public Optional<Boolean> isErasureCoded() {
      return erasureCoded;
    }

    @Override
    @InterfaceStability.Unstable
    public String toString() {
      StringBuilder content = new StringBuilder();
      content.append("CreateEvent [INodeType=").append(iNodeType)
          .append(", path=").append(path)
          .append(", ctime=").append(ctime)
          .append(", replication=").append(replication)
          .append(", ownerName=").append(ownerName)
          .append(", groupName=").append(groupName)
          .append(", perms=").append(perms).append(", ");

      if (symlinkTarget != null) {
        content.append("symlinkTarget=").append(symlinkTarget).append(", ");
      }

      content.append("overwrite=").append(overwrite)
          .append(", defaultBlockSize=").append(defaultBlockSize)
          .append(", erasureCoded=").append(erasureCoded)
          .append("]");
      return content.toString();
    }

  }

  /**
   * Sent when there is an update to directory or file (none of the metadata
   * tracked here applies to symlinks) that is not associated with another
   * inotify event. The tracked metadata includes atime/mtime, replication,
   * owner/group, permissions, ACLs, and XAttributes. Fields not relevant to the
   * metadataType of the MetadataUpdateEvent will be null or will have their default
   * values.
   */
  @InterfaceAudience.Public
  public static class MetadataUpdateEvent extends Event {

    public enum MetadataType {
      TIMES, REPLICATION, OWNER, PERMS, ACLS, XATTRS
    }

    private String path;
    private MetadataType metadataType;
    private long mtime;
    private long atime;
    private int replication;
    private String ownerName;
    private String groupName;
    private FsPermission perms;
    private List<AclEntry> acls;
    private List<XAttr> xAttrs;
    private boolean xAttrsRemoved;

    public static class Builder {
      private String path;
      private MetadataType metadataType;
      private long mtime;
      private long atime;
      private int replication;
      private String ownerName;
      private String groupName;
      private FsPermission perms;
      private List<AclEntry> acls;
      private List<XAttr> xAttrs;
      private boolean xAttrsRemoved;

      public Builder path(String path) {
        this.path = path;
        return this;
      }

      public Builder metadataType(MetadataType type) {
        this.metadataType = type;
        return this;
      }

      public Builder mtime(long mtime) {
        this.mtime = mtime;
        return this;
      }

      public Builder atime(long atime) {
        this.atime = atime;
        return this;
      }

      public Builder replication(int replication) {
        this.replication = replication;
        return this;
      }

      public Builder ownerName(String ownerName) {
        this.ownerName = ownerName;
        return this;
      }

      public Builder groupName(String groupName) {
        this.groupName = groupName;
        return this;
      }

      public Builder perms(FsPermission perms) {
        this.perms = perms;
        return this;
      }

      public Builder acls(List<AclEntry> acls) {
        this.acls = acls;
        return this;
      }

      public Builder xAttrs(List<XAttr> xAttrs) {
        this.xAttrs = xAttrs;
        return this;
      }

      public Builder xAttrsRemoved(boolean xAttrsRemoved) {
        this.xAttrsRemoved = xAttrsRemoved;
        return this;
      }

      public MetadataUpdateEvent build() {
        return new MetadataUpdateEvent(this);
      }
    }

    private MetadataUpdateEvent(Builder b) {
      super(EventType.METADATA);
      this.path = b.path;
      this.metadataType = b.metadataType;
      this.mtime = b.mtime;
      this.atime = b.atime;
      this.replication = b.replication;
      this.ownerName = b.ownerName;
      this.groupName = b.groupName;
      this.perms = b.perms;
      this.acls = b.acls;
      this.xAttrs = b.xAttrs;
      this.xAttrsRemoved = b.xAttrsRemoved;
    }

    public String getPath() {
      return path;
    }

    public MetadataType getMetadataType() {
      return metadataType;
    }

    public long getMtime() {
      return mtime;
    }

    public long getAtime() {
      return atime;
    }

    public int getReplication() {
      return replication;
    }

    public String getOwnerName() {
      return ownerName;
    }

    public String getGroupName() {
      return groupName;
    }

    public FsPermission getPerms() {
      return perms;
    }

    /**
     * The full set of ACLs currently associated with this file or directory.
     * May be null if all ACLs were removed.
     */
    public List<AclEntry> getAcls() {
      return acls;
    }

    public List<XAttr> getxAttrs() {
      return xAttrs;
    }

    /**
     * Whether the xAttrs returned by getxAttrs() were removed (as opposed to
     * added).
     */
    public boolean isxAttrsRemoved() {
      return xAttrsRemoved;
    }

    @Override
    @InterfaceStability.Unstable
    public String toString() {
      StringBuilder content = new StringBuilder();
      content.append("MetadataUpdateEvent [path=").append(path)
          .append(", metadataType=").append(metadataType);
      switch (metadataType) {
      case TIMES:
        content.append(", mtime=").append(mtime)
            .append(", atime=").append(atime);
        break;
      case REPLICATION:
        content.append(", replication=").append(replication);
        break;
      case OWNER:
        content.append(", ownerName=").append(ownerName)
            .append(", groupName=").append(groupName);
        break;
      case PERMS:
        content.append(", perms=").append(perms);
        break;
      case ACLS:
        content.append(", acls=").append(acls);
        break;
      case XATTRS:
        content.append(", xAttrs=").append(xAttrs)
            .append(", xAttrsRemoved=").append(xAttrsRemoved);
        break;
      default:
        break;
      }
      content.append(']');
      return content.toString();
    }
  }

  /**
   * Sent when a file, directory, or symlink is renamed.
   */
  @InterfaceAudience.Public
  public static class RenameEvent extends Event {
    private String srcPath;
    private String dstPath;
    private long timestamp;

    public static class Builder {
      private String srcPath;
      private String dstPath;
      private long timestamp;

      public Builder srcPath(String srcPath) {
        this.srcPath = srcPath;
        return this;
      }

      public Builder dstPath(String dstPath) {
        this.dstPath = dstPath;
        return this;
      }

      public Builder timestamp(long timestamp) {
        this.timestamp = timestamp;
        return this;
      }

      public RenameEvent build() {
        return new RenameEvent(this);
      }
    }

    private RenameEvent(Builder builder) {
      super(EventType.RENAME);
      this.srcPath = builder.srcPath;
      this.dstPath = builder.dstPath;
      this.timestamp = builder.timestamp;
    }

    public String getSrcPath() {
      return srcPath;
    }

    public String getDstPath() {
      return dstPath;
    }

    /**
     * The time when this event occurred, in milliseconds since the epoch.
     */
    public long getTimestamp() {
      return timestamp;
    }

    @Override
    @InterfaceStability.Unstable
    public String toString() {
      return "RenameEvent [srcPath=" + srcPath + ", dstPath=" + dstPath
          + ", timestamp=" + timestamp + "]";
    }

  }

  /**
   * Sent when an existing file is opened for append.
   */
  @InterfaceAudience.Public
  public static class AppendEvent extends Event {
    private String path;
    private boolean newBlock;

    public static class Builder {
      private String path;
      private boolean newBlock;

      public Builder path(String path) {
        this.path = path;
        return this;
      }

      public Builder newBlock(boolean newBlock) {
        this.newBlock = newBlock;
        return this;
      }

      public AppendEvent build() {
        return new AppendEvent(this);
      }
    }

    private AppendEvent(Builder b) {
      super(EventType.APPEND);
      this.path = b.path;
      this.newBlock = b.newBlock;
    }

    public String getPath() {
      return path;
    }

    public boolean toNewBlock() {
      return newBlock;
    }

    @Override
    @InterfaceStability.Unstable
    public String toString() {
      return "AppendEvent [path=" + path + ", newBlock=" + newBlock + "]";
    }

  }

  /**
   * Sent when a file, directory, or symlink is deleted.
   */
  @InterfaceAudience.Public
  public static class UnlinkEvent extends Event {
    private String path;
    private long timestamp;

    public static class Builder {
      private String path;
      private long timestamp;

      public Builder path(String path) {
        this.path = path;
        return this;
      }

      public Builder timestamp(long timestamp) {
        this.timestamp = timestamp;
        return this;
      }

      public UnlinkEvent build() {
        return new UnlinkEvent(this);
      }
    }

    private UnlinkEvent(Builder builder) {
      super(EventType.UNLINK);
      this.path = builder.path;
      this.timestamp = builder.timestamp;
    }

    public String getPath() {
      return path;
    }

    /**
     * The time when this event occurred, in milliseconds since the epoch.
     */
    public long getTimestamp() {
      return timestamp;
    }

    @Override
    @InterfaceStability.Unstable
    public String toString() {
      return "UnlinkEvent [path=" + path + ", timestamp=" + timestamp + "]";
    }
  }

  /**
   * Sent when a file is truncated.
   */
  @InterfaceAudience.Public
  public static class TruncateEvent extends Event {
    private String path;
    private long fileSize;
    private long timestamp;


    public TruncateEvent(String path, long fileSize, long timestamp) {
      super(EventType.TRUNCATE);
      this.path = path;
      this.fileSize = fileSize;
      this.timestamp = timestamp;
    }

    public String getPath() {
      return path;
    }

    /**
     * The size of the truncated file in bytes.
     */
    public long getFileSize() {
      return fileSize;
    }

    /**
     * The time when this event occurred, in milliseconds since the epoch.
     */
    public long getTimestamp() {
      return timestamp;
    }

    @Override
    @InterfaceStability.Unstable
    public String toString() {
      return "TruncateEvent [path=" + path + ", fileSize=" + fileSize
          + ", timestamp=" + timestamp + "]";
    }
  }
}
