import React from 'react';
import {
  Platform,
  requireNativeComponent,
  NativeModules,
  UIManager,
  findNodeHandle,
  ViewProps,
  NativeSyntheticEvent,
  ListRenderItemInfo,
  ImageSourcePropType,
} from 'react-native';
// @ts-ignore
import resolveAssetSource from 'react-native/Libraries/Image/resolveAssetSource';
import CallbacksManager from '../utils/CallbacksManager';
import {
  MapType,
  Animation,
  Point,
  DrivingInfo,
  MasstransitInfo,
  RoutesFoundEvent,
  Vehicles,
  CameraPosition,
  VisibleRegion,
  ScreenPoint,
  MapLoaded,
  InitialRegion,
  YandexLogoPosition,
  YandexLogoPadding,
  YamapNativeIcon,
} from '../interfaces';
import {processColorProps} from '../utils';

const {yamap: NativeYamapModule} = NativeModules;

export interface ClusteredYaMapProps<T = any> extends ViewProps {
  userLocationIcon?: ImageSourcePropType;
  userLocationNativeIcon?: YamapNativeIcon;
  userLocationIconScale?: number;
  clusteredMarkers: ReadonlyArray<{point: Point; data: T}>;
  renderMarker: (
    info: {point: Point; data: ListRenderItemInfo<T>},
    index: number,
  ) => React.ReactElement;
  clusterColor?: string;
  showUserPosition?: boolean;
  nightMode?: boolean;
  mapStyle?: string;
  mapType?: MapType;
  onCameraPositionChange?: (
    event: NativeSyntheticEvent<CameraPosition>,
  ) => void;
  onCameraPositionChangeEnd?: (
    event: NativeSyntheticEvent<CameraPosition>,
  ) => void;
  onMapPress?: (event: NativeSyntheticEvent<Point>) => void;
  onMapLongPress?: (event: NativeSyntheticEvent<Point>) => void;
  onMapLoaded?: (event: NativeSyntheticEvent<MapLoaded>) => void;
  userLocationAccuracyFillColor?: string;
  userLocationAccuracyStrokeColor?: string;
  userLocationAccuracyStrokeWidth?: number;
  scrollGesturesEnabled?: boolean;
  zoomGesturesEnabled?: boolean;
  tiltGesturesEnabled?: boolean;
  rotateGesturesEnabled?: boolean;
  fastTapEnabled?: boolean;
  initialRegion?: InitialRegion;
  maxFps?: number;
  followUser?: boolean;
  logoPosition?: YandexLogoPosition;
  logoPadding?: YandexLogoPadding;
}

const YaMapNativeComponent = requireNativeComponent<
  Omit<ClusteredYaMapProps, 'clusteredMarkers'> & {clusteredMarkers: Point[]}
>('ClusteredYamapView');

export class ClusteredYamap extends React.Component<ClusteredYaMapProps> {
  static defaultProps = {
    showUserPosition: true,
    clusterColor: 'red',
    maxFps: 60,
  };

  // @ts-ignore
  map = React.createRef<YaMapNativeComponent>();

  public static init(apiKey: string): Promise<void> {
    return NativeYamapModule.init(apiKey);
  }

  public static setLocale(locale: string): Promise<void> {
    return new Promise((resolve, reject) => {
      NativeYamapModule.setLocale(
        locale,
        () => resolve(),
        (err: string) => reject(new Error(err)),
      );
    });
  }

  public static getLocale(): Promise<string> {
    return new Promise((resolve, reject) => {
      NativeYamapModule.getLocale(
        (locale: string) => resolve(locale),
        (err: string) => reject(new Error(err)),
      );
    });
  }

  public static resetLocale(): Promise<void> {
    return new Promise((resolve, reject) => {
      NativeYamapModule.resetLocale(
        () => resolve(),
        (err: string) => reject(new Error(err)),
      );
    });
  }

  public findRoutes(
    points: Point[],
    vehicles: Vehicles[],
    callback: (event: RoutesFoundEvent<DrivingInfo | MasstransitInfo>) => void,
  ) {
    this._findRoutes(points, vehicles, callback);
  }

  public findPedestrianRoutes(
    points: Point[],
    callback: (event: RoutesFoundEvent<MasstransitInfo>) => void,
  ) {
    this._findRoutes(points, [], callback);
  }

  public findDrivingRoutes(
    points: Point[],
    callback: (event: RoutesFoundEvent<DrivingInfo>) => void,
  ) {
    this._findRoutes(points, ['car'], callback);
  }

  public fitAllMarkers() {
    UIManager.dispatchViewManagerCommand(
      findNodeHandle(this),
      this.getCommand('fitAllMarkers'),
      [],
    );
  }

  public setTrafficVisible(isVisible: boolean) {
    UIManager.dispatchViewManagerCommand(
      findNodeHandle(this),
      this.getCommand('setTrafficVisible'),
      [isVisible],
    );
  }

  public fitMarkers(points: Point[]) {
    UIManager.dispatchViewManagerCommand(
      findNodeHandle(this),
      this.getCommand('fitMarkers'),
      [points],
    );
  }

  public setCenter(
    center: {lon: number; lat: number; zoom?: number},
    zoom: number = center.zoom || 10,
    azimuth: number = 0,
    tilt: number = 0,
    duration: number = 0,
    animation: Animation = Animation.SMOOTH,
  ) {
    UIManager.dispatchViewManagerCommand(
      findNodeHandle(this),
      this.getCommand('setCenter'),
      [center, zoom, azimuth, tilt, duration, animation],
    );
  }

  public setZoom(
    zoom: number,
    duration: number = 0,
    animation: Animation = Animation.SMOOTH,
  ) {
    UIManager.dispatchViewManagerCommand(
      findNodeHandle(this),
      this.getCommand('setZoom'),
      [zoom, duration, animation],
    );
  }

  public getCameraPosition(callback: (position: CameraPosition) => void) {
    const cbId = CallbacksManager.addCallback(callback);
    UIManager.dispatchViewManagerCommand(
      findNodeHandle(this),
      this.getCommand('getCameraPosition'),
      [cbId],
    );
  }

  public getVisibleRegion(callback: (VisibleRegion: VisibleRegion) => void) {
    const cbId = CallbacksManager.addCallback(callback);
    UIManager.dispatchViewManagerCommand(
      findNodeHandle(this),
      this.getCommand('getVisibleRegion'),
      [cbId],
    );
  }

  public getScreenPoints(
    points: Point[],
    callback: (screenPoint: ScreenPoint) => void,
  ) {
    const cbId = CallbacksManager.addCallback(callback);
    UIManager.dispatchViewManagerCommand(
      findNodeHandle(this),
      this.getCommand('getScreenPoints'),
      [points, cbId],
    );
  }

  public getWorldPoints(
    points: ScreenPoint[],
    callback: (point: Point) => void,
  ) {
    const cbId = CallbacksManager.addCallback(callback);
    UIManager.dispatchViewManagerCommand(
      findNodeHandle(this),
      this.getCommand('getWorldPoints'),
      [points, cbId],
    );
  }

  private _findRoutes(
    points: Point[],
    vehicles: Vehicles[],
    callback:
      | ((event: RoutesFoundEvent<DrivingInfo | MasstransitInfo>) => void)
      | ((event: RoutesFoundEvent<DrivingInfo>) => void)
      | ((event: RoutesFoundEvent<MasstransitInfo>) => void),
  ) {
    const cbId = CallbacksManager.addCallback(callback);
    const args =
      Platform.OS === 'ios'
        ? [{points, vehicles, id: cbId}]
        : [points, vehicles, cbId];

    UIManager.dispatchViewManagerCommand(
      findNodeHandle(this),
      this.getCommand('findRoutes'),
      args,
    );
  }

  private getCommand(cmd: string): any {
    return Platform.OS === 'ios'
      ? UIManager.getViewManagerConfig('ClusteredYamapView').Commands[cmd]
      : cmd;
  }

  private processRoute(
    event: NativeSyntheticEvent<
      {id: string} & RoutesFoundEvent<DrivingInfo | MasstransitInfo>
    >,
  ) {
    const {id, ...routes} = event.nativeEvent;
    CallbacksManager.call(id, routes);
  }

  private processCameraPosition(
    event: NativeSyntheticEvent<{id: string} & CameraPosition>,
  ) {
    const {id, ...point} = event.nativeEvent;
    CallbacksManager.call(id, point);
  }

  private processVisibleRegion(
    event: NativeSyntheticEvent<{id: string} & VisibleRegion>,
  ) {
    const {id, ...visibleRegion} = event.nativeEvent;
    CallbacksManager.call(id, visibleRegion);
  }

  private processWorldToScreenPointsReceived(
    event: NativeSyntheticEvent<{id: string} & ScreenPoint[]>,
  ) {
    const {id, ...screenPoints} = event.nativeEvent;
    CallbacksManager.call(id, screenPoints);
  }

  private processScreenToWorldPointsReceived(
    event: NativeSyntheticEvent<{id: string} & Point[]>,
  ) {
    const {id, ...worldPoints} = event.nativeEvent;
    CallbacksManager.call(id, worldPoints);
  }
  private resolveImageUri(img: ImageSourcePropType) {
    return img ? resolveAssetSource(img).uri : '';
  }

  private getProps() {
    const props = {
      ...this.props,
      clusteredMarkers: this.props.clusteredMarkers.map(mark => mark.point),
      children: this.props.clusteredMarkers.map(this.props.renderMarker),
      onRouteFound: this.processRoute,
      onCameraPositionReceived: this.processCameraPosition,
      onVisibleRegionReceived: this.processVisibleRegion,
      onWorldToScreenPointsReceived: this.processWorldToScreenPointsReceived,
      onScreenToWorldPointsReceived: this.processScreenToWorldPointsReceived,
      userLocationIcon: this.props.userLocationIcon
        ? this.resolveImageUri(this.props.userLocationIcon)
        : undefined,
    };
    processColorProps(props, 'clusterColor' as keyof ClusteredYaMapProps);
    processColorProps(
      props,
      'userLocationAccuracyFillColor' as keyof ClusteredYaMapProps,
    );
    processColorProps(
      props,
      'userLocationAccuracyStrokeColor' as keyof ClusteredYaMapProps,
    );

    return props;
  }

  render() {
    return <YaMapNativeComponent {...this.getProps()} ref={this.map} />;
  }
}
