/**
 * Licensed to the Apache Software Foundation (ASF) under one
 * or more contributor license agreements.  See the NOTICE file
 * distributed with this work for additional information
 * regarding copyright ownership.  The ASF licenses this file
 * to you under the Apache License, Version 2.0 (the
 * "License"); you may not use this file except in compliance
 * with the License.  You may obtain a copy of the License at
 *
 *   http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing,
 * software distributed under the License is distributed on an
 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
 * KIND, either express or implied.  See the License for the
 * specific language governing permissions and limitations
 * under the License.
 */
import { useCallback, useEffect, useMemo, useState, memo } from 'react';
import PropTypes from 'prop-types';
import { t, usePrevious, useTheme, styled } from '@superset-ui/core';
import { useSelector } from 'react-redux';
import { Icons } from '@superset-ui/core/components/Icons';
import { LOG_ACTIONS_SELECT_DASHBOARD_TAB } from 'src/logger/LogUtils';
import { Modal } from '@superset-ui/core/components';
import { DROP_LEFT, DROP_RIGHT } from 'src/dashboard/util/getDropPosition';
import { Draggable } from '../../dnd/DragDroppable';
import DashboardComponent from '../../../containers/DashboardComponent';
import findTabIndexByComponentId from '../../../util/findTabIndexByComponentId';
import getDirectPathToTabIndex from '../../../util/getDirectPathToTabIndex';
import getLeafComponentIdFromPath from '../../../util/getLeafComponentIdFromPath';
import { componentShape } from '../../../util/propShapes';
import { NEW_TAB_ID } from '../../../util/constants';
import { RENDER_TAB, RENDER_TAB_CONTENT } from '../Tab';
import { TABS_TYPE, TAB_TYPE } from '../../../util/componentTypes';
import TabsRenderer from '../TabsRenderer';

const propTypes = {
  id: PropTypes.string.isRequired,
  parentId: PropTypes.string.isRequired,
  component: componentShape.isRequired,
  parentComponent: componentShape.isRequired,
  index: PropTypes.number.isRequired,
  depth: PropTypes.number.isRequired,
  renderTabContent: PropTypes.bool, // whether to render tabs + content or just tabs
  editMode: PropTypes.bool.isRequired,
  renderHoverMenu: PropTypes.bool,
  activeTabs: PropTypes.arrayOf(PropTypes.string),

  // actions (from DashboardComponent.jsx)
  logEvent: PropTypes.func.isRequired,
  setActiveTab: PropTypes.func,

  // grid related
  availableColumnCount: PropTypes.number,
  columnWidth: PropTypes.number,
  onResizeStart: PropTypes.func,
  onResize: PropTypes.func,
  onResizeStop: PropTypes.func,

  // dnd
  createComponent: PropTypes.func.isRequired,
  handleComponentDrop: PropTypes.func.isRequired,
  onChangeTab: PropTypes.func.isRequired,
  deleteComponent: PropTypes.func.isRequired,
  updateComponents: PropTypes.func.isRequired,
};

const defaultProps = {
  setActiveTab() {},
  onResizeStart() {},
  onResize() {},
  onResizeStop() {},
};

const DropIndicator = styled.div`
  border: 2px solid ${({ theme }) => theme.colorPrimary};
  width: 5px;
  height: 100%;
  position: absolute;
  top: 0;
  ${({ pos }) => (pos === 'left' ? 'left: -4px' : 'right: -4px')};
  border-radius: 2px;
`;

const CloseIconWithDropIndicator = props => (
  <>
    <Icons.CloseOutlined iconSize="s" />
    {props.showDropIndicators.right && (
      <DropIndicator className="drop-indicator-right" pos="right" />
    )}
  </>
);

const Tabs = props => {
  const theme = useTheme();

  const nativeFilters = useSelector(state => state.nativeFilters);
  const activeTabs = useSelector(state => state.dashboardState.activeTabs);
  const directPathToChild = useSelector(
    state => state.dashboardState.directPathToChild,
  );
  const nativeFiltersBarOpen = useSelector(
    state => state.dashboardState.nativeFiltersBarOpen ?? false,
  );

  const { tabIndex: initTabIndex, activeKey: initActiveKey } = useMemo(() => {
    let tabIndex = Math.max(
      0,
      findTabIndexByComponentId({
        currentComponent: props.component,
        directPathToChild,
      }),
    );
    if (tabIndex === 0 && activeTabs?.length) {
      props.component.children.forEach((tabId, index) => {
        if (tabIndex === 0 && activeTabs?.includes(tabId)) {
          tabIndex = index;
        }
      });
    }
    const { children: tabIds } = props.component;
    const activeKey = tabIds[tabIndex];

    return {
      tabIndex,
      activeKey,
    };
  }, [activeTabs, props.component, directPathToChild]);

  const [activeKey, setActiveKey] = useState(initActiveKey);
  const [selectedTabIndex, setSelectedTabIndex] = useState(initTabIndex);
  const [dropPosition, setDropPosition] = useState(null);
  const [dragOverTabIndex, setDragOverTabIndex] = useState(null);
  const [draggingTabId, setDraggingTabId] = useState(null);
  const [tabToDelete, setTabToDelete] = useState(null);
  const prevActiveKey = usePrevious(activeKey);
  const prevDashboardId = usePrevious(props.dashboardId);
  const prevDirectPathToChild = usePrevious(directPathToChild);
  const prevTabIds = usePrevious(props.component.children);

  useEffect(() => {
    if (prevActiveKey) {
      props.setActiveTab(activeKey, prevActiveKey);
    } else {
      props.setActiveTab(activeKey);
    }
  }, [props.setActiveTab, prevActiveKey, activeKey]);

  useEffect(() => {
    if (prevDashboardId && props.dashboardId !== prevDashboardId) {
      setSelectedTabIndex(initTabIndex);
      setActiveKey(initActiveKey);
    }
  }, [props.dashboardId, prevDashboardId, initTabIndex, initActiveKey]);

  useEffect(() => {
    const maxIndex = Math.max(0, props.component.children.length - 1);
    if (selectedTabIndex > maxIndex) {
      setSelectedTabIndex(maxIndex);
    }
  }, [selectedTabIndex, props.component.children.length, setSelectedTabIndex]);

  useEffect(() => {
    const currTabsIds = props.component.children;

    if (props.isComponentVisible) {
      const nextFocusComponent = getLeafComponentIdFromPath(directPathToChild);
      const currentFocusComponent = getLeafComponentIdFromPath(
        prevDirectPathToChild,
      );

      // If the currently selected component is different than the new one,
      // or the tab length/order changed, calculate the new tab index and
      // replace it if it's different than the current one
      if (
        nextFocusComponent !== currentFocusComponent ||
        (nextFocusComponent === currentFocusComponent &&
          currTabsIds !== prevTabIds)
      ) {
        const nextTabIndex = findTabIndexByComponentId({
          currentComponent: props.component,
          directPathToChild,
        });

        // make sure nextFocusComponent is under this tabs component
        if (nextTabIndex > -1 && nextTabIndex !== selectedTabIndex) {
          setSelectedTabIndex(nextTabIndex);
          setActiveKey(currTabsIds[nextTabIndex]);
        }
      }
    }
  }, [
    props.component,
    directPathToChild,
    props.isComponentVisible,
    selectedTabIndex,
    prevDirectPathToChild,
    prevTabIds,
  ]);

  const handleClickTab = useCallback(
    tabIndex => {
      const { component } = props;
      const { children: tabIds } = component;

      if (tabIndex !== selectedTabIndex) {
        const pathToTabIndex = getDirectPathToTabIndex(component, tabIndex);
        const targetTabId = pathToTabIndex[pathToTabIndex.length - 1];
        props.logEvent(LOG_ACTIONS_SELECT_DASHBOARD_TAB, {
          target_id: targetTabId,
          index: tabIndex,
        });

        props.onChangeTab({ pathToTabIndex });
      }
      setActiveKey(tabIds[tabIndex]);
    },
    [
      props.component,
      props.logEvent,
      props.onChangeTab,
      selectedTabIndex,
      setActiveKey,
    ],
  );

  const handleDropOnTab = useCallback(
    dropResult => {
      const { component } = props;

      // Ensure dropped tab is visible
      const { destination } = dropResult;
      if (destination) {
        const dropTabIndex =
          destination.id === component.id
            ? destination.index // dropped ON tabs
            : component.children.indexOf(destination.id); // dropped IN tab

        if (dropTabIndex > -1) {
          setTimeout(() => {
            handleClickTab(dropTabIndex);
          }, 30);
        }
      }
    },
    [props.component, handleClickTab],
  );

  const handleDrop = useCallback(
    dropResult => {
      if (dropResult.dragging.type !== TABS_TYPE) {
        props.handleComponentDrop(dropResult);
      }
    },
    [props.handleComponentDrop],
  );

  const handleDeleteTab = useCallback(
    tabIndex => {
      // If we're removing the currently selected tab,
      // select the previous one (if any)
      if (selectedTabIndex === tabIndex) {
        handleClickTab(Math.max(0, tabIndex - 1));
      }
    },
    [selectedTabIndex, handleClickTab],
  );

  const showDeleteConfirmModal = useCallback(key => {
    setTabToDelete(key);
  }, []);

  const handleConfirmTabDelete = useCallback(() => {
    if (tabToDelete) {
      const { component, deleteComponent } = props;
      deleteComponent(tabToDelete, component.id);
      const tabIndex = component.children.indexOf(tabToDelete);
      handleDeleteTab(tabIndex);
      setTabToDelete(null);
    }
  }, [tabToDelete, props.component, props.deleteComponent, handleDeleteTab]);

  const handleCancelTabDelete = useCallback(() => {
    setTabToDelete(null);
  }, []);

  const handleEdit = useCallback(
    (event, action) => {
      const { component, createComponent } = props;
      if (action === 'add') {
        // Prevent the tab container to be selected
        event?.stopPropagation?.();

        createComponent({
          destination: {
            id: component.id,
            type: component.type,
            index: component.children.length,
          },
          dragging: {
            id: NEW_TAB_ID,
            type: TAB_TYPE,
          },
        });
      } else if (action === 'remove') {
        showDeleteConfirmModal(event);
      }
    },
    [props.component, props.createComponent, showDeleteConfirmModal],
  );

  const handleDeleteComponent = useCallback(() => {
    const { deleteComponent, id, parentId } = props;
    deleteComponent(id, parentId);
  }, [props.deleteComponent, props.id, props.parentId]);

  const handleGetDropPosition = useCallback(dragObject => {
    const { dropIndicator, isDraggingOver, index } = dragObject;

    if (isDraggingOver) {
      setDropPosition(dropIndicator);
      setDragOverTabIndex(index);
    } else {
      setDropPosition(null);
    }
  }, []);

  const handleDragggingTab = useCallback(tabId => {
    if (tabId) {
      setDraggingTabId(tabId);
    } else {
      setDraggingTabId(null);
    }
  }, []);

  const {
    depth,
    component: tabsComponent,
    parentComponent,
    index,
    availableColumnCount = 0,
    columnWidth = 0,
    onResizeStart,
    onResize,
    onResizeStop,
    renderTabContent = true,
    renderHoverMenu = true,
    isComponentVisible: isCurrentTabVisible,
    editMode,
  } = props;

  const { children: tabIds } = tabsComponent;

  const tabBarPaddingLeft =
    renderTabContent === false
      ? nativeFiltersBarOpen
        ? 0
        : theme.sizeUnit * 4
      : 0;

  const showDropIndicators = useCallback(
    currentDropTabIndex =>
      currentDropTabIndex === dragOverTabIndex && {
        left: editMode && dropPosition === DROP_LEFT,
        right: editMode && dropPosition === DROP_RIGHT,
      },
    [dragOverTabIndex, dropPosition, editMode],
  );

  const removeDraggedTab = useCallback(
    tabID => draggingTabId === tabID,
    [draggingTabId],
  );

  // Extract tab highlighting logic into a hook
  const useTabHighlighting = useCallback(() => {
    const highlightedFilterId =
      nativeFilters?.focusedFilterId || nativeFilters?.hoveredFilterId;
    return highlightedFilterId
      ? nativeFilters.filters[highlightedFilterId]?.tabsInScope
      : undefined;
  }, [nativeFilters]);

  const tabsToHighlight = useTabHighlighting();

  // Extract tab items creation logic into a memoized value (not a hook inside hook)
  const tabItems = useMemo(
    () =>
      tabIds.map((tabId, tabIndex) => ({
        key: tabId,
        label: removeDraggedTab(tabId) ? (
          <></>
        ) : (
          <>
            {showDropIndicators(tabIndex).left && (
              <DropIndicator className="drop-indicator-left" pos="left" />
            )}
            <DashboardComponent
              id={tabId}
              parentId={tabsComponent.id}
              depth={depth}
              index={tabIndex}
              renderType={RENDER_TAB}
              availableColumnCount={availableColumnCount}
              columnWidth={columnWidth}
              onDropOnTab={handleDropOnTab}
              onDropPositionChange={handleGetDropPosition}
              onDragTab={handleDragggingTab}
              onHoverTab={() => handleClickTab(tabIndex)}
              isFocused={activeKey === tabId}
              isHighlighted={
                activeKey !== tabId && tabsToHighlight?.includes(tabId)
              }
            />
          </>
        ),
        closeIcon: removeDraggedTab(tabId) ? (
          <></>
        ) : (
          <CloseIconWithDropIndicator
            role="button"
            tabIndex={tabIndex}
            showDropIndicators={showDropIndicators(tabIndex)}
          />
        ),
        children: renderTabContent && (
          <DashboardComponent
            id={tabId}
            parentId={tabsComponent.id}
            depth={depth} // see isValidChild.js for why tabs don't increment child depth
            index={tabIndex}
            renderType={RENDER_TAB_CONTENT}
            availableColumnCount={availableColumnCount}
            columnWidth={columnWidth}
            onResizeStart={onResizeStart}
            onResize={onResize}
            onResizeStop={onResizeStop}
            onDropOnTab={handleDropOnTab}
            isComponentVisible={
              selectedTabIndex === tabIndex && isCurrentTabVisible
            }
          />
        ),
      })),
    [
      tabIds,
      removeDraggedTab,
      showDropIndicators,
      tabsComponent.id,
      depth,
      availableColumnCount,
      columnWidth,
      handleDropOnTab,
      handleGetDropPosition,
      handleDragggingTab,
      handleClickTab,
      activeKey,
      tabsToHighlight,
      renderTabContent,
      onResizeStart,
      onResize,
      onResizeStop,
      selectedTabIndex,
      isCurrentTabVisible,
    ],
  );

  const renderChild = useCallback(
    ({ dragSourceRef: tabsDragSourceRef }) => (
      <TabsRenderer
        tabItems={tabItems}
        editMode={editMode}
        renderHoverMenu={renderHoverMenu}
        tabsDragSourceRef={tabsDragSourceRef}
        handleDeleteComponent={handleDeleteComponent}
        tabsComponent={tabsComponent}
        activeKey={activeKey}
        tabIds={tabIds}
        handleClickTab={handleClickTab}
        handleEdit={handleEdit}
        tabBarPaddingLeft={tabBarPaddingLeft}
      />
    ),
    [
      tabItems,
      editMode,
      renderHoverMenu,
      handleDeleteComponent,
      tabsComponent,
      activeKey,
      tabIds,
      handleClickTab,
      handleEdit,
      tabBarPaddingLeft,
    ],
  );

  return (
    <>
      <Draggable
        component={tabsComponent}
        parentComponent={parentComponent}
        orientation="row"
        index={index}
        depth={depth}
        onDrop={handleDrop}
        editMode={editMode}
      >
        {renderChild}
      </Draggable>
      {tabToDelete && (
        <Modal
          show={!!tabToDelete}
          onHide={handleCancelTabDelete}
          onHandledPrimaryAction={handleConfirmTabDelete}
          primaryButtonName={t('DELETE')}
          primaryButtonStyle="danger"
          title={t('Delete dashboard tab?')}
          centered
        >
          <span>
            {t(
              'Deleting a tab will remove all content within it and will deactivate any related alerts or reports. You may still ' +
                'reverse this action with the',
            )}{' '}
            <b>{t('undo')}</b>{' '}
            {t('button (cmd + z) until you save your changes.')}
          </span>
        </Modal>
      )}
    </>
  );
};

Tabs.propTypes = propTypes;
Tabs.defaultProps = defaultProps;

export default memo(Tabs);
