// Copyright (c) 2015, the Dart project authors.  Please see the AUTHORS file
// for details. All rights reserved. Use of this source code is governed by a
// BSD-style license that can be found in the LICENSE file.

import 'dart:async';
import 'dart:collection';

import 'package:analyzer/exception/exception.dart';
import 'package:analyzer/file_system/file_system.dart';
import 'package:analyzer/src/generated/engine.dart';
import 'package:analyzer/src/generated/source.dart';
import 'package:analyzer/src/generated/utilities_collection.dart';
import 'package:analyzer/src/task/api/model.dart';
import 'package:analyzer/src/task/model.dart';

/**
 * The cache results visiting function type.
 */
typedef void CacheResultVisitor(AnalysisTarget target, ResultData data);

/**
 * Return `true` if the [result] of the [target] should be flushed.
 */
typedef bool FlushResultFilter<V>(
    AnalysisTarget target, ResultDescriptor<V> result);

/**
 * Return `true` if the given [target] is a priority one.
 */
typedef bool IsPriorityAnalysisTarget(AnalysisTarget target);

/**
 * An LRU cache of results produced by analysis.
 */
class AnalysisCache {
  /**
   * A flag used to control whether trace information should be produced when
   * the content of the cache is modified.
   */
  static bool _TRACE_CHANGES = false;

  /**
   * An array containing the partitions of which this cache is comprised.
   */
  final List<CachePartition> _partitions;

  /**
   * The [StreamController] reporting [InvalidatedResult]s.
   */
  final ReentrantSynchronousStream<InvalidatedResult> onResultInvalidated =
      new ReentrantSynchronousStream<InvalidatedResult>();

  final List<ReentrantSynchronousStreamSubscription>
      onResultInvalidatedPartitionSubscriptions =
      <ReentrantSynchronousStreamSubscription>[];

  /**
   * Initialize a newly created cache to have the given [_partitions]. The
   * partitions will be searched in the order in which they appear in the array,
   * so the most specific partition (usually an [SdkCachePartition]) should be
   * first and the most general (usually a [UniversalCachePartition]) last.
   */
  AnalysisCache(this._partitions) {
    for (CachePartition partition in _partitions) {
      partition.containingCaches.add(this);
      ReentrantSynchronousStreamSubscription<InvalidatedResult> subscription =
          partition.onResultInvalidated.listen((InvalidatedResult event) {
        onResultInvalidated.add(event);
      });
      onResultInvalidatedPartitionSubscriptions.add(subscription);
    }
  }

  /**
   * Return an iterator returning all of the [Source] targets.
   */
  Iterable<Source> get sources {
    return _partitions
        .map((CachePartition partition) => partition.sources)
        .expand((Iterable<Source> sources) => sources);
  }

  // TODO(brianwilkerson) Implement or delete this.
//  /**
//   * Return information about each of the partitions in this cache.
//   */
//  List<AnalysisContextStatistics_PartitionData> get partitionData {
//    int count = _partitions.length;
//    List<AnalysisContextStatistics_PartitionData> data =
//        new List<AnalysisContextStatistics_PartitionData>(count);
//    for (int i = 0; i < count; i++) {
//      CachePartition partition = _partitions[i];
//      data[i] = new AnalysisContextStatisticsImpl_PartitionDataImpl(
//          partition.astSize,
//          partition.map.length);
//    }
//    return data;
//  }

  /**
   * Free any allocated resources and references.
   */
  void dispose() {
    for (ReentrantSynchronousStreamSubscription subscription
        in onResultInvalidatedPartitionSubscriptions) {
      subscription.cancel();
    }
    for (CachePartition partition in _partitions) {
      partition.containingCaches.remove(this);
    }
  }

  /**
   * Flush results that satisfy the given [filter].
   */
  void flush(FlushResultFilter filter) {
    for (CachePartition partition in _partitions) {
      partition.flush(filter);
    }
  }

  /**
   * Return the entry associated with the given [target].
   */
  CacheEntry get(AnalysisTarget target) {
    int count = _partitions.length;
    for (int i = 0; i < count; i++) {
      CachePartition partition = _partitions[i];
      if (partition.isResponsibleFor(target)) {
        return partition.get(target);
      }
    }
    //
    // We should never get to this point because the last partition should
    // always be a universal partition, except in the case of the SDK context,
    // in which case the target should always be part of the SDK.
    //
    return null;
  }

  /**
   * Return the context to which the given [target] was explicitly added.
   */
  InternalAnalysisContext getContextFor(AnalysisTarget target) {
    int count = _partitions.length;
    for (int i = 0; i < count; i++) {
      CachePartition partition = _partitions[i];
      if (partition.isResponsibleFor(target)) {
        return partition.context;
      }
    }
    //
    // We should never get to this point because the last partition should
    // always be a universal partition, except in the case of the SDK context,
    // in which case the target should always be part of the SDK.
    //
    // TODO(brianwilkerson) Throw an exception here.
    AnalysisEngine.instance.logger.logInformation(
        'Could not find context for $target',
        new CaughtException(new AnalysisException(), null));
    return null;
  }

  /**
   * Return [Source]s whose full path is equal to the given [path].
   * Maybe empty, but not `null`.
   */
  List<Source> getSourcesWithFullName(String path) {
    List<Source> sources = <Source>[];
    for (CachePartition partition in _partitions) {
      List<Source> partitionSources = partition.getSourcesWithFullName(path);
      sources.addAll(partitionSources);
    }
    return sources;
  }

  /**
   * Return the state of the given [result] for the given [target].
   *
   * It does not update the cache, if the corresponding [CacheEntry] does not
   * exist, then [CacheState.INVALID] is returned.
   */
  CacheState getState(AnalysisTarget target, ResultDescriptor result) {
    CacheEntry entry = get(target);
    if (entry == null) {
      return CacheState.INVALID;
    }
    return entry.getState(result);
  }

  /**
   * Return the value of the given [result] for the given [target].
   *
   * It does not update the cache, if the corresponding [CacheEntry] does not
   * exist, then the default value is returned.
   */
  V getValue<V>(AnalysisTarget target, ResultDescriptor<V> result) {
    CacheEntry entry = get(target);
    if (entry == null) {
      return result.defaultValue;
    }
    return entry.getValue(result);
  }

  /**
   * Return an iterator returning all of the map entries mapping targets to
   * cache entries. If the [context] is not `null`, then only entries that are
   * owned by the given context will be returned.
   */
  MapIterator<AnalysisTarget, CacheEntry> iterator(
      {InternalAnalysisContext context: null}) {
    List<Map<AnalysisTarget, CacheEntry>> maps =
        <Map<AnalysisTarget, CacheEntry>>[];
    for (CachePartition partition in _partitions) {
      if (context == null || partition.context == context) {
        maps.add(partition.entryMap);
      }
    }
    return new MultipleMapIterator<AnalysisTarget, CacheEntry>(maps);
  }

  /**
   * Puts the given [entry] into the cache.
   */
  void put(CacheEntry entry) {
    AnalysisTarget target = entry.target;
    entry.fixExceptionState();
    int count = _partitions.length;
    for (int i = 0; i < count; i++) {
      CachePartition partition = _partitions[i];
      if (partition.isResponsibleFor(target)) {
        if (_TRACE_CHANGES) {
          CacheEntry oldEntry = partition.get(target);
          if (oldEntry == null) {
            AnalysisEngine.instance.logger
                .logInformation('Added a cache entry for $target.');
          } else {
            AnalysisEngine.instance.logger
                .logInformation('Modified the cache entry for $target.');
//                'Diff = ${entry.getDiff(oldEntry)}');
          }
        }
        partition.put(entry);
        return;
      }
    }
    // TODO(brianwilkerson) Handle the case where no partition was found,
    // possibly by throwing an exception.
  }

  /**
   * Remove all information related to the given [target] from this cache.
   * Return the entry associated with the target, or `null` if there was cache
   * entry for the target.
   */
  CacheEntry remove(AnalysisTarget target) {
    int count = _partitions.length;
    for (int i = 0; i < count; i++) {
      CachePartition partition = _partitions[i];
      if (partition.isResponsibleFor(target)) {
        if (_TRACE_CHANGES) {
          AnalysisEngine.instance.logger
              .logInformation('Removed the cache entry for $target.');
        }
        CacheEntry entry = partition.remove(target);
        if (entry != null) {
          entry.dispose();
        }
        return entry;
      }
    }
    return null;
  }

  /**
   * Return the number of targets that are mapped to cache entries.
   */
  int size() {
    int size = 0;
    int count = _partitions.length;
    for (int i = 0; i < count; i++) {
      size += _partitions[i].size();
    }
    return size;
  }
}

/**
 * The information cached by an analysis context about an individual target.
 */
class CacheEntry {
  /**
   * The index of the flag indicating whether the source was explicitly added to
   * the context or whether the source was implicitly added because it was
   * referenced by another source.
   */
  static int _EXPLICITLY_ADDED_FLAG = 0;

  /**
   * The next visit process identifier.
   */
  static int nextVisitId = 0;

  /**
   * A table containing the number of times the value of a result descriptor was
   * recomputed after having been flushed.
   */
  static final Map<ResultDescriptor, int> recomputedCounts =
      new HashMap<ResultDescriptor, int>();

  /**
   * The target this entry is about.
   */
  final AnalysisTarget target;

  /**
   * The partition that is responsible for this entry.
   */
  CachePartition _partition;

  /**
   * The most recent time at which the state of the target matched the state
   * represented by this entry, `-1` if the target does not exist.
   */
  int modificationTime = -1;

  /**
   * The exception that caused one or more values to have a state of
   * [CacheState.ERROR].
   */
  CaughtException _exception;

  /**
   * A bit-encoding of boolean flags associated with this entry's target.
   */
  int _flags = 0;

  /**
   * A table mapping result descriptors to the cached values of those results.
   */
  Map<ResultDescriptor, ResultData> _resultMap =
      new HashMap<ResultDescriptor, ResultData>();

  CacheEntry(this.target);

  /**
   * The exception that caused one or more values to have a state of
   * [CacheState.ERROR].
   */
  CaughtException get exception => _exception;

  /**
   * Return `true` if the source was explicitly added to the context or `false`
   * if the source was implicitly added because it was referenced by another
   * source.
   */
  bool get explicitlyAdded => _getFlag(_EXPLICITLY_ADDED_FLAG);

  /**
   * Set whether the source was explicitly added to the context to match the
   * [explicitlyAdded] flag.
   */
  void set explicitlyAdded(bool explicitlyAdded) {
    _setFlag(_EXPLICITLY_ADDED_FLAG, explicitlyAdded);
  }

  /**
   * Return a list of result descriptors for results whose state is not
   * [CacheState.INVALID].
   */
  List<ResultDescriptor> get nonInvalidResults => _resultMap.keys.toList();

  /**
   * Notifies the entry that the client is going to stop using it.
   */
  void dispose() {
    _resultMap.forEach((ResultDescriptor descriptor, ResultData data) {
      TargetedResult result = new TargetedResult(target, descriptor);
      for (TargetedResult dependedOnResult in data.dependedOnResults) {
        for (AnalysisCache cache in _partition.containingCaches) {
          CacheEntry entry = cache.get(dependedOnResult.target);
          if (entry != null) {
            ResultData data =
                entry.getResultDataOrNull(dependedOnResult.result);
            if (data != null) {
              data.dependentResults.remove(result);
            }
          }
        }
      }
    });
    _resultMap.clear();
  }

  /**
   * Fix the state of the [exception] to match the current state of the entry.
   */
  void fixExceptionState() {
    if (!hasErrorState()) {
      _exception = null;
    }
  }

  /**
   * Flush results that satisfy the given [filter].
   */
  void flush(FlushResultFilter filter) {
    _resultMap.forEach((ResultDescriptor result, ResultData data) {
      if (filter(target, result)) {
        data.flush();
      }
    });
  }

  /**
   * Return the result data associated with the [descriptor], creating one if it
   * isn't there.
   */
  ResultData getResultData(ResultDescriptor descriptor) {
    return _resultMap.putIfAbsent(descriptor, () => new ResultData(descriptor));
  }

  /**
   * Return the result data associated with the [descriptor], or `null` if there
   * is no data currently associated with the descriptor.
   */
  ResultData getResultDataOrNull(ResultDescriptor descriptor) =>
      _resultMap[descriptor];

  /**
   * Return the state of the result represented by the given [descriptor].
   */
  CacheState getState(ResultDescriptor descriptor) {
    ResultData data = _resultMap[descriptor];
    if (data == null) {
      return CacheState.INVALID;
    }
    return data.state;
  }

  /**
   * Return the value of the result represented by the given [descriptor], or
   * the default value for the result if this entry does not have a valid value.
   */
  V getValue<V>(ResultDescriptor<V> descriptor) {
    ResultData data = _resultMap[descriptor];
    if (data == null) {
      return descriptor.defaultValue;
    }
    if (_partition != null) {
      _partition.resultAccessed(target, descriptor);
    }
    return data.value as V;
  }

  /**
   * Return `true` if the state of any data value is [CacheState.ERROR].
   */
  bool hasErrorState() {
    for (ResultData data in _resultMap.values) {
      if (data.state == CacheState.ERROR) {
        return true;
      }
    }
    return false;
  }

  /**
   * Invalidate all of the information associated with this entry's target.
   */
  void invalidateAllInformation() {
    _resultMap.clear();
    _exception = null;
  }

  /**
   * Return `true` if the state of the result represented by the given
   * [descriptor] is [CacheState.INVALID].
   */
  bool isInvalid(ResultDescriptor descriptor) =>
      getState(descriptor) == CacheState.INVALID;

  /**
   * Return `true` if the state of the result represented by the given
   * [descriptor] is [CacheState.VALID].
   */
  bool isValid(ResultDescriptor descriptor) =>
      getState(descriptor) == CacheState.VALID;

  /**
   * For each of the given [descriptors], set their states to
   * [CacheState.ERROR], their values to the corresponding default values, and
   * remember the [exception] that caused this state.
   */
  void setErrorState(
      CaughtException exception, List<ResultDescriptor> descriptors) {
    if (descriptors == null || descriptors.isEmpty) {
      throw new ArgumentError('at least one descriptor is expected');
    }
    if (exception == null) {
      throw new ArgumentError('an exception is expected');
    }
    this._exception = exception;
    for (ResultDescriptor descriptor in descriptors) {
      _setErrorState(descriptor, exception);
    }
  }

  /**
   * Set the state of the result represented by the given [descriptor] to the
   * given [state].
   */
  void setState(ResultDescriptor descriptor, CacheState state, {Delta delta}) {
    if (state == CacheState.ERROR) {
      throw new ArgumentError('use setErrorState() to set the state to ERROR');
    }
    if (state == CacheState.VALID) {
      throw new ArgumentError('use setValue() to set the state to VALID');
    }
    _validateStateChange(descriptor, state);
    if (state == CacheState.INVALID) {
      ResultData data = _resultMap[descriptor];
      if (data != null) {
        bool canUseDelta =
            _gatherResultsInvalidatedByDelta(descriptor, delta, 0);
        if (!canUseDelta) {
          delta = null;
        }
        _invalidate(nextVisitId++, descriptor, delta, 0);
      }
    } else {
      ResultData data = getResultData(descriptor);
      data.state = state;
      if (state != CacheState.IN_PROCESS) {
        //
        // If the state is in-process, we can leave the current value in the
        // cache for any 'get' methods to access.
        //
        data.value = descriptor.defaultValue;
      }
    }
  }

  /**
   * Set the value of the result represented by the given [descriptor] to the
   * given [value].
   */
  void setValue<V>(ResultDescriptor<V> descriptor, V value,
      List<TargetedResult> dependedOn) {
//    {
//      String valueStr = '$value';
//      if (valueStr.length > 20) {
//        valueStr = valueStr.substring(0, 20) + '...';
//      }
//      valueStr = valueStr.replaceAll('\n', '\\n');
//      print(
//          'setValue $descriptor for $target value=$valueStr dependedOn=$dependedOn');
//    }
    _validateStateChange(descriptor, CacheState.VALID);
    TargetedResult thisResult = new TargetedResult(target, descriptor);
    if (_partition != null) {
      _partition.resultStored(thisResult, value);
    }
    ResultData data = getResultData(descriptor);
    _setDependedOnResults(data, thisResult, dependedOn);
    if (data.state == CacheState.FLUSHED) {
      int count = recomputedCounts[descriptor] ?? 0;
      recomputedCounts[descriptor] = count + 1;
    }
    data.state = CacheState.VALID;
    data.value = value ?? descriptor.defaultValue;
  }

  /**
   * If the result represented by the given [descriptor] is valid, set
   * it to the given [value], keep its dependency, and if [invalidateDependent]
   * invalidate all the dependent result.
   */
  void setValueIncremental(
      ResultDescriptor descriptor, dynamic value, bool invalidateDependent) {
    ResultData data = getResultData(descriptor);
    if (data.state == CacheState.VALID || data.state == CacheState.FLUSHED) {
      data.value = value;
    }
    if (invalidateDependent) {
      _invalidateDependentResults(nextVisitId++, data, null, 0);
    }
  }

  @override
  String toString() {
    StringBuffer buffer = new StringBuffer();
    _writeOn(buffer);
    return buffer.toString();
  }

  /**
   * Visit the given [result] and all results that depend on it, and
   * ask [delta] to gather changes. Return `true` if the [delta] can be used
   * to perform limited invalidation, or `false` if the changes collection
   * process does not stop (should not happen).
   */
  bool _gatherResultsInvalidatedByDelta(
      ResultDescriptor result, Delta delta, int level) {
    if (delta == null) {
      return false;
    }
    if (!delta.shouldGatherChanges) {
      return true;
    }
    for (int i = 0; i < 64; i++) {
      bool hasVisitChanges = false;
      _visitResults(nextVisitId++, result,
          (AnalysisTarget target, ResultData data) {
        bool hasDeltaChanges = delta.gatherChanges(
            _partition.context, target, data.descriptor, data.value);
        if (hasDeltaChanges) {
          hasVisitChanges = true;
        }
      });
      delta.gatherEnd();
      if (!hasVisitChanges) {
        return true;
      }
    }
    return false;
  }

  /**
   * Return the value of the flag with the given [index].
   */
  bool _getFlag(int index) => BooleanArray.get(_flags, index);

  /**
   * Invalidate the result represented by the given [descriptor] and propagate
   * invalidation to other results that depend on it.
   */
  void _invalidate(
      int id, ResultDescriptor descriptor, Delta delta, int level) {
    ResultData thisData = _resultMap[descriptor];
    if (thisData == null) {
      return;
    }
    // Stop if already validated.
    if (delta != null) {
      if (thisData.visitId == id) {
        return;
      }
      thisData.visitId = id;
    }
    // Ask the delta to validate.
    DeltaResult deltaResult = null;
    if (delta != null) {
      deltaResult = delta.validate(
          _partition.context, target, descriptor, thisData.value);
      if (deltaResult == DeltaResult.STOP) {
        return;
      }
    }
//    if (deltaResult != null) {
//      String indent = '  ' * level;
//      String deltaResultName = deltaResult.toString().split('.').last;
//      print('[$id]$indent$deltaResultName $descriptor for $target');
//    }
    if (deltaResult == DeltaResult.INVALIDATE_NO_DELTA) {
      delta = null;
    }
    if (deltaResult == DeltaResult.INVALIDATE_KEEP_DEPENDENCIES) {
      thisData.value = descriptor.defaultValue;
      thisData.state = CacheState.INVALID;
    } else if (deltaResult == null ||
        deltaResult == DeltaResult.INVALIDATE ||
        deltaResult == DeltaResult.INVALIDATE_NO_DELTA) {
      _resultMap.remove(descriptor);
      // Stop depending on other results.
      {
        TargetedResult thisResult = new TargetedResult(target, descriptor);
        List<AnalysisCache> caches = _partition.containingCaches;
        int cacheLength = caches.length;
        List<TargetedResult> results = thisData.dependedOnResults;
        int resultLength = results.length;
        for (int i = 0; i < resultLength; i++) {
          TargetedResult dependedOnResult = results[i];
          for (int j = 0; j < cacheLength; j++) {
            AnalysisCache cache = caches[j];
            CacheEntry entry = cache.get(dependedOnResult.target);
            if (entry != null) {
              ResultData data =
                  entry.getResultDataOrNull(dependedOnResult.result);
              if (data != null) {
                data.dependentResults.remove(thisResult);
              }
            }
          }
        }
      }
//      if (deltaResult == null) {
//        String indent = '  ' * level;
//        print('[$id]$indent invalidate $descriptor for $target');
//      }
    }
    // Invalidate results that depend on this result.
    _invalidateDependentResults(id, thisData, delta, level + 1);
    // If empty and not explicitly added, remove the entry altogether.
    if (_resultMap.isEmpty && !explicitlyAdded) {
      CacheEntry entry = _partition.entryMap.remove(target);
      if (entry != null) {
        entry.dispose();
      }
      _partition._removeIfSource(target);
    }
    // Notify controller.
    if (deltaResult != DeltaResult.KEEP_CONTINUE) {
      _partition.onResultInvalidated
          .add(new InvalidatedResult(this, descriptor, thisData.value));
    }
  }

  /**
   * Invalidates all the results of this entry, with propagation.
   */
  void _invalidateAll() {
    List<ResultDescriptor> results = _resultMap.keys.toList();
    int length = results.length;
    for (int i = 0; i < length; i++) {
      ResultDescriptor result = results[i];
      _invalidate(nextVisitId++, result, null, 0);
    }
  }

  /**
   * Invalidate results that depend on [thisData].
   */
  void _invalidateDependentResults(
      int id, ResultData thisData, Delta delta, int level) {
    // It is necessary to copy the results to a list to avoid a concurrent
    // modification of the set of dependent results.
    List<AnalysisCache> caches = _partition.containingCaches;
    int cacheLength = caches.length;
    List<TargetedResult> dependentResults = thisData.dependentResults.toList();
    int resultLength = dependentResults.length;
    for (int i = 0; i < resultLength; i++) {
      TargetedResult dependentResult = dependentResults[i];
      for (int j = 0; j < cacheLength; j++) {
        AnalysisCache cache = caches[j];
        CacheEntry entry = cache.get(dependentResult.target);
        if (entry != null) {
          entry._invalidate(id, dependentResult.result, delta, level);
        }
      }
    }
  }

  /**
   * Set the [dependedOn] on which this result depends.
   */
  void _setDependedOnResults(ResultData thisData, TargetedResult thisResult,
      List<TargetedResult> dependedOn) {
    List<AnalysisCache> caches = _partition.containingCaches;
    int cacheLength = caches.length;

    List<TargetedResult> oldResults = thisData.dependedOnResults;
    int oldLength = oldResults.length;
    for (int i = 0; i < oldLength; i++) {
      TargetedResult dependedOnResult = oldResults[i];
      for (int j = 0; j < cacheLength; j++) {
        AnalysisCache cache = caches[j];
        CacheEntry entry = cache.get(dependedOnResult.target);
        if (entry != null) {
          ResultData data = entry.getResultDataOrNull(dependedOnResult.result);
          if (data != null) {
            data.dependentResults.remove(thisResult);
          }
        }
      }
    }
    thisData.dependedOnResults = dependedOn;
    int newLength = dependedOn.length;
    for (int i = 0; i < newLength; i++) {
      TargetedResult dependedOnResult = dependedOn[i];
      for (int j = 0; j < cacheLength; j++) {
        AnalysisCache cache = caches[j];
        CacheEntry entry = cache.get(dependedOnResult.target);
        if (entry != null) {
          ResultData data = entry.getResultData(dependedOnResult.result);
          data.dependentResults.add(thisResult);
        }
      }
    }
  }

  /**
   * Set states of the given and dependent results to [CacheState.ERROR] and
   * their values to the corresponding default values
   */
  void _setErrorState(ResultDescriptor descriptor, CaughtException exception) {
    ResultData thisData = getResultData(descriptor);
    // Set the error state.
    _exception = exception;
    thisData.state = CacheState.ERROR;
    thisData.value = descriptor.defaultValue;
    // Propagate the error state.
    List<AnalysisCache> caches = _partition.containingCaches;
    int cacheLength = caches.length;
    thisData.dependentResults.forEach((TargetedResult dependentResult) {
      for (int i = 0; i < cacheLength; i++) {
        AnalysisCache cache = caches[i];
        CacheEntry entry = cache.get(dependentResult.target);
        if (entry != null) {
          entry._setErrorState(dependentResult.result, exception);
        }
      }
    });
  }

  /**
   * Set the value of the flag with the given [index] to the given [value].
   */
  void _setFlag(int index, bool value) {
    _flags = BooleanArray.set(_flags, index, value);
  }

  /**
   * If the state of the value described by the given [descriptor] is changing
   * from ERROR to anything else, capture the information. This is an attempt to
   * discover the underlying cause of a long-standing bug.
   */
  void _validateStateChange(ResultDescriptor descriptor, CacheState newState) {
    // TODO(brianwilkerson) Decide whether we still want to capture this data.
//    if (descriptor != CONTENT) {
//      return;
//    }
//    ResultData data = resultMap[CONTENT];
//    if (data != null && data.state == CacheState.ERROR) {
//      String message =
//          'contentState changing from ${data.state} to $newState';
//      InstrumentationBuilder builder =
//          Instrumentation.builder2('CacheEntry-validateStateChange');
//      builder.data3('message', message);
//      //builder.data('source', source.getFullName());
//      builder.record(new CaughtException(new AnalysisException(message), null));
//      builder.log();
//    }
  }

  /**
   * Call [visitor] for the result described by the given [descriptor] and all
   * results that depend on directly or indirectly. Each result is visited
   * only once.
   */
  void _visitResults(
      int id, ResultDescriptor descriptor, CacheResultVisitor visitor) {
    ResultData thisData = _resultMap[descriptor];
    if (thisData == null) {
      return;
    }
    // Stop if already visited.
    if (thisData.visitId == id) {
      return;
    }
    thisData.visitId = id;
    // Visit this result.
    visitor(target, thisData);
    // Visit results that depend on this result.
    List<AnalysisCache> caches = _partition.containingCaches;
    int cacheLength = caches.length;
    List<TargetedResult> dependentResults = thisData.dependentResults.toList();
    int resultLength = dependentResults.length;
    for (int i = 0; i < resultLength; i++) {
      TargetedResult dependentResult = dependentResults[i];
      for (int j = 0; j < cacheLength; j++) {
        AnalysisCache cache = caches[j];
        CacheEntry entry = cache.get(dependentResult.target);
        if (entry != null) {
          entry._visitResults(id, dependentResult.result, visitor);
        }
      }
    }
  }

  /**
   * Write a textual representation of this entry to the given [buffer]. The
   * result should only be used for debugging purposes.
   */
  void _writeOn(StringBuffer buffer) {
    buffer.write('time = ');
    buffer.write(modificationTime);
    List<ResultDescriptor> results = _resultMap.keys.toList();
    results.sort((ResultDescriptor first, ResultDescriptor second) =>
        first.toString().compareTo(second.toString()));
    for (ResultDescriptor result in results) {
      ResultData data = _resultMap[result];
      buffer.write('; ');
      buffer.write(result.toString());
      buffer.write(' = ');
      buffer.write(data.state);
    }
  }
}

/**
 * An object that controls flushing of analysis results from the cache.
 */
class CacheFlushManager<T> {
  final IsPriorityAnalysisTarget isPriorityAnalysisTarget;
  final ResultCachingPolicy policy;
  final int maxActiveSize;
  final int maxIdleSize;

  /**
   * A map of the stored [TargetedResult] to their sizes.
   */
  final HashMap<TargetedResult, int> resultSizeMap =
      new HashMap<TargetedResult, int>();

  /**
   * A linked set containing the most recently accessed results with the most
   * recently used at the end of the list. When more results are added than the
   * maximum size allowed then the least recently used results will be flushed
   * from the cache.
   */
  final LinkedHashSet<TargetedResult> recentlyUsed =
      new LinkedHashSet<TargetedResult>();

  /**
   * The current size of stored results.
   */
  int currentSize = 0;

  /**
   * The current maximum cache size.
   */
  int maxSize;

  CacheFlushManager(ResultCachingPolicy policy, this.isPriorityAnalysisTarget)
      : policy = policy,
        maxActiveSize = policy.maxActiveSize,
        maxIdleSize = policy.maxIdleSize,
        maxSize = policy.maxActiveSize;

  /**
   * If [currentSize] is already less than [maxSize], returns an empty list.
   * Otherwise returns [TargetedResult]s to flush from the cache to make
   * [currentSize] less or equal to [maxSize].
   *
   * Results for priority files are never flushed, so this method might leave
   * [currentSize] greater than [maxSize].
   */
  List<TargetedResult> flushToSize() {
    // If still under the cap, done.
    if (currentSize <= maxSize) {
      return TargetedResult.EMPTY_LIST;
    }
    // Flush results until we are under the cap.
    List<TargetedResult> resultsToFlush = <TargetedResult>[];
    for (TargetedResult result in recentlyUsed) {
      if (isPriorityAnalysisTarget(result.target)) {
        continue;
      }
      resultsToFlush.add(result);
      int size = resultSizeMap.remove(result);
      assert(size != null);
      currentSize -= size;
      if (currentSize <= maxSize) {
        break;
      }
    }
    recentlyUsed.removeAll(resultsToFlush);
    return resultsToFlush;
  }

  /**
   * Notifies this manager that the corresponding analysis context is active.
   */
  void madeActive() {
    maxSize = maxActiveSize;
  }

  /**
   * Notifies this manager that the corresponding analysis context is idle.
   * Returns [TargetedResult]s that should be flushed from the cache.
   */
  List<TargetedResult> madeIdle() {
    maxSize = maxIdleSize;
    return flushToSize();
  }

  /**
   * Records that the given [result] was just read from the cache.
   */
  void resultAccessed(TargetedResult result) {
    if (recentlyUsed.remove(result)) {
      recentlyUsed.add(result);
    }
  }

  /**
   * Records that the given [newResult] and [newValue] were stored to the cache.
   * Returns [TargetedResult]s that should be flushed from the cache.
   */
  List<TargetedResult> resultStored(TargetedResult newResult, T newValue) {
    if (!recentlyUsed.remove(newResult)) {
      int size = policy.measure(newValue);
      resultSizeMap[newResult] = size;
      currentSize += size;
    }
    recentlyUsed.add(newResult);
    return flushToSize();
  }

  /**
   * Records that the given [target] was just removed from to the cache.
   */
  void targetRemoved(AnalysisTarget target) {
    List<TargetedResult> resultsToRemove = <TargetedResult>[];
    for (TargetedResult result in recentlyUsed) {
      if (result.target == target) {
        resultsToRemove.add(result);
        int size = resultSizeMap.remove(result);
        assert(size != null);
        currentSize -= size;
      }
    }
    recentlyUsed.removeAll(resultsToRemove);
  }
}

/**
 * A single partition in an LRU cache of information related to analysis.
 */
abstract class CachePartition {
  /**
   * The context that owns this partition. Multiple contexts can reference a
   * partition, but only one context can own it.
   */
  final InternalAnalysisContext context;

  /**
   * A list of the caches that contain this partition. This includes the cache
   * associated with the context that owns this partition.
   */
  final List<AnalysisCache> containingCaches = <AnalysisCache>[];

  /**
   * A table mapping caching policies to the cache flush managers.
   */
  final HashMap<ResultCachingPolicy, CacheFlushManager> _flushManagerMap =
      new HashMap<ResultCachingPolicy, CacheFlushManager>();

  /**
   * The [StreamController] reporting [InvalidatedResult]s.
   */
  final ReentrantSynchronousStream<InvalidatedResult> onResultInvalidated =
      new ReentrantSynchronousStream<InvalidatedResult>();

  /**
   * A table mapping the targets belonging to this partition to the information
   * known about those targets.
   */
  final HashMap<AnalysisTarget, CacheEntry> entryMap =
      new HashMap<AnalysisTarget, CacheEntry>();

  /**
   * A set of the [Source] targets.
   */
  final HashSet<Source> sources = new HashSet<Source>();

  /**
   * A table mapping full paths to lists of [Source]s with these full paths.
   */
  final Map<String, List<Source>> pathToSource = <String, List<Source>>{};

  /**
   * Initialize a newly created cache partition, belonging to the given
   * [context].
   */
  CachePartition(this.context);

  /**
   * Specify whether a context that uses this partition is being analyzed.
   */
  set isActive(bool active) {
    for (CacheFlushManager manager in _flushManagerMap.values) {
      if (active) {
        manager.madeActive();
      } else {
        List<TargetedResult> resultsToFlush = manager.madeIdle();
        _flushResults(resultsToFlush);
      }
    }
  }

  /**
   * Notifies the partition that the client is going to stop using it.
   */
  void dispose() {
    for (CacheEntry entry in entryMap.values) {
      entry.dispose();
    }
    entryMap.clear();
    sources.clear();
    pathToSource.clear();
  }

  /**
   * Flush results that satisfy the given [filter].
   */
  void flush(FlushResultFilter filter) {
    for (CacheEntry entry in entryMap.values) {
      entry.flush(filter);
    }
  }

  /**
   * Return the entry associated with the given [target].
   */
  CacheEntry get(AnalysisTarget target) => entryMap[target];

  /**
   * Return [Source]s whose full path is equal to the given [path].
   * Maybe empty, but not `null`.
   */
  List<Source> getSourcesWithFullName(String path) {
    List<Source> sources = pathToSource[path];
    return sources ?? Source.EMPTY_LIST;
  }

  /**
   * Return `true` if this partition is responsible for the given [target].
   */
  bool isResponsibleFor(AnalysisTarget target);

  /**
   * Return an iterator returning all of the map entries mapping targets to
   * cache entries.
   */
  MapIterator<AnalysisTarget, CacheEntry> iterator() =>
      new SingleMapIterator<AnalysisTarget, CacheEntry>(entryMap);

  /**
   * Puts the given [entry] into the partition.
   */
  void put(CacheEntry entry) {
    AnalysisTarget target = entry.target;
    if (entry._partition != null) {
      throw new StateError(
          'The entry for $target is already in ${entry._partition}');
    }
    entry._partition = this;
    entry.fixExceptionState();
    entryMap[target] = entry;
    _addIfSource(target);
  }

  /**
   * Remove all information related to the given [target] from this partition.
   * Return the entry associated with the target, or `null` if there was cache
   * entry for the target.
   */
  CacheEntry remove(AnalysisTarget target) {
    for (CacheFlushManager flushManager in _flushManagerMap.values) {
      flushManager.targetRemoved(target);
    }
    CacheEntry entry = entryMap.remove(target);
    if (entry != null) {
      entry._invalidateAll();
    }
    _removeIfSource(target);
    return entry;
  }

  /**
   * Records that a value of the result described by the given [descriptor]
   * for the given [target] was just read from the cache.
   */
  void resultAccessed(AnalysisTarget target, ResultDescriptor descriptor) {
    CacheFlushManager flushManager = _getFlushManager(descriptor);
    TargetedResult result = new TargetedResult(target, descriptor);
    flushManager.resultAccessed(result);
  }

  /**
   * Records that the given [result] was just stored into the cache.
   */
  void resultStored(TargetedResult result, Object value) {
    CacheFlushManager flushManager = _getFlushManager(result.result);
    List<TargetedResult> resultsToFlush =
        flushManager.resultStored(result, value);
    _flushResults(resultsToFlush);
  }

  /**
   * Return the number of targets that are mapped to cache entries.
   */
  int size() => entryMap.length;

  /**
   * If the given [target] is a [Source], adds it to [sources].
   */
  void _addIfSource(AnalysisTarget target) {
    if (target is Source) {
      sources.add(target);
      String fullName = target.fullName;
      pathToSource.putIfAbsent(fullName, () => <Source>[]).add(target);
    }
  }

  /**
   * Flush the given [resultsToFlush].
   */
  void _flushResults(List<TargetedResult> resultsToFlush) {
    for (TargetedResult result in resultsToFlush) {
      CacheEntry entry = get(result.target);
      if (entry != null) {
        ResultData data = entry._resultMap[result.result];
        if (data != null) {
          data.flush();
        }
      }
    }
  }

  /**
   * Return the [CacheFlushManager] for the given [descriptor], not `null`.
   */
  CacheFlushManager _getFlushManager(ResultDescriptor descriptor) {
    ResultCachingPolicy policy = descriptor.cachingPolicy;
    if (identical(policy, DEFAULT_CACHING_POLICY) ||
        context.analysisOptions.disableCacheFlushing) {
      return UnlimitedCacheFlushManager.INSTANCE;
    }
    CacheFlushManager manager = _flushManagerMap[policy];
    if (manager == null) {
      manager = new CacheFlushManager(policy, _isPriorityAnalysisTarget);
      _flushManagerMap[policy] = manager;
    }
    return manager;
  }

  bool _isPriorityAnalysisTarget(AnalysisTarget target) {
    Source source = target.source;
    return source != null && context.prioritySources.contains(source);
  }

  /**
   * If the given [target] is a [Source], remove it from the list of [sources].
   */
  void _removeIfSource(AnalysisTarget target) {
    if (target is Source) {
      sources.remove(target);
      String path = target.fullName;
      List<Source> pathSources = pathToSource[path];
      if (pathSources != null) {
        pathSources.remove(target);
        if (pathSources.isEmpty) {
          pathToSource.remove(path);
        }
      }
    }
  }
}

/**
 * The description for a change.
 */
class Delta {
  final Source source;

  Delta(this.source);

  /**
   * Return `true` if this delta needs cache walking to gather additional
   * changes before it can be used to [validate].  In this case [gatherChanges]
   * is invoked for every targeted result in transitive dependencies, and
   * [gatherEnd] is invoked after cache walking is done.
   */
  bool get shouldGatherChanges => false;

  /**
   * This method is called during a cache walk, so that the delta can gather
   * additional changes to which are caused by the changes it already knows
   * about.  Return `true` if a new change was added, so that one more cache
   * walk will be performed (to include changes that depend on results which we
   * decided to be changed later in the previous cache walk).
   */
  bool gatherChanges(InternalAnalysisContext context, AnalysisTarget target,
      ResultDescriptor descriptor, Object value) {
    return false;
  }

  /**
   * The current cache results visit is done.
   */
  void gatherEnd() {}

  /**
   * Check whether this delta affects the result described by the given
   * [descriptor] and [target]. The current [value] of the result is provided.
   */
  DeltaResult validate(InternalAnalysisContext context, AnalysisTarget target,
      ResultDescriptor descriptor, Object value) {
    return DeltaResult.INVALIDATE;
  }
}

/**
 * The possible results of validating analysis results against a [Delta].
 */
enum DeltaResult {
  /**
   * Invalidate this result and continue visiting dependent results
   * with this [Delta]. Remove the result and all its dependencies.
   */
  INVALIDATE,

  /**
   * Invalidate this result and continue visiting dependent results
   * with this [Delta]. Keep the dependencies of this result.
   */
  INVALIDATE_KEEP_DEPENDENCIES,

  /**
   * Invalidate this result and stop using this [Delta], so unconditionally
   * invalidate all the dependent results.
   */
  INVALIDATE_NO_DELTA,

  /**
   * Keep this result and continue validating dependent results
   * with this [Delta].
   */
  KEEP_CONTINUE,

  /**
   * Keep this result and stop visiting results that depend on this one.
   */
  STOP
}

/**
 * [InvalidatedResult] describes an invalidated result.
 */
class InvalidatedResult<V> {
  /**
   * The target in which the result was invalidated.
   */
  final CacheEntry entry;

  /**
   * The descriptor of the result which was invalidated.
   */
  final ResultDescriptor<V> descriptor;

  /**
   * The value of the result before it was invalidated, may be the default
   * value if the result was flushed.
   */
  final V value;

  InvalidatedResult(this.entry, this.descriptor, this.value);

  @override
  String toString() => '$descriptor of ${entry.target}';
}

/**
 * A cache partition that contains all of the targets in a single package.
 */
class PackageCachePartition extends CachePartition {
  /**
   * The root of the directory representing the package.
   */
  final Folder packageRoot;

  /**
   * Initialize a newly created cache partition, belonging to the given
   * [context].
   */
  PackageCachePartition(InternalAnalysisContext context, this.packageRoot)
      : super(context);

  @override
  bool isResponsibleFor(AnalysisTarget target) {
    Source source = target.source;
    return source != null && packageRoot.contains(source.fullName);
  }
}

/**
 * A Stream-like interface, which broadcasts events synchronously.
 * If a second event is fired while delivering a first event, then the second
 * event will be delivered first, and then delivering of the first will be
 * continued.
 */
class ReentrantSynchronousStream<T> {
  final List<Function> listeners = <Function>[];

  /**
   * Send the given [event] to the stream.
   */
  void add(T event) {
    List<Function> listeners = this.listeners.toList();
    for (Function listener in listeners) {
      listener(event);
    }
  }

  /**
   * Listen for the events in this stream.
   * Note that if the [listener] fires a new event, then the [listener] will be
   * invoked again before returning from the [add] invocation.
   */
  ReentrantSynchronousStreamSubscription<T> listen(void listener(T event)) {
    listeners.add(listener);
    return new ReentrantSynchronousStreamSubscription<T>(this, listener);
  }
}

/**
 * A subscription on events from a [ReentrantSynchronousStream].
 */
class ReentrantSynchronousStreamSubscription<T> {
  final ReentrantSynchronousStream<T> _stream;
  final Function _listener;

  ReentrantSynchronousStreamSubscription(this._stream, this._listener);

  /**
   * Cancels this subscription.
   * It will no longer receive events.
   */
  void cancel() {
    _stream.listeners.remove(_listener);
  }
}

/**
 * The data about a single analysis result that is stored in a [CacheEntry].
 */
// TODO(brianwilkerson) Consider making this a generic class so that the value
// can be typed.
class ResultData {
  /**
   * The [ResultDescriptor] this result is for.
   */
  final ResultDescriptor descriptor;

  /**
   * The state of the cached value.
   */
  CacheState state;

  /**
   * The value being cached, or the default value for the result if there is no
   * value (for example, when the [state] is [CacheState.INVALID]).
   */
  Object value;

  /**
   * The identifier of the most recent visiting process. We use it to visit
   * every result only once.
   */
  int visitId = -1;

  /**
   * A list of the results on which this result depends.
   */
  List<TargetedResult> dependedOnResults = <TargetedResult>[];

  /**
   * A list of the results that depend on this result.
   */
  Set<TargetedResult> dependentResults = new Set<TargetedResult>();

  /**
   * Initialize a newly created result holder to represent the value of data
   * described by the given [descriptor].
   */
  ResultData(this.descriptor) {
    state = CacheState.INVALID;
    value = descriptor.defaultValue;
  }

  /**
   * Flush this value.
   */
  void flush() {
    state = CacheState.FLUSHED;
    value = descriptor.defaultValue;
  }
}

/**
 * A cache partition that contains all of the targets in the SDK.
 */
class SdkCachePartition extends CachePartition {
  /**
   * Initialize a newly created cache partition, belonging to the given
   * [context].
   */
  SdkCachePartition(InternalAnalysisContext context) : super(context);

  @override
  bool isResponsibleFor(AnalysisTarget target) {
    if (target is AnalysisContextTarget) {
      return true;
    }
    Source source = target.source;
    return source != null && source.isInSystemLibrary;
  }
}

/**
 * A cache partition that contains all targets not contained in other partitions.
 */
class UniversalCachePartition extends CachePartition {
  /**
   * Initialize a newly created cache partition, belonging to the given
   * [context].
   */
  UniversalCachePartition(InternalAnalysisContext context) : super(context);

  @override
  bool isResponsibleFor(AnalysisTarget target) => true;
}

/**
 * [CacheFlushManager] that does nothing, results are never flushed.
 */
class UnlimitedCacheFlushManager extends CacheFlushManager {
  static final CacheFlushManager INSTANCE = new UnlimitedCacheFlushManager();

  UnlimitedCacheFlushManager() : super(DEFAULT_CACHING_POLICY, (_) => false);

  @override
  void resultAccessed(TargetedResult result) {}

  @override
  List<TargetedResult> resultStored(TargetedResult newResult, newValue) {
    return TargetedResult.EMPTY_LIST;
  }

  @override
  void targetRemoved(AnalysisTarget target) {}
}
