import {
  ref,
  watch,
  computed,
  reactive,
  nextTick,
  PropType,
  ComputedRef,
  onActivated,
  CSSProperties,
  defineComponent,
  ComponentPublicInstance,
  ExtractPropTypes,
} from 'vue';

// Utils
import {
  isDef,
  addUnit,
  isHidden,
  unitToPx,
  getVisibleTop,
  getElementTop,
  createNamespace,
  getVisibleHeight,
  setRootScrollTop,
  ComponentInstance,
} from '../utils';
import { scrollLeftTo, scrollTopTo } from './utils';
import { BORDER_TOP_BOTTOM } from '../utils/constant';
import { callInterceptor, Interceptor } from '../utils/interceptor';

// Composables
import {
  useChildren,
  useWindowSize,
  useScrollParent,
  useEventListener,
  onMountedOrActivated,
} from '@vant/use';
import { route, RouteProps } from '../composables/use-route';
import { useRefs } from '../composables/use-refs';
import { useExpose } from '../composables/use-expose';

// Components
import { Sticky } from '../sticky';
import TabsTitle from './TabsTitle';
import TabsContent from './TabsContent';

const [name, bem] = createNamespace('tabs');

export const TABS_KEY = Symbol(name);

export type TabsType = 'line' | 'card';

const props = {
  color: String,
  border: Boolean,
  sticky: Boolean,
  animated: Boolean,
  swipeable: Boolean,
  scrollspy: Boolean,
  background: String,
  lineWidth: [Number, String],
  lineHeight: [Number, String],
  beforeChange: Function as PropType<Interceptor>,
  titleActiveColor: String,
  titleInactiveColor: String,
  type: {
    type: String as PropType<TabsType>,
    default: 'line',
  },
  active: {
    type: [Number, String],
    default: 0,
  },
  ellipsis: {
    type: Boolean,
    default: true,
  },
  duration: {
    type: [Number, String],
    default: 0.3,
  },
  offsetTop: {
    type: [Number, String],
    default: 0,
  },
  lazyRender: {
    type: Boolean,
    default: true,
  },
  swipeThreshold: {
    type: [Number, String],
    default: 5,
  },
};

export type TabsProvide = {
  props: ExtractPropTypes<typeof props>;
  setLine: () => void;
  onRendered: (name: string | number, title?: string) => void;
  scrollIntoView: (immediate?: boolean) => void;
  currentName: ComputedRef<number | string | undefined>;
};

export default defineComponent({
  name,

  props,

  emits: ['click', 'change', 'scroll', 'disabled', 'rendered', 'update:active'],

  setup(props, { emit, slots }) {
    let tabHeight: number;
    let lockScroll: boolean;
    let stickyFixed: boolean;

    const root = ref<HTMLElement>();
    const navRef = ref<HTMLElement>();
    const wrapRef = ref<HTMLElement>();

    const windowSize = useWindowSize();
    const scroller = useScrollParent(root);
    const [titleRefs, setTitleRefs] = useRefs<ComponentInstance>();
    const { children, linkChildren } = useChildren<ComponentInstance>(TABS_KEY);

    const state = reactive({
      inited: false,
      position: '',
      currentIndex: -1,
      lineStyle: {
        backgroundColor: props.color,
      } as CSSProperties,
    });

    // whether the nav is scrollable
    const scrollable = computed(
      () => children.length > props.swipeThreshold || !props.ellipsis
    );

    const navStyle = computed(() => ({
      borderColor: props.color,
      background: props.background,
    }));

    const getTabName = (
      tab: ComponentInstance,
      index: number
    ): number | string => tab.name ?? index;

    const currentName = computed(() => {
      const activeTab = children[state.currentIndex];

      if (activeTab) {
        return getTabName(activeTab, state.currentIndex);
      }
    });

    const offsetTopPx = computed(() => unitToPx(props.offsetTop));

    const scrollOffset = computed(() => {
      if (props.sticky) {
        return offsetTopPx.value + tabHeight;
      }
      return 0;
    });

    // scroll active tab into view
    const scrollIntoView = (immediate?: boolean) => {
      const nav = navRef.value;
      const titles = titleRefs.value;

      if (!scrollable.value || !nav || !titles || !titles[state.currentIndex]) {
        return;
      }

      const title = titles[state.currentIndex].$el;
      const to = title.offsetLeft - (nav.offsetWidth - title.offsetWidth) / 2;

      scrollLeftTo(nav, to, immediate ? 0 : +props.duration);
    };

    // update nav bar style
    const setLine = () => {
      const shouldAnimate = state.inited;

      nextTick(() => {
        const titles = titleRefs.value;

        if (
          !titles ||
          !titles[state.currentIndex] ||
          props.type !== 'line' ||
          isHidden(root.value!)
        ) {
          return;
        }

        const title = titles[state.currentIndex].$el;
        const { lineWidth, lineHeight } = props;
        const left = title.offsetLeft + title.offsetWidth / 2;

        const lineStyle: CSSProperties = {
          width: addUnit(lineWidth),
          backgroundColor: props.color,
          transform: `translateX(${left}px) translateX(-50%)`,
        };

        if (shouldAnimate) {
          lineStyle.transitionDuration = `${props.duration}s`;
        }

        if (isDef(lineHeight)) {
          const height = addUnit(lineHeight);
          lineStyle.height = height;
          lineStyle.borderRadius = height;
        }

        state.lineStyle = lineStyle;
      });
    };

    const findAvailableTab = (index: number) => {
      const diff = index < state.currentIndex ? -1 : 1;

      while (index >= 0 && index < children.length) {
        if (!children[index].disabled) {
          return index;
        }

        index += diff;
      }
    };

    const setCurrentIndex = (currentIndex: number) => {
      const newIndex = findAvailableTab(currentIndex);

      if (!isDef(newIndex)) {
        return;
      }

      const newTab = children[newIndex];
      const newName = getTabName(newTab, newIndex);
      const shouldEmitChange = state.currentIndex !== null;

      state.currentIndex = newIndex;

      if (newName !== props.active) {
        emit('update:active', newName);

        if (shouldEmitChange) {
          emit('change', newName, newTab.title);
        }
      }
    };

    // correct the index of active tab
    const setCurrentIndexByName = (name: number | string) => {
      const matched = children.find(
        (tab, index) => getTabName(tab, index) === name
      );

      const index = matched ? children.indexOf(matched) : 0;
      setCurrentIndex(index);
    };

    const scrollToCurrentContent = (immediate = false) => {
      if (props.scrollspy) {
        const target = children[state.currentIndex].$el;

        if (target && scroller.value) {
          const to = getElementTop(target, scroller.value) - scrollOffset.value;

          lockScroll = true;
          scrollTopTo(
            scroller.value,
            to,
            immediate ? 0 : +props.duration,
            () => {
              lockScroll = false;
            }
          );
        }
      }
    };

    // emit event when clicked
    const onClick = (item: ComponentInstance, index: number) => {
      const { title, disabled } = children[index];
      const name = getTabName(children[index], index);

      if (disabled) {
        emit('disabled', name, title);
      } else {
        callInterceptor({
          interceptor: props.beforeChange,
          args: [name],
          done: () => {
            setCurrentIndex(index);
            scrollToCurrentContent();
          },
        });

        emit('click', name, title);
        route(item as ComponentPublicInstance<RouteProps>);
      }
    };

    const onStickyScroll = (params: {
      isFixed: boolean;
      scrollTop: number;
    }) => {
      stickyFixed = params.isFixed;
      emit('scroll', params);
    };

    const scrollTo = (name: number | string) => {
      nextTick(() => {
        setCurrentIndexByName(name);
        scrollToCurrentContent(true);
      });
    };

    const getCurrentIndexOnScroll = () => {
      for (let index = 0; index < children.length; index++) {
        const top = getVisibleTop(children[index].$el);

        if (top > scrollOffset.value) {
          return index === 0 ? 0 : index - 1;
        }
      }

      return children.length - 1;
    };

    const onScroll = () => {
      if (props.scrollspy && !lockScroll) {
        const index = getCurrentIndexOnScroll();
        setCurrentIndex(index);
      }
    };

    const renderNav = () =>
      children.map((item, index) => (
        <TabsTitle
          ref={setTitleRefs(index)}
          dot={item.dot}
          type={props.type}
          badge={item.badge}
          title={item.title}
          color={props.color}
          style={item.titleStyle}
          class={item.titleClass}
          isActive={index === state.currentIndex}
          disabled={item.disabled}
          scrollable={scrollable.value}
          renderTitle={item.$slots.title}
          activeColor={props.titleActiveColor}
          inactiveColor={props.titleInactiveColor}
          onClick={() => {
            onClick(item, index);
          }}
        />
      ));

    const renderHeader = () => {
      const { type, border } = props;
      return (
        <div
          ref={wrapRef}
          class={[
            bem('wrap', { scrollable: scrollable.value }),
            { [BORDER_TOP_BOTTOM]: type === 'line' && border },
          ]}
        >
          <div
            ref={navRef}
            role="tablist"
            class={bem('nav', [type, { complete: scrollable.value }])}
            style={navStyle.value}
          >
            {slots['nav-left']?.()}
            {renderNav()}
            {type === 'line' && (
              <div class={bem('line')} style={state.lineStyle} />
            )}
            {slots['nav-right']?.()}
          </div>
        </div>
      );
    };

    watch([() => props.color, windowSize.width], setLine);

    watch(
      () => props.active,
      (value) => {
        if (value !== currentName.value) {
          setCurrentIndexByName(value);
        }
      }
    );

    watch(
      () => children.length,
      () => {
        if (state.inited) {
          setCurrentIndexByName(props.active);
          setLine();
          nextTick(() => {
            scrollIntoView(true);
          });
        }
      }
    );

    watch(
      () => state.currentIndex,
      () => {
        scrollIntoView();
        setLine();

        // scroll to correct position
        if (stickyFixed && !props.scrollspy) {
          setRootScrollTop(
            Math.ceil(getElementTop(root.value!) - offsetTopPx.value)
          );
        }
      }
    );

    const init = () => {
      setCurrentIndexByName(props.active);
      nextTick(() => {
        state.inited = true;
        tabHeight = getVisibleHeight(wrapRef.value!);
        scrollIntoView(true);
      });
    };

    const onRendered = (name: string | number, title?: string) =>
      emit('rendered', name, title);

    useExpose({
      resize: setLine,
      scrollTo,
    });

    onActivated(setLine);
    onMountedOrActivated(init);
    useEventListener('scroll', onScroll, { target: scroller.value });

    linkChildren({
      props,
      setLine,
      onRendered,
      currentName,
      scrollIntoView,
    });

    return () => (
      <div ref={root} class={bem([props.type])}>
        {props.sticky ? (
          <Sticky
            container={root.value}
            offsetTop={offsetTopPx.value}
            onScroll={onStickyScroll}
          >
            {renderHeader()}
          </Sticky>
        ) : (
          renderHeader()
        )}
        <TabsContent
          count={children.length}
          inited={state.inited}
          animated={props.animated}
          duration={props.duration}
          swipeable={props.swipeable}
          lazyRender={props.lazyRender}
          currentIndex={state.currentIndex}
          onChange={setCurrentIndex}
        >
          {slots.default?.()}
        </TabsContent>
      </div>
    );
  },
});
