@function __base-sort-by($list, $comparer) {
    $length: length($list);
    $list: __sort($list, $comparer);
    $list: __to-map($list);

    @while ($length > 0) {
        $list: __set($list, $length, __get(__get($list, $length), 'value'));
        $length: $length - 1;
    }

    @return __to-list($list);
}


@function __base-compare-ascending($value, $other) {
    @if __is-string($value) or __is-string($other)
        or __is-color($value) or __is-color($other) {
        @return __string-compare-ascending($value, $other);
    }

    $value: if(__is-falsey($value), 0, $value);
    $other: if(__is-falsey($other), 0, $other);

    @if $value > $other {
        @return 1;
    }

    @if $value < $other {
        @return -1;
    }

    @return 0;
}


@function __string-compare-ascending($value, $other) {
    @return __sort-predicate-string($value, $other, 'asc');
}


@function __compare-ascending($map, $other) {
    @return __either(__base-compare-ascending(__get($map, 'criteria'), __get($other, 'criteria')), (__get($map, 'index') - __get($other, 'index')));
}


@function __compare-multiple-ascending($map, $other) {
    $index: 1;
    $map-criteria: __get($map, 'criteria');
    $other-criteria: __get($other, 'criteria');
    $length: length($map-criteria);

    @while ($index <= $length) {
        $result: __base-compare-ascending(nth($map-criteria, $index), nth($other-criteria, $index));

        @if ($result) {
            @return $result;
        }

        $index: $index + 1;
    }

    @return (__get($map, 'index') - __get($other, 'index'));
}


@function __sort-predicate-asc($first, $second) {
    @if __is-string($first) or __is-string($second)
        or __is-color($first) or __is-color($second) {
        @return __sort-predicate-string($first, $second, 'asc');
    }

    @return if($first > $second, 1, if($first < $second, -1, 0));
}


@function __sort-predicate-desc($first, $second) {
    @if __is-string($first) or __is-string($second)
        or __is-color($first) or __is-color($second) {
        @return __sort-predicate-string($first, $second, 'desc');
    }

    @return if($first < $second, 1, if($first > $second, -1, 0));
}


@function __sort-predicate-string($first, $second, $direction: 'asc') {
    $index: 1;
    $result: 0;

    $first-length: str-length($first);
    $second-length: str-length($second);

    @while $result == 0
    and $index <= $first-length
    and $index <= $second-length {
        $first-char-code: __char-code-at($first, 1);
        $second-char-code: __char-code-at($second, 1);

        $result: if($first-char-code > $second-char-code, 1, if($first-char-code < $second-char-code, -1, 0));

        $index: $index + 1;
    }

    @if $result == 0 and $first-length != $second-length {
        $result: if($first-length > $second-length, 1, if($first-length < $second-length, -1, 0))
    }

    @if $direction == 'desc' {
        $result: $result * -1;
    }

    @return $result;
}


@function __sort-iteratee($value, $index, $collection, $predicate, $args...) {
  @return __sort($value, $predicate);
}


@function __sort($list, $predicate: 'asc') {
    $predicate-map: (
        'asc': '__sort-predicate-asc',
        'desc': '__sort-predicate-desc'
    );

    $comparison-function: __either(__get($predicate-map, $predicate), $predicate);
    $less:  ();
    $equal: ();
    $large: ();

    @if length($list) > 1 {

        $seed: nth($list, ceil(length($list) / 2));

        @each $item in $list {
            $comparison: __exec($comparison-function, $item, $seed);

            @if $comparison == 0 {
                $equal: append($equal, $item);
            } @else if $comparison < 0 {
                $less: append($less, $item);
            } @else if $comparison > 0 {
                $large: append($large, $item);
            }
        }

        @return join(join(__sort($less, $predicate), $equal), __sort($large, $predicate));
    }

    @return $list;
}


@function __sort-by-iteratee($value, $key, $collection) {
    $result: __this('result');
    $iteratee: __this('iteratee');
    $index: __this('index');
    $result: append($result, (
        'criteria': __exec($iteratee, $value, $key, $collection),
        'index': $index,
        'value': $value
    ));
    $_: __this('result', $result);
    $_: __this('index', $index + 1);

    @return true;
}


@function __sort-by($collection, $iteratee: '__identity', $this-arg: $__undefined__) {
    $index: 1;
    $length: if(__is-map-like($collection), length($collection), 0);
    $result: ();

    @if $length == 0 {
        @return $result;
    }

    @if (__is-truthy($this-arg) and __is-iteratee-call($collection, $iteratee, $this-arg)) {
        $iteratee: null;
    }

    $iteratee: __get-callback($iteratee, $this-arg, 3);
    $_: __scope((
        'result': $result,
        'iteratee': $iteratee,
        'index': $index
    ));
    $sort-by-iteratee: __bind('__sort-by-iteratee');
    $_: __base-each($collection, $sort-by-iteratee);
    $result: __this('result');
    $_: __scope(false);

    @return __base-sort-by($result, '__compare-ascending');
}


@function __sort-by-all-iteratee($value, $key, $collection) {
    $props: __this('props');
    $result: __this('result');
    $index: __this('index');
    $length: length($props);
    $criteria: ();

    @while ($length > 0) {
        $criteria: __set($criteria, $length, if($value == null, $__undefined__, __get($value, __get($props, $length))));
        $length: $length - 1;
    }

    $criteria: __to-list($criteria);
    $result: append($result, (
        'criteria': $criteria,
        'index': $index,
        'value': $value
    ));
    $_: __this('result', $result);
    $_: __this('index', $index + 1);

    @return true;
}


@function __sort-by-all($arguments...) {
    $collection: nth($arguments, 1);
    $args: $arguments;

    @if (length($args) > 3 and __is-iteratee-call(nth($args, 2), nth($args, 3), nth($args, 4))) {
        $args: ($collection, nth($args, 2));
    }

    $index: 1;
    $length: if($collection, length($collection), 0);
    $props: __base-flatten($args, false, false, 1);
    $result: ();

    $_: __scope((
        'props': $props,
        'result': $result,
        'index': $index
    ));
    $iteratee: __bind('__sort-by-all-iteratee');
    $_: __base-each($collection, $iteratee);
    $result: __this('result');
    $_: __scope(false);

    @return __base-sort-by($result, '__compare-multiple-ascending');
}


/// Sorts elements of a list by `$predicate` 
/// and returns a new list with the sorted elements.
/// 
/// @access public
/// @group List
/// @param {List} $list The list to sort.
/// @param {Function|String} $predicate ['asc'] The predicate used for comparison.
/// @returns {List} Returns sorted list.
/// @example scss
/// $foo: _sort(3 1 4 2 6 5 8 7 9);
/// // => 1 2 3 4 5 6 7 8 9
/// 
/// $foo: _sort(3 1 4 2 6 5 8 7 9, 'desc');
/// // => 9 8 7 6 5 4 3 2 1

@function _sort($args...) {
    @return call(get-function('__sort'), $args...);
}


/// Creates a list of elements, sorted in ascending order by the results of
/// running each element in a collection through `$iteratee`. This method performs
/// a stable sort, that is, it preserves the original sort order of equal elements.
/// 
/// The `$iteratee` is bound to `$this-arg` and invoked with three arguments;
/// (value, index|key, collection).
/// 
/// If a property name is provided for `$predicate` the created `_property`
/// style callback returns the property value of the given element.
/// 
/// If a value is also provided for `$this-arg` the created `_matches-property`
/// style callback returns `true` for elements that have a matching property
/// value, else `false`.
/// 
/// If a map is provided for `$predicate` the created `_matches` style
/// callback returns `true` for elements that have the properties of the given
/// object, else `false`.
///
///
/// @access public
/// @group Collection
/// @param {List|Map|string} $collection The collection to iterate over.
/// @param {List|Function|Map|string} $iteratee [_identity] - The function
///  invoked per iteration. If a property name or a map is provided it is
///  used to create a `_property` or `_matches` style callback respectively.
/// @param {*} $this-arg [null] - The `_this` binding of `$iteratee`.
/// @returns {List} Returns the new sorted list.
/// @example scss
/// $users: (
///   ( 'user': 'fred' ),
///   ( 'user': 'pebbles' ),
///   ( 'user': 'barney' )
/// );
/// // using the `_property` callback shorthand
/// $foo: _pluck(_sort-by($users, 'user'), 'user');
/// // => ('barney', 'fred', 'pebbles')

@function _sort-by($args...) {
    @return call(get-function('__sort-by'), $args...);
}


/// This method is like `_sort-by` except that it sorts by property names
/// instead of an iteratee function.
///
///
/// @access public
/// @group Collection
/// @param {List|Map|string} $collection The collection to iterate over.
/// @param {String...|List(String)} $props The property names to sort by,
///  specified as individual property names or lists of property names.
/// @returns {List} Returns the new sorted list.
/// @example scss
/// $users: (
///   ( 'user': 'barney', 'age': 36 ),
///   ( 'user': 'fred',   'age': 40 ),
///   ( 'user': 'barney', 'age': 26 ),
///   ( 'user': 'fred',   'age': 30 )
/// );
/// $foo: _map(_sort-by-all($users, ('user', 'age')), _values);
/// // => (('barney', 26), ('barney', 36), ('fred', 30), ('fred', 40))

@function _sort-by-all($args...) {
    @return call(get-function('__sort-by-all'), $args...);
}