/*
 * Copyright (c) 2016-present Invertase Limited & Contributors
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this library except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *   http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 *
 */

import {
  Base64,
  getDataUrlParts,
  hasOwnProperty,
  isInteger,
  isNumber,
  isObject,
  isString,
  isUndefined,
  pathChild,
  pathLastComponent,
  pathParent,
  ReferenceBase,
  toFilePath,
} from '@react-native-firebase/app/dist/module/common';
import StorageDownloadTask from './StorageDownloadTask';
import StorageListResult, { provideStorageReferenceClass } from './StorageListResult';
import { StringFormat } from './StorageStatics';
import StorageUploadTask from './StorageUploadTask';
import { validateMetadata } from './utils';
import type {
  StorageReference,
  SettableMetadata,
  UploadMetadata,
  ListOptions,
  FullMetadata,
  Task,
  FirebaseStorage,
} from './types/storage';
import type { ListResultInternal, StorageInternal } from './types/internal';

export default class Reference extends ReferenceBase implements StorageReference {
  _storage: StorageInternal;

  constructor(storage: StorageInternal, path: string) {
    super(path);
    this._storage = storage;
  }

  /**
   * @url https://firebase.google.com/docs/reference/js/firebase.storage.Reference#bucket
   */
  get bucket(): string {
    return this._storage._customUrlOrRegion!.replace('gs://', '');
  }

  /**
   * @url https://firebase.google.com/docs/reference/js/firebase.storage.Reference#fullPath
   */
  get fullPath(): string {
    return this.path;
  }

  /**
   * @url https://firebase.google.com/docs/reference/js/firebase.storage.Reference#name
   */
  get name(): string {
    return pathLastComponent(this.path);
  }

  /**
   * @url https://firebase.google.com/docs/reference/js/firebase.storage.Reference#parent
   */
  get parent(): StorageReference | null {
    const parentPath = pathParent(this.path);
    if (parentPath === null) {
      return parentPath;
    }
    return new Reference(this._storage, parentPath);
  }

  /**
   * @url https://firebase.google.com/docs/reference/js/firebase.storage.Reference#root
   */
  get root(): StorageReference {
    return new Reference(this._storage, '/');
  }

  /**
   * @url https://firebase.google.com/docs/reference/js/firebase.storage.Reference#storage
   */
  get storage(): FirebaseStorage {
    return this._storage;
  }

  /**
   * @url https://firebase.google.com/docs/reference/js/firebase.storage.Reference#child
   */
  child(path: string): StorageReference {
    const childPath = pathChild(this.path, path);
    return new Reference(this._storage, childPath);
  }

  /**
   * @url https://firebase.google.com/docs/reference/js/firebase.storage.Reference#delete
   */
  delete(): Promise<void> {
    return this._storage.native.deleteObject(this.toString());
  }

  /**
   * @url https://firebase.google.com/docs/reference/js/firebase.storage.Reference#getDownloadURL
   */
  getDownloadURL(): Promise<string> {
    return this._storage.native.getDownloadURL(this.toString());
  }

  /**
   * @url https://firebase.google.com/docs/reference/js/firebase.storage.Reference#getMetadata
   */
  getMetadata(): Promise<FullMetadata> {
    return this._storage.native.getMetadata(this.toString());
  }

  /**
   * @url https://firebase.google.com/docs/reference/js/firebase.storage.Reference#list
   */
  list(options?: ListOptions): Promise<StorageListResult> {
    if (!isUndefined(options) && !isObject(options)) {
      throw new Error(
        "firebase.storage.StorageReference.list(*) 'options' expected an object value.",
      );
    }

    const listOptions: { maxResults: number; pageToken?: string } = {
      maxResults: 1000,
    };

    if (options) {
      if (hasOwnProperty(options, 'maxResults')) {
        const maxResults = options.maxResults;
        if (maxResults === null || isUndefined(maxResults)) {
          // no-op (keep default)
        } else {
          if (!isNumber(maxResults) || !isInteger(maxResults)) {
            throw new Error(
              "firebase.storage.StorageReference.list(*) 'options.maxResults' expected a number value.",
            );
          }

          if (maxResults < 1 || maxResults > 1000) {
            throw new Error(
              "firebase.storage.StorageReference.list(*) 'options.maxResults' expected a number value between 1-1000.",
            );
          }

          listOptions.maxResults = maxResults;
        }
      }

      if (hasOwnProperty(options, 'pageToken')) {
        const pageToken = options.pageToken;
        if (pageToken === null || isUndefined(pageToken)) {
          // no-op
        } else if (!isUndefined(pageToken) && pageToken !== null) {
          if (!isString(pageToken)) {
            throw new Error(
              "firebase.storage.StorageReference.list(*) 'options.pageToken' expected a string value.",
            );
          }

          listOptions.pageToken = pageToken;
        }
      }
    }

    return this._storage.native
      .list(this.toString(), listOptions)
      .then((data: ListResultInternal) => new StorageListResult(this._storage, data));
  }

  /**
   * @url https://firebase.google.com/docs/reference/js/firebase.storage.Reference#listAll
   */
  listAll(): Promise<StorageListResult> {
    return this._storage.native
      .listAll(this.toString())
      .then((data: ListResultInternal) => new StorageListResult(this._storage, data));
  }

  /**
   * @url https://firebase.google.com/docs/reference/js/firebase.storage.Reference#put
   */
  put(data: Blob | Uint8Array | ArrayBuffer, metadata?: UploadMetadata): Task {
    const validatedMetadata = isUndefined(metadata) ? metadata : validateMetadata(metadata, false);

    return new StorageUploadTask(this, task =>
      Base64.fromData(data).then(({ string, format }) => {
        const { _string, _format, _metadata } = this._updateString(
          string as string,
          format,
          validatedMetadata,
          false,
        );
        return this._storage.native.putString(
          this.toString(),
          _string,
          _format,
          _metadata,
          task._id,
        );
      }),
    );
  }

  /**
   * @url https://firebase.google.com/docs/reference/js/firebase.storage.Reference#putString
   */
  putString(
    string: string,
    format: (typeof StringFormat)[keyof typeof StringFormat] = StringFormat.RAW,
    metadata?: UploadMetadata,
  ): Task {
    const { _string, _format, _metadata } = this._updateString(string, format, metadata, false);

    return new StorageUploadTask(this, task =>
      this._storage.native.putString(this.toString(), _string, _format, _metadata, task._id),
    );
  }

  /**
   * @url https://firebase.google.com/docs/reference/js/firebase.storage.Reference#fullPath
   */
  toString(): string {
    if (this.path.length <= 1) {
      return `${this._storage._customUrlOrRegion}`;
    }

    return `${this._storage._customUrlOrRegion}/${this.path}`;
  }

  /**
   * @url https://firebase.google.com/docs/reference/js/firebase.storage.Reference#updateMetadata
   */
  updateMetadata(metadata: SettableMetadata): Promise<FullMetadata> {
    const validatedMetadata = validateMetadata(metadata);
    return this._storage.native.updateMetadata(this.toString(), validatedMetadata);
  }

  /* ----------------------------------------
   *   EXTRA APIS (DO NOT ON EXIST WEB SDK)
   * ---------------------------------------- */

  /**
   * @url https://firebase.google.com/docs/reference/js/firebase.storage.Reference
   */
  writeToFile(filePath: string): Task {
    if (!isString(filePath)) {
      throw new Error(
        "firebase.storage.StorageReference.writeToFile(*) 'filePath' expects a string value.",
      );
    }

    return new StorageDownloadTask(this, task =>
      this._storage.native.writeToFile(this.toString(), toFilePath(filePath), task._id),
    );
  }

  /**
   * @url https://firebase.google.com/docs/reference/js/firebase.storage.Reference
   */
  putFile(filePath: string, metadata?: UploadMetadata): Task {
    const validatedMetadata = isUndefined(metadata) ? metadata : validateMetadata(metadata, false);

    if (!isString(filePath)) {
      throw new Error(
        "firebase.storage.StorageReference.putFile(*, _) 'filePath' expects a string value.",
      );
    }

    return new StorageUploadTask(this, task =>
      this._storage.native.putFile(
        this.toString(),
        toFilePath(filePath),
        validatedMetadata,
        task._id,
      ),
    );
  }

  _updateString(
    string: string,
    format: (typeof StringFormat)[keyof typeof StringFormat],
    metadata: UploadMetadata | undefined,
    update = false,
  ): { _string: string; _format: string; _metadata: UploadMetadata | undefined } {
    if (!isString(string)) {
      throw new Error(
        "firebase.storage.StorageReference.putString(*, _, _) 'string' expects a string value.",
      );
    }

    if (!Object.values(StringFormat).includes(format)) {
      throw new Error(
        `firebase.storage.StorageReference.putString(_, *, _) 'format' provided is invalid, must be one of ${Object.values(
          StringFormat,
        ).join(',')}.`,
      );
    }

    if (!isUndefined(metadata)) {
      metadata = validateMetadata(metadata, update) as UploadMetadata;
    }

    let _string = string;
    let _format = format;
    let _metadata = metadata;

    if (format === StringFormat.RAW) {
      _string = Base64.btoa(_string);
      _format = StringFormat.BASE64;
    } else if (format === StringFormat.DATA_URL) {
      const { mediaType, base64String } = getDataUrlParts(_string);
      if (isUndefined(base64String)) {
        throw new Error(
          'firebase.storage.StorageReference.putString(*, _, _) invalid data_url string provided.',
        );
      }

      if (isUndefined(metadata) || isUndefined(metadata.contentType)) {
        if (isUndefined(metadata)) {
          _metadata = {};
        }
        _metadata!.contentType = mediaType;
        _string = base64String;
        _format = StringFormat.BASE64;
      }
    }
    return { _string, _metadata, _format };
  }
}

provideStorageReferenceClass(Reference);
