import {useCallback, useEffect, useRef} from 'react'
import {Keyboard} from 'react-native'
import {File} from 'expo-file-system'
import {type ImagePickerAsset} from 'expo-image-picker'
import {msg, plural} from '@lingui/core/macro'
import {useLingui} from '@lingui/react'

import {
  VIDEO_MAX_DURATION_MS,
  VIDEO_MAX_SIZE,
  VIDEO_MAX_SIZE_MB,
} from '#/lib/constants'
import {
  usePhotoLibraryPermission,
  useVideoLibraryPermission,
} from '#/lib/hooks/usePermissions'
import {openUnifiedPicker} from '#/lib/media/picker'
import {extractDataUriMime} from '#/lib/media/util'
import {useEnableSquareButtons} from '#/state/preferences/enable-square-buttons'
import {MAX_GALLERY_IMAGES} from '#/view/com/composer/state/composer'
import {atoms as a, useTheme} from '#/alf'
import {Button} from '#/components/Button'
import {useSheetWrapper} from '#/components/Dialog/sheet-wrapper'
import {Image_Stroke2_Corner0_Rounded as ImageIcon} from '#/components/icons/Image'
import * as toast from '#/components/Toast'
import {IS_NATIVE, IS_WEB} from '#/env'
import {isAnimatedGif} from './videos/isAnimatedGif'

export type SelectMediaButtonProps = {
  disabled?: boolean
  /**
   * If set, this limits the types of assets that can be selected.
   */
  allowedAssetTypes: AssetType | undefined
  selectedAssetsCount: number
  onSelectAssets: (props: {
    type: AssetType
    assets: ImagePickerAsset[]
    errors: string[]
  }) => void | Promise<void>
  /**
   * If true, automatically open the media picker when the component mounts.
   */
  autoOpen?: boolean
}

/**
 * Generic asset classes, or buckets, that we support.
 */
export type AssetType = 'video' | 'image' | 'gif'

/**
 * Shadows `ImagePickerAsset` from `expo-image-picker`, but with a guaranteed `mimeType`
 */
type ValidatedImagePickerAsset = Omit<ImagePickerAsset, 'mimeType'> & {
  mimeType: string
}

/**
 * Codes for known validation states
 */
enum SelectedAssetError {
  Unsupported = 'Unsupported',
  MixedTypes = 'MixedTypes',
  MaxImages = 'MaxImages',
  MaxVideos = 'MaxVideos',
  VideoTooLong = 'VideoTooLong',
  FileTooBig = 'FileTooBig',
  MaxGIFs = 'MaxGIFs',
}

/**
 * Supported video mime types. This differs slightly from
 * `SUPPORTED_MIME_TYPES` from `#/lib/constants` because we only care about
 * videos here.
 */
const SUPPORTED_VIDEO_MIME_TYPES = [
  'video/mp4',
  'video/mpeg',
  'video/webm',
  'video/quicktime',
] as const
type SupportedVideoMimeType = (typeof SUPPORTED_VIDEO_MIME_TYPES)[number]
function isSupportedVideoMimeType(
  mimeType: string,
): mimeType is SupportedVideoMimeType {
  return SUPPORTED_VIDEO_MIME_TYPES.includes(mimeType as SupportedVideoMimeType)
}

/**
 * Supported image mime types.
 */
const SUPPORTED_IMAGE_MIME_TYPES = (
  [
    'image/gif',
    'image/jpeg',
    'image/png',
    'image/svg+xml',
    'image/webp',
    'image/avif',
    IS_NATIVE && 'image/heic',
  ] as const
).filter(Boolean)
type SupportedImageMimeType = Exclude<
  (typeof SUPPORTED_IMAGE_MIME_TYPES)[number],
  boolean
>
function isSupportedImageMimeType(
  mimeType: string,
): mimeType is SupportedImageMimeType {
  return SUPPORTED_IMAGE_MIME_TYPES.includes(mimeType as SupportedImageMimeType)
}

/**
 * This is a last-ditch effort type thing here, try not to rely on this.
 */
const extensionToMimeType: Record<
  string,
  SupportedVideoMimeType | SupportedImageMimeType
> = {
  mp4: 'video/mp4',
  mov: 'video/quicktime',
  webm: 'video/webm',
  webp: 'image/webp',
  gif: 'image/gif',
  jpg: 'image/jpeg',
  jpeg: 'image/jpeg',
  png: 'image/png',
  svg: 'image/svg+xml',
  heic: 'image/heic',
}

/**
 * Attempts to bucket the given asset into one of our known types based on its
 * `mimeType`. If `mimeType` is not available, we try to infer it through
 * various means.
 */
async function classifyImagePickerAsset(asset: ImagePickerAsset): Promise<
  | {
      success: true
      type: AssetType
      mimeType: string
    }
  | {
      success: false
      type: undefined
      mimeType: undefined
    }
> {
  /*
   * Try to use the `mimeType` reported by `expo-image-picker` first.
   */
  let mimeType = asset.mimeType

  if (!mimeType) {
    /*
     * We can try to infer this from the data-uri.
     */
    const maybeMimeType = extractDataUriMime(asset.uri)

    if (
      maybeMimeType.startsWith('image/') ||
      maybeMimeType.startsWith('video/')
    ) {
      mimeType = maybeMimeType
    } else if (maybeMimeType.startsWith('file/')) {
      /*
       * On the off-chance we get a `file/*` mime, try to infer from the
       * extension.
       */
      const extension = asset.uri.split('.').pop()?.toLowerCase()
      mimeType = extensionToMimeType[extension || '']
    }
  }

  if (!mimeType) {
    return {
      success: false,
      type: undefined,
      mimeType: undefined,
    }
  }

  /*
   * Distill this down into a type "class".
   */
  let type: AssetType | undefined
  if (mimeType === 'image/gif') {
    let bytes: ArrayBuffer | undefined
    if (IS_WEB) {
      bytes = await asset.file?.arrayBuffer()
    } else {
      const file = new File(asset.uri)
      if (file.exists) {
        bytes = await file.arrayBuffer()
      }
    }
    if (bytes) {
      const {isAnimated} = isAnimatedGif(bytes)
      type = isAnimated ? 'gif' : 'image'
    } else {
      // If we can't read the file, assume it's animated
      type = 'gif'
    }
  } else if (mimeType?.startsWith('video/')) {
    type = 'video'
  } else if (mimeType?.startsWith('image/')) {
    type = 'image'
  }

  /*
   * If we weren't able to find a valid type, we don't support this asset.
   */
  if (!type) {
    return {
      success: false,
      type: undefined,
      mimeType: undefined,
    }
  }

  return {
    success: true,
    type,
    mimeType,
  }
}

/**
 * Takes in raw assets from `expo-image-picker` and applies validation. Returns
 * the dominant `AssetType`, any valid assets, and any errors encountered along
 * the way.
 */
async function processImagePickerAssets(
  assets: ImagePickerAsset[],
  {
    selectionCountRemaining,
    allowedAssetTypes,
  }: {
    selectionCountRemaining: number
    allowedAssetTypes: AssetType | undefined
  },
) {
  /*
   * A deduped set of error codes, which we'll use later
   */
  const errors = new Set<SelectedAssetError>()

  /*
   * We only support selecting a single type of media at a time, so this gets
   * set to whatever the first valid asset type is, OR to whatever
   * `allowedAssetTypes` is set to.
   */
  let selectableAssetType: AssetType | undefined

  /*
   * This will hold the assets that we can actually use, after filtering
   */
  let supportedAssets: ValidatedImagePickerAsset[] = []

  for (const asset of assets) {
    const {success, type, mimeType} = await classifyImagePickerAsset(asset)

    if (!success) {
      errors.add(SelectedAssetError.Unsupported)
      continue
    }

    /*
     * If we have an `allowedAssetTypes` prop, constrain to that. Otherwise,
     * set this to the first valid asset type we see, and then use that to
     * constrain all remaining selected assets.
     */
    selectableAssetType = allowedAssetTypes || selectableAssetType || type

    // ignore mixed types
    if (type !== selectableAssetType) {
      errors.add(SelectedAssetError.MixedTypes)
      continue
    }

    if (type === 'video') {
      /**
       * We don't care too much about mimeType at this point on native,
       * since the `processVideo` step later on will convert to `.mp4`.
       */
      if (IS_WEB && !isSupportedVideoMimeType(mimeType)) {
        errors.add(SelectedAssetError.Unsupported)
        continue
      }

      /*
       * Filesize appears to be stable across all platforms, so we can use it
       * to filter out large files on web. On native, we compress these anyway,
       * so we only check on web.
       */
      if (IS_WEB && asset.fileSize && asset.fileSize > VIDEO_MAX_SIZE) {
        errors.add(SelectedAssetError.FileTooBig)
        continue
      }
    }

    if (type === 'image') {
      if (!isSupportedImageMimeType(mimeType)) {
        errors.add(SelectedAssetError.Unsupported)
        continue
      }
    }

    if (type === 'gif') {
      /*
       * Filesize appears to be stable across all platforms, so we can use it
       * to filter out large files on web. On native, we compress GIFs as
       * videos anyway, so we only check on web.
       */
      if (IS_WEB && asset.fileSize && asset.fileSize > VIDEO_MAX_SIZE) {
        errors.add(SelectedAssetError.FileTooBig)
        continue
      }
    }

    /*
     * All validations passed, we have an asset!
     */
    supportedAssets.push({
      mimeType,
      ...asset,
      /*
       * In `expo-image-picker` >= v17, `uri` is now a `blob:` URL, not a
       * data-uri. Our handling elsewhere in the app (for web) relies on the
       * base64 data-uri, so we construct it here for web only.
       */
      uri:
        IS_WEB && asset.base64
          ? `data:${mimeType};base64,${asset.base64}`
          : asset.uri,
    })
  }

  if (supportedAssets.length > 0) {
    if (selectableAssetType === 'image') {
      if (supportedAssets.length > selectionCountRemaining) {
        errors.add(SelectedAssetError.MaxImages)
        supportedAssets = supportedAssets.slice(0, selectionCountRemaining)
      }
    } else if (selectableAssetType === 'video') {
      if (supportedAssets.length > 1) {
        errors.add(SelectedAssetError.MaxVideos)
        supportedAssets = supportedAssets.slice(0, 1)
      }

      if (supportedAssets[0].duration) {
        if (IS_WEB) {
          /*
           * Web reports duration as seconds
           */
          supportedAssets[0].duration = supportedAssets[0].duration * 1000
        }

        if (supportedAssets[0].duration > VIDEO_MAX_DURATION_MS) {
          errors.add(SelectedAssetError.VideoTooLong)
          supportedAssets = []
        }
      } else {
        errors.add(SelectedAssetError.Unsupported)
        supportedAssets = []
      }
    } else if (selectableAssetType === 'gif') {
      if (supportedAssets.length > 1) {
        errors.add(SelectedAssetError.MaxGIFs)
        supportedAssets = supportedAssets.slice(0, 1)
      }
    }
  }

  return {
    type: selectableAssetType!, // set above
    assets: supportedAssets,
    errors,
  }
}

export function SelectMediaButton({
  disabled,
  allowedAssetTypes,
  selectedAssetsCount,
  onSelectAssets,
  autoOpen,
}: SelectMediaButtonProps) {
  const {_} = useLingui()
  const {requestPhotoAccessIfNeeded} = usePhotoLibraryPermission()
  const {requestVideoAccessIfNeeded} = useVideoLibraryPermission()
  const sheetWrapper = useSheetWrapper()
  const t = useTheme()
  const hasAutoOpened = useRef(false)

  // Picker uses the gallery cap; the reducer decides which embed variant
  // to land in based on the final image count.
  const selectionCountRemaining = MAX_GALLERY_IMAGES - selectedAssetsCount

  const processSelectedAssets = useCallback(
    async (rawAssets: ImagePickerAsset[]) => {
      const {
        type,
        assets,
        errors: errorCodes,
      } = await processImagePickerAssets(rawAssets, {
        selectionCountRemaining,
        allowedAssetTypes,
      })

      /*
       * Convert error codes to user-friendly messages.
       */
      const errors = Array.from(errorCodes).map(error => {
        return {
          [SelectedAssetError.Unsupported]: _(
            msg`One or more of your selected files are not supported.`,
          ),
          [SelectedAssetError.MixedTypes]: _(
            msg`Selecting multiple media types is not supported.`,
          ),
          [SelectedAssetError.MaxImages]: _(
            msg({
              message: `You can select up to ${plural(MAX_GALLERY_IMAGES, {
                other: '# images',
              })} in total.`,
              comment: `Error message for maximum number of images that can be selected to add to a post.`,
            }),
          ),
          [SelectedAssetError.MaxVideos]: _(
            msg`You can only select one video at a time.`,
          ),
          [SelectedAssetError.VideoTooLong]: _(
            msg`Videos must be less than 3 minutes long.`,
          ),
          [SelectedAssetError.MaxGIFs]: _(
            msg`You can only select one GIF at a time.`,
          ),
          [SelectedAssetError.FileTooBig]: _(
            msg`One or more of your selected files are too large. Maximum size is ${VIDEO_MAX_SIZE_MB} MB.`,
          ),
        }[error]
      })

      /*
       * Report the selected assets and any errors back to the
       * composer.
       */
      onSelectAssets({
        type,
        assets,
        errors,
      })
    },
    [_, onSelectAssets, selectionCountRemaining, allowedAssetTypes],
  )

  const onPressSelectMedia = useCallback(async () => {
    if (IS_NATIVE) {
      const [photoAccess, videoAccess] = await Promise.all([
        requestPhotoAccessIfNeeded(),
        requestVideoAccessIfNeeded(),
      ])

      if (!photoAccess && !videoAccess) {
        toast.show(_(msg`You need to allow access to your media library.`), {
          type: 'error',
        })
        return
      }
    }

    if (IS_NATIVE && Keyboard.isVisible()) {
      Keyboard.dismiss()
    }

    const {assets, canceled} = await sheetWrapper(
      openUnifiedPicker({selectionCountRemaining}),
    )

    if (canceled) return

    await processSelectedAssets(assets)
  }, [
    _,
    requestPhotoAccessIfNeeded,
    requestVideoAccessIfNeeded,
    sheetWrapper,
    processSelectedAssets,
    selectionCountRemaining,
  ])

  const enableSquareButtons = useEnableSquareButtons()

  useEffect(() => {
    if (autoOpen && !hasAutoOpened.current && !disabled) {
      hasAutoOpened.current = true
      void onPressSelectMedia()
    }
  }, [autoOpen, disabled, onPressSelectMedia])

  return (
    <Button
      testID="openMediaBtn"
      onPress={onPressSelectMedia}
      label={_(
        msg({
          message: `Add media to post`,
          comment: `Accessibility label for button in composer to add images, a video, or a GIF to a post`,
        }),
      )}
      accessibilityHint={_(
        msg({
          message: `Opens device gallery to select up to ${plural(
            MAX_GALLERY_IMAGES,
            {other: '# images'},
          )}, or a single video or GIF.`,
          comment: `Accessibility hint for button in composer to add images, a video, or a GIF to a post.`,
        }),
      )}
      style={a.p_sm}
      variant="ghost"
      shape={enableSquareButtons ? 'square' : 'round'}
      color="primary"
      disabled={disabled}>
      <ImageIcon
        size="lg"
        style={disabled && t.atoms.text_contrast_low}
        accessibilityIgnoresInvertColors={true}
      />
    </Button>
  )
}
