/**
 * 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.client.impl;

import java.util.*;

import org.apache.hadoop.thirdparty.com.google.common.primitives.SignedBytes;

import org.apache.hadoop.util.ChunkedArrayList;
import org.apache.hadoop.hdfs.protocol.SnapshotDiffReportListing.DiffReportListingEntry;
import org.apache.hadoop.hdfs.protocol.SnapshotDiffReport;
import org.apache.hadoop.hdfs.protocol.SnapshotDiffReport.DiffReportEntry;
import org.apache.hadoop.hdfs.protocol.SnapshotDiffReport.DiffType;
/**
 * This class represents to end users the difference between two snapshots of
 * the same directory, or the difference between a snapshot of the directory and
 * its current state. Instead of capturing all the details of the diff, this
 * class only lists where the changes happened and their types.
 */
public class SnapshotDiffReportGenerator {
  /**
   * Compare two inodes based on their full names.
   */
  public static final Comparator<DiffReportListingEntry> INODE_COMPARATOR =
      new Comparator<DiffReportListingEntry>() {
        @Override
        public int compare(DiffReportListingEntry left,
            DiffReportListingEntry right) {
          final Comparator<byte[]> cmp =
              SignedBytes.lexicographicalComparator();
          //source path can never be null
          final byte[][] l = left.getSourcePath();
          final byte[][] r = right.getSourcePath();
          if (l.length == 1 && l[0] == null) {
            return -1;
          } else if (r.length == 1 && r[0] == null) {
            return 1;
          } else {
            for (int i = 0; i < l.length && i < r.length; i++) {
              final int diff = cmp.compare(l[i], r[i]);
              if (diff != 0) {
                return diff;
              }
            }
            return l.length == r.length ? 0 : l.length > r.length ? 1 : -1;
          }
        }
      };

  static class RenameEntry {
    private byte[][] sourcePath;
    private byte[][] targetPath;

    void setSource(byte[][] srcPath) {
      this.sourcePath = srcPath;
    }

    void setTarget(byte[][] target) {
      this.targetPath = target;
    }

    boolean isRename() {
      return sourcePath != null && targetPath != null;
    }

    byte[][] getSourcePath() {
      return sourcePath;
    }

    byte[][] getTargetPath() {
      return targetPath;
    }
  }

  /*
   * A class represnting the diff in a directory between two given snapshots
   * in two lists: createdList and deleted list.
   */
  static class ChildrenDiff {
    private final List<DiffReportListingEntry> createdList;
    private final List<DiffReportListingEntry> deletedList;

    ChildrenDiff(List<DiffReportListingEntry> createdList,
        List<DiffReportListingEntry> deletedList) {
      this.createdList = createdList != null ? createdList :
          Collections.emptyList();
      this.deletedList = deletedList != null ? deletedList :
          Collections.emptyList();
    }

    public List<DiffReportListingEntry> getCreatedList() {
      return createdList;
    }

    public List<DiffReportListingEntry> getDeletedList() {
      return deletedList;
    }
  }

  /**
   * snapshot root full path.
   */
  private final String snapshotRoot;

  /**
   * start point of the diff.
   */
  private final String fromSnapshot;

  /**
   * end point of the diff.
   */
  private final String toSnapshot;

  /**
   * Flag to indicate the diff is calculated from older to newer snapshot
   * or not.
   */
  private final boolean isFromEarlier;

  /**
   * A map capturing the detailed difference about file creation/deletion.
   * Each key indicates a directory inode whose children have been changed
   * between the two snapshots, while its associated value is a
   * {@link ChildrenDiff} storing the changes (creation/deletion) happened to
   * the children (files).
   */
  private final Map<Long, ChildrenDiff> dirDiffMap =
      new HashMap<>();

  private final Map<Long, RenameEntry> renameMap =
      new HashMap<>();

  private List<DiffReportListingEntry> mlist = null;
  private List<DiffReportListingEntry> clist = null;
  private List<DiffReportListingEntry> dlist = null;

  public SnapshotDiffReportGenerator(String snapshotRoot, String fromSnapshot,
      String toSnapshot, boolean isFromEarlier,
      List<DiffReportListingEntry> mlist, List<DiffReportListingEntry> clist,
      List<DiffReportListingEntry> dlist) {
    this.snapshotRoot = snapshotRoot;
    this.fromSnapshot = fromSnapshot;
    this.toSnapshot = toSnapshot;
    this.isFromEarlier = isFromEarlier;
    this.mlist =
        mlist != null ? mlist : Collections.emptyList();
    this.clist =
        clist != null ? clist : Collections.emptyList();
    this.dlist =
        dlist != null ? dlist : Collections.emptyList();
  }

  private RenameEntry getEntry(long inodeId) {
    RenameEntry entry = renameMap.get(inodeId);
    if (entry == null) {
      entry = new RenameEntry();
      renameMap.put(inodeId, entry);
    }
    return entry;
  }

  public void generateReportList() {
    mlist.sort(INODE_COMPARATOR);
    for (DiffReportListingEntry created : clist) {
      ChildrenDiff entry = dirDiffMap.get(created.getDirId());
      if (entry == null) {
        List<DiffReportListingEntry> createdList = new ChunkedArrayList<>();
        createdList.add(created);
        ChildrenDiff list = new ChildrenDiff(createdList, null);
        dirDiffMap.put(created.getDirId(), list);
      } else {
        dirDiffMap.get(created.getDirId()).getCreatedList().add(created);
      }
      if (created.isReference()) {
        RenameEntry renameEntry = getEntry(created.getFileId());
        if (renameEntry.getTargetPath() != null) {
          renameEntry.setTarget(created.getSourcePath());
        }
      }
    }
    for (DiffReportListingEntry deleted : dlist) {
      ChildrenDiff entry = dirDiffMap.get(deleted.getDirId());
      if (entry == null || (entry.getDeletedList().isEmpty())) {
        ChildrenDiff list;
        List<DiffReportListingEntry> deletedList = new ChunkedArrayList<>();
        deletedList.add(deleted);
        if (entry == null) {
          list = new ChildrenDiff(null, deletedList);
        } else {
          list = new ChildrenDiff(entry.getCreatedList(), deletedList);
        }
        dirDiffMap.put(deleted.getDirId(), list);
      } else {
        entry.getDeletedList().add(deleted);
      }
      if (deleted.isReference()) {
        RenameEntry renameEntry = getEntry(deleted.getFileId());
        renameEntry.setTarget(deleted.getTargetPath());
        renameEntry.setSource(deleted.getSourcePath());
      }
    }
  }

  public SnapshotDiffReport generateReport() {
    List<DiffReportEntry> diffReportList = new ChunkedArrayList<>();
    generateReportList();
    for (DiffReportListingEntry modified : mlist) {
      diffReportList.add(
          new DiffReportEntry(DiffType.MODIFY, modified.getSourcePath(), null));
      if (modified.isReference()
          && dirDiffMap.get(modified.getDirId()) != null) {
        List<DiffReportEntry> subList = generateReport(modified);
        diffReportList.addAll(subList);
      }
    }
    return new SnapshotDiffReport(snapshotRoot, fromSnapshot, toSnapshot,
        diffReportList);
  }

  private List<DiffReportEntry> generateReport(
      DiffReportListingEntry modified) {
    List<DiffReportEntry> diffReportList = new ChunkedArrayList<>();
    ChildrenDiff list = dirDiffMap.get(modified.getDirId());
    for (DiffReportListingEntry created : list.getCreatedList()) {
      RenameEntry entry = renameMap.get(created.getFileId());
      if (entry == null || !entry.isRename()) {
        diffReportList.add(new DiffReportEntry(
            isFromEarlier ? DiffType.CREATE : DiffType.DELETE,
            created.getSourcePath()));
      }
    }
    for (DiffReportListingEntry deleted : list.getDeletedList()) {
      RenameEntry entry = renameMap.get(deleted.getFileId());
      if (entry != null && entry.isRename()) {
        diffReportList.add(new DiffReportEntry(DiffType.RENAME,
            isFromEarlier ? entry.getSourcePath() : entry.getTargetPath(),
            isFromEarlier ? entry.getTargetPath() : entry.getSourcePath()));
      } else {
        diffReportList.add(new DiffReportEntry(
            isFromEarlier ? DiffType.DELETE : DiffType.CREATE,
            deleted.getSourcePath()));
      }
    }
    return diffReportList;
  }
}
