import type { AssetDescriptor } from './Asset';
import type { AssetMetadata, AssetSource } from './AssetSources';
import * as AssetUris from './AssetUris';
import * as ImageAssets from './ImageAssets';

export class Asset {
  private static byHash: Record<string, Asset | undefined> = {};
  private static byUri: Record<string, Asset | undefined> = {};

  public name: string;
  public readonly type: string;
  public readonly hash: string | null = null;
  public readonly uri: string;
  public localUri: string | null = null;
  public width: number | null = null;
  public height: number | null = null;
  public downloaded: boolean = true;

  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.name ??= AssetUris.getFilename(uri);
    this.type ??= AssetUris.getFileExtension(uri);

    // Essentially run the contents of downloadAsync here.
    if (ImageAssets.isImageType(this.type)) {
      this.width = 0;
      this.height = 0;
      this.name = AssetUris.getFilename(this.uri);
    } else {
      this.name = AssetUris.getFilename(this.uri);
    }
    this.localUri = this.uri;
  }

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

  static fromModule(
    virtualAssetModule: number | string | { uri: string; width: number; height: number }
  ): Asset {
    if (typeof virtualAssetModule === 'string') {
      return Asset.fromURI(virtualAssetModule);
    } else if (typeof virtualAssetModule === 'number') {
      throw new Error(
        'Cannot resolve numeric asset IDs on the server as they are non-deterministic identifiers.'
      );
    }
    if (
      typeof virtualAssetModule === 'object' &&
      'uri' in virtualAssetModule &&
      typeof virtualAssetModule.uri === 'string'
    ) {
      const extension = AssetUris.getFileExtension(virtualAssetModule.uri);
      return new Asset({
        name: '',
        type: extension.startsWith('.') ? extension.substring(1) : extension,
        hash: null,
        uri: virtualAssetModule.uri,
        width: virtualAssetModule.width,
        height: virtualAssetModule.height,
      });
    }
    throw new Error('Unexpected asset module ID type: ' + typeof virtualAssetModule);
  }

  static fromMetadata(meta: AssetMetadata): Asset {
    const metaHash = meta.hash;
    if (Asset.byHash[metaHash]) {
      return Asset.byHash[metaHash];
    }

    const { uri, hash } = 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<this> {
    return this;
  }
}

function pickScale(scales: number[], deviceScale: number): number {
  for (let i = 0; i < scales.length; i++) {
    if (scales[i] >= deviceScale) {
      return scales[i];
    }
  }
  return scales[scales.length - 1] || 1;
}

/**
 * Selects the best file for the given asset (ex: choosing the best scale for images) and returns
 * a { uri, hash } pair for the specific asset file.
 *
 * If the asset isn't an image with multiple scales, the first file is selected.
 */
function selectAssetSource(meta: AssetMetadata): AssetSource {
  // This logic is based on that of AssetSourceResolver, with additional support for file hashes and
  // explicitly provided URIs
  const scale = pickScale(meta.scales, 1);
  const index = meta.scales.findIndex((s) => s === scale);
  const hash = meta.fileHashes ? (meta.fileHashes[index] ?? meta.fileHashes[0]) : meta.hash;

  // Allow asset processors to directly provide the URL to load
  const uri = meta.fileUris ? (meta.fileUris[index] ?? meta.fileUris[0]) : meta.uri;
  if (uri) {
    return { uri, hash };
  }

  const fileScale = scale === 1 ? '' : `@${scale}x`;
  const fileExtension = meta.type ? `.${encodeURIComponent(meta.type)}` : '';
  const suffix = `/${encodeURIComponent(meta.name)}${fileScale}${fileExtension}`;
  const params = new URLSearchParams({
    platform: process.env.EXPO_OS!,
    hash: meta.hash,
  });

  // For assets with a specified absolute URL, we use the existing origin instead of prepending the
  // development server or production CDN URL origin
  if (/^https?:\/\//.test(meta.httpServerLocation)) {
    const uri = meta.httpServerLocation + suffix + '?' + params;
    return { uri, hash };
  }

  // In correctly configured apps, we arrive here if the asset is locally available on disk due to
  // being managed by expo-updates, and `getLocalAssetUri(hash)` must return a local URI for this
  // hash. Since the asset is local, we don't have a remote URL and specify an invalid URL (an empty
  // string) as a placeholder.
  return { uri: '', hash };
}
