import { ComponentType, PureComponent, ReactElement, ReactNode, RefObject } from 'react';
import { LayoutChangeEvent, View, ViewProps } from 'react-native';
import { IOContext, IOContextValue } from './IOContext';
import { ObserverInstance } from './IOManager';
import { Element } from './IntersectionObserver';

export interface RenderProps {
  inView: boolean;
  onChange: (inView: boolean) => void;
}

export interface Props {
  [key: string]: any;
}

export type InViewProps<T = Props> = T & {
  children: ReactNode | ((fields: RenderProps) => ReactElement<View>);
  as?: ComponentType<any>;
  triggerOnce?: boolean;
  onLayout?: (event: LayoutChangeEvent) => void;
  onChange?: (inView: boolean, areaThreshold: number) => void;
};

export type InViewWrapper = ComponentType<{
  ref?: RefObject<any> | ((ref: any) => void);
  onLayout?: (event: LayoutChangeEvent) => void;
}>;

/**
 * @public
 * @category 화면 제어
 * @name InView
 * @description
 * `InView` 컴포넌트는 화면에 요소가 보이기 시작하거나 화면에서 사라지는 것을 감지하는 컴포넌트예요.
 * 요소가 화면에 조금이라도 보이기 시작하면 `onChanged` 핸들러가 호출되고 첫 번째 인자로 `true` 값이 전달돼요. 반대로 요소가 화면에서 사라지면 `false` 값이 전달돼요.
 * `onChanged` 핸들러의 두 번째 인자로 요소의 화면 노출 비율이 전달돼요. 노출 비율 값은 `0`에서 `1.0` 사이예요. 예를 들어 `0.2`가 전달되면 컴포넌트가 20%만큼 화면에 노출된 상태라는 의미예요.
 *
::: warning 유의하세요

`InView`는 반드시 `IOContext`가 포함된 [IOScrollView](/reference/react-native-bedrock/화면%20제어/InView.md) 또는 [IOFlatList](/reference/react-native-bedrock/화면%20제어/IOFlatList.md) 내부에서 사용해야 해요.
만약 `IOContext` 외부에서 사용하면 `IOProviderMissingError`가 발생해요.

:::

 * @param {Object} props - 컴포넌트에 전달되는 props 객체예요.
 * @param {React.ReactNode} props.children - 컴포넌트 하위에 렌더링될 자식 컴포넌트들이에요.
 * @param {React.ComponentType} [prop.as=View] - 실제 렌더링할 컴포넌트를 지정해요. 기본값은 [View](https://reactnative.dev/docs/view) 컴포넌트예요.
 * @param {boolean} [triggerOnce=false] - 요소가 화면에 처음 보일 때 한 번만 `onChange` 콜백을 호출하려면 이 옵션을 사용해요.
 * @param {(event: LayoutChangeEvent) => void} [onLayout] - 레이아웃에 변경이 생겼을 때 호출되는 콜백 함수예요.
 * @param {(inView: boolean, areaThreshold: number) => void} [onChange] - 요소가 화면에 나타나거나 사라질 때 호출되는 콜백 함수예요. 첫 번째 인자로 노출 여부가, 두 번째 인자로 노출 비율이 전달돼요.
 * 
 * @example
 * 
 * ### `InView`컴포넌트로 요소의 `10%` 지점을 감지하기
 * 
 * ```tsx
 * import { LayoutChangeEvent, View, Text, Dimensions } from 'react-native';
 * import { InView, IOScrollView } from 'react-native-bedrock';
 * 
 * export function InViewExample() {
 *   const handleLayout = (event: LayoutChangeEvent) => {
 *     console.log('레이아웃 변경됨', event.nativeEvent.layout);
 *   };
 * 
 *   const handleChange = (inView: boolean, areaThreshold: number) => {
 *     if (inView) {
 *       console.log(`${areaThreshold * 100}% 비율만큼 화면에 보이는 상태`);
 *     } else {
 *       console.log('화면에 보이지 않는 상태');
 *     }
 *   };
 * 
 *   return (
 *     <IOScrollView>
 *       <View style={{ height: HEIGHT, width: '100%', backgroundColor: 'blue' }}>
 *         <Text style={{ color: 'white' }}>스크롤을 내려주세요</Text>
 *       </View>
 *       <InView onLayout={handleLayout} onChange={handleChange}>
 *         <View style={{ width: 100, height: 300, backgroundColor: 'yellow' }}>
 *           <View style={{ position: 'absolute', top: 30, width: 100, height: 1, borderWidth: 1 }}>
 *             <Text style={{ position: 'absolute', top: 0 }}>10% 지점</Text>
 *           </View>
 *         </View>
 *       </InView>
 *     </IOScrollView>
 *   );
 * }
 * ```
 */
export class InView<T = ViewProps> extends PureComponent<InViewProps<T>> {
  static contextType = IOContext;
  static defaultProps: Partial<InViewProps> = {
    triggerOnce: false,
    as: View,
  };

  context: undefined | IOContextValue = undefined;
  mounted = false;

  protected element: Element;
  protected instance: undefined | ObserverInstance;
  protected view: any;

  constructor(props: InViewProps<T>) {
    super(props);

    this.element = {
      inView: false,
      intersectionRatio: 0,
      layout: {
        x: 0,
        y: 0,
        width: 0,
        height: 0,
      },
      measureLayout: this.measureLayout,
    };
  }

  componentDidMount() {
    this.mounted = true;
    if (this.context?.manager) {
      this.instance = this.context.manager.observe(this.element, this.handleChange);
    }
  }

  componentWillUnmount() {
    this.mounted = false;
    if (this.context?.manager && this.instance) {
      this.context.manager.unobserve(this.element);
    }
  }

  protected handleChange = (inView: boolean, areaThreshold: number) => {
    if (this.mounted) {
      const { triggerOnce, onChange } = this.props;
      if (inView && triggerOnce) {
        if (this.context?.manager) {
          this.context?.manager.unobserve(this.element);
        }
      }
      if (onChange) {
        onChange(inView, areaThreshold);
      }
    }
  };

  protected handleRef = (ref: any) => {
    this.view = ref;
  };

  protected handleLayout = (event: LayoutChangeEvent) => {
    const {
      nativeEvent: { layout },
    } = event;
    if (layout.width !== this.element.layout.width || layout.height !== this.element.layout.height) {
      if (this.element.onLayout) {
        this.element.onLayout();
      }
    }
    const { onLayout } = this.props;
    if (onLayout) {
      onLayout(event);
    }
  };

  measure = (...args: any) => {
    this.view.measure(...args);
  };

  measureInWindow = (...args: any) => {
    this.view.measureInWindow(...args);
  };

  measureLayout = (...args: any) => {
    this.view.measureLayout(...args);
  };

  setNativeProps = (...args: any) => {
    this.view.setNativeProps(...args);
  };

  focus = (...args: any) => {
    this.view.focus(...args);
  };

  blur = (...args: any) => {
    this.view.blur(...args);
  };

  render() {
    const { as, children, ...props } = this.props;
    if (typeof children === 'function') {
      return null;
    }
    const ViewComponent: InViewWrapper = (as || View) as InViewWrapper;
    return (
      <ViewComponent {...props} ref={this.handleRef} onLayout={this.handleLayout}>
        {children}
      </ViewComponent>
    );
  }
}
