//
// Copyright 2022 Google LLC
// SPDX-License-Identifier: Apache-2.0
//

// go/keep-sorted start
@use 'sass:list';
@use 'sass:map';
@use 'sass:math';
// go/keep-sorted end
// go/keep-sorted start
@use '../../ripple/ripple';
@use '../../tokens';
// go/keep-sorted end

// Motion token values.
$_md-sys-motion: tokens.md-sys-motion-values();
// The stroke width of the icon marks.
$_mark-stroke: 2px;
// The coordinates in an 18px viewBox of the bottom left corner of the
// indeterminate icon. Y is negative to fix an issue in Safari (see below).
$_indeterminate-bottom-left: 4px, -10px;
// The coordinates in an 18px viewBox of the bottom left corner of the
// checkmark icon. Y is negative to fix an issue in Safari (see below).
$_checkmark-bottom-left: 7px, -14px;

@mixin theme($tokens) {
  $supported-tokens: tokens.$md-comp-checkbox-supported-tokens;
  @each $token, $value in $tokens {
    @if list.index($supported-tokens, $token) == null {
      @error 'Token `#{$token}` is not a supported token.';
    }

    @if $value {
      --md-checkbox-#{$token}: #{$value};
    }
  }
}

@mixin styles() {
  $tokens: tokens.md-comp-checkbox-values();

  :host {
    border-start-start-radius: map.get($tokens, 'container-shape-start-start');
    border-start-end-radius: map.get($tokens, 'container-shape-start-end');
    border-end-end-radius: map.get($tokens, 'container-shape-end-end');
    border-end-start-radius: map.get($tokens, 'container-shape-end-start');
    display: inline-flex;
    height: map.get($tokens, 'container-size');
    position: relative;
    vertical-align: top; // Fix extra space when placed inside display: block
    width: map.get($tokens, 'container-size');
    -webkit-tap-highlight-color: transparent;
    cursor: pointer;
  }

  :host([disabled]) {
    cursor: default;
  }

  :host([touch-target='wrapper']) {
    margin: max(0px, ((48px - map.get($tokens, 'container-size')) / 2));
  }

  md-focus-ring {
    height: 44px;
    inset: unset;
    width: 44px;
  }

  // <input> is also the touch target
  input {
    appearance: none;
    height: 48px;
    margin: 0;
    opacity: 0;
    outline: none;
    position: absolute;
    width: 48px;
    z-index: 1;
    cursor: inherit;
  }

  :host([touch-target='none']) input {
    height: 100%;
    width: 100%;
  }

  .container {
    border-radius: inherit;
    display: flex;
    height: 100%;
    place-content: center;
    place-items: center;
    position: relative;
    width: 100%;
  }

  .outline,
  .background,
  .icon {
    inset: 0;
    position: absolute;
  }

  .outline,
  .background {
    border-radius: inherit;
  }

  .outline {
    border-color: map.get($tokens, 'outline-color');
    border-style: solid;
    border-width: map.get($tokens, 'outline-width');
    box-sizing: border-box;
  }

  .background {
    background-color: map.get($tokens, 'selected-container-color');
  }

  // Background and icon transitions.
  .background,
  .icon {
    opacity: 0; // Background and icon fade in
    transition-duration: 150ms, 50ms; // Exit duration for scale and opacity.
    transition-property: transform, opacity;
    // Exit easing function for scale, linear for opacity.
    transition-timing-function: map.get(
        $_md-sys-motion,
        easing-emphasized-accelerate
      ),
      linear;
    transform: scale(0.6); // Background and icon scale from 60% to 100%.
  }

  :where(.selected) :is(.background, .icon) {
    opacity: 1;
    // Enter duration for scale and opacity.
    transition-duration: 350ms, 50ms;
    // Enter easing function for scale, linear for opacity.
    transition-timing-function: map.get(
        $_md-sys-motion,
        easing-emphasized-decelerate
      ),
      linear;
    transform: scale(1);
  }

  // Ripple

  md-ripple {
    border-radius: map.get($tokens, 'state-layer-shape');
    height: map.get($tokens, 'state-layer-size');
    inset: unset;
    width: map.get($tokens, 'state-layer-size');

    @include ripple.theme(
      (
        hover-color: map.get($tokens, 'hover-state-layer-color'),
        hover-opacity: map.get($tokens, 'hover-state-layer-opacity'),
        pressed-color: map.get($tokens, 'pressed-state-layer-color'),
        pressed-opacity: map.get($tokens, 'pressed-state-layer-opacity'),
      )
    );
  }

  .selected md-ripple {
    @include ripple.theme(
      (
        hover-color: map.get($tokens, 'selected-hover-state-layer-color'),
        hover-opacity: map.get($tokens, 'selected-hover-state-layer-opacity'),
        pressed-color: map.get($tokens, 'selected-pressed-state-layer-color'),
        pressed-opacity:
          map.get($tokens, 'selected-pressed-state-layer-opacity'),
      )
    );
  }

  // Icon and icon marks

  .icon {
    // The icon is created with two <rect> marks for animation:
    // 1. Short end
    //   - the smaller leading part of the checkmark
    //   - hidden behind long end for indeterminate mark
    // 2. Long end
    //   - the larger trailing part of the checkmark
    //   - the entirety of the indeterminate mark
    fill: map.get($tokens, 'selected-icon-color');
    height: map.get($tokens, 'icon-size');
    width: map.get($tokens, 'icon-size');
  }

  // The short end of the checkmark. Initially hidden underneath the
  // indeterminate mark.
  .mark.short {
    height: $_mark-stroke;
    transition-property: transform, height;
    width: $_mark-stroke;
  }

  // The long end of the checkmark. Initially the indeterminate mark.
  .mark.long {
    height: $_mark-stroke;
    transition-property: transform, width;
    width: 10px;
  }

  // Exit duration and easing.
  .mark {
    animation-duration: 150ms;
    animation-timing-function: map.get(
      $_md-sys-motion,
      easing-emphasized-accelerate
    );
    transition-duration: 150ms;
    transition-timing-function: map.get(
      $_md-sys-motion,
      easing-emphasized-accelerate
    );
  }

  // Enter duration and easing.
  .selected .mark {
    animation-duration: 350ms;
    animation-timing-function: map.get(
      $_md-sys-motion,
      easing-emphasized-decelerate
    );
    transition-duration: 350ms;
    transition-timing-function: map.get(
      $_md-sys-motion,
      easing-emphasized-decelerate
    );
  }

  // Creates the checkmark icon.
  .checked,
  // Keep the checkmark shape when unselecting a checked checkbox.
  .prev-checked.unselected {
    .mark {
      // Transform from the bottom left of the rectangles, whch turn into the
      // bottom-most point of the checkmark.
      // Fix Safari's transform-origin bug from "top left" to "bottom left"
      $scale: scaleY(-1);
      // Move the "bottom left" corner to the checkmark location.
      $translate: translate($_checkmark-bottom-left);
      // Rotate the checkmark.
      $rotate: rotate(45deg);
      transform: $scale $translate $rotate;
    }

    .mark.short {
      // The right triangle that forms the short end has an X, Y length of
      // 4dp, 4dp. The hypoteneuse is √(4*4 + 4*4), which is the length of the
      // short end when checked.
      height: 1px * math.sqrt(32);
    }

    .mark.long {
      // The right triangle that forms the long end has an X, Y length of
      // 8dp, 8dp. The hypoteneuse is √(8*8 + 8*8), which is the length of the
      // long end when checked.
      width: 1px * math.sqrt(128);
    }
  }

  // Creates the indeterminate icon.
  .indeterminate,
  // Keep the indeterminate shape when unselecting an indeterminate checkbox.
  .prev-indeterminate.unselected {
    .mark {
      transform: scaleY(-1) translate($_indeterminate-bottom-left) rotate(0deg);
    }
  }

  // When selecting an unselected checkbox, don't transition between the
  // checked and indeterminate states. The checkmark icon or indeterminate icon
  // should fade in from its final position.
  .prev-unselected .mark {
    transition-property: none;
  }

  // When checking a checkbox, the long mark of the checkmark grows from the
  // bottom-most point of the checkmark. An animation provides the starting
  // width to animate from.
  .prev-unselected.checked .mark.long {
    animation-name: prev-unselected-to-checked;
  }
  @keyframes prev-unselected-to-checked {
    from {
      width: 0;
    }
  }

  // States

  :where(:hover) .outline {
    border-color: map.get($tokens, 'hover-outline-color');
    border-width: map.get($tokens, 'hover-outline-width');
  }

  :where(:hover) .background {
    background: map.get($tokens, 'selected-hover-container-color');
  }

  :where(:hover) .icon {
    fill: map.get($tokens, 'selected-hover-icon-color');
  }

  :where(:focus-within) .outline {
    border-color: map.get($tokens, 'focus-outline-color');
    border-width: map.get($tokens, 'focus-outline-width');
  }

  :where(:focus-within) .background {
    background: map.get($tokens, 'selected-focus-container-color');
  }

  :where(:focus-within) .icon {
    fill: map.get($tokens, 'selected-focus-icon-color');
  }

  :where(:active) .outline {
    border-color: map.get($tokens, 'pressed-outline-color');
    border-width: map.get($tokens, 'pressed-outline-width');
  }

  :where(:active) .background {
    background: map.get($tokens, 'selected-pressed-container-color');
  }

  :where(:active) .icon {
    fill: map.get($tokens, 'selected-pressed-icon-color');
  }

  // Don't animate to/from disabled states because the outline is hidden when
  // selected. Without this, there'd be a FOUC if the checkbox state is
  // programmatically  changed while disabled.
  :where(.disabled, .prev-disabled) :is(.background, .icon, .mark) {
    animation-duration: 0s;
    transition-duration: 0s;
  }

  :where(.disabled) .outline {
    border-color: map.get($tokens, 'disabled-outline-color');
    border-width: map.get($tokens, 'disabled-outline-width');
    opacity: map.get($tokens, 'disabled-container-opacity');
  }

  :where(.selected.disabled) .outline {
    // Hide the outline behind the transparent selected container color.
    // This can be removed once disabled colors are flattened.
    visibility: hidden;
  }

  :where(.selected.disabled) .background {
    // Set disabled opacity only when selected since opacity is used to show
    // or hide the container background.
    background: map.get($tokens, 'selected-disabled-container-color');
    opacity: map.get($tokens, 'selected-disabled-container-opacity');
  }

  :where(.disabled) .icon {
    fill: map.get($tokens, 'selected-disabled-icon-color');
  }

  @media (forced-colors: active) {
    .background {
      background-color: CanvasText;
    }

    .selected.disabled .background {
      background-color: GrayText;
      opacity: 1;
    }

    .outline {
      border-color: CanvasText;
    }

    .disabled .outline {
      border-color: GrayText;
      opacity: 1;
    }

    .icon {
      fill: Canvas;
    }
  }
}
