// 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.

/// Utilities for manipulating and generating CSS class names.
library over_react.class_names;

import 'dart:collection';

// Must import these consts because they are used in the transformed code.
// ignore: unused_import
import 'package:over_react/over_react.dart' show PropDescriptor, ConsumedProps;
import 'package:over_react/src/component_declaration/annotations.dart';

/// Typed getters/setters for props related to CSS class manipulation, and used by all over_react components.
///
/// To be used as a mixin for React components and builders.
@PropsMixin(keyNamespace: '')
abstract class CssClassPropsMixin {    /* GENERATED CONSTANTS */ static const ConsumedProps $consumedProps = const ConsumedProps($props, $propKeys); static const PropDescriptor _$prop__className = const PropDescriptor(_$key__className), _$prop__classNameBlacklist = const PropDescriptor(_$key__classNameBlacklist); static const List<PropDescriptor> $props = const [_$prop__className, _$prop__classNameBlacklist]; static const String _$key__className = 'className', _$key__classNameBlacklist = 'classNameBlacklist'; static const List<String> $propKeys = const [_$key__className, _$key__classNameBlacklist]; 
  Map get props;

  /// String of space-delimited CSS classes to be added to the resultant DOM.
  ///
  /// All over_react components merge any added classes with this prop and the [classNameBlacklist] prop (see
  /// [UiComponent.forwardingClassNameBuilder]).
  String classNameString get className => props[_$key__className];  set className(String value) => props[_$key__className] = value;;

  /// String of space-delimited CSS classes to be blacklisted from being added to the resultant DOM.
  ///
  /// All over_react components merge any added classes with this prop and the [className] prop (see
  /// [UiComponent.forwardingClassNameBuilder]).
  String classNameBlacklistString get classNameBlacklist => props[_$key__classNameBlacklist];  set classNameBlacklist(String value) => props[_$key__classNameBlacklist] = value;;
}

/// A MapView with the typed getters/setters for all CSS-class-related props.
class CssClassPropsMapView extends MapView with CssClassPropsMixin {
  /// Create a new instance backed by the specified map.
  CssClassPropsMapView(Map map) : super(map);

  /// The props to be manipulated via the getters/setters.
  /// In this case, it's the current MapView object.
  @override
  Map get props => this;
}

/// StringBuffer-backed className builder optimized for adding classNames, with support for blacklisting CSS classes.
class ClassNameBuilder {
  StringBuffer _classNamesBuffer = new StringBuffer();
  StringBuffer _blacklistBuffer;

  /// Creates a new, empty ClassNameBuilder.
  ClassNameBuilder();

  /// Creates a new ClassNameBuilder with className and blacklist values added from [CssClassProps.className] and
  /// [CssClassProps.classNameBlackList], if they are specified.
  ///
  /// This method gracefully handles null [props], as well as unspecified/null prop values.
  ClassNameBuilder.fromProps(Map props) {
    addFromProps(props);
  }

  /// Adds the className and blacklist values from a [props] Map, using the
  /// [CssClassProps.className] and [CssClassProps.classNameBlackList] values.
  ///
  /// This method gracefully handles null [props], as well as unspecified/null prop values.
  ///
  /// This method, along with [toProps], is useful for merging sets of className/blacklist props.
  void addFromProps(Map props) {
    if (props == null) {
      return;
    }

    var cssClassProps = new CssClassPropsMapView(props);

    this
      ..add(cssClassProps.className)
      ..blacklist(cssClassProps.classNameBlacklist);
  }

  /// Adds a className string. May be a single CSS class 'token', or multiple space-delimited classes,
  /// IF [should] is true, otherwise, does nothing (convenience for helping to inline addition conditionals).
  ///
  /// There is no checking for duplicate CSS classes.
  void add(String className, [bool should = true]) {
    if (!should || className == null || className == '') {
      return;
    }

    if (_classNamesBuffer.isNotEmpty) {
      _classNamesBuffer.write(' ');
    }
    _classNamesBuffer.write(className);
  }

  /// Adds all of the CSS classes represented by [className] (a space-delimited list) to the blacklist,
  /// IF [should] is true, otherwise, does nothing (convenience for helping to inline blacklisting conditionals).
  ///
  /// Classes added to the blacklist will not appear in the result of [toClassName].
  void blacklist(String className, [bool should = true]) {
    if (!should || className == null || className == '') {
      return;
    }

    if (_blacklistBuffer == null) {
      _blacklistBuffer = new StringBuffer();
    } else {
      if (_blacklistBuffer.isNotEmpty) {
        _blacklistBuffer.write(' ');
      }
    }
    _blacklistBuffer.write(className);
  }

  /// Returns a String representation of the built className, which includes any added classes, and none of the blacklisted classes.
  ///
  /// Duplicate classes will be added.
  String toClassName() {
    String className = _classNamesBuffer.toString();

    if (_blacklistBuffer != null && _blacklistBuffer.isNotEmpty) {
      List blacklistedClasses = splitSpaceDelimitedString(_blacklistBuffer.toString());

      className = splitSpaceDelimitedString(className)
        .where((String cssClass) => !blacklistedClasses.contains(cssClass))
        .join(' ');
    }

    return className;
  }

  /// Returns a String representation of only the blacklisted classes.
  /// Useful for blacklist forwarding.
  ///
  /// Duplicate classes will be added.
  String toClassNameBlacklist() {
    return _blacklistBuffer == null || _blacklistBuffer.isEmpty
      ? null
      : _blacklistBuffer.toString();
  }

  /// Returns a Map with the [CssClassProps.className] and [CssClassProps.classNameBlackList] props
  /// populated from the return values of [toClassName] and [toClassNameBlacklist], respectively.
  ///
  /// This method, along with [addFromProps], is useful for merging sets of className/blacklist props.
  Map toProps() {
    return new CssClassPropsMapView({})
      ..className = toClassName()
      ..classNameBlacklist = toClassNameBlacklist();
  }

  @override
  String toString() {
    return '$runtimeType _classNamesBuffer: $_classNamesBuffer, _blacklistBuffer: $_blacklistBuffer, toClassName(): ${toClassName()}';
  }
}

/// Returns a List of space-delimited tokens efficiently split from the specified string.
///
/// Useful for splitting CSS class name strings into class tokens, or `data-test-id` values into individual test IDs.
///
/// Handles leading and trailing spaces, as well as token separated by multiple spaces.
///
/// Example:
///
///     splitSpaceDelimitedString('   foo bar     baz') // ['foo', 'bar', 'baz']
List<String> splitSpaceDelimitedString(String string) {
  const int SPACE = 32; // ' '.codeUnits.first;

  List<String> strings = [];
  int start = 0;

  while (start != string.length) {
    while (string.codeUnitAt(start) == SPACE) {
      start++;
      if (start == string.length) {
        return strings;
      }
    }

    int end = start;
    while (string.codeUnitAt(end) != SPACE) {
      end++;
      if (end == string.length) {
        strings.add(string.substring(start, end));
        return strings;
      }
    }

    strings.add(string.substring(start, end));

    start = end;
  }

  return strings;
}