/**
   * Insert the specified file groups into a newly created merge file group.
   *
   * @param subFileGroups The sub file groups to be added.
   * @param filesNeeded An array of files that are actually needed by the action that uses them.
   * @return The ID of the newly-created merge file group.
   */
  private int createMergeFileGroup(List<Integer> subFileGroups, Integer[] filesNeeded) {
    /*
     * Create the new fileGroup. Even though we won't populate it until the multiOp
     * is executed, we must still allocate a new fileGroup ID number.
     */
    int fileGroupId = fileGroupMgr.newMergeGroup(destPkgId);
    if (fileGroupId < 0) {
      throw new FatalError("Unable to create new merge file group");
    }

    /*
     * Determine whether any filters are required (since we don't necessarily need all
     * the files that appear in the subFileGroups.
     */
    List<Integer> filteredSubFileGroups = new ArrayList<Integer>();
    for (int subFileGroup : subFileGroups) {
      filteredSubFileGroups.add(createFilterIfNeeded(subFileGroup, filesNeeded));
    }

    FileGroupUndoOp op = new FileGroupUndoOp(buildStore, fileGroupId);
    op.recordMembershipChange(new ArrayList<Integer>(), filteredSubFileGroups);
    multiOp.add(op);

    return fileGroupId;
  }
  /**
   * @param inputFileGroupId The group ID we're planning to attach to an action's input.
   * @param filesNeeded The files that are actually required by the action.
   * @return If all files in the fileGroup are required, return inputFileGroupId, else return a
   *     filterID (which depends on inputFileGroupId) that restricts the input to only those files
   *     that are required.
   */
  private int createFilterIfNeeded(int inputFileGroupId, Integer[] filesNeeded) {

    /*
     * Look through all of the files in the file group, and learn which of them are needed.
     * This is an O(n*n), but for relatively small file groups (not 1000s of items), this is OK.
     */
    List<Integer> fileGroupMembers = fileGroupCache.get(inputFileGroupId);
    if (fileGroupMembers == null) {
      throw new FatalError("Can't find members for fileGroup with ID: " + inputFileGroupId);
    }
    List<Integer> neededFileGroupMembers = new ArrayList<Integer>();
    for (int pathId : fileGroupMembers) {
      for (int i = 0; i < filesNeeded.length; i++) {
        if (pathId == filesNeeded[i]) {
          neededFileGroupMembers.add(pathId);
          break;
        }
      }
    }

    /*
     * If the set of needed members is smaller than the set of total members, we must add
     * a filter so that only the required files are kept.
     */
    if (neededFileGroupMembers.size() < fileGroupMembers.size()) {
      int filterGroupId = fileGroupMgr.newFilterGroup(destPkgId, inputFileGroupId);
      if (filterGroupId < 0) {
        throw new FatalError("Unable to create new filter file group");
      }

      /*
       * Populate the filter with patterns that match the needed paths. Note that
       * the file is not (yet) in the destination package, so we just pretend its
       * there.
       */
      List<String> neededPatterns = new ArrayList<String>();
      for (int pathId : neededFileGroupMembers) {
        String pathString = fileMgr.getPathName(pathId, destPkgId);
        if (pathString == null) {
          throw new FatalError("Invalid pathId in file group");
        }
        neededPatterns.add("ia:" + pathString);
      }

      /* schedule the population of the filter group with patterns */
      FileGroupUndoOp op = new FileGroupUndoOp(buildStore, filterGroupId);
      op.recordMembershipChange(new ArrayList<String>(), neededPatterns);
      multiOp.add(op);

      return filterGroupId;
    }

    /* else, no filter required - return the original file group */
    else {

      return inputFileGroupId;
    }
  }
  /**
   * Given a list of file IDs, create a new source file group and schedule the members to be added
   * to it (by appending to the multiOp). In addition to moving the file group into the destination
   * package, all the individual files are also moved. If any files are not within the source root,
   * throw a {@link CanNotRefactorException} with cause code of PATH_OUT_OF_RANGE.
   *
   * @param members A list of file IDs to be added to the file group.
   * @return The ID of the newly-created file group.
   * @throws CanNotRefactorException Something went wrong during the refactoring.
   */
  private int createSourceFileGroup(List<Integer> members) throws CanNotRefactorException {

    /*
     * Create the new fileGroup. Even though we won't populate it until the multiOp
     * is executed, we must still allocate a new fileGroup ID number.
     */
    int fileGroupId = fileGroupMgr.newSourceGroup(destPkgId);
    if (fileGroupId < 0) {
      throw new FatalError("Unable to create new file group");
    }
    FileGroupUndoOp op = new FileGroupUndoOp(buildStore, fileGroupId);
    op.recordMembershipChange(new ArrayList<Integer>(), members);
    multiOp.add(op);

    /*
     * Move all the files into the destination package, using FileUndoOps. Before
     * a file can be moved, we must ensure that it's within the source root of the package.
     */
    List<Integer> filesOutOfRange = new ArrayList<Integer>();

    /*
     * For each loose file, validate if it's within the package roots, and if so,
     * schedule an UndoOp to make the necessary change. If not, throw an exception.
     */
    for (int pathId : members) {
      PackageDesc oldDesc = pkgMemberMgr.getPackageOfMember(IPackageMemberMgr.TYPE_FILE, pathId);
      if (oldDesc == null) {
        throw new FatalError("Can't find pathId");
      }

      /*
       * Check that this path is within the source root. If so, schedule it to be move to
       * the destination package.
       */
      if (fileMgr.isAncestorOf(pkgRootId, pathId)) {
        FileUndoOp pkgChangeOp = new FileUndoOp(buildStore, pathId);
        pkgChangeOp.recordChangePackage(
            oldDesc.pkgId, oldDesc.pkgScopeId, destPkgId, IPackageMemberMgr.SCOPE_PRIVATE);
        multiOp.add(pkgChangeOp);
      }

      /*
       * Else, record this pathID as being out of range. We'll report an exception
       * once we've collected the complete list of invalid paths.
       */
      else {
        filesOutOfRange.add(pathId);
      }
    }

    /*
     * If any files were out of range, throw an exception.
     */
    if (filesOutOfRange.size() > 0) {
      throw new CanNotRefactorException(
          Cause.PATH_OUT_OF_RANGE, filesOutOfRange.toArray(new Integer[0]));
    }

    /* update the cache, with the members that the file group will contain (once the multiOp is executed) */
    fileGroupCache.put(fileGroupId, members);

    return fileGroupId;
  }
  /**
   * Validate the list of members that is input into moveMembersToPackage(). There are many possible
   * errors, including invalid member types, or undefined action, file, or fileGroup ID numbers.
   * Errors are report via exceptions.
   *
   * @param members The list of MemberDesc to be validated.
   * @throws CanNotRefactorException The reason why the members list is invalid.
   */
  private void validateMembersList(List<MemberDesc> members) throws CanNotRefactorException {

    IFileMgr fileMgr = buildStore.getFileMgr();
    IFileGroupMgr fileGroupMgr = buildStore.getFileGroupMgr();
    IActionMgr actionMgr = buildStore.getActionMgr();

    /* can't be null! */
    if (members == null) {
      throw new CanNotRefactorException(Cause.INVALID_MEMBER, -1);
    }

    /* keep a lists of invalid "things" - we can provide the whole list in the exception */
    List<Integer> invalidFiles = new ArrayList<Integer>();
    List<Integer> invalidFileGroups = new ArrayList<Integer>();
    List<Integer> invalidActions = new ArrayList<Integer>();

    /*
     * Scan the list of members, validating their type, and whether each ID is valid.
     * Invalid entries are logged, and will be reported as exceptions once we've seen
     * all the members.
     */
    for (MemberDesc member : members) {
      int id = member.memberId;
      switch (member.memberType) {
        case IPackageMemberMgr.TYPE_FILE:
          PathType pathType = fileMgr.getPathType(id);
          if (pathType == PathType.TYPE_INVALID) {
            invalidFiles.add(id);
          }
          break;

        case IPackageMemberMgr.TYPE_FILE_GROUP:
          if (fileGroupMgr.getGroupType(id) == ErrorCode.NOT_FOUND) {
            invalidFileGroups.add(id);
          }
          break;

        case IPackageMemberMgr.TYPE_ACTION:
          if (!actionMgr.isActionValid(id)) {
            invalidActions.add(id);
          }
          break;

        default:
          throw new CanNotRefactorException(Cause.INVALID_MEMBER, member.memberType);
      }
    }

    /* Thrown exceptions if we found anything invalid */
    if (!invalidFiles.isEmpty()) {
      throw new CanNotRefactorException(Cause.INVALID_PATH, invalidFiles.toArray(new Integer[0]));
    }
    if (!invalidActions.isEmpty()) {
      throw new CanNotRefactorException(
          Cause.INVALID_ACTION, invalidActions.toArray(new Integer[0]));
    }
    if (!invalidFileGroups.isEmpty()) {
      throw new CanNotRefactorException(
          Cause.INVALID_FILE_GROUP, invalidFileGroups.toArray(new Integer[0]));
    }
  }