import UUID from 'uuid-js';
import { Platform } from 'react-native';
import { EventEmitter, Subscription, UnavailabilityError } from '@unimodules/core';

import ExponentFileSystem from './ExponentFileSystem';

import {
  DownloadOptions,
  DownloadResult,
  DownloadProgressCallback,
  DownloadProgressData,
  DownloadPauseState,
  FileInfo,
  EncodingType,
  ReadingOptions,
  WritingOptions,
  ProgressEvent,
} from './FileSystem.types';

if (!ExponentFileSystem) {
  console.warn(
    "No native ExponentFileSystem module found, are you sure the expo-file-system's module is linked properly?"
  );
}
// Prevent webpack from pruning this.
const _unused = new EventEmitter(ExponentFileSystem); // eslint-disable-line

export {
  DownloadOptions,
  DownloadResult,
  DownloadProgressCallback,
  DownloadProgressData,
  DownloadPauseState,
  FileInfo,
  EncodingType,
  ReadingOptions,
  WritingOptions,
  ProgressEvent,
};

function normalizeEndingSlash(p: string | null): string | null {
  if (p != null) {
    return p.replace(/\/*$/, '') + '/';
  }
  return null;
}

export const documentDirectory = normalizeEndingSlash(ExponentFileSystem.documentDirectory);
export const cacheDirectory = normalizeEndingSlash(ExponentFileSystem.cacheDirectory);

export const { bundledAssets, bundleDirectory } = ExponentFileSystem;

export async function getInfoAsync(
  fileUri: string,
  options: { md5?: boolean; size?: boolean } = {}
): Promise<FileInfo> {
  if (!ExponentFileSystem.getInfoAsync) {
    throw new UnavailabilityError('expo-file-system', 'getInfoAsync');
  }
  return await ExponentFileSystem.getInfoAsync(fileUri, options);
}

export async function readAsStringAsync(
  fileUri: string,
  options?: ReadingOptions
): Promise<string> {
  if (!ExponentFileSystem.readAsStringAsync) {
    throw new UnavailabilityError('expo-file-system', 'readAsStringAsync');
  }
  return await ExponentFileSystem.readAsStringAsync(fileUri, options || {});
}

export async function getContentUriAsync(fileUri: string): Promise<string> {
  if (Platform.OS === 'android') {
    if (!ExponentFileSystem.getContentUriAsync) {
      throw new UnavailabilityError('expo-file-system', 'getContentUriAsync');
    }
    return await ExponentFileSystem.getContentUriAsync(fileUri);
  } else {
    return new Promise(function(resolve, reject) {
      resolve(fileUri);
    });
  }
}

export async function writeAsStringAsync(
  fileUri: string,
  contents: string,
  options: WritingOptions = {}
): Promise<void> {
  if (!ExponentFileSystem.writeAsStringAsync) {
    throw new UnavailabilityError('expo-file-system', 'writeAsStringAsync');
  }
  return await ExponentFileSystem.writeAsStringAsync(fileUri, contents, options);
}

export async function deleteAsync(
  fileUri: string,
  options: { idempotent?: boolean } = {}
): Promise<void> {
  if (!ExponentFileSystem.deleteAsync) {
    throw new UnavailabilityError('expo-file-system', 'deleteAsync');
  }
  return await ExponentFileSystem.deleteAsync(fileUri, options);
}

export async function moveAsync(options: { from: string; to: string }): Promise<void> {
  if (!ExponentFileSystem.moveAsync) {
    throw new UnavailabilityError('expo-file-system', 'moveAsync');
  }
  return await ExponentFileSystem.moveAsync(options);
}

export async function copyAsync(options: { from: string; to: string }): Promise<void> {
  if (!ExponentFileSystem.copyAsync) {
    throw new UnavailabilityError('expo-file-system', 'copyAsync');
  }
  return await ExponentFileSystem.copyAsync(options);
}

export async function makeDirectoryAsync(
  fileUri: string,
  options: { intermediates?: boolean } = {}
): Promise<void> {
  if (!ExponentFileSystem.makeDirectoryAsync) {
    throw new UnavailabilityError('expo-file-system', 'makeDirectoryAsync');
  }
  return await ExponentFileSystem.makeDirectoryAsync(fileUri, options);
}

export async function readDirectoryAsync(fileUri: string): Promise<string[]> {
  if (!ExponentFileSystem.readDirectoryAsync) {
    throw new UnavailabilityError('expo-file-system', 'readDirectoryAsync');
  }
  return await ExponentFileSystem.readDirectoryAsync(fileUri, {});
}

export async function getFreeDiskStorageAsync(): Promise<number> {
  if (!ExponentFileSystem.getFreeDiskStorageAsync) {
    throw new UnavailabilityError('expo-file-system', 'getFreeDiskStorageAsync');
  }
  return await ExponentFileSystem.getFreeDiskStorageAsync();
}

export async function getTotalDiskCapacityAsync(): Promise<number> {
  if (!ExponentFileSystem.getTotalDiskCapacityAsync) {
    throw new UnavailabilityError('expo-file-system', 'getTotalDiskCapacityAsync');
  }
  return await ExponentFileSystem.getTotalDiskCapacityAsync();
}

export async function downloadAsync(
  uri: string,
  fileUri: string,
  options: DownloadOptions = {}
): Promise<DownloadResult> {
  if (!ExponentFileSystem.downloadAsync) {
    throw new UnavailabilityError('expo-file-system', 'downloadAsync');
  }
  return await ExponentFileSystem.downloadAsync(uri, fileUri, options);
}

export function createDownloadResumable(
  uri: string,
  fileUri: string,
  options?: DownloadOptions,
  callback?: DownloadProgressCallback,
  resumeData?: string
): DownloadResumable {
  return new DownloadResumable(uri, fileUri, options, callback, resumeData);
}

export class DownloadResumable {
  _uuid: string;
  _url: string;
  _fileUri: string;
  _options: DownloadOptions;
  _resumeData?: string;
  _callback?: DownloadProgressCallback;
  _subscription?: Subscription | null;
  _emitter: EventEmitter;

  constructor(
    url: string,
    fileUri: string,
    options: DownloadOptions = {},
    callback?: DownloadProgressCallback,
    resumeData?: string
  ) {
    this._uuid = UUID.create(4).toString();
    this._url = url;
    this._fileUri = fileUri;
    this._options = options;
    this._resumeData = resumeData;
    this._callback = callback;
    this._subscription = null;
    this._emitter = new EventEmitter(ExponentFileSystem);
  }

  async downloadAsync(): Promise<DownloadResult | undefined> {
    if (!ExponentFileSystem.downloadResumableStartAsync) {
      throw new UnavailabilityError('expo-file-system', 'downloadResumableStartAsync');
    }
    this._addSubscription();
    return await ExponentFileSystem.downloadResumableStartAsync(
      this._url,
      this._fileUri,
      this._uuid,
      this._options,
      this._resumeData
    );
  }

  async pauseAsync(): Promise<DownloadPauseState> {
    if (!ExponentFileSystem.downloadResumablePauseAsync) {
      throw new UnavailabilityError('expo-file-system', 'downloadResumablePauseAsync');
    }
    const pauseResult = await ExponentFileSystem.downloadResumablePauseAsync(this._uuid);
    this._removeSubscription();
    if (pauseResult) {
      this._resumeData = pauseResult.resumeData;
      return this.savable();
    } else {
      throw new Error('Unable to generate a savable pause state');
    }
  }

  async resumeAsync(): Promise<DownloadResult | undefined> {
    if (!ExponentFileSystem.downloadResumableStartAsync) {
      throw new UnavailabilityError('expo-file-system', 'downloadResumableStartAsync');
    }
    this._addSubscription();
    return await ExponentFileSystem.downloadResumableStartAsync(
      this._url,
      this._fileUri,
      this._uuid,
      this._options,
      this._resumeData
    );
  }

  savable(): DownloadPauseState {
    return {
      url: this._url,
      fileUri: this._fileUri,
      options: this._options,
      resumeData: this._resumeData,
    };
  }

  _addSubscription(): void {
    if (this._subscription) {
      return;
    }
    this._subscription = this._emitter.addListener(
      'Exponent.downloadProgress',
      (event: ProgressEvent) => {
        if (event.uuid === this._uuid) {
          const callback = this._callback;
          if (callback) {
            callback(event.data);
          }
        }
      }
    );
  }

  _removeSubscription(): void {
    if (!this._subscription) {
      return;
    }
    this._emitter.removeSubscription(this._subscription);
    this._subscription = null;
  }
}