// stylelint-disable scss/operator-no-newline-after
// stylelint-disable max-line-length
@use 'sass:string';
@use 'sass:meta';
@use 'sass:list';
@use 'sass:selector';
@use 'sass:map';

////
/// @group bem
/// @author <a href="https://github.com/runningskull" target="_blank">Juan Patten</a>
/// @author <a href="https://github.com/simeonoff" target="_blank">Simeon Simeonoff</a>
/// @package theming
////

/// @type String - The Element separator used. Default '__'.
/// @access private
$bem--sep-elem: if(meta.variable-exists(bem--sep-elem), $bem--sep-elem, '__');

/// @type String - The Modifier separator used. Default '--'.
/// @access private
$bem--sep-mod: if(meta.variable-exists(bem--sep-mod), $bem--sep-mod, '--');

/// @type String - The Modifier Value separator used. Default '-'.
/// @access private
$bem--sep-mod-val: if(meta.variable-exists(bem--sep-mod-val), $bem--sep-mod-val, '-');

/// Cache for modifier string conversions
/// @type Map
$_bem-mod-cache: () !default;

/// Converts a passed selector value into plain string.
/// @access private
/// @param {String} $s - The selector to be converted.
@function bem--selector-to-string($s) {
    @if not($s) {
        @return '';
    }

    @return string.slice(meta.inspect($s), 2, -3);
}

/// Prepends a dot to the passed BEM selector.
/// @access private
/// @param {String} $x - The BEM selector to prepend a dot to.
@function bem--with-dot($x) {
    $first: string.slice($x, 0, 1);

    @return if($first == '.', $x, string.insert($x, '.', -100));
}

/// Converts a key-value map into a modifier string
/// @access private
/// @param {String|Map} $m - The modifier to convert
/// @return {String} The converted modifier string
@function bem--mod-str($m) {
    $cache-key: meta.inspect($m);

    @if map.has-key($_bem-mod-cache, $cache-key) {
        @return map.get($_bem-mod-cache, $cache-key);
    }

    $result: if(meta.type-of($m) == 'map', list.nth($m, 1) + $bem--sep-mod-val + list.nth($m, 2), $m);
    $_bem-mod-cache: map.set($_bem-mod-cache, $cache-key, $result) !global;

    @return $result;
}

/// Prefixes the block name to an element selector string,
/// with the element separator string used as a divider.
/// @access private
/// @param {String} $b - The block name.
/// @param {String} $e - The element name.
@function bem--elem-str($b, $e) {
    @return $b + $bem--sep-elem + $e;
}

/// Returns a block selector string affixed by the modifier selector,
/// followed by the element selector string.
/// @access private
/// @param {String} $block - The block name.
/// @param {String} $elem - The sub-element name.
/// @param {String} $mod - The modifier name.
@function bem--bem-str($block, $elem, $mod) {
    $elem: if($elem, ' ' + $elem, '');

    @return ($block + $bem--sep-mod + bem--mod-str($mod) + $elem);
}

/// Checks if the element separator string is part of the passed string.
/// @access private
/// @param {String} $x - The string to check.
/// @returns {number|null} Will return the index of the occurance,
/// or null if the element separator name is not part of the passed string.
@function bem--contains-elem($x) {
    // if you set the separators to common strings, this could fail
    @return string.index($x, $bem--sep-elem);
}

/// Checks if the modifier separator string is part of the passed string.
/// @access private
/// @param {String} $x - The string to check.
/// @returns {number|null} Will return the index of the occurance,
/// or null if the modifier separator string is not part of the passed string.
@function bem--contains-mod($x) {
    // if you set the separators to common strings, this could fail
    @return string.index($x, $bem--sep-mod);
}

/// Checks if the passed selector string contains a colon.
/// @access private
/// @param {String} $x - The string to check for colons.
/// @returns {number|null} Will return the index of the occurance,
/// or null if the string does not contain any colons.
@function bem--contains-pseudo($x) {
    @return string.index($x, ':');
}

/// Returns the BEM block-name that generated `$x`. Does not include leading ".".
/// @access private
/// @param {String} $x - The block name.
@function bem--extract-block($x) {
    @if bem--contains-mod($x) {
        @return string.slice($x, 1, string.index($x, $bem--sep-mod) - 1);
    } @else if bem--contains-elem($x) {
        @return string.slice($x, 1, string.index($x, $bem--sep-elem) - 1);
    } @else if bem--contains-pseudo($x) {
        @return string.slice($x, 1, string.index($x, ':') - 1);
    }

    @return $x;
}

/// Returns the first selector of a nested selector string.
/// @access private
@function bem--extract-first-selector($x) {
    $parens: string.index($x, '(');
    $eow: if($parens, string.index($x, ')'), string.index($x, ' ') or -1);

    @return string.slice($x, 1, $eow);
}

@function bem--validate-elem-context($context, $elem) {
    @if $context == '' {
        @error 'Detected an Element that is not inside a Block: #{$elem}';
    }

    @if bem--contains-elem($context) {
        @error 'Detected a multi-level nested Element (#{$context} #{$elem})! ' +
            'Bem doesn\'t support nested elements because they do not have BEM nature ' +
            '(www.getbem.com/faq/#css-nested-elements). ' +
            'If you must do it, use a hardcoded selector like &__subsubelem';
    }

    @return true;
}

/// Generates a list of modifiers for an element
/// @access private
@function bem--generate-mod-list($block, $elem, $mods) {
    $mod-list: ();

    @if $mods != null and meta.type-of($mods) == 'list' {
        @each $mod in $mods {
            $mod-list: list.append($mod-list, #{bem-selector($block, $e: ($elem, $mod))});
        }
    }

    @return $mod-list;
}

/// Generates the appropriate selector for a BEM element
/// @access private
/// @param {String} $block - The block name
/// @param {String} $elem - The element name
/// @param {String|Map} $mod - Optional modifier
/// @param {String} $first - Optional first selector for nesting
/// @return {String} The generated selector
/// @throw {Error} If invalid parameter types are provided
@function bem--generate-elem-selector($block, $elem, $mod, $first: null) {
    @if $first and not meta.type-of($first) == 'string' {
        @error 'First selector must be a string, got #{meta.type-of($first)}';
    }

    @if $first {
        @return $first + ' ' + bem-selector($block, $e: ($elem, $mod));
    }

    @return bem-selector($block, $e: ($elem, $mod));
}

/// Generates a full BEM selector.
/// @access private
/// @param {String} $block - Required. A string block name.
/// @param {String|List} $elem - Optional. A sub-element name. If `$mod` is not present, it is
/// joined with $block. If $mod is present, it is nested under `$block--$mod`.
/// @param {String|Map} $mod - Optional. A block modifier.
/// @example scss Include a block
///   @include bem-selector(block); // outputs .block
/// @example scss Include a block and an element
///   @include bem-selector(block, $e:elem); // outputs .block__elem
/// @example scss Include block, element, and element modifier
///   @include bem-selector(block, $e:(elem, $mod); // outputs .block__elem-emod
/// @example scss Include block and block modifier
///   @include bem-selector(block, $m:mod) // outputs .block--mod
/// @example scss Include block and modifier value
///   @include bem-selector(block, $m:(mod:val)); // outputs .block--mod-val
/// @example scss Include block modifier followed by block sub-element
///   @include bem-selector(block, $m:mod, $e:elem); // outputs .block--mod .block__elem
/// @return {String} The generated BEM selector string
@function bem-selector($block, $e: null, $elem: null, $m: null, $mod: null, $mods: null) {
    $block: bem--with-dot($block);
    $elem: $e or $elem;

    // Return early if possible
    $mods: $m or $mod or $mods;

    @if not($elem or $mods) {
        @return $block;
    }

    @if $elem {
        // User passed an element-specific modifier
        @if meta.type-of($elem) == list and list.nth($elem, 2) {
            // For now we don't support multiple elem-mods at once
            @if meta.type-of(list.nth($elem, 2)) == 'list' {
                @error 'Only one element-modifier allowed.';
            }

            $elem: string.slice(bem-selector(list.nth($elem, 1), $m: list.nth($elem, 2)), 2);
        }

        $elem: bem--elem-str($block, $elem);
    }

    @if not $mods {
        @return bem--with-dot($elem);
    }

    @if meta.type-of($mods) != 'list' {
        $mods: ($mods);
    }

    $bemcls: '';

    @for $i from 1 to list.length($mods) {
        $bemcls: $bemcls + bem--bem-str($block, $elem, list.nth($mods, $i)) + ', ';
    }

    $bemcls: $bemcls + bem--bem-str($block, $elem, list.nth($mods, -1));

    @return $bemcls;
}

/// Wraps content with :not selector if needed
/// @access private
@mixin bem--apply-not($not, $block, $selector, $elem: null) {
    @if $not {
        $not-selectors: ();

        @if meta.type-of($not) == 'list' {
            @each $n in $not {
                $not-selectors: list.append(
                    $not-selectors,
                    if($elem, bem-selector($block, $e: ($elem, $n)), bem-selector($block, $m: $n)),
                    $separator: comma
                );
            }
        } @else {
            $not-selectors: if($elem, bem-selector($block, $e: ($elem, $not)), bem-selector($block, $m: $not));
        }

        #{$selector}:not(#{$not-selectors}) {
            @content;
        }
    } @else {
        #{$selector} {
            @content;
        }
    }
}

/// Returns a BEM element selector string. If a modifier is provided, it will be appended to the element selector.
/// @access public
/// @param {String} $block - The block name.
/// @param {String} $elem - The element name.
/// @param {String} $mod - Optional. The modifier name.
/// @return {String} The BEM element selector string.
/// @example scss
///   elem('button', 'icon')        // returns '.button__icon'
///   elem('button', 'icon', 'big') // returns '.button__icon--big'
@function elem($block, $elem, $mod: null) {
    @if not meta.type-of($block) == 'string' {
        @error 'Block must be a string, got #{meta.type-of($block)}';
    }

    @if not meta.type-of($elem) == 'string' {
        @error 'Element must be a string, got #{meta.type-of($elem)}';
    }

    @if $mod and not meta.type-of($mod) == 'string' {
        @error 'Modifier must be a string, got #{meta.type-of($mod)}';
    }

    @if $mod {
        @return bem-selector(bem-selector($block, $e: $elem), $m: $mod);
    }

    @return bem-selector($block, $e: $elem);
}

/// Returns a BEM modifier selector string for a block.
/// @access public
/// @param {String} $block - The block name.
/// @param {String} $mod - The modifier name.
/// @return {String} The BEM modifier selector string.
/// @example scss
///   mod('button', 'primary') // returns '.button--primary'
///   mod('card', 'outlined') // returns '.card--outlined'
@function mod($block, $mod) {
    @return bem-selector($block, $m: $mod);
}

/// Simply unrolls into a class-selector. The main purpose of using this mixin
/// is to clearly denote the start of a BEM block.
/// @access public scss
/// @param {String} $block - The block name.
/// @example
///   @include bem-block(block) {
///      background: green;
///   }
///   // Return
///   .block { background: green; }
@mixin bem-block($block) {
    @at-root {
        #{bem-selector($block)} {
            @content;
        }
    }
}

/// Unrolls into a proper BEM element selector, depending on the context.
/// Inside just a block, yields a root-level `.block__elem`.
/// Inside a mod or pseudo-selector, yields a nested `.block--mod .block__elem`.
/// If $mod is included, it is appended to the block selector. Multiple
/// $mods are not supported.
/// @access public
/// @param {String} $elem - The sub-element name.
/// @param {String} $m - The modifier name.
/// @param {String} $mod - An alias of `$m`.
/// @example scss
///   @include bem-block(block) {
///      @include bem-elem(element) {
///        background: blue;
///      }
///   }
///   // Return
///   .block__element { background: blue; }
@mixin bem-elem($elem, $m: null, $mod: null, $mods: null, $not: null) {
    // Setup and validation
    $context: bem--selector-to-string(&);
    $block: bem--extract-block($context);
    $first: bem--extract-first-selector($context);
    $nested: bem--contains-pseudo($context) or bem--contains-mod($context);
    $mod: $m or $mod;

    // Validate context
    $valid: bem--validate-elem-context($context, $elem);

    // Generate modifier list if needed
    $mod-list: bem--generate-mod-list($block, $elem, $mods);

    @at-root {
        @if not($nested) {
            // Simple case - no nesting
            @if not($mods) {
                @include bem--apply-not($not, $block, bem--generate-elem-selector($block, $elem, $mod), $elem) {
                    @content;
                }
            } @else {
                @include bem--apply-not($not, $block, selector.append($mod-list...), $elem) {
                    @content;
                }
            }
        } @else {
            // Nested case with pseudo or modifier
            @if not($mods) {
                @include bem--apply-not($not, $block, bem--generate-elem-selector($block, $elem, $mod, $first), $elem) {
                    @content;
                }
            } @else {
                @include bem--apply-not($not, $block, $first + ' ' + selector.append($mod-list...), $elem) {
                    @content;
                }
            }
        }
    }
}

/// Generates selectors for multiple block modifiers with optional exclusions
/// @access public
/// @param {List} $mods - List of modifier names
/// @param {Map} $options - Optional configuration map
/// @param {String|List} $options.not - Modifier(s) to exclude with :not()
/// @throw {Error} If used outside a block context
/// @throw {Error} If used inside an element or pseudo-selector
/// @example scss
///   @include bem-block(card) {
///     @include bem-mods(large, primary, (not: outline)) {
///       // Applies to .card--large, .card--primary
///       // but not when .card--outline is present
///     }
///   }
@mixin bem-mod($mod, $not: null) {
    $skip: false;
    $this: bem--selector-to-string(&);

    @if $this == '' {
        @error 'Detected a Modifier that is not inside a Block: ' + $mod;
    }

    @if bem--contains-elem($this) {
        @error 'Nesting a Modifier inside an Element (#{$this} #{$mod}) ' + 'is not supported. Instead, use bem-elem(myelem, elem-mod) syntax.';
    }

    @if bem--contains-pseudo($this) {
        @error 'Nesting a Modifier inside a pseudo-selector is not supported: #{$this} #{$mod}';
    }

    @at-root {
        #{bem-selector($this, $m: $mod)} {
            @if $not {
                &:not(#{bem-selector($this, $m: $not)}) {
                    @content;
                }
            } @else {
                @content;
            }
        }
    }
}

/// Unrolls into a block--modifier.[block--modifier...] .block__el
/// This mixin is useful when we want to apply classes to a block,
/// or block element when two or more modifiers are applied in tandem
/// @access public
/// @param {List} $mods - A list of modifiers
/// @example scss
///   @include bem-block(block) {
///      @include bem-mods(error, warn) {
///        position: absolute;
///      }
///   }
///   // Return
///   .block--error,
///   .block--warn {
///       position: absolute;
///    }
///
@mixin bem-mods($mods...) {
    $this: bem--selector-to-string(&);
    $mod-classes: ();
    $not: null;
    $not-selectors: ();

    // Check if the last argument is a map with a 'not' key
    @if meta.type-of(list.nth($mods, -1)) == 'map' {
        $last: list.nth($mods, -1);

        @if map.has-key($last, 'not') {
            $not: map.get($last, 'not');
            $new-mods: ();

            @for $i from 1 through list.length($mods) - 1 {
                $new-mods: list.append($new-mods, list.nth($mods, $i));
            }

            $mods: $new-mods;

            // Handle both single and list of negative modifiers
            @if meta.type-of($not) == 'list' {
                @each $n in $not {
                    $not-selectors: list.append($not-selectors, bem-selector($this, $m: $n), $separator: comma);
                }
            } @else {
                $not-selectors: bem-selector($this, $m: $not);
            }
        }
    }

    @each $mod in $mods {
        @if $this == '' {
            @error 'Detected a Modifier that is not inside a Block: ' + $mod;
        }

        @if bem--contains-elem($this) {
            @error 'Nesting a Modifier inside an Element (#{$this} #{$mod}) ' + 'is not supported. Instead, use bem-elem(myelem, elem-mod) syntax.';
        }

        @if bem--contains-pseudo($this) {
            @error 'Nesting a Modifier inside a pseudo-selector is not supported: #{$this} #{$mod}';
        }

        $mod-classes: list.append(
            $mod-classes,
            #{bem-selector(
                    $block: $this,
                    $m: $mod,
                )}
        );
    }

    @at-root {
        #{selector.append($mod-classes...)} {
            @if $not {
                &:not(#{$not-selectors}) {
                    @content;
                }
            } @else {
                @content;
            }
        }
    }
}

/// @alias bem-selector
/// @see bem-selector
@mixin bem($block, $e: null, $elem: null, $m: null, $mod: null, $mods: null) {
    #{bem-selector($block, $e: $e, $elem: $elem, $m: $m, $mod: $mod, $mods: $mods)} {
        @content;
    }
}

/// @alias bem-block
/// @see bem-block
@mixin block($block) {
    @include bem-block($block) {
        @content;
    }
}

/// @alias bem-elem
/// @see bem-elem
@mixin elem($elem, $m: null, $mod: null, $mods: null, $not: null) {
    @include bem-elem($elem, $m: $m, $mod: $mod, $mods: $mods, $not: $not) {
        @content;
    }
}

/// @alias bem-mod
/// @see bem-mod
@mixin mod($mod, $not: null) {
    @include bem-mod($mod, $not) {
        @content;
    }
}

/// @alias bem-mods
/// @see bem-mods
@mixin mods($mods...) {
    @include bem-mods($mods...) {
        @content;
    }
}

/// @alias bem-block
/// @see bem-block
@mixin b($block) {
    @include bem-block($block) {
        @content;
    }
}

/// @alias bem-elem
/// @see bem-elem
@mixin e($elem, $m: null, $mod: null, $mods: null, $not: null) {
    @include bem-elem($elem, $m: $m, $mod: $mod, $mods: $mods, $not: $not) {
        @content;
    }
}

/// @alias bem-mod
/// @see bem-mod
@mixin m($mod, $not: null) {
    @include bem-mod($mod, $not) {
        @content;
    }
}

/// @alias bem-mods
/// @see bem-mods
@mixin mx($mods...) {
    @include bem-mods($mods...) {
        @content;
    }
}
