import { clsx } from 'clsx';
import clamp from 'lodash.clamp';
import { Component, createRef } from 'react';

import { Width, Direction } from '../common';
import { DirectionContext } from '../provider/direction';

import Tab from './Tab';
import TabList from './TabList';
import TabPanel from './TabPanel';
import {
  getSwipeDifference,
  swipedLeftToRight,
  swipedRightToLeft,
  swipeShouldChangeTab,
  Swipe,
} from './utils';

const MIN_INDEX = 0;

export interface TabItem {
  title: string;
  content: React.ReactNode;
  disabled: boolean;
}

export interface TabsProps {
  tabs: TabItem[];
  selected: number;
  name: string;
  /** @default true */
  changeTabOnSwipe?: boolean;
  className?: string;
  /** @default 'block' */
  headerWidth?: `${Width}`;
  onTabSelect: (index: number) => void;
}

interface TabsState {
  start: Swipe | null;
  translateLineX: string | null;
  isSwiping: boolean;
  isScrolling: boolean;
  fullWidthTabs: boolean;
}

export default class Tabs extends Component<TabsProps, TabsState> {
  declare props: TabsProps & Required<Pick<TabsProps, keyof typeof Tabs.defaultProps>>;

  static defaultProps = {
    changeTabOnSwipe: true,
    headerWidth: Width.BLOCK,
  } satisfies Partial<TabsProps>;

  containerReference = createRef<HTMLDivElement>();

  constructor(props: Tabs['props']) {
    super(props);
    this.state = {
      start: null,
      translateLineX: null,
      isSwiping: false,
      isScrolling: false,
      fullWidthTabs: props.headerWidth === Width.BLOCK,
    };
  }

  container: HTMLDivElement | null = null;

  containerWidth = 0;

  tabRefs: (HTMLLIElement | null)[] = [];

  get MAX_INDEX() {
    return this.props.tabs.length - 1;
  }

  componentDidMount() {
    const { selected } = this.props;

    this.setTabWidth();
    this.switchTab(clamp(selected, MIN_INDEX, this.MAX_INDEX));
    this.animateLine(clamp(selected, MIN_INDEX, this.MAX_INDEX));
    document.body.addEventListener('touchmove', this.disableScroll, { passive: false });
    document.body.addEventListener('touchforcechange', this.disableScroll, { passive: false });
    window.addEventListener('resize', this.handleResize);
  }

  componentDidUpdate(previousProps: TabsProps) {
    const currentSelected = this.props.selected;
    const previousSelected = previousProps.selected;
    const currentSelectedTab = this.props.tabs[currentSelected];
    const currentSelectedTabIsDisabled = currentSelectedTab?.disabled;
    const previousSelectedTab = previousProps.tabs[previousSelected];
    const previousSelectedTabIsDisabled = previousSelectedTab?.disabled;
    const currentTabsLength = this.props.tabs.length;
    const previousTabsLength = previousProps.tabs.length;
    const currentDisabledTabsLength = this.props.tabs.filter((tab) => !tab.disabled).length;
    const previousDisabledTabsLength = previousProps.tabs.filter((tab) => !tab.disabled).length;
    const currentHeaderWidth = this.props.headerWidth;
    const previousFullHeaderWidth = previousProps.headerWidth;

    if (
      currentHeaderWidth !== previousFullHeaderWidth ||
      currentTabsLength !== previousTabsLength
    ) {
      this.setTabWidth();
    }

    if (
      currentSelected !== previousSelected ||
      currentDisabledTabsLength !== previousDisabledTabsLength ||
      currentSelectedTabIsDisabled !== previousSelectedTabIsDisabled
    ) {
      this.animateLine(clamp(currentSelected, MIN_INDEX, this.MAX_INDEX));
    }
  }

  componentWillUnmount() {
    document.body.removeEventListener('touchmove', this.disableScroll);
    document.body.removeEventListener('touchforcechange', this.disableScroll);
    window.removeEventListener('resize', this.handleResize);
  }

  handleResize = () => {
    this.setContainerWidth(this.container);
  };

  setContainerRefAndWidth = (node: HTMLDivElement | null) => {
    this.container = node;
    this.setContainerWidth(node);
  };

  setContainerWidth = (node: HTMLDivElement | null) => {
    if (!node) {
      return;
    }

    const { width } = node.getBoundingClientRect();

    this.containerWidth = width;
  };

  isTabDisabled = (index: number) => {
    const { tabs } = this.props;

    return tabs[index]?.disabled ?? false;
  };

  getAllTabsWidth = () => {
    return this.tabRefs
      .map((reference) => {
        return reference ? reference.getBoundingClientRect().width : 0;
      })
      .reduce((a, b) => a + b, 0);
  };

  getDistanceToSelectedTab = (selectedTabIndex: number) => {
    return this.tabRefs
      .filter((_, idx) => idx < selectedTabIndex)
      .map((reference) => (reference ? reference.getBoundingClientRect().width : 0))
      .reduce((a, b) => a + b, 0);
  };

  setTabWidth = () => {
    const { fullWidthTabs } = this.state;
    const { headerWidth, selected } = this.props;

    const allTabsWidth = this.getAllTabsWidth();

    if (!fullWidthTabs && (headerWidth === Width.BLOCK || this.containerWidth < allTabsWidth)) {
      this.setState({ fullWidthTabs: true, translateLineX: `${selected * 100}%` });
    }
    if (fullWidthTabs && headerWidth === Width.AUTO && this.containerWidth >= allTabsWidth) {
      this.setState({
        fullWidthTabs: false,
        translateLineX: `${this.getDistanceToSelectedTab(selected)}px`,
      });
    }
  };

  getTabLineWidth = () => {
    const { fullWidthTabs } = this.state;
    const { selected, tabs } = this.props;

    if (fullWidthTabs) {
      return `${(1 / tabs.length) * 100}%`;
    }

    const reference = this.tabRefs[selected] || this.tabRefs[this.tabRefs.length - 1];
    const width = reference ? reference.getBoundingClientRect().width : 0;
    return `${width}px`;
  };

  /*
   * Gets the next tab that should be selected based on the swipe direction
   * and the current selected tab (is called recursively to account for disabled tabs).
   */
  getTabToSelect = (selected: number, start: Swipe, end: Swipe): number => {
    let nextSelected = selected;

    if (swipedLeftToRight(start, end)) {
      nextSelected -= 1;

      if (nextSelected > MIN_INDEX && this.isTabDisabled(nextSelected)) {
        return this.getTabToSelect(nextSelected, start, end);
      }
    } else if (swipedRightToLeft(start, end)) {
      nextSelected += 1;

      if (nextSelected < this.MAX_INDEX && this.isTabDisabled(nextSelected)) {
        return this.getTabToSelect(nextSelected, start, end);
      }
    }

    nextSelected = clamp(
      nextSelected,
      Math.max(selected - 1, MIN_INDEX),
      Math.min(selected + 1, this.MAX_INDEX),
    );

    if (this.isTabDisabled(nextSelected)) {
      return selected;
    }

    return nextSelected;
  };

  swipedOverHalfOfContainer = (difference: number) => difference / this.containerWidth >= 0.5;

  switchTab = (index: number) => {
    const { onTabSelect } = this.props;
    onTabSelect(index);
  };

  animateLine = (index: number) => {
    this.setState((previousState) => ({
      translateLineX: previousState.fullWidthTabs
        ? `${index * 100}%`
        : `${this.getDistanceToSelectedTab(index)}px`,
    }));
  };

  disableScroll = (event: Event) => {
    const { isSwiping } = this.state;

    if (isSwiping) {
      event.preventDefault();
    }
  };

  handleTabClick = (index: number) => () => {
    this.switchTab(index);
  };

  onKeyDown = (index: number) => (event: React.KeyboardEvent<HTMLLIElement>) => {
    if (event && event.key === 'Enter') {
      this.switchTab(index);
    }
  };

  handleTouchStart: React.TouchEventHandler<HTMLDivElement> = (event) => {
    const start = {
      x: event.nativeEvent.touches[0].clientX,
      y: event.nativeEvent.touches[0].clientY,
      time: Date.now(),
    };
    this.setState({
      start,
    });
  };

  handleTouchMove: React.TouchEventHandler<HTMLDivElement> = (event) => {
    const { start } = this.state;
    if (start == null) {
      return;
    }

    const { selected: currentSelectedFromProps } = this.props;
    const end: Swipe = {
      x: event.nativeEvent.changedTouches[0].clientX,
      y: event.nativeEvent.changedTouches[0].clientY,
      time: Date.now(),
    };
    const difference = getSwipeDifference(start, end);
    const yAxisDifference = getSwipeDifference(start, end, 'y');
    let { isScrolling, isSwiping } = this.state;

    if (!isScrolling && !isSwiping) {
      if (difference > yAxisDifference) {
        isSwiping = true;
      } else {
        isScrolling = true;
      }
    }

    this.setState({ isScrolling, isSwiping });

    if (isSwiping) {
      const nextSelected = this.getTabToSelect(currentSelectedFromProps, start, end);
      this.animateLine(
        this.swipedOverHalfOfContainer(difference) ? nextSelected : currentSelectedFromProps,
      );
    }
  };

  handleTouchEnd: React.TouchEventHandler<HTMLDivElement> = (event) => {
    const { start, isSwiping } = this.state;
    if (start == null) {
      return;
    }

    const { selected } = this.props;
    const end: Swipe = {
      x: event.nativeEvent.changedTouches[0].clientX,
      y: event.nativeEvent.changedTouches[0].clientY,
      time: Date.now(),
    };
    const difference = getSwipeDifference(start, end);

    let nextSelected = selected;

    if (isSwiping) {
      if (swipeShouldChangeTab(start, end) || this.swipedOverHalfOfContainer(difference)) {
        nextSelected = this.getTabToSelect(nextSelected, start, end);
      }

      if (nextSelected !== selected) {
        this.switchTab(nextSelected);
      } else {
        this.animateLine(nextSelected);
      }
    }

    this.setState({ isSwiping: false, isScrolling: false });
  };

  render() {
    const { tabs, changeTabOnSwipe, name, selected, className, headerWidth } = this.props;
    const { translateLineX, fullWidthTabs } = this.state;

    const selectedTab = tabs[selected];

    return (
      <DirectionContext.Consumer>
        {(direction) => {
          const isRTL = direction === Direction.RTL;
          return (
            <div
              ref={this.containerReference}
              className={clsx('tabs', className, {
                'tabs--auto-width': headerWidth === Width.AUTO,
              })}
              onTouchStart={changeTabOnSwipe ? this.handleTouchStart : undefined}
              onTouchEnd={changeTabOnSwipe ? this.handleTouchEnd : undefined}
              onTouchMove={changeTabOnSwipe ? this.handleTouchMove : undefined}
            >
              <TabList>
                {tabs.map(({ title, disabled }, index) => {
                  return (
                    <Tab
                      key={title}
                      ref={(node) => {
                        this.tabRefs[index] = node;
                      }}
                      id={`${name}-tab-${index}`}
                      panelId={`${name}-panel-${index}`}
                      selected={selected === index}
                      disabled={disabled}
                      focusTab={() => {
                        if (this.containerReference.current?.contains(document.activeElement)) {
                          this.tabRefs[index]?.focus();
                        }
                      }}
                      onClick={disabled ? undefined : this.handleTabClick(index)}
                      onKeyDown={this.onKeyDown(index)}
                      {...(fullWidthTabs
                        ? { style: { width: `${(1 / tabs.length) * 100}%` } }
                        : {})}
                    >
                      {title}
                    </Tab>
                  );
                })}
                {translateLineX ? (
                  <li
                    role="none"
                    className="tabs__line"
                    style={{
                      width: this.getTabLineWidth(),
                      transform: isRTL
                        ? `translateX(-${translateLineX})`
                        : `translateX(${translateLineX})`,
                    }}
                  />
                ) : null}
              </TabList>
              <div ref={this.setContainerRefAndWidth} className="tabs__panel-container">
                {selectedTab && !selectedTab.disabled ? (
                  <TabPanel
                    key={selectedTab.title}
                    tabId={`${name}-tab-${selected}`}
                    id={`${name}-panel-${selected}`}
                    style={{
                      width: '100%',
                    }}
                  >
                    {selectedTab.content}
                  </TabPanel>
                ) : null}
              </div>
            </div>
          );
        }}
      </DirectionContext.Consumer>
    );
  }
}
