// Copyright 2016 Workiva Inc.
//
// 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.

library over_react.pretty_print;

/// Returns a pretty-printed version of [map], with namespaced keys grouped together.
///
/// Useful for debugging props/state maps (and build in to [UiProps.toString]/[UiState.toString]).
///
/// __Only for use when debugging.__
///
/// Example:
///
///     var map = {'foo': 'bar'};
///     Foo(map)
///       ..bar = 1
///       ..baz = 2;
///     Bar(map)
///       ..foo = 1
///       ..baz = 2;
///
///     assert(map.toString == '{foo: bar, Foo.bar: true, Foo.baz: false, Bar.foo: 1, Bar.baz: 2}');
///     assert(prettyPrintMap(map) == '''
///     {
///       Foo…
///         .bar: true,
///         .baz: false,
///
///       Bar…
///         .foo: 1,
///         .baz: 2,
///
///       foo: bar
///     }
///     '''.trim());
///
String prettyPrintMap(Map map) {
  return _prettyObj(map);
}

const String _indent = '  ';
const int _maxKeyValuePairsPerLine = 1;
const int _maxListItemsPerLine = 4;

/// Indents [str] by [_indent], trimming any trailing whitespace.
String _indentString(String str) {
  return str.split('\n').map((line) => (_indent + line).trimRight()).join('\n');
}

String _prettyObj(Object obj) {
  if (obj is List) {
    var items = obj.map(_prettyObj).toList();

    if (items.length > _maxListItemsPerLine || items.any((items) => items.contains('\n'))) {
      var inner = _indentString(items.join(',\n'));
      return '[\n$inner\n]';
    } else {
      var inner = items.join(', ');
      return '[$inner]';
    }
  } else if (obj is Map) {
    final namespacedKeys = <String, List<String>>{};
    final otherKeys = [];

    obj.keys.forEach((dynamic key) {
      const namespaceSeparator = '.';
      if (key is String && key.contains(namespaceSeparator)) {
        var index = key.indexOf(namespaceSeparator);
        var namespace = key.substring(0, index);
        var subkey = key.substring(index);

        namespacedKeys[namespace] ??= <String>[];
        namespacedKeys[namespace].add(subkey);
      } else {
        otherKeys.add(key);
      }
    });

    final pairs = <String>[];

    pairs.addAll(namespacedKeys.keys.map((String namespace) {
      String renderSubKey(String subkey) {
        var key = '$namespace$subkey';
        var value = obj[key];

        return '$subkey: ' + _prettyObj(value);
      }

      Iterable<String> subkeys = namespacedKeys[namespace];

      return '$namespace…\n' + _indentString(subkeys.map(renderSubKey).map((pair) => pair + ',\n').join());
    }));

    pairs.addAll(otherKeys.map((dynamic key) {
      return '$key: ' + _prettyObj(obj[key]) + ',';
    }));

    final RegExp trailingComma = new RegExp(r'\s*,\s*$');

    if (pairs.length > _maxKeyValuePairsPerLine || pairs.any((pair) => pair.contains('\n'))) {
      var inner = _indentString(pairs.join('\n')).replaceFirst(trailingComma, '');
      return '{\n$inner\n}';
    } else {
      var inner = pairs.join(' ').replaceFirst(trailingComma, '');
      return '{$inner}';
    }
  } else {
    return obj.toString();
  }
}