<template>
  <div ref="anchorRef" :class="cls">
    <div
        v-if="!lineLess"
        ref="lineSliderRef"
        :class="`${prefixCls}-line-slider`"
    />
    <ul :class="`${prefixCls}-list`">
      <slot />
    </ul>
  </div>
</template>

<script setup>
import {
  computed,
  defineComponent,
  nextTick,
  onBeforeUnmount,
  onMounted,
  provide,
  reactive,
  ref,
  watch,
} from 'vue';
import { compute as computeScrollIntoView } from 'compute-scroll-into-view';
import { isNumber } from 'lodash-es'
import { isWindow, getElement, off, on, throttleByRaf } from '../utils/dom';
import { slide } from '../utils/slide';
import { anchorInjectionKey } from './context';


const props = defineProps({
  /**
   * @zh 滚动边界值，设置该值为数字后，将会在距离滚动容器 `boundary` 距离时停止滚动。
   * @en Scrolling boundary value. After setting the value to a number, it will stop scrolling when the distance is `boundary` from the scrolling container.
   * @values 'start', 'end', 'center', 'nearest', number
   */
  boundary: {
    type: [Number, String],
    default: 'start',
    validator: (value) => {
      const BOUNDARY_POSITIONS = ['start', 'end', 'center', 'nearest'];
      return isNumber(value) || BOUNDARY_POSITIONS.includes(value);
    },
  },
  /**
   * @zh 是否显示左侧轴线
   * @en Whether to show the left axis
   */
  lineLess: {
    type: Boolean,
    default: false,
  },
  /**
   * @zh 滚动容器
   * @en Scroll container
   */
  scrollContainer: {
    type: [String, Object],
  },
  /**
   * @zh 是否改变hash。设置为 `false` 时点击锚点不会改变页面的 hash
   * @en Whether to change the hash. When set to `false`, clicking on the anchor will not change the hash of the page
   */
  changeHash: {
    type: Boolean,
    default: true,
  },
  /**
   * @zh 是否使用平滑滚动
   * @en Whether to use smooth scrolling
   */
  smooth: {
    type: Boolean,
    default: true,
  },
})

const emit = defineEmits(['select', 'change']);

const prefixCls = 'arco-anchor';
const anchorRef = ref();
const lineSliderRef = ref();
const links = reactive({});
const currentLink = ref('');
const isScrolling = ref(false);

const scrollContainerEle = ref();
const containerEle = ref();

const addLink = (hash, node) => {
  if (!hash) return;
  links[hash] = node;
};

const removeLink = (hash) => {
  delete links[hash];
};

const handleClick = (e, hash) => {
  if (!props.changeHash) {
    e.preventDefault();
  }
  if (hash) {
    scrollIntoView(hash);
    handleAnchorChange(hash);
  }
  emit('select', hash, currentLink.value);
};

const scrollIntoView = (hash) => {
  try {
    const element = getElement(hash);
    if (!element) return;
    let block;
    let diff = 0;
    if (isNumber(props.boundary)) {
      block = 'start';
      diff = props.boundary;
    } else {
      block = props.boundary;
    }

    const actions = computeScrollIntoView(element, { block });
    if (!actions.length) return;
    const { el, top } = actions[0];
    const targetTop = top - diff;
    // if (!this.props.animation) {
    //   return;
    // }
    slide(el, targetTop, () => {
      isScrolling.value = false;
    });
    isScrolling.value = true;
  } catch (e) {
    // eslint-disable-next-line no-console
    console.error(e);
  }
};

const handleScroll = throttleByRaf(() => {
  if (isScrolling.value) return;
  const element = getFirstInViewportEle();
  if (element && element.id) {
    const hash = `#${element.id}`;
    handleAnchorChange(hash);
  }
});

const handleAnchorChange = (hash) => {
  if (!links[hash] && anchorRef.value) {
    const element = getElement(`a[data-href='${hash}']`, anchorRef.value);
    if (!element) return;

    links[hash] = element;
  }
  if (hash !== currentLink.value) {
    currentLink.value = hash;
    nextTick(() => {
      emit('change', hash);
    });
  }
};

const getFirstInViewportEle = () => {
  if (!scrollContainerEle.value || !containerEle.value) {
    return undefined;
  }

  const boundary = isNumber(props.boundary) ? props.boundary : 0;
  const containerRect = containerEle.value.getBoundingClientRect();

  for (const hash of Object.keys(links)) {
    const element = getElement(hash);
    if (element) {
      const { top } = element.getBoundingClientRect();
      const offsetTop = isWindow(scrollContainerEle.value)
          ? top - boundary
          : top - containerRect.top - boundary;
      if (offsetTop >= 0 && offsetTop <= containerRect.height / 2) {
        return element;
      }
    }
  }
  return undefined;
};

watch(currentLink, () => {
  const link = links[currentLink.value];
  if (!props.lineLess && link && lineSliderRef.value) {
    lineSliderRef.value.style.top = `${link.offsetTop}px`;
  }
});

const bindScrollEvent = () => {
  if (scrollContainerEle.value) {
    on(scrollContainerEle.value, 'scroll', handleScroll);
  }
};

const unbindScrollEvent = () => {
  if (scrollContainerEle.value) {
    off(scrollContainerEle.value, 'scroll', handleScroll);
  }
};

const getContainer = () => {
  if (props.scrollContainer) {
    scrollContainerEle.value = isWindow(props.scrollContainer)
        ? window
        : getElement(props.scrollContainer);
    containerEle.value = isWindow(props.scrollContainer)
        ? document.documentElement
        : getElement(props.scrollContainer);
  } else {
    scrollContainerEle.value = window;
    containerEle.value = document.documentElement;
  }
};

onMounted(() => {
  getContainer();

  const hash = decodeURIComponent(window.location.hash);
  if (hash) {
    scrollIntoView(hash);
    handleAnchorChange(hash);
  } else {
    handleScroll();
  }

  bindScrollEvent();
});

onBeforeUnmount(() => {
  unbindScrollEvent();
});

provide(
    anchorInjectionKey,
    reactive({
      currentLink,
      addLink,
      removeLink,
      handleClick,
      changeHash: props.changeHash,
    })
);

const cls = computed(() => [
  prefixCls,
  {
    [`${prefixCls}-line-less`]: props.lineLess,
  },
]);
</script>
<style lang="less">
.arco-anchor {
  position: relative;
  width: 150px;
  overflow: auto;
}
.arco-anchor-line-slider {
  position: absolute;
  top: 0;
  left: 0;
  z-index: 1;
  width: 2px;
  height: 32px;
  background-color: rgb(0,145,234);
  transition: top 0.2s cubic-bezier(0.34, 0.69, 0.1, 1);
}
.arco-anchor-list {
  position: relative;
  margin-top: 0;
  margin-bottom: 0;
  margin-left: 4px;
  padding-left: 0;
  list-style: none;
}
.arco-anchor-list::before {
  position: absolute;
  left: -4px;
  width: 2px;
  height: 100%;
  background-color: rgb(229, 230, 235);
  content: '';
}
.arco-anchor-sublist {
  margin-top: 0;
  margin-bottom: 0;
  padding-left: 0;
  list-style: none;
}
.arco-anchor-link-item {
  margin-bottom: 2px;
}
.arco-anchor-link-item .arco-anchor-link {
  display: block;
  margin-bottom: 2px;
  padding: 4px 8px;
  overflow: hidden;
  color: rgb(79, 89, 105);
  font-size: 14px;
  line-height: 1.5715;
  white-space: nowrap;
  text-decoration: none;
  text-overflow: ellipsis;
  border-radius: 3px;
  cursor: pointer;
}
.arco-anchor-link-item .arco-anchor-link:hover {
  color: rgb(29, 33, 41);
  font-weight: 500;
  background-color: rgb(242, 243, 245);
}
.arco-anchor-link-active > .arco-anchor-link {
  color: rgb(0, 145, 234);
  transition: all 0.1s cubic-bezier(0, 0, 1, 1);
}
.arco-anchor-link-item .arco-anchor-link-item {
  margin-left: 16px;
}
.arco-anchor-line-less .arco-anchor-list {
  margin-left: 0;
}
.arco-anchor-line-less .arco-anchor-list::before {
  display: none;
}
.arco-anchor-line-less .arco-anchor-link-active > .arco-anchor-link {
  color: rgb(0, 145, 234);
  font-weight: 500;
  background-color: rgb(242, 243, 245);
}
</style>
