import { Platform, type EventSubscription, UnavailabilityError } from 'expo-modules-core';
import { Dimensions } from 'react-native';

import ExpoScreenOrientation from './ExpoScreenOrientation';
import {
  Orientation,
  type OrientationChangeEvent,
  type OrientationChangeListener,
  OrientationLock,
  type PlatformOrientationInfo,
  WebOrientationLock,
} from './ScreenOrientation.types';

export {
  Orientation,
  OrientationLock,
  type PlatformOrientationInfo,
  type OrientationChangeListener,
  type OrientationChangeEvent,
  WebOrientationLock,
  WebOrientation,
  SizeClassIOS,
  type ScreenOrientationInfo,
} from './ScreenOrientation.types';

// TODO(@kitten): Remove re-export from EMC
export type { EventSubscription as Subscription } from 'expo-modules-core';

let _orientationChangeSubscribers: EventSubscription[] = [];

let _lastOrientationLock: OrientationLock = OrientationLock.UNKNOWN;

// @needsAudit
/**
 * Lock the screen orientation to a particular `OrientationLock`.
 * @param orientationLock The orientation lock to apply. See the [`OrientationLock`](#orientationlock)
 * enum for possible values.
 * @return Returns a promise with `void` value, which fulfils when the orientation is set.
 *
 * @example
 * ```ts
 * async function changeScreenOrientation() {
 *   await ScreenOrientation.lockAsync(ScreenOrientation.OrientationLock.LANDSCAPE_LEFT);
 * }
 * ```
 */
export async function lockAsync(orientationLock: OrientationLock): Promise<void> {
  if (!ExpoScreenOrientation.lockAsync) {
    throw new UnavailabilityError('ScreenOrientation', 'lockAsync');
  }

  const orientationLocks = Object.values(OrientationLock);
  if (!orientationLocks.includes(orientationLock)) {
    throw new TypeError(`Invalid Orientation Lock: ${orientationLock}`);
  }

  if (orientationLock === OrientationLock.OTHER) {
    return;
  }

  await ExpoScreenOrientation.lockAsync(orientationLock);
  _lastOrientationLock = orientationLock;
}

// @needsAudit @docsMissing
/**
 * @param options The platform specific lock to apply. See the [`PlatformOrientationInfo`](#platformorientationinfo)
 * object type for the different platform formats.
 * @return Returns a promise with `void` value, resolving when the orientation is set and rejecting
 * if an invalid option or value is passed.
 */
export async function lockPlatformAsync(options: PlatformOrientationInfo): Promise<void> {
  if (!ExpoScreenOrientation.lockPlatformAsync) {
    throw new UnavailabilityError('ScreenOrientation', 'lockPlatformAsync');
  }

  const { screenOrientationConstantAndroid, screenOrientationArrayIOS, screenOrientationLockWeb } =
    options;
  let platformOrientationParam: any;
  if (Platform.OS === 'android' && screenOrientationConstantAndroid) {
    if (isNaN(screenOrientationConstantAndroid)) {
      throw new TypeError(
        `lockPlatformAsync Android platform: screenOrientationConstantAndroid cannot be called with ${screenOrientationConstantAndroid}`
      );
    }
    platformOrientationParam = screenOrientationConstantAndroid;
  } else if (Platform.OS === 'ios' && screenOrientationArrayIOS) {
    if (!Array.isArray(screenOrientationArrayIOS)) {
      throw new TypeError(
        `lockPlatformAsync iOS platform: screenOrientationArrayIOS cannot be called with ${screenOrientationArrayIOS}`
      );
    }

    const orientations = Object.values(Orientation);
    for (const orientation of screenOrientationArrayIOS) {
      if (!orientations.includes(orientation)) {
        throw new TypeError(
          `lockPlatformAsync iOS platform: ${orientation} is not a valid Orientation`
        );
      }
    }
    platformOrientationParam = screenOrientationArrayIOS;
  } else if (Platform.OS === 'web' && screenOrientationLockWeb) {
    const webOrientationLocks = Object.values(WebOrientationLock);
    if (!webOrientationLocks.includes(screenOrientationLockWeb)) {
      throw new TypeError(`Invalid Web Orientation Lock: ${screenOrientationLockWeb}`);
    }
    platformOrientationParam = screenOrientationLockWeb;
  }

  if (!platformOrientationParam) {
    throw new TypeError('lockPlatformAsync cannot be called with undefined option properties');
  }
  await ExpoScreenOrientation.lockPlatformAsync(platformOrientationParam);
  _lastOrientationLock = OrientationLock.OTHER;
}

// @needsAudit
/**
 * Sets the screen orientation back to the `OrientationLock.DEFAULT` policy.
 * @return Returns a promise with `void` value, which fulfils when the orientation is set.
 */
export async function unlockAsync(): Promise<void> {
  if (!ExpoScreenOrientation.lockAsync) {
    throw new UnavailabilityError('ScreenOrientation', 'lockAsync');
  }
  await ExpoScreenOrientation.lockAsync(OrientationLock.DEFAULT);
}

// @needsAudit
/**
 * Gets the current screen orientation.
 * @return Returns a promise that fulfils with an [`Orientation`](#orientation)
 * value that reflects the current screen orientation.
 */
export async function getOrientationAsync(): Promise<Orientation> {
  if (!ExpoScreenOrientation.getOrientationAsync) {
    throw new UnavailabilityError('ScreenOrientation', 'getOrientationAsync');
  }
  return await ExpoScreenOrientation.getOrientationAsync();
}

// @needsAudit
/**
 * Gets the current screen orientation lock type.
 * @return Returns a promise which fulfils with an [`OrientationLock`](#orientationlock)
 * value.
 */
export async function getOrientationLockAsync(): Promise<OrientationLock> {
  if (!ExpoScreenOrientation.getOrientationLockAsync) {
    return _lastOrientationLock;
  }
  return await ExpoScreenOrientation.getOrientationLockAsync();
}

// @needsAudit
/**
 * Gets the platform specific screen orientation lock type.
 * @return Returns a promise which fulfils with a [`PlatformOrientationInfo`](#platformorientationinfo)
 * value.
 */
export async function getPlatformOrientationLockAsync(): Promise<PlatformOrientationInfo> {
  const platformOrientationLock = await ExpoScreenOrientation.getPlatformOrientationLockAsync();
  if (Platform.OS === 'android') {
    return {
      screenOrientationConstantAndroid: platformOrientationLock,
    };
  } else if (Platform.OS === 'ios') {
    return {
      screenOrientationArrayIOS: platformOrientationLock,
    };
  } else if (Platform.OS === 'web') {
    return {
      screenOrientationLockWeb: platformOrientationLock,
    };
  } else {
    return {};
  }
}

// @needsAudit @docsMissing
/**
 * Returns whether the [`OrientationLock`](#orientationlock) policy is supported on
 * the device.
 * @param orientationLock
 * @return Returns a promise that resolves to a `boolean` value that reflects whether or not the
 * orientationLock is supported.
 */
export async function supportsOrientationLockAsync(
  orientationLock: OrientationLock
): Promise<boolean> {
  if (!ExpoScreenOrientation.supportsOrientationLockAsync) {
    throw new UnavailabilityError('ScreenOrientation', 'supportsOrientationLockAsync');
  }

  const orientationLocks = Object.values(OrientationLock);
  if (!orientationLocks.includes(orientationLock)) {
    throw new TypeError(`Invalid Orientation Lock: ${orientationLock}`);
  }

  return await ExpoScreenOrientation.supportsOrientationLockAsync(orientationLock);
}
// We rely on RN to emit `didUpdateDimensions`
// If this method no longer works, it's possible that the underlying RN implementation has changed
// see https://github.com/facebook/react-native/blob/c31f79fe478b882540d7fd31ee37b53ddbd60a17/ReactAndroid/src/main/java/com/facebook/react/modules/deviceinfo/DeviceInfoModule.java#L90
// @needsAudit
/**
 * Invokes the `listener` function when the screen orientation changes from `portrait` to `landscape`
 * or from `landscape` to `portrait`. For example, it won't be invoked when screen orientation
 * change from `portrait up` to `portrait down`, but it will be called when there was a change from
 * `portrait up` to `landscape left`.
 * @param listener Each orientation update will pass an object with the new [`OrientationChangeEvent`](#orientationchangeevent)
 * to the listener.
 */
export function addOrientationChangeListener(
  listener: OrientationChangeListener
): EventSubscription {
  if (typeof listener !== 'function') {
    throw new TypeError(`addOrientationChangeListener cannot be called with ${listener}`);
  }

  const subscription = createDidUpdateDimensionsSubscription(listener);
  _orientationChangeSubscribers.push(subscription);
  return subscription;
}

// We need to keep track of our own subscribers because EventEmitter uses a shared subscriber
// from NativeEventEmitter that is registered to the same eventTypes as us. Directly calling
// removeAllListeners(eventName) will remove other module's subscribers.
// @needsAudit
/**
 * Removes all listeners subscribed to orientation change updates.
 * @deprecated this function will be removed in future versions. Keep track of your own subscriptions.
 */
export function removeOrientationChangeListeners(): void {
  // Remove listener by subscription instead of eventType to avoid clobbering Dimension module's subscription of didUpdateDimensions
  let i = _orientationChangeSubscribers.length;
  while (i--) {
    const subscriber = _orientationChangeSubscribers[i];
    subscriber?.remove();

    // remove after a successful unsubscribe
    _orientationChangeSubscribers.pop();
  }
}

// @needsAudit
/**
 * Unsubscribes the listener associated with the `Subscription` object from all orientation change
 * updates.
 * @param subscription A subscription object that manages the updates passed to a listener function
 * on an orientation change.
 * @deprecated this function will be removed in a future version. Use `subscription.remove()` instead.
 */
export function removeOrientationChangeListener(subscription: EventSubscription): void {
  if (!subscription || !subscription.remove) {
    throw new TypeError(`Must pass in a valid subscription`);
  }
  subscription.remove();
  _orientationChangeSubscribers = _orientationChangeSubscribers.filter(
    (sub) => sub !== subscription
  );
}

function createDidUpdateDimensionsSubscription(
  listener: OrientationChangeListener
): EventSubscription {
  if (Platform.OS === 'web' || Platform.OS === 'ios') {
    return ExpoScreenOrientation.addListener(
      'expoDidUpdateDimensions',
      async (update: OrientationChangeEvent) => {
        listener(update);
      }
    );
  }

  // We rely on the RN Dimensions to emit the `didUpdateDimensions` event on Android
  return Dimensions.addEventListener('change', async () => {
    const [orientationLock, orientation] = await Promise.all([
      getOrientationLockAsync(),
      getOrientationAsync(),
    ]);
    listener({ orientationInfo: { orientation }, orientationLock });
  });
}
