import { Platform } from '@unimodules/core';

import { getAssetByID } from './AssetRegistry';
import * as AssetSources from './AssetSources';
import * as AssetUris from './AssetUris';
import { getEmbeddedAssetUri } from './EmbeddedAssets';
import * as ImageAssets from './ImageAssets';
import { downloadAsync, IS_MANAGED_ENV } from './PlatformUtils';
import resolveAssetSource from './resolveAssetSource';

type AssetDescriptor = {
  name: string;
  type: string;
  hash?: string | null;
  uri: string;
  width?: number | null;
  height?: number | null;
};

type DownloadPromiseCallbacks = {
  resolve: () => void;
  reject: (error: Error) => void;
};

export type AssetMetadata = AssetSources.AssetMetadata;

export class Asset {
  static byHash = {};
  static byUri = {};

  name: string;
  type: string;
  hash: string | null = null;
  uri: string;
  localUri: string | null = null;
  width: number | null = null;
  height: number | null = null;
  downloading: boolean = false;
  downloaded: boolean = false;
  _downloadCallbacks: DownloadPromiseCallbacks[] = [];

  constructor({ name, type, hash = null, uri, width, height }: AssetDescriptor) {
    this.name = name;
    this.type = type;
    this.hash = hash;
    this.uri = uri;

    if (typeof width === 'number') {
      this.width = width;
    }
    if (typeof height === 'number') {
      this.height = height;
    }

    // This only applies to assets that are bundled in Expo standalone apps
    if (IS_MANAGED_ENV && hash) {
      this.localUri = getEmbeddedAssetUri(hash, type);
      if (this.localUri) {
        this.downloaded = true;
      }
    }
    if (Platform.OS === 'web') {
      if (!name) {
        this.name = AssetUris.getFilename(uri);
      }
      if (!type) {
        this.type = AssetUris.getFileExtension(uri);
      }
    }
  }

  static loadAsync(moduleId: number | number[]): Promise<void[]> {
    const moduleIds = Array.isArray(moduleId) ? moduleId : [moduleId];
    return Promise.all(moduleIds.map(moduleId => Asset.fromModule(moduleId).downloadAsync()));
  }

  static fromModule(virtualAssetModule: number | string): Asset {
    if (typeof virtualAssetModule === 'string') {
      return Asset.fromURI(virtualAssetModule);
    }

    const meta = getAssetByID(virtualAssetModule);
    if (!meta) {
      throw new Error(`Module "${virtualAssetModule}" is missing from the asset registry`);
    }

    // Outside of the managed env we need the moduleId to initialize the asset
    // because resolveAssetSource depends on it
    if (!IS_MANAGED_ENV) {
      const { uri } = resolveAssetSource(virtualAssetModule);
      const asset = new Asset({
        name: meta.name,
        type: meta.type,
        hash: meta.hash,
        uri,
        width: meta.width,
        height: meta.height,
      });

      // TODO: FileSystem should probably support 'downloading' from drawable
      // resources But for now it doesn't (it only supports raw resources) and
      // React Native's Image works fine with drawable resource names for
      // images.
      if (Platform.OS === 'android' && !uri.includes(':') && (meta.width || meta.height)) {
        asset.localUri = asset.uri;
        asset.downloaded = true;
      }

      Asset.byHash[meta.hash] = asset;
      return asset;
    }

    return Asset.fromMetadata(meta);
  }

  static fromMetadata(meta: AssetMetadata): Asset {
    // The hash of the whole asset, not to be confused with the hash of a specific file returned
    // from `selectAssetSource`
    const metaHash = meta.hash;
    if (Asset.byHash[metaHash]) {
      return Asset.byHash[metaHash];
    } else if (!IS_MANAGED_ENV && !Asset.byHash[metaHash]) {
      throw new Error('Assets must be initialized with Asset.fromModule');
    }

    const { uri, hash } = AssetSources.selectAssetSource(meta);
    const asset = new Asset({
      name: meta.name,
      type: meta.type,
      hash,
      uri,
      width: meta.width,
      height: meta.height,
    });
    Asset.byHash[metaHash] = asset;
    return asset;
  }

  static fromURI(uri: string): Asset {
    if (Asset.byUri[uri]) {
      return Asset.byUri[uri];
    }

    // Possibly a Base64-encoded URI
    let type = '';
    if (uri.indexOf(';base64') > -1) {
      type = uri.split(';')[0].split('/')[1];
    } else {
      const extension = AssetUris.getFileExtension(uri);
      type = extension.startsWith('.') ? extension.substring(1) : extension;
    }

    const asset = new Asset({
      name: '',
      type,
      hash: null,
      uri,
    });

    Asset.byUri[uri] = asset;

    return asset;
  }

  async downloadAsync(): Promise<void> {
    if (this.downloaded) {
      return;
    }
    if (this.downloading) {
      await new Promise((resolve, reject) => {
        this._downloadCallbacks.push({ resolve, reject });
      });
      return;
    }
    this.downloading = true;

    try {
      if (Platform.OS === 'web') {
        if (ImageAssets.isImageType(this.type)) {
          const { width, height, name } = await ImageAssets.getImageInfoAsync(this.uri);
          this.width = width;
          this.height = height;
          this.name = name;
        } else {
          this.name = AssetUris.getFilename(this.uri);
        }
      }
      this.localUri = await downloadAsync(this.uri, this.hash, this.type, this.name);

      this.downloaded = true;
      this._downloadCallbacks.forEach(({ resolve }) => resolve());
    } catch (e) {
      this._downloadCallbacks.forEach(({ reject }) => reject(e));
      throw e;
    } finally {
      this.downloading = false;
      this._downloadCallbacks = [];
    }
  }
}