// Copyright 2015 The Bazel Authors. All rights reserved.
//
// 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 com.google.devtools.build.lib.skyframe;

import static com.google.devtools.build.lib.skyframe.SkyFunctions.DIRECTORY_LISTING_STATE;
import static com.google.devtools.build.lib.skyframe.SkyFunctions.FILE_STATE;

import com.google.common.base.Objects;
import com.google.common.collect.ImmutableList;
import com.google.devtools.build.lib.skyframe.ExternalFilesHelper.FileType;
import com.google.devtools.build.lib.util.Preconditions;
import com.google.devtools.build.lib.util.io.TimestampGranularityMonitor;
import com.google.devtools.build.lib.vfs.Path;
import com.google.devtools.build.lib.vfs.RootedPath;
import com.google.devtools.build.skyframe.SkyKey;
import com.google.devtools.build.skyframe.SkyValue;

import java.io.IOException;
import java.util.EnumSet;
import java.util.Set;

import javax.annotation.Nullable;

/** Utilities for checking dirtiness of keys (mainly filesystem keys) in the graph. */
public class DirtinessCheckerUtils {
  private DirtinessCheckerUtils() {}

  static class FileDirtinessChecker extends SkyValueDirtinessChecker {
    @Override
    public boolean applies(SkyKey skyKey) {
      return skyKey.functionName().equals(FILE_STATE);
    }

    @Override
    @Nullable
    public SkyValue createNewValue(SkyKey key, @Nullable TimestampGranularityMonitor tsgm) {
      RootedPath rootedPath = (RootedPath) key.argument();
      try {
        return FileStateValue.create(rootedPath, tsgm);
      } catch (InconsistentFilesystemException | IOException e) {
        // TODO(bazel-team): An IOException indicates a failure to get a file digest or a symlink
        // target, not a missing file. Such a failure really shouldn't happen, so failing early
        // may be better here.
        return null;
      }
    }
  }

  static class DirectoryDirtinessChecker extends SkyValueDirtinessChecker {
    @Override
    public boolean applies(SkyKey skyKey) {
      return skyKey.functionName().equals(DIRECTORY_LISTING_STATE);
    }

    @Override
    @Nullable
    public SkyValue createNewValue(SkyKey key, @Nullable TimestampGranularityMonitor tsgm) {
      RootedPath rootedPath = (RootedPath) key.argument();
      try {
        return DirectoryListingStateValue.create(rootedPath);
      } catch (IOException e) {
        return null;
      }
    }
  }

  static class BasicFilesystemDirtinessChecker extends SkyValueDirtinessChecker {
    private final FileDirtinessChecker fdc = new FileDirtinessChecker();
    private final DirectoryDirtinessChecker ddc = new DirectoryDirtinessChecker();
    private final UnionDirtinessChecker checker =
        new UnionDirtinessChecker(ImmutableList.of(fdc, ddc));

    @Override
    public boolean applies(SkyKey skyKey) {
      return fdc.applies(skyKey) || ddc.applies(skyKey);
    }

    @Override
    @Nullable
    public SkyValue createNewValue(SkyKey key, @Nullable TimestampGranularityMonitor tsgm) {
      return checker.createNewValue(key, tsgm);
    }
  }

  static final class MissingDiffDirtinessChecker extends BasicFilesystemDirtinessChecker {
    private final Set<Path> missingDiffPackageRoots;

    MissingDiffDirtinessChecker(final Set<Path> missingDiffPackageRoots) {
      this.missingDiffPackageRoots = missingDiffPackageRoots;
    }

    @Override
    public boolean applies(SkyKey key) {
      return super.applies(key)
          && missingDiffPackageRoots.contains(((RootedPath) key.argument()).getRoot());
    }
  }

  /** Checks files outside of the package roots for changes. */
  static final class ExternalDirtinessChecker extends BasicFilesystemDirtinessChecker {
    private final ExternalFilesHelper externalFilesHelper;
    private final EnumSet<FileType> fileTypesToCheck;

    ExternalDirtinessChecker(ExternalFilesHelper externalFilesHelper,
        EnumSet<FileType> fileTypesToCheck) {
      this.externalFilesHelper = externalFilesHelper;
      this.fileTypesToCheck = fileTypesToCheck;
    }

    @Override
    public boolean applies(SkyKey key) {
      if (!super.applies(key)) {
        return false;
      }
      FileType fileType = externalFilesHelper.getAndNoteFileType((RootedPath) key.argument());
      return fileTypesToCheck.contains(fileType);
    }

    @Nullable
    @Override
    public SkyValue createNewValue(SkyKey key, @Nullable TimestampGranularityMonitor tsgm) {
      throw new UnsupportedOperationException();
    }

    @Override
    public SkyValueDirtinessChecker.DirtyResult check(
        SkyKey skyKey, SkyValue oldValue, @Nullable TimestampGranularityMonitor tsgm) {
      SkyValue newValue = super.createNewValue(skyKey, tsgm);
      if (Objects.equal(newValue, oldValue)) {
        return SkyValueDirtinessChecker.DirtyResult.notDirty(oldValue);
      }
      FileType fileType = externalFilesHelper.getAndNoteFileType((RootedPath) skyKey.argument());
      if (fileType == FileType.EXTERNAL_REPO) {
        // Files under output_base/external have a dependency on the WORKSPACE file, so we don't add
        // a new SkyValue to the graph yet because it might change once the WORKSPACE file has been
        // parsed.
        return SkyValueDirtinessChecker.DirtyResult.dirty(oldValue);
      }
      return SkyValueDirtinessChecker.DirtyResult.dirtyWithNewValue(oldValue, newValue);
    }
  }

  /** {@link SkyValueDirtinessChecker} that encompasses a union of other dirtiness checkers. */
  static final class UnionDirtinessChecker extends SkyValueDirtinessChecker {
    private final Iterable<SkyValueDirtinessChecker> dirtinessCheckers;

    UnionDirtinessChecker(Iterable<SkyValueDirtinessChecker> dirtinessCheckers) {
      this.dirtinessCheckers = dirtinessCheckers;
    }

    @Nullable
    private SkyValueDirtinessChecker getChecker(SkyKey key) {
      for (SkyValueDirtinessChecker dirtinessChecker : dirtinessCheckers) {
        if (dirtinessChecker.applies(key)) {
          return dirtinessChecker;
        }
      }
      return null;
    }

    @Override
    public boolean applies(SkyKey key) {
      return getChecker(key) != null;
    }

    @Override
    @Nullable
    public SkyValue createNewValue(SkyKey key, @Nullable TimestampGranularityMonitor tsgm) {
      return Preconditions.checkNotNull(getChecker(key), key).createNewValue(key, tsgm);
    }

    @Override
    public DirtyResult check(SkyKey key, @Nullable SkyValue oldValue,
        @Nullable TimestampGranularityMonitor tsgm) {
      return Preconditions.checkNotNull(getChecker(key), key).check(key, oldValue, tsgm);
    }
  }
}
