part of '../types.dart';

class TypeGraph {
  final _ResolvedVariables variables = _ResolvedVariables();

  final Map<Typeable, ResolvedType> _knownTypes = {};
  final Map<Typeable, bool> _knownNullability = {};

  final List<TypeRelation> _relations = [];

  final Map<Typeable, List<TypeRelation>> _edges = {};
  final Set<Typeable> _candidateForLaxMultiPropagation = {};
  final List<DefaultType> _defaultTypes = [];

  ResolvedType operator [](Typeable t) {
    final normalized = variables.normalize(t);

    if (_knownTypes.containsKey(normalized)) {
      final type = _knownTypes[normalized];
      final nullability = _knownNullability[normalized];

      if (nullability != null) {
        return type.withNullable(nullability);
      }
      return type;
    }

    return null;
  }

  void operator []=(Typeable t, ResolvedType type) {
    final normalized = variables.normalize(t);
    _knownTypes[normalized] = type;

    if (type.nullable != null && !_knownNullability.containsKey(normalized)) {
      // nullability is known
      _knownNullability[normalized] = type.nullable;
    }
  }

  bool knowsType(Typeable t) => _knownTypes.containsKey(variables.normalize(t));

  void addRelation(TypeRelation relation) {
    _relations.add(relation);
  }

  void markNullability(Typeable t, bool isNullable) {
    _knownNullability[variables.normalize(t)] = isNullable;
  }

  void performResolve() {
    _indexRelations();

    var queue = List.of(_knownTypes.keys);
    while (queue.isNotEmpty) {
      _propagateTypeInfo(queue, queue.removeLast());
    }

    // propagate many-to-one sources where we don't know each source type, but
    // some of them.
    queue = List.of(_candidateForLaxMultiPropagation);
    while (queue.isNotEmpty) {
      _propagateTypeInfo(queue, queue.removeLast(),
          laxMultiSourcePropagation: true);
    }

    // apply default types
    for (final applyDefault in _defaultTypes) {
      final target = applyDefault.target;

      final type = applyDefault.defaultType;
      if (type != null && !knowsType(target)) {
        this[target] = applyDefault.defaultType;
      }

      final nullability = applyDefault.isNullable;
      if (nullability != null && _knownNullability.containsKey(target)) {
        markNullability(target, nullability);
      }
    }
  }

  void _propagateTypeInfo(List<Typeable> resolved, Typeable t,
      {bool laxMultiSourcePropagation = false}) {
    if (!_edges.containsKey(t)) return;

    // propagate changes
    for (final edge in _edges[t]) {
      if (edge is CopyTypeFrom) {
        var type = this[edge.other];
        if (edge.array != null) {
          type = type.toArray(edge.array);
        }
        _copyType(resolved, edge.other, edge.target, type);
      } else if (edge is HaveSameType) {
        _copyType(resolved, t, edge.getOther(t));
      } else if (edge is CopyAndCast) {
        _copyType(resolved, t, edge.target, this[t].cast(edge.cast));
      } else if (edge is MultiSourceRelation) {
        // handle many-to-one changes, if all targets have been resolved or
        // lax handling is enabled.
        if (laxMultiSourcePropagation || edge.from.every(knowsType)) {
          _propagateManyToOne(edge, resolved);

          _candidateForLaxMultiPropagation.removeAll(edge.from);
        } else {
          _candidateForLaxMultiPropagation.add(t);
        }
      }
    }
  }

  void _propagateManyToOne(MultiSourceRelation edge, List<Typeable> resolved) {
    if (edge is CopyEncapsulating) {
      if (!knowsType(edge.target)) {
        final fromTypes = edge.from.map((t) => this[t]).where((e) => e != null);
        var encapsulated = _encapsulate(fromTypes);

        if (encapsulated != null) {
          if (edge.cast != null) {
            encapsulated = encapsulated.cast(edge.cast);
          }
          this[edge.target] = encapsulated;
          resolved.add(edge.target);
        }
      }
    } else if (edge is NullableIfSomeOtherIs &&
        !_knownNullability.containsKey(edge.target)) {
      final nullable = edge.from
          .map((e) => _knownNullability[e])
          .any((nullable) => nullable == true);

      _knownNullability[edge.target] = nullable;
    }
  }

  void _copyType(List<Typeable> resolved, Typeable from, Typeable to,
      [ResolvedType type]) {
    // if the target hasn't been resolved yet, copy the current type and
    // visit the target later
    if (!knowsType(to)) {
      this[to] = type ?? this[from];
      resolved.add(to);
    }
  }

  ResolvedType /*?*/ _encapsulate(Iterable<ResolvedType> targets) {
    return targets.map((e) => e.withoutNullabilityInfo).fold<ResolvedType>(null,
        (previous, element) {
      if (previous == null) return element;

      final previousType = previous.type;
      final elementType = element.type;

      if (previousType == elementType || elementType == BasicType.nullType) {
        return previous;
      }
      if (previousType == BasicType.nullType) return element;

      bool isIntOrNumeric(BasicType type) {
        return type == BasicType.int || type == BasicType.real;
      }

      // encapsulate two different numeric types to real
      if (isIntOrNumeric(previousType) && isIntOrNumeric(elementType)) {
        return const ResolvedType(type: BasicType.real);
      }

      // fallback to text if everything else fails
      return const ResolvedType(type: BasicType.text);
    });
  }

  void _indexRelations() {
    _edges.clear();

    void put(Typeable t, TypeRelation r) {
      _edges.putIfAbsent(t, () => []).add(r);
    }

    void putAll(MultiSourceRelation r) {
      for (final element in r.from) {
        put(element, r);
      }
    }

    for (final relation in _relations) {
      if (relation is NullableIfSomeOtherIs) {
        putAll(relation);
      } else if (relation is CopyTypeFrom) {
        put(relation.other, relation);
      } else if (relation is CopyEncapsulating) {
        putAll(relation);
      } else if (relation is HaveSameType) {
        put(relation.first, relation);
        put(relation.second, relation);
      } else if (relation is DefaultType) {
        _defaultTypes.add(relation);
      } else if (relation is CopyAndCast) {
        put(relation.other, relation);
      } else {
        throw AssertionError('Unknown type relation: $relation');
      }
    }
  }
}

/// Describes how the type of different [Typeable] instances has an effect on
/// others.
///
/// Note that all logic is handled in the type graph, these are logic-less model
/// classes only.
abstract class TypeRelation {}

/// Relation that only has an effect on one [Typeable] -- namely, [target].
abstract class DirectedRelation implements TypeRelation {
  /// The only [Typeable] effected by this relation.
  Typeable get target;
}

/// Relation where the type of multiple [Typeable] instances must be known.
abstract class MultiSourceRelation implements DirectedRelation {
  List<Typeable> get from;
}

/// Keeps track of resolved variable types so that they can be re-used.
/// Different [Variable] instances can refer to the same logical sql variable,
/// so we keep track of them.
class _ResolvedVariables {
  final Map<int, Variable> _referenceForIndex = {};

  Typeable normalize(Typeable t) {
    if (t is! Variable) return t;

    final normalized = t as Variable;
    return _referenceForIndex[normalized.resolvedIndex] ??= normalized;
  }
}

extension ResolvedTypeUtils on ResolvedType {
  ResolvedType cast(CastMode mode) {
    switch (mode) {
      case CastMode.numeric:
      case CastMode.numericPreferInt:
        if (type == BasicType.int || type == BasicType.real) return this;

        return mode == CastMode.numeric
            ? const ResolvedType(type: BasicType.real)
            : const ResolvedType(type: BasicType.int);
      case CastMode.boolean:
        return const ResolvedType.bool();
    }

    throw AssertionError('all switch statements handled');
  }
}
