/* global File, FileReader */
/**
 * React Native {@link PubNub} File object module.
 */

import { PubNubFileInterface } from '../../core/types/file';

// --------------------------------------------------------
// ------------------------ Types -------------------------
// --------------------------------------------------------

// region Types
/**
 * File path-based file.
 */
type FileUri = { uri: string; name: string; mimeType?: string };

/**
 * Asynchronously fetched file content.
 */
type ReadableFile = { arrayBuffer: () => Promise<ArrayBuffer>; blob: () => Promise<Blob>; text: () => Promise<string> };

/**
 * PubNub File instance creation parameters.
 */
export type PubNubFileParameters =
  | File
  | FileUri
  | ReadableFile
  | { data: string | Blob | ArrayBuffer | ArrayBufferView; name: string; mimeType?: string };
// endregion

export class PubNubFile implements PubNubFileInterface {
  // region Class properties
  /**
   * Whether {@link Blob} data supported by platform or not.
   */
  static supportsBlob = typeof Blob !== 'undefined';

  /**
   * Whether {@link File} data supported by platform or not.
   */
  static supportsFile = typeof File !== 'undefined';

  /**
   * Whether {@link Buffer} data supported by platform or not.
   */
  static supportsBuffer = false;

  /**
   * Whether {@link Stream} data supported by platform or not.
   */
  static supportsStream = false;

  /**
   * Whether {@link String} data supported by platform or not.
   */
  static supportsString = true;

  /**
   * Whether {@link ArrayBuffer} supported by platform or not.
   */
  static supportsArrayBuffer = true;

  /**
   * Whether {@link PubNub} File object encryption supported or not.
   */
  static supportsEncryptFile = false;

  /**
   * Whether `File Uri` data supported by platform or not.
   */
  static supportsFileUri = true;
  // endregion

  // region Instance properties
  /**
   * File object content source.
   */
  readonly data: File | FileUri | ReadableFile;

  /**
   * File object content length.
   */
  contentLength?: number;

  /**
   * File object content type.
   */
  mimeType: string;

  /**
   * File object name.
   */
  name: string;
  // endregion

  static create(file: PubNubFileParameters) {
    return new PubNubFile(file);
  }

  constructor(file: PubNubFileParameters) {
    let fileData: PubNubFile['data'] | undefined;
    let contentLength: number | undefined;
    let fileMimeType: string | undefined;
    let fileName: string | undefined;

    if (file instanceof File) {
      fileData = file;

      fileName = file.name;
      fileMimeType = file.type;
      contentLength = file.size;
    } else if ('data' in file) {
      const contents = file.data;

      fileMimeType = file.mimeType;
      fileName = file.name;

      // create a ReadableFile wrapper for ArrayBuffer data
      if (contents instanceof ArrayBuffer || ArrayBuffer.isView(contents)) {
        // Create ReadableFile wrapper for ArrayBuffer
        const arrayBuffer = contents instanceof ArrayBuffer ? contents : contents.buffer;
        contentLength = arrayBuffer.byteLength;

        fileData = {
          arrayBuffer: async () => arrayBuffer,
          blob: async () => {
            throw new Error('toBlob() is not supported in React Native environment. Use toArrayBuffer() instead.');
          },
          text: async () => {
            const decoder = new TextDecoder();
            return decoder.decode(arrayBuffer);
          },
        } as ReadableFile;
      } else if (typeof contents === 'string') {
        // Handle string data
        const encoder = new TextEncoder();
        const arrayBuffer = encoder.encode(contents).buffer;
        contentLength = arrayBuffer.byteLength;

        fileData = {
          arrayBuffer: async () => arrayBuffer,
          blob: async () => new Blob([contents], { type: fileMimeType }),
          text: async () => contents,
        } as ReadableFile;
      } else if (contents instanceof Blob) {
        // Handle Blob data
        contentLength = contents.size;
        fileData = {
          arrayBuffer: async () => await contents.arrayBuffer(),
          blob: async () => contents,
          text: async () => await contents.text(),
        } as ReadableFile;
      } else {
        // Fallback: Try to use File constructor (may still fail in some environments)
        try {
          fileData = new File([contents], fileName, { type: fileMimeType });
          contentLength = fileData.size;
        } catch (error) {
          throw new Error(
            `Unable to create file from provided data type. ArrayBuffer, Blob, or string expected.Error: ${error}`,
          );
        }
      }
    } else if ('uri' in file) {
      fileMimeType = file.mimeType;
      fileName = file.name;
      fileData = {
        uri: file.uri,
        name: file.name,
        type: file.mimeType!,
      };
    } else throw new Error("Couldn't construct a file out of supplied options. URI or file data required.");

    if (fileData === undefined) throw new Error("Couldn't construct a file out of supplied options.");
    if (fileName === undefined) throw new Error("Couldn't guess filename out of the options. Please provide one.");

    if (contentLength) this.contentLength = contentLength;
    this.mimeType = fileMimeType!;
    this.data = fileData;
    this.name = fileName;
  }

  /**
   * Convert {@link PubNub} File object content to {@link Buffer}.
   *
   * @throws Error because {@link Buffer} not available in React Native environment.
   */
  async toBuffer() {
    throw new Error('This feature is only supported in Node.js environments.');
  }

  /**
   * Convert {@link PubNub} File object content to {@link ArrayBuffer}.
   *
   * @returns Asynchronous results of conversion to the {@link ArrayBuffer}.
   *
   * @throws Error if provided {@link PubNub} File object content is not supported for this
   * operation.
   */
  async toArrayBuffer(): Promise<ArrayBuffer> {
    if (this.data && this.data instanceof File) {
      const data = this.data;

      return new Promise((resolve, reject) => {
        const reader = new FileReader();

        reader.addEventListener('load', () => {
          if (reader.result instanceof ArrayBuffer) return resolve(reader.result);
        });
        reader.addEventListener('error', () => reject(reader.error));
        reader.readAsArrayBuffer(data);
      });
    } else if (this.data && 'uri' in this.data) {
      throw new Error('This file contains a file URI and does not contain the file contents.');
    } else if (this.data) {
      let result: ArrayBuffer | undefined;

      try {
        result = await this.data.arrayBuffer();
      } catch (error) {
        throw new Error(`Unable to support toArrayBuffer in ReactNative environment: ${error}`);
      }

      return result;
    }

    throw new Error('Unable convert provided file content type to ArrayBuffer');
  }

  /**
   * Convert {@link PubNub} File object content to {@link string}.
   *
   * @returns Asynchronous results of conversion to the {@link string}.
   */
  async toString(): Promise<string> {
    if (this.data && 'uri' in this.data) return JSON.stringify(this.data);
    else if (this.data && this.data instanceof File) {
      const data = this.data;

      return new Promise((resolve, reject) => {
        const reader = new FileReader();

        reader.addEventListener('load', () => {
          if (typeof reader.result === 'string') return resolve(reader.result);
        });
        reader.addEventListener('error', () => reject(reader.error));
        reader.readAsBinaryString(data);
      });
    }

    return this.data.text();
  }

  /**
   * Convert {@link PubNub} File object content to {@link Readable} stream.
   *
   * @throws Error because {@link Readable} stream not available in React Native environment.
   */
  async toStream() {
    throw new Error('This feature is only supported in Node.js environments.');
  }

  /**
   * Convert {@link PubNub} File object content to {@link File}.
   *
   * @returns Asynchronous results of conversion to the {@link File}.
   *
   * @throws Error if provided {@link PubNub} File object content is not supported for this
   * operation.
   */
  async toFile() {
    if (this.data instanceof File) return this.data;
    else if ('uri' in this.data)
      throw new Error('This file contains a file URI and does not contain the file contents.');
    else return this.data.blob();
  }

  /**
   * Convert {@link PubNub} File object content to file `Uri`.
   *
   * @returns Asynchronous results of conversion to file `Uri`.
   *
   * @throws Error if provided {@link PubNub} File object content is not supported for this
   * operation.
   */
  async toFileUri() {
    if (this.data && 'uri' in this.data) return this.data;

    throw new Error('This file does not contain a file URI');
  }

  /**
   * Convert {@link PubNub} File object content to {@link Blob}.
   *
   * @returns Asynchronous results of conversion to the {@link Blob}.
   *
   * @throws Error if provided {@link PubNub} File object content is not supported for this
   * operation.
   */
  async toBlob() {
    if (this.data instanceof File) return this.data;
    else if (this.data && 'uri' in this.data)
      throw new Error('This file contains a file URI and does not contain the file contents.');
    else return this.data.blob();
  }
}

export default PubNubFile;
