//
// Copyright 2016 Google Inc.
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in
// all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
// THE SOFTWARE.
//

// stylelint-disable selector-class-pattern --
// Selector '.mdc-*' should only be used in this project.

@use 'sass:color';
@use 'sass:map';
@use '../animation/functions' as functions2;
@use '../animation/variables' as variables2;
@use '../base/mixins' as base-mixins;
@use '../feature-targeting/feature-targeting';
@use '../theme/css';
@use '../theme/custom-properties';
@use '../theme/theme';
@use '../theme/keys';
@use '../theme/shadow-dom';
@use '../theme/theme-color';

$custom-property-prefix: 'ripple';

$fade-in-duration: 75ms !default;
$fade-out-duration: 150ms !default;
$translate-duration: 225ms !default;
$states-wash-duration: 15ms !default;

// Notes on states:
// * focus takes precedence over hover (i.e. if an element is both focused and hovered, only focus value applies)
// * press state applies to a separate pseudo-element, so it has an additive effect on top of other states
// * selected/activated are applied additively to hover/focus via calculations at preprocessing time

$dark-ink-opacities: (
  hover: 0.04,
  focus: 0.12,
  press: 0.12,
  selected: 0.08,
  activated: 0.12,
) !default;

$light-ink-opacities: (
  hover: 0.08,
  focus: 0.24,
  press: 0.24,
  selected: 0.16,
  activated: 0.24,
) !default;

// Legacy

$pressed-dark-ink-opacity: 0.16 !default;
$pressed-light-ink-opacity: 0.32 !default;

// State selector variables used for state selector mixins below.
$_hover-selector: '&:hover';
$_focus-selector: '&.mdc-ripple-upgraded--background-focused, &:not(.mdc-ripple-upgraded):focus';
$_active-selector: '&:not(:disabled):active';

$light-theme: (
  focus-state-layer-color: theme-color.$on-surface,
  focus-state-layer-opacity: map.get($dark-ink-opacities, focus),
  hover-state-layer-color: theme-color.$on-surface,
  hover-state-layer-opacity: map.get($dark-ink-opacities, hover),
  pressed-state-layer-color: theme-color.$on-surface,
  pressed-state-layer-opacity: map.get($dark-ink-opacities, press),
);

@mixin theme($theme) {
  @include keys.declare-custom-properties(
    $theme,
    $prefix: $custom-property-prefix
  );

  @if shadow-dom.$css-selector-fallback-declarations {
    .mdc-ripple-surface {
      @include theme-styles($theme);
    }
  }
}

$_ripple-theme: (
  hover-state-layer-color: null,
  focus-state-layer-color: null,
  pressed-state-layer-color: null,
  hover-state-layer-opacity: null,
  focus-state-layer-opacity: null,
  pressed-state-layer-opacity: null,
);

@mixin theme-styles($theme, $ripple-target: '&') {
  $theme: keys.create-theme-properties(
    $theme,
    $prefix: $custom-property-prefix
  );

  // TODO(b/191298796): Support states layer color for every interactive states.
  // Use only hover state layer color, ignoring focus and pressed color.
  @include internal-theme-styles($theme, $ripple-target);
}

@mixin internal-theme-styles($theme, $ripple-target: '&') {
  @include theme.validate-theme-keys($_ripple-theme, $theme);

  @include states-base-color(
    map.get($theme, hover-state-layer-color),
    $ripple-target: $ripple-target
  );
  @include states-hover-opacity(
    map.get($theme, hover-state-layer-opacity),
    $ripple-target: $ripple-target
  );
  @include states-focus-opacity(
    map.get($theme, focus-state-layer-opacity),
    $ripple-target: $ripple-target
  );
  @include states-press-opacity(
    map.get($theme, pressed-state-layer-opacity),
    $ripple-target: $ripple-target
  );
}

@mixin states-base-color(
  $color,
  $query: feature-targeting.all(),
  $ripple-target: '&'
) {
  $feat-color: feature-targeting.create-target($query, color);

  @if $color {
    @if not custom-properties.is-custom-prop($color) {
      $color: custom-properties.create(
        ripple-color,
        theme-color.get-custom-property($color)
      );
    }

    #{$ripple-target}::before,
    #{$ripple-target}::after {
      @include feature-targeting.targets($feat-color) {
        @include theme.property(background-color, $color);
      }
    }
  }
}

///
/// Customizes ripple opacities in `hover`, `focus`, or `press` states
/// @param {map} $opacity-map - map specifying custom opacity of zero or more states
/// @param {bool} $has-nested-focusable-element - whether the component contains a focusable element in the root
/// @param {string} $ripple-target - the optional selector for the ripple element
///
@mixin states-opacities(
  $opacity-map: (),
  $has-nested-focusable-element: false,
  $ripple-target: '&',
  $query: feature-targeting.all()
) {
  // Ensure sufficient specificity to override base state opacities
  @if map.get($opacity-map, hover) {
    @include states-hover-opacity(
      map.get($opacity-map, hover),
      $ripple-target: $ripple-target,
      $query: $query
    );
  }

  @if map.get($opacity-map, focus) {
    @include states-focus-opacity(
      map.get($opacity-map, focus),
      $ripple-target: $ripple-target,
      $has-nested-focusable-element: $has-nested-focusable-element,
      $query: $query
    );
  }

  @if map.get($opacity-map, press) {
    @include states-press-opacity(
      map.get($opacity-map, press),
      $ripple-target: $ripple-target,
      $query: $query
    );
  }
}

@mixin states-hover-opacity(
  $opacity,
  $query: feature-targeting.all(),
  $ripple-target: '&'
) {
  $feat-color: feature-targeting.create-target($query, color);

  @if $opacity and not custom-properties.is-custom-prop($opacity) {
    $opacity: custom-properties.create(ripple-hover-opacity, $opacity);
  }

  // Background wash styles, for both CSS-only and upgraded stateful surfaces
  &:hover,
  &.mdc-ripple-surface--hover {
    @include states-background-selector($ripple-target) {
      // Opacity falls under color because the chosen opacity is color-dependent in typical usage
      @include feature-targeting.targets($feat-color) {
        @include theme.property(opacity, $opacity);
      }
    }
  }
}

@mixin states-focus-opacity(
  $opacity,
  $has-nested-focusable-element: false,
  $query: feature-targeting.all(),
  $ripple-target: '&'
) {
  // Focus overrides hover by reusing the ::before pseudo-element.
  // :focus-within generally works on non-MS browsers and matches when a *child* of the element has focus.
  // It is useful for cases where a component has a focusable element within the root node, e.g. text field,
  // but undesirable in general in case of nested stateful components.
  // We use a modifier class for JS-enabled surfaces to support all use cases in all browsers.
  @if $has-nested-focusable-element {
    // JS-enabled selectors.
    &.mdc-ripple-upgraded--background-focused,
    &.mdc-ripple-upgraded:focus-within,
    // CSS-only selectors.
    &:not(.mdc-ripple-upgraded):focus,
    &:not(.mdc-ripple-upgraded):focus-within {
      @include states-background-selector($ripple-target) {
        @include states-focus-opacity-properties_(
          $opacity: $opacity,
          $query: $query
        );
      }
    }
  } @else {
    // JS-enabled selectors.
    &.mdc-ripple-upgraded--background-focused,
    // CSS-only selectors.
    &:not(.mdc-ripple-upgraded):focus {
      @include states-background-selector($ripple-target) {
        @include states-focus-opacity-properties_(
          $opacity: $opacity,
          $query: $query
        );
      }
    }
  }
}

@mixin states-focus-opacity-properties_($opacity, $query) {
  $feat-animation: feature-targeting.create-target($query, animation);
  // Opacity falls under color because the chosen opacity is color-dependent in typical usage
  $feat-color: feature-targeting.create-target($query, color);

  @if $opacity {
    @if not custom-properties.is-custom-prop($opacity) {
      $opacity: custom-properties.create(ripple-focus-opacity, $opacity);
    }

    // Note that this duration is only effective on focus, not blur
    @include feature-targeting.targets($feat-animation) {
      transition-duration: 75ms;
    }

    @include feature-targeting.targets($feat-color) {
      @include theme.property(opacity, $opacity);
    }
  }
}

@mixin states-press-opacity(
  $opacity,
  $query: feature-targeting.all(),
  $ripple-target: '&'
) {
  $feat-animation: feature-targeting.create-target($query, animation);
  $feat-color: feature-targeting.create-target($query, color);

  // Styles for non-upgraded (CSS-only) stateful surfaces

  @if $opacity {
    @if not custom-properties.is-custom-prop($opacity) {
      $opacity: custom-properties.create(ripple-press-opacity, $opacity);
    }

    &:not(.mdc-ripple-upgraded) {
      // Apply press additively by using the ::after pseudo-element
      #{$ripple-target}::after {
        @include feature-targeting.targets($feat-animation) {
          transition: opacity $fade-out-duration linear;
        }
      }

      &:active {
        #{$ripple-target}::after {
          @include feature-targeting.targets($feat-animation) {
            transition-duration: $fade-in-duration;
          }

          // Opacity falls under color because the chosen opacity is color-dependent in typical usage
          @include feature-targeting.targets($feat-color) {
            @include theme.property(opacity, $opacity);
          }
        }
      }
    }

    &.mdc-ripple-upgraded {
      @include feature-targeting.targets($feat-color) {
        // Upgraded ripple should always emit custom property, regardless of
        // configuration, since ripple itself feature detects custom property
        // support at runtime.
        @include custom-properties.configure($emit-custom-properties: true) {
          @include theme.property(
            custom-properties.create(ripple-fg-opacity, $opacity)
          );
        }
      }
    }
  }
}

// Simple mixin for base states which automatically selects opacity values based on whether the ink color is
// light or dark.
@mixin states(
  $color: theme-color.prop-value(on-surface),
  $has-nested-focusable-element: false,
  $query: feature-targeting.all(),
  $ripple-target: '&',
  $opacity-map: null
) {
  @include states-interactions_(
    $color: $color,
    $has-nested-focusable-element: $has-nested-focusable-element,
    $query: $query,
    $ripple-target: $ripple-target,
    $opacity-map: $opacity-map
  );
}

// Simple mixin for activated states which automatically selects opacity values based on whether the ink color is
// light or dark.
@mixin states-activated(
  $color,
  $has-nested-focusable-element: false,
  $query: feature-targeting.all(),
  $ripple-target: '&'
) {
  $feat-color: feature-targeting.create-target($query, color);
  $activated-opacity: states-opacity($color, activated);

  &--activated {
    // Stylelint seems to think that '&' qualifies as a type selector here?
    @include states-background-selector($ripple-target) {
      // Opacity falls under color because the chosen opacity is color-dependent.
      @include feature-targeting.targets($feat-color) {
        @include theme.property(
          opacity,
          custom-properties.create(
            --mdc-ripple-activated-opacity,
            $activated-opacity
          )
        );
      }
    }

    @include states-interactions_(
      $color: $color,
      $has-nested-focusable-element: $has-nested-focusable-element,
      $opacity-modifier: $activated-opacity,
      $query: $query,
      $ripple-target: $ripple-target
    );
  }
}

// Simple mixin for selected states which automatically selects opacity values based on whether the ink color is
// light or dark.
@mixin states-selected(
  $color,
  $has-nested-focusable-element: false,
  $query: feature-targeting.all(),
  $ripple-target: '&'
) {
  $feat-color: feature-targeting.create-target($query, color);
  $selected-opacity: states-opacity($color, selected);

  &--selected {
    @include states-background-selector($ripple-target) {
      // Opacity falls under color because the chosen opacity is color-dependent.
      @include feature-targeting.targets($feat-color) {
        @include theme.property(
          opacity,
          custom-properties.create(
            --mdc-ripple-selected-opacity,
            $selected-opacity
          )
        );
      }
    }

    @include states-interactions_(
      $color: $color,
      $has-nested-focusable-element: $has-nested-focusable-element,
      $opacity-modifier: $selected-opacity,
      $query: $query,
      $ripple-target: $ripple-target
    );
  }
}

@mixin states-interactions_(
  $color,
  $has-nested-focusable-element,
  $opacity-modifier: 0,
  $query: feature-targeting.all(),
  $ripple-target: '&',
  $opacity-map: null
) {
  @include target-selector($ripple-target) {
    @include states-base-color($color, $query);
  }

  @if $opacity-map == null {
    $opacity-map: (
      hover: states-opacity($color, hover) + $opacity-modifier,
      focus: states-opacity($color, focus) + $opacity-modifier,
      press: states-opacity($color, press) + $opacity-modifier,
    );
  }

  @include states-opacities(
    $opacity-map,
    $has-nested-focusable-element: $has-nested-focusable-element,
    $ripple-target: $ripple-target,
    $query: $query
  );
}

// Wraps content in the `ripple-target` selector if it exists.
@mixin target-selector($ripple-target: '&') {
  @if $ripple-target == '&' {
    @content;
  } @else {
    #{$ripple-target} {
      @content;
    }
  }
}

/// Selector for hover, active and focus states.
@mixin states-selector() {
  #{$_hover-selector},
  #{$_focus-selector},
  #{$_active-selector} {
    @content;
  }
}

@mixin hover() {
  #{$_hover-selector} {
    @content;
  }
}

// Selector for focus state. Using ':not(.mdc-ripple-upgraded)' to continue
// applying focus styles on JS-disabled components, and control focus
// on JS-enabled components with '.mdc-ripple-upgraded--background-focused'.
@mixin focus() {
  #{$_focus-selector} {
    @content;
  }
}

// Selector for active state. Using `:active:active` to override focus styles.
@mixin pressed() {
  #{$_active-selector} {
    @content;
  }
}

// @deprecated Use `pressed()` mixin - renamed for consistency.
@mixin active() {
  @include pressed() {
    @content;
  }
}

/// Keep the ripple (State overlay) behind the content.
@mixin behind-content(
  $ripple-target,
  $content-root-selector: '&',
  $query: feature-targeting.all()
) {
  // Needed for IE11. Without this, IE11 renders the state layer completely
  // underneath the container, making it invisible.
  $feat-structure: feature-targeting.create-target($query, structure);

  #{$content-root-selector} {
    @include feature-targeting.targets($feat-structure) {
      z-index: 0;
    }
  }

  #{$ripple-target}::before,
  #{$ripple-target}::after {
    @include feature-targeting.targets($feat-structure) {
      @include theme.property(
        z-index,
        custom-properties.create(--mdc-ripple-z-index, -1)
      );
    }
  }
}

@function states-opacity($color, $state) {
  $color-value: theme-color.prop-value($color);
  $opacity-map: if(
    theme-color.tone($color-value) == 'light',
    $light-ink-opacities,
    $dark-ink-opacities
  );

  @if not map.has-key($opacity-map, $state) {
    @error "Invalid state: '#{$state}'. Choose one of: #{map.keys($opacity-map)}";
  }

  @return map.get($opacity-map, $state);
}

@mixin states-background-selector($ripple-target) {
  #{$ripple-target}::before {
    @content;
  }
}
