/*
 * SPDX-License-Identifier: Apache-2.0
 *
 * The OpenSearch Contributors require contributions made to
 * this file be licensed under the Apache-2.0 license or a
 * compatible open source license.
 *
 * Modifications Copyright OpenSearch Contributors. See
 * GitHub history for details.
 */

/*
 * Licensed to Elasticsearch B.V. under one or more contributor
 * license agreements. See the NOTICE file distributed with
 * this work for additional information regarding copyright
 * ownership. Elasticsearch B.V. 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 React, {
  ReactNode,
  ReactElement,
  useEffect,
  useRef,
  useCallback,
  CSSProperties,
  FunctionComponent,
  HTMLAttributes,
  ComponentType,
} from 'react';
import classNames from 'classnames';

import { CommonProps } from '../common';
import { keys } from '../../services';
import { useResizeObserver } from '../observer/resize_observer';
import { OuiResizableContainerContextProvider } from './context';
import {
  OuiResizableButtonProps,
  ouiResizableButtonWithControls,
} from './resizable_button';
import {
  OuiResizablePanelProps,
  ouiResizablePanelWithControls,
  getModeType,
  ToggleCollapseCallback,
} from './resizable_panel';
import { useContainerCallbacks, getPosition } from './helpers';
import {
  OuiResizableButtonMouseEvent,
  OuiResizableButtonKeyDownEvent,
  OuiResizableContainerState,
  OuiResizableContainerActions,
} from './types';

const containerDirections = {
  vertical: 'vertical',
  horizontal: 'horizontal',
};

export interface OuiResizableContainerProps
  extends HTMLAttributes<HTMLDivElement>,
    CommonProps {
  /**
   * Specify the container direction
   */
  direction?: keyof typeof containerDirections;
  /**
   * Pure function which accepts Panel and Resizer components in arguments
   * and returns a component tree
   */
  children: (
    Panel: ComponentType<OuiResizablePanelProps>,
    Resizer: ComponentType<OuiResizableButtonProps>,
    actions: Partial<OuiResizableContainerActions>
  ) => ReactNode;
  /**
   * Pure function which accepts an object where keys are IDs of panels, which sizes were changed,
   * and values are actual sizes in percents
   */
  onPanelWidthChange?: ({}: { [key: string]: number }) => any;
  onToggleCollapsed?: ToggleCollapseCallback;
  style?: CSSProperties;
}

const initialState: OuiResizableContainerState = {
  isDragging: false,
  currentResizerPos: -1,
  prevPanelId: null,
  nextPanelId: null,
  containerSize: 1,
  panels: {},
  resizers: {},
};

export const OuiResizableContainer: FunctionComponent<OuiResizableContainerProps> = ({
  direction = 'horizontal',
  children,
  className,
  onPanelWidthChange,
  onToggleCollapsed,
  ...rest
}) => {
  const containerRef = useRef<HTMLDivElement>(null);
  const isHorizontal = direction === containerDirections.horizontal;

  const classes = classNames(
    'ouiResizableContainer',
    {
      'ouiResizableContainer--vertical': !isHorizontal,
      'ouiResizableContainer--horizontal': isHorizontal,
    },
    className
  );

  const [actions, reducerState] = useContainerCallbacks({
    initialState: { ...initialState, isHorizontal },
    containerRef,
    onPanelWidthChange,
  });

  const containerSize = useResizeObserver(
    containerRef.current,
    isHorizontal ? 'width' : 'height'
  );

  const initialize = useCallback(() => {
    actions.initContainer(isHorizontal);
  }, [actions, isHorizontal]);

  useEffect(() => {
    if (containerSize.width > 0 && containerSize.height > 0) {
      initialize();
    }
  }, [initialize, containerSize]);

  const onMouseDown = useCallback(
    (event: OuiResizableButtonMouseEvent) => {
      const currentTarget = event.currentTarget;
      const prevPanel = currentTarget.previousElementSibling;
      const nextPanel = currentTarget.nextElementSibling;
      if (!prevPanel || !nextPanel) return;
      const prevPanelId = prevPanel!.id;
      const nextPanelId = nextPanel!.id;
      const position = getPosition(event, isHorizontal);
      actions.dragStart({ position, prevPanelId, nextPanelId });
    },
    [actions, isHorizontal]
  );

  const onMouseMove = useCallback(
    (event: React.MouseEvent | React.TouchEvent) => {
      if (
        !reducerState.prevPanelId ||
        !reducerState.nextPanelId ||
        !reducerState.isDragging
      )
        return;
      const position = getPosition(event, isHorizontal);
      actions.dragMove({
        position,
        prevPanelId: reducerState.prevPanelId,
        nextPanelId: reducerState.nextPanelId,
      });
    },
    [
      actions,
      isHorizontal,
      reducerState.prevPanelId,
      reducerState.nextPanelId,
      reducerState.isDragging,
    ]
  );

  const onKeyDown = useCallback(
    (event: OuiResizableButtonKeyDownEvent) => {
      const { key, currentTarget } = event;
      const shouldResizeHorizontalPanel =
        isHorizontal && (key === keys.ARROW_LEFT || key === keys.ARROW_RIGHT);
      const shouldResizeVerticalPanel =
        !isHorizontal && (key === keys.ARROW_UP || key === keys.ARROW_DOWN);
      const prevPanelId = currentTarget.previousElementSibling!.id;
      const nextPanelId = currentTarget.nextElementSibling!.id;
      let direction;
      if (key === keys.ARROW_DOWN || key === keys.ARROW_RIGHT) {
        direction = 'forward';
      }
      if (key === keys.ARROW_UP || key === keys.ARROW_LEFT) {
        direction = 'backward';
      }

      if (
        direction === 'forward' ||
        (direction === 'backward' &&
          (shouldResizeHorizontalPanel || shouldResizeVerticalPanel) &&
          prevPanelId &&
          nextPanelId)
      ) {
        event.preventDefault();
        actions.keyMove({ direction, prevPanelId, nextPanelId });
      }
    },
    [actions, isHorizontal]
  );

  const onMouseUp = useCallback(() => {
    actions.reset();
  }, [actions]);

  // eslint-disable-next-line react-hooks/exhaustive-deps
  const OuiResizableButton = useCallback(
    ouiResizableButtonWithControls({
      onKeyDown,
      onMouseDown,
      onTouchStart: onMouseDown,
      onFocus: actions.resizerFocus,
      onBlur: actions.resizerBlur,
      isHorizontal,
      registration: {
        register: actions.registerResizer,
        deregister: actions.deregisterResizer,
      },
    }),
    [actions, isHorizontal]
  );

  // eslint-disable-next-line react-hooks/exhaustive-deps
  const OuiResizablePanel = useCallback(
    ouiResizablePanelWithControls({
      isHorizontal,
      registration: {
        register: actions.registerPanel,
        deregister: actions.deregisterPanel,
      },
      onToggleCollapsed,
      onToggleCollapsedInternal: actions.togglePanel,
    }),
    [actions, isHorizontal]
  );

  const render = () => {
    const DEFAULT = 'custom';
    const content = children(OuiResizablePanel, OuiResizableButton, {
      togglePanel: actions.togglePanel,
    });
    const modes = React.isValidElement(content)
      ? content.props.children.map(
          (el: ReactElement) => getModeType(el.props.mode) || DEFAULT
        )
      : null;
    if (
      modes &&
      (['collapsible', 'main'].every((i) => modes.includes(i)) ||
        modes.every((i?: string) => i === DEFAULT))
    ) {
      return content;
    } else {
      throw new Error(
        'Both `collapsible` and `main` mode panels are required.'
      );
    }
  };

  return (
    <OuiResizableContainerContextProvider
      registry={{
        panels: reducerState.panels,
        resizers: reducerState.resizers,
      }}>
      <div
        className={classes}
        ref={containerRef}
        onMouseMove={reducerState.isDragging ? onMouseMove : undefined}
        onMouseUp={onMouseUp}
        onMouseLeave={onMouseUp}
        onTouchMove={onMouseMove}
        onTouchEnd={onMouseUp}
        {...rest}>
        {render()}
      </div>
    </OuiResizableContainerContextProvider>
  );
};
