<template lang="pug">
component(
  ref="formEl"
  :is="formRegister ? 'w-form-element' : 'div'"
  v-bind="formRegister && { validators, inputValue: rating, disabled: isDisabled, readonly: isReadonly, noBlurValidation }"
  v-model:valid="valid"
  @reset="$emit('update:modelValue', rating = null);$emit('input', null)"
  :class="classes")
  input(:id="inputId" :name="inputName" type="hidden" :value="rating")
  div(role="group" :aria-label="ariaLabel || 'Rating'")
    template(v-for="i in max" :key="i")
      slot(v-if="$slots.item" name="item" :index="i + 1")
      button.w-rating__button(
        :disabled="isDisabled || isReadonly"
        @mouseenter="hover = i"
        @mouseleave="hover = 0"
        @click="onButtonClick(i)"
        @focus="onFocus"
        @blur="onBlur"
        @keydown="onKeydown"
        :aria-label="`${i} of ${max}`"
        :aria-pressed="(rating >= i).toString()"
        :class="buttonClasses(i)"
        type="button"
        :tabindex="i === 1 ? 0 : -1")
        i.w-icon(
          v-if="i - 1 === ~~rating && rating - ~~rating"
          :class="`${icon} ${color}`"
          aria-hidden="true"
          :style="halfStarStyle")
</template>

<script>
import FormElementMixin, { useWaveUiFormIds } from '../mixins/form-elements'
import { guardFocusable, focusElement, vFocusLifecycleMixin } from '../utils/focus'

export default {
  name: 'w-rating',
  focusable: true,
  expose: ['focus'],
  mixins: [FormElementMixin, vFocusLifecycleMixin],

  setup () {
    return useWaveUiFormIds()
  },

  props: {
    modelValue: {},
    ariaLabel: { type: String }, // Accessible group label (default: 'Rating').
    max: { type: [Number, String], default: 5 },
    color: { type: String, default: 'primary' },
    bgColor: { type: String },
    icon: { type: String, default: 'wi-star' },
    xs: { type: Boolean },
    sm: { type: Boolean },
    md: { type: Boolean },
    lg: { type: Boolean },
    xl: { type: Boolean },
    noRipple: { type: Boolean },
    dark: { type: Boolean },
    light: { type: Boolean }
    // Props from mixin: id, name, disabled, readonly, required, validators.
    // Computed from mixin: inputId, inputName, isDisabled & isReadonly.
  },

  emits: ['input', 'update:modelValue', 'focus', 'blur'],

  data () {
    return {
      rating: parseFloat(this.modelValue || 0),
      hover: 0, // The index (starts at 1) of the currently hovered button.
      hasFocus: 0, // The index (starts at 1) of the currently focused button.
      ripple: {
        start: false,
        end: false,
        timeout: null
      }
    }
  },

  computed: {
    size () {
      return (
        (this.xs && 'xs') ||
        (this.sm && 'sm') ||
        (this.lg && 'lg') ||
        (this.xl && 'xl') ||
        'md'
      )
    },
    classes () {
      return {
        'w-rating': true,
        'w-rating--dark': this.dark,
        'w-rating--light': this.light,
        'w-rating--focus': this.hasFocus,
        'w-rating--hover': this.hover,
        'w-rating--disabled': this.isDisabled,
        'w-rating--readonly': this.isReadonly,
        'w-rating--ripple': this.ripple.start,
        'w-rating--rippled': this.ripple.end
      }
    },

    halfStarStyle () {
      return {
        width: this.hover <= ~~this.rating && `${(this.rating - ~~this.rating) * 100}%`
      }
    }
  },

  methods: {
    focus () {
      if (!guardFocusable(this)) return
      focusElement(this.$el.querySelector('.w-rating__button:not([disabled])'))
    },

    onButtonClick (i) {
      this.rating = i
      this.$emit('update:modelValue', this.rating)
      this.$emit('input', this.rating)

      if (!this.noRipple) {
        this.ripple.start = true
        this.ripple.timeout = setTimeout(() => {
          this.ripple.start = false
          this.ripple.end = true
          setTimeout(() => (this.ripple.end = false), 100)
        }, 700)
      }
    },

    onFocus (e) {
      this.hasFocus = true
      this.$emit('focus', e)
    },

    onBlur (e) {
      this.hasFocus = false
      this.$emit('blur', e)
    },

    onKeydown (e) {
      if ([37, 38, 39, 40].includes(e.keyCode)) {
        if ([39, 40].includes(e.keyCode)) this.rating <= this.max - 1 && this.rating++
        else if (this.rating > 1) this.rating--

        const sibling = this.$el.querySelectorAll('button')[this.rating - 1]
        if (sibling) {
          sibling.focus()
          sibling.click()
        }

        e.preventDefault()
      }
    },

    buttonClasses (i) {
      const isHalf = i - 1 === ~~this.rating && this.rating - ~~this.rating
      const isOn = this.hover >= i || (!isHalf && this.hover === 0 && this.rating >= i)
      return {
        'w-rating__button--on': isOn,
        'w-rating__button--half': isHalf,
        [this.icon]: true,
        [`size--${this.size}`]: true,
        [this.color]: isOn,
        [this.bgColor]: this.bgColor && !isOn
      }
    }
  },

  watch: {
    value (value) {
      this.rating = parseFloat(value)
    }
  }
}
</script>

<style lang="scss">
.w-rating {
  display: inline-flex;
  align-items: center;

  @include themeable;

  &__button {
    position: relative;
    width: 1.1em;
    aspect-ratio: 1;
    display: inline-flex;
    align-items: center;
    justify-content: center;
    border: none;
    background: none;
    color: var(--w-rating-bg-color);
    cursor: pointer;
    -webkit-tap-highlight-color: transparent;
    @include default-transition(var(--w-transition-duration-fast));

    // Disabled & readonly.
    .w-rating--disabled & {opacity: 0.6;cursor: not-allowed;}
    .w-rating--readonly & {cursor: auto;}

    // Sizes.
    &.size--xs {font-size: round(nearest, calc(0.85 * var(--w-base-font-size)), 1px);}
    &.size--sm {font-size: round(nearest, calc(1.15 * var(--w-base-font-size)), 1px);}
    &.size--md {font-size: round(nearest, calc(1.4 * var(--w-base-font-size)), 1px);}
    &.size--lg {font-size: round(nearest, calc(1.7 * var(--w-base-font-size)), 1px);}
    &.size--xl {font-size: calc(2 * var(--w-base-font-size));margin-left: 0;}

    &:before {font-size: 1.1em;}
    &:before, .w-icon:before {
      width: 100%;
      height: 1em;
      display: inline-flex;
      transition: 0.15s transform;
    }
  }

  &--hover &__button--on:before,
  &--hover &__button--on .w-icon:before,
  &--focus &__button--on:before,
  &--focus &__button--on .w-icon:before {transform: scale(1.12);}

  // Disabled & readonly.
  &--readonly &__button--on:before,
  &--readonly.w-rating--hover &__button--on:before,
  &--readonly &__button--on .w-icon:before,
  &--readonly.w-rating--hover &__button--on .w-icon:before,
  &--disabled &__button--on:before,
  &--disabled.w-rating--hover &__button--on:before,
  &--disabled &__button--on .w-icon:before,
  &--disabled.w-rating--hover &__button--on .w-icon:before {transform: none;}

  // Half star.
  &__button .w-icon {
    position: absolute;
    left: 0;
    width: 100%;
    height: 100%;
    font-size: 1em;
    justify-content: flex-start;
    overflow: hidden;
    display: inline-flex;
    border-radius: 0;

    &:before {padding-left: 0.05em;padding-right: 0.05em;}
  }
  .w-rating--hover &__button .w-icon {display: none;}

  // The focus outline & ripple on button click.
  &__button:after {
    content: "";
    position: absolute;
    inset: 0;
    background-color: currentColor;
    border-radius: 100%;
    transform: translateX(100%) scale(0);
    opacity: 0;
    pointer-events: none;
    transition: 0.25s ease-in-out;
  }

  &--ripple &__button:focus:after {
    background-color: transparent;
    animation: w-rating-ripple 0.55s ease;
  }

  &__button:focus:after,
  &__button:active:after {
    transform: scale(1.8);
    opacity: 0.2;
  }
  &__button--on:focus:after {
    transform: scale(1.8);
  }
  .w-rating--disabled &__button:after,
  .w-rating--readonly &__button:after {opacity: 0;}

  // After ripple reset to default state, then remove the class via js and the
  // `:focus + &__button:after` will re-transition to normal focused outline.
  &--rippled &__button:focus:after {
    transition: none;
    transform: scale(0);
    opacity: 0;
  }

  &__button * {pointer-events: none;}
}

@keyframes w-rating-ripple {
  0% {opacity: 0.8;transform: scale(1);background-color: currentColor;} // Start with visible ripple.
  100% {opacity: 0;transform: scale(2.8);} // Propagate ripple to max radius and fade out.
}
</style>
