import React, { forwardRef, memo } from 'react'
import {
    ColorValue,
    View,
    Image,
    StyleSheet,
    StyleProp,
    ViewStyle,
    ImageRequireSource,
    Platform,
    AccessibilityProps,
    ViewProps,
} from 'react-native'
import { cacheControl, priority, resizeMode } from './constants'
import { fromStyle, resolveObjectFit } from './objectFit'
import { objectPositionOrCentered, type ObjectPosition } from './objectPosition'
import FastImageView from './specs/FastImageViewNativeComponent'
import NativeFastImageModule from './specs/NativeFastImageModule'
import { warnIfChildren, warnOnce } from './warnings'

/** How the image fits the view: see [`objectFit`](#objectfit). */
export type ObjectFit = 'fill' | 'contain' | 'cover' | 'none' | 'scale-down'

export type { ObjectPosition, PositionLength } from './objectPosition'

/** An image's load priority: see [`source.priority`](#sourcepriority). */
export type Priority = 'low' | 'normal' | 'high'

/** How fresh an image must be: see [`source.cache`](#sourcecache). */
export type Cache = 'immutable' | 'web' | 'cacheOnly'

/** A remote image to load, and how to load and cache it. */
export type Source = {
    /**
     * Remote url to load the image from. e.g. `'https://example.com/image.jpg'`.
     *
     * Also loads local files (`file://`, and on Android `content://`), images
     * in the app by name (e.g. `'my_image'`: in its asset catalog on iOS, a
     * drawable on Android), photo library images on iOS and tvOS (`ph://`,
     * see [Photo library images](#photo-library-images-ios-and-tvos)) and SVG
     * images (see [SVG images](docs/formats.md#svg-images)).
     */
    uri?: string
    /** Headers to load the image with. e.g. `{ Authorization: 'someAuthToken' }`. */
    headers?: { [key: string]: string }
    /**
     * A hint for which images to start loading first when several are
     * waiting: `'high'` ones before `'normal'` ones, and `'low'` ones after.
     * It's best effort, not an order: several images load at once, and when
     * each finishes depends on its size and the network.
     *
     * - `'low'`: e.g. images further down a list.
     * - `'normal'`: the default.
     * - `'high'`: e.g. the image the screen is about.
     *
     * @default 'normal'
     */
    priority?: Priority
    /**
     * How fresh the image must be. See
     * [how caching is handled](docs/how-is-caching-handled.md) for how the
     * options fit together.
     *
     * - `'immutable'`: loads the image once, then shows the cached copy until
     *   its url (or `cacheKey`) changes.
     * - `'web'`: follows the server's HTTP cache headers, as a browser does,
     *   checking with the server when it loads. These responses are kept in
     *   their own HTTP cache (50 MB on each platform), which `clearDiskCache`
     *   also clears.
     * - `'cacheOnly'`: only shows a cached image, without making a request.
     *
     * @default 'immutable'
     */
    cache?: Cache
    /**
     * The key the image is cached under, instead of its uri. Use it for urls
     * that change while the image stays the same, e.g. signed urls with a
     * token or an expiry. Use something that identifies the image, and change
     * it when the image changes (e.g. include a version or the time it was
     * updated), or the old image keeps showing.
     *
     * Not used with `cache: 'web'`, which follows the HTTP cache (keyed by
     * url).
     *
     * @example
     * ```jsx
     * <FastImage
     *     source={{
     *         uri: signedUrl,
     *         cacheKey: `avatar-${user.id}-${user.avatarUpdatedAt}`,
     *     }}
     * />
     * ```
     */
    cacheKey?: string
    /**
     * Whether the decoded image is kept in the memory cache. With `false`
     * it's only kept on disk: a view doesn't leave it in memory once it stops
     * showing it, and `FastImage.preload` downloads it without decoding it.
     * Use it for large images that are shown once or rarely, like a
     * full-screen photo: a decoded photo can take tens of MB of memory. Images
     * shown again, like a list scrolled back, are decoded from disk again.
     *
     * @default true
     */
    memoryCache?: boolean
    /**
     * With several sources (`source` as an array), the image's width at this
     * uri, in pixels (times `scale`, if given).
     */
    width?: number
    /**
     * With several sources (`source` as an array), the image's height at this
     * uri, in pixels (times `scale`, if given).
     */
    height?: number
    /** With several sources, the scale `width` and `height` are multiplied by. */
    scale?: number
}

/** How the image fades in: see [`transition`](#transition). */
export type Transition = {
    /**
     * How long the fade takes, in milliseconds; 0 means no fade. Defaults to
     * the default transition's (see `FastImage.setDefaults`) if it fades,
     * otherwise the platform's usual length: 300 ms on Android, 250 ms on iOS.
     */
    duration?: number
    /**
     * Also fades between images: a new `source` replacing an image that's
     * showing cross-dissolves from it. Otherwise only an image that appears
     * over nothing (or over `defaultSource`) fades in, and a new source
     * replaces the image showing at once, once it has loaded.
     * @default false
     */
    betweenImages?: boolean
    /**
     * Skips the fade for an image from a cache, so images already loaded
     * show at once, e.g. in a list scrolled back up or a reused row.
     *
     * - `'memory'`: skips it for images from the memory cache.
     * - `'all'`: skips it for images from the memory or disk cache too, so
     *   images that download fade in, and local files and bundled images
     *   usually only the first time (the disk cache usually keeps them too).
     * - `'none'`: always fades.
     *
     * On Android, a `cache: 'web'` image from its HTTP cache, without a request,
     * counts as one from the memory cache (these images aren't kept there).
     * @default 'memory'
     */
    skipOnCacheHit?: 'none' | 'memory' | 'all' | null
}

/**
 * `onLoad`'s event: `nativeEvent` has the image's own `width` and `height`
 * (see `onLoad`), and `target`, the view's React tag (on iOS and Android; not
 * on the web).
 */
export interface OnLoadEvent {
    nativeEvent: {
        width: number
        height: number
        target?: number
    }
}

/**
 * `onError`'s event: `nativeEvent.error` says what went wrong, e.g. an HTTP
 * status code or an image that can't be decoded.
 */
export interface OnErrorEvent {
    nativeEvent: {
        error: string
    }
}

/**
 * A load's result, as `onLoadEnd` gets it: `ok` with the image's size, or not
 * `ok` with the error (as `onLoad` and `onError` get them).
 */
export type LoadResult =
    | { ok: true; width: number; height: number }
    | { ok: false; error: string }

/**
 * `configureCache`'s changes: a number (0 for no limit), or `null` to go back
 * to the app's native config (Info.plist, AndroidManifest.xml) or the
 * platform's default. Leave one out to keep it. Changes are saved, and used on
 * the next launches too. Android only has `maxDiskSize`.
 */
export interface CacheLimits {
    /**
     * The most bytes of images kept on disk. When it's over, the least
     * recently used are removed (on iOS, until it's half this size). Default:
     * no limit on iOS, 250 MB on Android.
     */
    maxDiskSize?: number | null
    /**
     * iOS: the seconds an image is kept on disk after it was last used, or 0
     * to keep them until `maxDiskSize` removes them. Default: 1 week.
     */
    maxDiskAge?: number | null
    /**
     * iOS: the most bytes of decoded images kept in memory. Default: no limit
     * (they're removed when the system is low on memory).
     */
    maxMemorySize?: number | null
}

/**
 * `configureCache`'s result: the limits in effect (0 for no limit), and the
 * bytes the disk cache uses now. Android only has `maxDiskSize` and
 * `diskSize`, and neither if the app has its own `AppGlideModule` (or
 * expo-image, which has one).
 */
export interface CacheState {
    maxDiskSize?: number
    maxDiskAge?: number
    maxMemorySize?: number
    /** The bytes the disk cache uses now. */
    diskSize?: number
}

/**
 * `getCachePath`'s and `writeToCache`'s result: `ok` with the file's path, or
 * not `ok` with the error.
 */
export type CachePathResult =
    | { ok: true; path: string }
    | { ok: false; error: string }

/**
 * `clearMemoryCache`'s and `clearDiskCache`'s result: `ok` once the cache is
 * cleared, or not `ok` with the error.
 */
export type ClearCacheResult = { ok: true } | { ok: false; error: string }

/**
 * `onProgress`'s event: `nativeEvent` has the bytes `loaded` and the `total`,
 * and `progress`, `loaded / total` from 0 to 1.
 */
export interface OnProgressEvent {
    nativeEvent: {
        loaded: number
        total: number
        progress: number
    }
}

// Extends ViewStyle rather than FlexStyle/TransformsStyle/ShadowStyleIOS, which
// React Native's Strict TypeScript API (types_generated: opt-in from 0.80, the
// default from 0.87) doesn't export. Only the image's own keys are added; the
// rest (radii, opacity, colors) come from ViewStyle, so they're the app's
// React Native types (e.g. string radii, Animated values).
export interface ImageStyle extends ViewStyle {
    tintColor?: ColorValue
    objectFit?: ObjectFit
}

export interface FastImageProps extends AccessibilityProps, ViewProps {
    /**
     * Source for the remote image to load: a `Source`, a `require()`d image,
     * or an array of the same image at different sizes.
     *
     * When `source` changes, the image that's showing stays until the new one
     * has loaded. In views that get reused for other content, such as rows in
     * FlashList or recyclerlistview, set `recyclingKey` so a reused row
     * doesn't show the previous row's image.
     *
     * **Several sizes of an image.** `source` can also be an array of the
     * same image at different sizes, each with its `width` and `height` in
     * pixels (times `scale`, if it has one). The view loads the one whose size
     * is closest to its own, in pixels, so a small view downloads a small
     * image:
     *
     * ```jsx
     * <FastImage
     *     style={{ width: 120, height: 120 }}
     *     source={[
     *         { uri: 'https://example.com/photo-200.jpg', width: 200, height: 200 },
     *         { uri: 'https://example.com/photo-800.jpg', width: 800, height: 800 },
     *     ]}
     * />
     * ```
     *
     * - The view picks once it has been laid out. One that has no size (e.g.
     *   sized from `onLoad`) loads the largest.
     * - When the view's size changes so that another size fits better, it
     *   loads that one (with its load events), and keeps showing the current
     *   image until then, without a `transition`.
     * - Each size is cached separately. A `cacheKey` applies to its own entry:
     *   give each size its own key (or none), or the sizes would replace each
     *   other in the cache.
     * - `FastImage.preload`, `getCachePath` and `writeToCache` take one size:
     *   pass the one a view will show. An array fails with
     *   `{ ok: false, error }`.
     * - An array of one is the same as that source.
     */
    source?: Source | Source[] | ImageRequireSource
    /**
     * An asset loaded with `require(...)`, shown while the first image loads,
     * and if an image fails to load. When `source` changes, the previous image
     * shows while the new one loads instead (see `source` and
     * `recyclingKey`).
     *
     * On Android, `defaultSource` doesn't show in debug builds: there the dev
     * server serves `require()`d images, and `defaultSource` is only loaded
     * from the app's resources.
     */
    defaultSource?: ImageRequireSource
    /**
     * How the image fits the view, as CSS's `object-fit` does.
     *
     * - `'cover'`: scales it uniformly (keeping its aspect ratio) so it covers
     *   the view (inside its borders), cropping what doesn't fit.
     * - `'contain'`: scales it uniformly (keeping its aspect ratio) so all of
     *   it fits in the view (inside its borders).
     * - `'fill'`: scales its width and height separately to fill the view,
     *   which can change its aspect ratio.
     * - `'none'`: shows it at its own size, centered, cropped if it's larger
     *   than the view.
     * - `'scale-down'`: shows it at its own size, centered, or scaled down
     *   uniformly to fit if it's larger than the view (the smaller of `'none'`
     *   and `'contain'`).
     *
     * An image's own size is its size in pixels, as points (dp on Android),
     * as CSS counts an image's pixels: a 300 × 200 image is 300 × 200 points.
     * A bundled image (`require()`) is its size in points.
     *
     * It can also be set in `style`; the prop wins.
     *
     * @default 'cover'
     */
    objectFit?: ObjectFit
    /**
     * Where the image is in the view, as CSS's `object-position` places it:
     * where its edges are when it doesn't fill the view exactly (`contain`,
     * `cover`, `none` and `scale-down`; with `fill` it always fills it).
     *
     * - A string in CSS's syntax: one or two keywords (`'top'`,
     *   `'left bottom'`), lengths in points (`'10px 20px'`, with `px`) or
     *   percentages (`'25% 75%'`), or offsets from an edge
     *   (`'right 10px bottom 20%'`). A percentage puts that point of the image
     *   at that point of the view: `'0% 0%'` is the top-left corner,
     *   `'100% 100%'` the bottom-right.
     * - An object with one of `top` and `bottom` and one of `left` and
     *   `right`, each in points (a number) or a percentage (`'25%'`), e.g.
     *   `{ right: 10, bottom: '20%' }`. A missing axis is centered.
     *
     * Points are dp on Android. An invalid value is ignored (the image is
     * centered), with a warning in development. `defaultSource` is placed
     * the same way.
     *
     * @default '50% 50%'
     * @example
     * ```jsx
     * <FastImage source={{ uri }} objectFit="cover" objectPosition="top" />
     * ```
     */
    objectPosition?: ObjectPosition
    /**
     * For views that get reused for other content, such as rows in FlashList
     * or recyclerlistview. Set it to something that identifies the content,
     * e.g. the item's id. When it changes, the image is cleared right away (to
     * `defaultSource`, or blank) instead of staying until the new one has
     * loaded. Unlike changing `key`, this keeps the view, which is what list
     * recycling is for.
     *
     * @example
     * ```jsx
     * <FastImage recyclingKey={item.id} source={{ uri: item.imageUrl }} />
     * ```
     */
    recyclingKey?: string | null
    /**
     * How many times an animated image (GIF, animated WebP, APNG, or animated
     * AVIF) plays:
     *
     * - Not set: as many times as the file says (like a browser).
     * - `true`: forever.
     * - `false`: once.
     * - A number: that many times.
     *
     * Changing it restarts the animation.
     */
    loop?: boolean | number
    /**
     * How the image is filtered when it's drawn smaller or larger than its
     * size (like CSS's `image-rendering`):
     *
     * - `'auto'`: the platform's usual filtering.
     * - `'smooth'`: iOS only. Keeps a large image drawn much smaller than its
     *   size (e.g. a big photo as a thumbnail, or fine lines and text) from
     *   looking jagged or noisy. Such an image is already decoded at about the
     *   view's size by default (see `downsample`), so this is for images shown
     *   at less than half their size with `downsample={false}`, or a little
     *   smaller than their size. **It uses more memory:** the image is also
     *   kept at smaller sizes for drawing, about a third more than the decoded
     *   image. On Android it's the same as `'auto'`.
     * - `'pixelated'`: sharp pixels, without smoothing, e.g. for pixel art
     *   drawn larger than its size. On Android, animated images are still
     *   smoothed.
     *
     * @default 'auto'
     */
    imageRendering?: 'auto' | 'smooth' | 'pixelated'
    /**
     * Pauses an animated image (GIF, animated WebP, APNG, or animated AVIF) on
     * the frame it's showing; `false` plays it again from there
     * (on Android, an animated WebP or AVIF plays again from its first frame:
     * Android can't resume one). Each image animates on its own, so pausing
     * one doesn't pause others showing the same file.
     */
    paused?: boolean
    /**
     * Fades the image in when it loads. `true` fades for the default's
     * duration (see `FastImage.setDefaults`), or the platform's usual length
     * if the default doesn't fade; a number is the duration in milliseconds,
     * `false` (or 0) is no fade, or pass a `Transition`.
     *
     * By default, images fade in on Android (300 ms), and not on iOS.
     * `FastImage.setDefaults` changes that for every image; an image's own
     * `transition` wins, and the settings it leaves out come from the
     * default. `null` is the default.
     *
     * Downloads, local files (`file://`, `content://`) and bundled images
     * (`require()`) fade in. In lists that reuse views (e.g. FlashList), set
     * `recyclingKey`, so a reused view starts empty and its image fades in,
     * instead of showing the previous item's image until it loads.
     *
     * @example
     * ```jsx
     * <FastImage source={{ uri }} transition />
     * <FastImage source={{ uri }} transition={500} />
     * <FastImage source={{ uri }} transition={{ betweenImages: true, skipOnCacheHit: 'none' }} />
     * ```
     */
    transition?: boolean | number | Transition | null
    /**
     * Decodes a large image at about the size it's shown at, instead of at
     * full size, so it takes much less memory.
     *
     * - `true`: an image at least twice the size its view needs is decoded
     *   at about the view's size (on Android, any image larger than its
     *   view). If the view grows, the image is decoded again for its new size
     *   (from the disk cache).
     * - `false`: images are decoded at full size, e.g. for an image that's
     *   zoomed in on with a transform (a pinch-to-zoom viewer), which would
     *   otherwise show the smaller copy enlarged. **It uses more memory:** a
     *   12-megapixel photo takes about 48 MB decoded, however small it's
     *   shown. On Android, an image over 16 megapixels (4096 × 4096) is
     *   decoded at the largest size under that, which Android can still
     *   draw.
     *
     * It doesn't change `onLoad`'s width and height (the image's own size) or
     * the cached file. Photo library images are always decoded at about the
     * view's size. Android 16 and later decode animated WebP at full size
     * either way, and scale it as they draw it.
     *
     * If you control the images, serve them at the size they're shown
     * (resized on your server or by an image CDN), which also saves
     * bandwidth.
     *
     * @default true
     */
    downsample?: boolean
    // The radius is converted from points to the bitmap's pixels on both
    // platforms, so it matches React Native's Image.
    /**
     * Blurs the image by this radius, in points (the same radius looks about
     * the same on iOS and Android). `0` is no blur.
     *
     * It's for still images, or a radius that changes now and then (e.g.
     * blurring a photo behind a sheet). Each change blurs the image again on
     * the CPU, so don't animate it.
     *
     * - Only the loaded image is blurred, not `defaultSource`.
     * - An animated image (GIF, animated WebP, APNG or AVIF) shows its first frame,
     *   blurred, and doesn't animate.
     * - With `tintColor`, the blurred image is tinted.
     * - The image is blurred at about the size it's shown at, off the main
     *   thread. The cached file stays the original image, so `getCachePath`
     *   and other views of it aren't affected.
     * - Changing it blurs the image that's showing again, without sending the
     *   load events again.
     *
     * To animate a blur, or to blur an animated image, use React Native's
     * `filter` style instead, which the GPU draws:
     * `style={{ filter: [{ blur: 6 }] }}`. React Native's docs list `blur` for Android 12+ only; on iOS it's behind
     * an experimental React Native feature flag (`enableSwiftUIBasedFilters`,
     * SwiftUI-based filters).
     *
     * @default 0
     */
    blurRadius?: number

    /** Called when the image starts to load. */
    onLoadStart?(): void

    /**
     * Called while the image downloads, with the bytes `loaded` so far, the
     * `total`, and `progress` (`loaded / total`, from 0 to 1; the last event
     * has 1). Not called while the total is unknown (a response without a
     * `Content-Length`).
     *
     * @example
     * ```jsx
     * onProgress={e => console.log(e.nativeEvent.progress)}
     * ```
     */
    onProgress?(event: OnProgressEvent): void

    /**
     * Called on a successful image fetch. Called with the width and height of
     * the image itself, not of the view (on iOS, in points: an `@2x` asset
     * reports half its pixel size).
     *
     * @example
     * ```jsx
     * onLoad={e => console.log(e.nativeEvent.width, e.nativeEvent.height)}
     * ```
     */
    onLoad?(event: OnLoadEvent): void

    /**
     * Called on an image fetching error, with a message describing it (e.g.
     * the HTTP status code). On iOS and Android, a download that gets nothing
     * from the server for 15 seconds fails too (on Android, unless the app's
     * OkHttp client has timeouts of its own).
     *
     * @example
     * ```jsx
     * onError={e => console.log(e.nativeEvent.error)}
     * ```
     */
    onError?(event: OnErrorEvent): void

    /**
     * Called when the image finishes loading, whether it was successful or an
     * error, with the result: `{ ok: true, width, height }` (the image's size,
     * as `onLoad` gets it) or `{ ok: false, error }` (as `onError` gets it).
     * TypeScript makes you check `ok` before reading the size or the error.
     *
     * @example
     * ```jsx
     * <FastImage
     *     source={{ uri }}
     *     onLoadEnd={(result) => {
     *         if (result.ok) setAspectRatio(result.width / result.height)
     *         else setFailed(result.error)
     *     }}
     * />
     * ```
     */
    onLoadEnd?(result: LoadResult): void

    /**
     * The image's style: View's style props (`borderRadius` clips the image,
     * and borders inset it, but padding doesn't), and `tintColor`, as with
     * React Native's `Image` (the `tintColor` prop wins).
     */
    style?: StyleProp<ImageStyle>

    /**
     * If supplied, changes the color of all the non-transparent pixels to the
     * given color.
     */
    tintColor?: ColorValue

    /**
     * FastImage is a single native view, which doesn't take children (since
     * 10.0): use `FastImageBackground` for content over an image.
     *
     * @hidden
     */
    children?: "FastImage doesn't take children: use FastImageBackground for content over an image"
}

const resolveDefaultSource = (
    defaultSource?: ImageRequireSource,
): string | object | null => {
    if (!defaultSource) {
        return null
    }
    const resolved = Image.resolveAssetSource(defaultSource)
    if (!resolved) {
        return null
    }
    // Android takes the uri, and loads it as a Drawable with React Native's
    // helpers; iOS the image source ({ uri, width, height, scale }).
    return Platform.OS === 'android' ? resolved.uri : resolved
}

// The native loopCount: -1 for the file's own, 0 for forever, or a number of
// plays. Always sent, since native would reset a removed prop to 0 (forever).
function loopCount(loop: boolean | number | undefined) {
    if (loop === undefined) return -1
    if (loop === true) return 0
    if (loop === false) return 1
    return Number.isFinite(loop) ? Math.max(1, Math.floor(loop)) : 0
}

// onLoadEnd's result from the native event (ok with the image's size, or not
// ok with the error).
function loadResult(event: {
    ok?: boolean
    width?: number
    height?: number
    error?: string
}): LoadResult {
    return event.ok
        ? { ok: true, width: event.width ?? 0, height: event.height ?? 0 }
        : { ok: false, error: event.error ?? 'Failed to load the image' }
}

// The native view's handledEvents (see specs/FastImageViewNativeComponent.ts):
// the events with a handler, which are the only ones it sends.
function handledEvents({
    onLoadStart,
    onProgress,
    onLoad,
    onError,
    onLoadEnd,
}: FastImageProps) {
    return (
        (onLoadStart ? 1 : 0) |
        (onProgress ? 2 : 0) |
        (onLoad ? 4 : 0) |
        (onError ? 8 : 0) |
        (onLoadEnd ? 16 : 0)
    )
}

// The platform's usual fade length: Glide's and React Native's Image's on
// Android, Core Animation's default on iOS.
function usualFadeMs() {
    return Platform.OS === 'ios' ? 250 : 300
}

// The native transition props. Always sent, so removing `transition` goes
// back to the default.
type TransitionProps = {
    transitionDuration: number
    transitionBetweenImages: boolean
    transitionSkipOnCacheHit: string
}

const NO_TRANSITION: TransitionProps = {
    transitionDuration: 0,
    transitionBetweenImages: false,
    transitionSkipOnCacheHit: 'memory',
}

// A transition (an image's, or setDefaults') over `base`: the settings it
// leaves out come from base, and null (or undefined) is base. true, or a
// missing duration, is base's duration if it fades, otherwise the platform's
// usual one.
function resolveTransition(
    transition: FastImageProps['transition'],
    base: TransitionProps,
): TransitionProps {
    if (transition == null) return base
    if (transition === false) return { ...base, transitionDuration: 0 }
    const usual =
        base.transitionDuration > 0 ? base.transitionDuration : usualFadeMs()
    if (transition === true) return { ...base, transitionDuration: usual }
    const { duration, betweenImages, skipOnCacheHit }: Transition =
        typeof transition === 'number' ? { duration: transition } : transition
    const resolvedDuration = duration ?? usual
    return {
        transitionDuration:
            typeof resolvedDuration === 'number' && resolvedDuration > 0
                ? resolvedDuration
                : 0,
        transitionBetweenImages: betweenImages ?? base.transitionBetweenImages,
        transitionSkipOnCacheHit:
            skipOnCacheHit ?? base.transitionSkipOnCacheHit,
    }
}

// The built-in default: Android fades images in, as React Native's Image does;
// iOS doesn't, as UIImageView and SDWebImage don't.
function builtInTransition() {
    return resolveTransition(Platform.OS === 'android', NO_TRANSITION)
}

// The default transition set with FastImage.setDefaults (undefined: the
// built-in one).
let defaultTransition: TransitionProps | undefined

// React Native's global: true in development (Metro and React Native's Jest
// preset set it), false in release builds.
declare const __DEV__: boolean

function FastImageBase({
    source,
    defaultSource,
    tintColor,
    onLoadStart,
    onProgress,
    onLoad,
    onError,
    onLoadEnd,
    style,
    children,
    objectFit,
    objectPosition,
    loop,
    transition,
    forwardedRef,
    ...viewProps
}: FastImageProps & { forwardedRef: React.Ref<any> }) {
    const {
        fallback,
        resizeMode: _resizeMode,
        ...props
    } = viewProps as typeof viewProps & {
        // Removed in 10.0 (React Native's Image instead of FastImage's view).
        fallback?: unknown
        // Removed in 10.0 (objectFit instead): ignored, so it reaches no
        // native prop.
        resizeMode?: unknown
    }
    if (__DEV__ && fallback !== undefined) {
        warnOnce(
            'fallback',
            "react-native-fast-image: `fallback` was removed in 10.0, and does nothing: images always load natively. Remove it, or use React Native's Image for those images.",
            'warn',
        )
    }
    // FastImage is a single native view: children aren't rendered (as with
    // React Native's Image).
    if (__DEV__) {
        warnIfChildren(children)
    }
    // tintColor can also be set in style, as with React Native's Image. The
    // prop wins.
    const resolvedTintColor =
        tintColor != null ? tintColor : fromStyle(style, 'tintColor')
    const fit = resolveObjectFit(objectFit, style)
    const position = objectPositionOrCentered(objectPosition)

    // Several sources are picked from natively, for the view's size; one in
    // an array is a plain source.
    const sources = Array.isArray(source)
        ? source.length === 1
            ? undefined
            : source
        : undefined
    const single = Array.isArray(source) ? source[0] : source
    const resolvedSource = sources
        ? undefined
        : Image.resolveAssetSource(single as any)
    const resolvedDefaultSource = resolveDefaultSource(defaultSource)

    return (
        <FastImageView
            {...props}
            ref={forwardedRef}
            // Clips the image to borderRadius by default, as React Native's
            // Image does.
            style={[styles.image, style]}
            // null (no key) as undefined, which the native prop takes.
            recyclingKey={props.recyclingKey ?? undefined}
            loopCount={loopCount(loop)}
            {...resolveTransition(
                transition,
                defaultTransition ?? builtInTransition(),
            )}
            source={resolvedSource}
            sources={sources}
            defaultSource={resolvedDefaultSource}
            onFastImageLoadStart={onLoadStart}
            onFastImageProgress={onProgress}
            // The native views only send the events with a handler (and only
            // track progress with onProgress).
            handledEvents={handledEvents({
                onLoadStart,
                onProgress,
                onLoad,
                onError,
                onLoadEnd,
            })}
            onFastImageLoad={onLoad}
            onFastImageError={onError}
            onFastImageLoadEnd={
                onLoadEnd &&
                ((event: { nativeEvent: any }) =>
                    onLoadEnd(loadResult(event.nativeEvent)))
            }
            // After style, whose tintColor and objectFit reach the native
            // view's props too: these win.
            tintColor={resolvedTintColor}
            objectFit={fit}
            objectPositionX={position.x.fraction}
            objectPositionXOffset={position.x.offset}
            objectPositionY={position.y.fraction}
            objectPositionYOffset={position.y.offset}
        />
    )
}

const FastImageMemo = memo(FastImageBase)

// What a ref to FastImage or FastImageBackground gets: FastImage's native
// view, or FastImageBackground's view. Typed as a View's (they have the same
// methods, e.g. measure). ComponentRef, as
// React Native's types declare View as a class, and its Strict TypeScript API
// (opt-in from 0.80, the default from 0.87) as a function component that takes
// a ref.
type ViewRef = React.ComponentRef<typeof View>

const FastImageComponent: React.ForwardRefExoticComponent<
    FastImageProps & React.RefAttributes<ViewRef>
> = forwardRef((props: FastImageProps, ref: React.Ref<ViewRef>) => (
    <FastImageMemo forwardedRef={ref} {...props} />
))

FastImageComponent.displayName = 'FastImage'

/** A preloaded source that loaded (and is now cached). */
export interface PreloadSuccess {
    /** The source's uri. */
    uri: string
    ok: true
    /** The image's width (as in `onLoad`). */
    width: number
    /** The image's height (as in `onLoad`). */
    height: number
}

/** A preloaded source that failed to load. */
export interface PreloadFailure {
    /** The source's uri (none for a source without one, e.g. `null`). */
    uri?: string
    ok: false
    /** What went wrong. */
    error: string
}

/**
 * `preload`'s result for a source. Check `ok` to tell which it is: e.g.
 * `if (result.ok)` narrows it to a `PreloadSuccess`, with its size. Reading
 * `width` or `error` without checking is a type error.
 */
export type PreloadResult = PreloadSuccess | PreloadFailure

// A result as native sends it (without the uri).
type NativePreloadResult =
    | Omit<PreloadSuccess, 'uri'>
    | Omit<PreloadFailure, 'uri'>

const noResult: NativePreloadResult = { ok: false, error: 'No result' }

// The native module, or an error saying why it's missing, thrown when a
// function is called (as React Native's own optional modules do), so
// importing FastImage works without it (Jest, Expo Go).
function nativeModule(): NonNullable<typeof NativeFastImageModule> {
    if (!NativeFastImageModule) {
        throw new Error(
            "react-native-fast-image: the native module FastImageModule isn't " +
                'in this app. Rebuild the app after installing the package ' +
                '(run pod install on iOS). It needs the New Architecture, and ' +
                "isn't in Expo Go: use a development build. In Jest, mock the " +
                'function (e.g. jest.spyOn(FastImage, "preload")).',
        )
    }
    return NativeFastImageModule
}

export interface FastImageStaticProperties {
    /**
     * @deprecated `resizeMode` was removed in 10.0, and a `resizeMode` prop
     * is ignored: use `objectFit` instead (`stretch` is `fill`, `center` is
     * `scale-down`). Kept so code that reads it doesn't throw.
     */
    resizeMode: typeof resizeMode
    /**
     * @deprecated Use the strings instead, e.g. `priority: 'high'`.
     */
    priority: typeof priority
    /**
     * @deprecated Use the strings instead, e.g. `cache: 'immutable'`.
     */
    cacheControl: typeof cacheControl
    preload: (sources: Source[]) => Promise<PreloadResult[]>
    /**
     * Removes every image from the memory cache. Resolves once it's done, or
     * not `ok` with the error if it couldn't (Android: Glide failed to
     * start). Never rejects.
     */
    clearMemoryCache: () => Promise<ClearCacheResult>
    /**
     * Removes every image from the disk cache, and the HTTP cache of
     * `cache: 'web'` images. Resolves once it's done, or not `ok` with the
     * error if it couldn't (Android: Glide failed to start). Never rejects.
     */
    clearDiskCache: () => Promise<ClearCacheResult>
    /**
     * The path of the source's downloaded file in the disk cache, downloading
     * it first if it isn't there (without decoding it). With
     * `cache: 'cacheOnly'`, or a `cacheKey` without a `uri`, it doesn't
     * download. Never rejects.
     */
    getCachePath: (source: Source) => Promise<CachePathResult>
    /**
     * Stores a local image file (a `file://` uri or a path, or on Android a
     * `content://` uri) as the source's image in the disk cache, so views and
     * preloads of the source show it without downloading it. A source with a
     * `cacheKey` doesn't need a `uri`. Resolves with its cached file. Doesn't
     * replace an image that's already cached, and isn't for `cache: 'web'`
     * sources. Never rejects.
     */
    writeToCache: (source: Source, file: string) => Promise<CachePathResult>
    /**
     * Changes the cache's limits at runtime (only those given; 0 for no
     * limit, null to go back to the app's native config) and saves them, then
     * resolves with the limits in effect and the disk cache's size; without
     * limits, it only resolves with them. iOS applies changes at once.
     * Android only has `maxDiskSize`, applied when Glide starts: at once if
     * it hasn't started yet in this launch, otherwise from the next launch.
     * FastImage starts it when its first view is created (before the view has
     * a source), and for `preload`, `getCachePath`, `writeToCache`,
     * `clearDiskCache` and `configureCache` itself, even without limits (e.g.
     * to read the disk cache's size).
     */
    configureCache: (limits?: CacheLimits) => Promise<CacheState>
    /**
     * Changes the defaults of every image (only those given; null goes back
     * to the built-in one). An image already showing uses a change once one
     * of its props changes, for its next load. Not saved: call it before
     * images render, e.g. in index.js.
     */
    setDefaults: (defaults: FastImageDefaults) => void
}

/** `FastImage.setDefaults`'s settings. */
export interface FastImageDefaults {
    /**
     * The `transition` of images that don't set one (an image's own settings
     * win, and those it leaves out come from this). `null` goes back to the
     * built-in default (see `transition`).
     */
    transition?: boolean | number | Transition | null
}

const FastImage: React.ForwardRefExoticComponent<
    FastImageProps & React.RefAttributes<ViewRef>
> &
    FastImageStaticProperties = FastImageComponent as any

FastImage.cacheControl = cacheControl

FastImage.priority = priority

FastImage.resizeMode = resizeMode

FastImage.setDefaults = ({ transition }: FastImageDefaults = {}) => {
    if (transition === null) {
        defaultTransition = undefined
    } else if (transition !== undefined) {
        defaultTransition = resolveTransition(transition, builtInTransition())
    }
}

// preload, getCachePath and writeToCache take one source per image, not
// several sizes of one: pick the size (e.g. the one a view will show).
const ONE_SOURCE =
    'Takes one source, not an array of sizes: pass the size to use'

// Whether a source is an object, which native can read. Other values (null, a
// url string, a number, an array) are sent as {}, which fails as a source
// without a uri: native would crash reading them as a source.
const isSourceObject = (source: unknown): source is Source =>
    typeof source === 'object' && source !== null && !Array.isArray(source)

FastImage.preload = (sources: Source[]) =>
    // Entries that aren't source objects are sent as {}, so native results
    // line up with the sources (iOS drops null entries). Arrays' results are
    // replaced.
    Promise.resolve(
        nativeModule().preload(
            sources.map((s) => (isSourceObject(s) ? s : {})),
        ) as Promise<NativePreloadResult[]>,
    ).then((results?: NativePreloadResult[]) =>
        sources.map((source, i): PreloadResult => {
            if (Array.isArray(source)) {
                return { ok: false, error: ONE_SOURCE, uri: undefined }
            }
            const uri = isSourceObject(source) ? source.uri : undefined
            const result = results?.[i] ?? noResult
            if (!result.ok) return { ...result, uri }
            // Native already fails a source without a uri.
            return typeof uri === 'string'
                ? { ...result, uri }
                : { ok: false, error: 'Invalid source: no uri', uri }
        }),
    )

FastImage.clearMemoryCache = (): Promise<ClearCacheResult> =>
    nativeModule().clearMemoryCache() as Promise<ClearCacheResult>

FastImage.clearDiskCache = (): Promise<ClearCacheResult> =>
    nativeModule().clearDiskCache() as Promise<ClearCacheResult>

FastImage.configureCache = (limits?: CacheLimits): Promise<CacheState> => {
    // Sent as the limits to set (numbers) and the names of those to reset
    // (null), never as null values: iOS's TurboModule leaves those out of the
    // object it passes, so both platforms read resets from `reset`.
    const set: { [name: string]: number } = {}
    const reset: string[] = []
    for (const [name, value] of Object.entries(limits ?? {})) {
        if (value === null) reset.push(name)
        else if (typeof value === 'number') set[name] = value
    }
    return nativeModule().configureCache({
        ...set,
        reset,
    }) as Promise<CacheState>
}

FastImage.writeToCache = (
    source: Source,
    file: string,
): Promise<CachePathResult> =>
    Array.isArray(source)
        ? Promise.resolve({ ok: false, error: ONE_SOURCE })
        : Promise.resolve(
              nativeModule().writeToCache(
                  isSourceObject(source) ? source : {},
                  file,
              ) as Promise<CachePathResult>,
          )

FastImage.getCachePath = (source: Source): Promise<CachePathResult> =>
    Array.isArray(source)
        ? Promise.resolve({ ok: false, error: ONE_SOURCE })
        : Promise.resolve(
              nativeModule().getCachePath(
                  isSourceObject(source) ? source : {},
              ) as Promise<CachePathResult>,
          )

const styles = StyleSheet.create({
    image: {
        overflow: 'hidden',
    },
})

export interface FastImageBackgroundProps extends Omit<
    FastImageProps,
    'style' | 'children'
> {
    /** The container's style; the image fills it. */
    style?: StyleProp<ViewStyle>
    /** The image's style. */
    imageStyle?: StyleProp<ImageStyle>
    /** A ref to the image (the FastImage inside). */
    imageRef?: React.Ref<any>
    /** Content shown on top of the image. */
    children?: React.ReactNode
}

/**
 * An image with content on top of it, like React Native's `ImageBackground`: a
 * view that the image fills, with the children on top (`FastImage` is a single
 * native view, which doesn't take children). The other props go to the image;
 * the ref is the view's.
 */
export const FastImageBackground: React.ForwardRefExoticComponent<
    FastImageBackgroundProps & React.RefAttributes<ViewRef>
> = forwardRef(
    (
        {
            style,
            imageStyle,
            imageRef,
            children,
            importantForAccessibility,
            ...props
        }: FastImageBackgroundProps,
        ref: React.Ref<ViewRef>,
    ) => (
        <View
            accessibilityIgnoresInvertColors
            importantForAccessibility={importantForAccessibility}
            style={style}
            ref={ref}
        >
            <FastImageComponent
                {...props}
                importantForAccessibility={importantForAccessibility}
                style={[StyleSheet.absoluteFill, imageStyle]}
                ref={imageRef}
            />
            {children}
        </View>
    ),
)

FastImageBackground.displayName = 'FastImageBackground'

export default FastImage
