import { PictureHelper } from './../../ui/files/picture-helper';
import { Extensions } from '../../constants/constants';
import { bindable, bindingMode, observable, autoinject, TaskQueue, inlineView } from 'aurelia-framework';
import { EventAggregator } from 'aurelia-event-aggregator';
import { Box } from '../../modules/box';
import { Container } from 'aurelia-framework';
import { I18N } from 'aurelia-i18n';
import { CustomLogger } from '../../modules/custom-logger';
import Compressor from 'compressorjs'
import { CompressorOptions } from '../../model/common-model';
import { AuthService } from '../../security/auth-service';
import heic2any from './utilities/heic2any';
import { RemoteFileStorage } from './../../services/file-storage-service';

//#region Declarations
declare let Flow: any;
let fileTypes = /(\.|\/)(msg|eml|gif|jpe?g|png|docx?|dotx?|xlsx?|xlsm?|pptx?|pdf|zip|x-zip-compressed|msword|ms-excel|ms-powerpoint|vnd.openxmlformats-officedocument.wordprocessingml.document|vnd.openxmlformats-officedocument.spreadsheetml.sheet|vnd.openxmlformats-officedocument.presentationml.presentation|vnd.ms-excel.sheet.macroEnabled.12)$/i,
  fileSize = 104857600; // 100 MB
//#endregion

export enum ExistenceAction { None, Replace, Duplicate, Cancel };

export class FileFlow {
  constructor(fileFlow: flowjs.FlowFile, private ea: EventAggregator) {
    this.uid = fileFlow.uniqueIdentifier;
    this.name = fileFlow.name;
    this.size = fileFlow.size;
    this.flow = fileFlow.flowObj;
    this.fileFlow = fileFlow;

    let reader = new FileReader();
    reader.readAsDataURL(this.fileFlow.file);
    reader.onloadend = () => {
      this.previewSrc = reader.result;
    }
  }
  public name = "";
  public displayName = "";
  public size = 0;
  public averageSpeed = 0;
  public speed = 0;
  public isPaused = false;
  public isReuploaded: boolean = false;
  public isCanceled = false;
  @observable
  public progress = 0;
  @observable
  public isComplete = false;
  @observable
  public errorMessage = "";
  @observable
  public isError = false;
  @observable
  public get isUploading(): boolean {
    return !this.isError && !this.isPaused && !this.isComplete;
  }
  @observable
  public previewSrc: string | ArrayBuffer;
  public linkEntity: any;
  public serverId = 0;
  public url: string;
  public thumbnail;
  public log;
  public uid: string;
  public entityId: number;
  private flow: flowjs.Flow;
  public fileFlow: flowjs.FlowFile;
  public pause() {
    this.fileFlow.pause();
    this.isPaused = true;
  }
  public resume() {
    //this.fileFlow.star
    this.fileFlow.resume();
    this.isPaused = true;
  }
  public cancel() {
    this.isPaused = true;
    this.isCanceled = true;
    this.fileFlow.cancel();
  }
  public retry() {
    this.isError = false;
    this.isComplete = false;

    this.fileFlow.bootstrap();
    this.fileFlow.resume();
  }
}
export class FlowHelper {
  public box: Box;
  public fileUploaded: (ff: FileFlow) => void;
  public fileAdded: (ff: FileFlow) => Promise<void>;
  public checkExistence: (file) => Promise<ExistenceAction>;
  public uploadComplete: (ffArray: Array<FileFlow>) => void;
  public showLoader: (value: boolean) => void;

  @observable
  public isUploading: boolean = false;
  @observable
  public progress: number = 0;
  @observable
  private innerUploadingFiles: Array<FileFlow> = [];
  private flow: flowjs.Flow;
  public i18n: I18N;
  private fileTypes: RegExp;
  @observable
  public folderName: string;
  @observable
  private params: any;
  public useUniqueName: boolean;
  public resizePicture: boolean = false;
  public autoConvert: boolean = false;
  public pictureQuality: number;

  constructor(public logger: CustomLogger, target: string, private eventAggregator: EventAggregator, allowDuplicateUploads: boolean, confirmUploadFn: Function = null, private acceptedExtensions: string = null, resizePicture: boolean = false, folderName = "", accessToken: any = null, private compressorOptions: CompressorOptions, autoConvert: boolean = false, useUniqueName: boolean = false, pictureQuality: number = 0.7) {
    this.folderName = folderName;
    this.useUniqueName = useUniqueName;
    this.resizePicture = resizePicture;
    this.autoConvert = autoConvert;
    this.pictureQuality = pictureQuality;
    this.params = {
      target: target,
      allowDuplicateUploads: allowDuplicateUploads,
      chunkSize: 1024 * 1024,
      query: {
        "folderName": this.folderName
      },
      generateUniqueIdentifier: () => Math.random().toString(36).substr(2, 9)
    };
    if (accessToken != null) {
      this.params.headers = {
        "Authorization": `Bearer ${accessToken}`
      }
    }
    this.i18n = Container.instance.get(I18N);

    this.setFileTypes(acceptedExtensions);
    this.flow = new Flow(this.params);

    this.flow.on("uploadStart", () => {
      this.isUploading = true;
    });

    this.flow.on('filesSubmitted', async (array: Array<flowjs.FlowFile>, event: Event) => {
      if (array.length == 0) return false;
      // Autoriser maximum 1 uploader � la fois
      if (array.length > 1 && allowDuplicateUploads == false) {
        this.logger.LogError(this.i18n.tr('fileUpload.onlyOneDocument', { ns: 'common' }), null, null, true);
        return false;
      }

      let startUploading = async () => {
        this.showLoader(true);
        await this.checkArray(array);
        if (this.autoConvert) {
          await this.convertHeicToJpeg(array);
        }
        if (this.resizePicture) {
          await this.processArray(array);
        }

        this.flow.opts.query["folderName"] = this.folderName;
        await Promise.all(array.map(async (val: flowjs.FlowFile) => {
          let fileFlow = new FileFlow(val, this.eventAggregator);
          fileFlow.isReuploaded = (val as flowjs.FlowFile & { isReuploaded: boolean }).isReuploaded;
          await this.fileAdded(fileFlow);
          this.innerUploadingFiles.push(fileFlow);
        }));

        this.flow.upload();
      };
      if (confirmUploadFn != null) {
        let confirm = await confirmUploadFn({ array: array, event: event });
        if (confirm == true) {
          startUploading();
        }
        else {
          this.flow.cancel();
          console.log("upload cancelled by user");
        }
      }
      else {
        startUploading();
      }
    });

    this.flow.on('fileAdded', async (file: flowjs.FlowFile, event: Event) => {
      // Important d'avoir l'extention en minuscule pour être accepté côté serveur
      let filename: string = PictureHelper.getFileNameWithoutExtension(file.name);

      if (this.useUniqueName) {
        filename += "_" + new Date().getTime().toString();
      }
      let extension = PictureHelper.getFileExtension(file.name);
      (file as any).displayName = file.name;
      file.name = filename + extension;

      // Validation (type de fichier, taille max)
      let uploadErrors = [];
      if (!this.fileTypes.test(extension))
        uploadErrors.push(this.i18n.tr('fileUpload.invalidFileType', { ns: 'common' }));
      if (file.size > fileSize)
        uploadErrors.push(this.i18n.tr('fileUpload.invalidFileSize', { ns: 'common' }));
      if (uploadErrors.length > 0) {
        this.logger.LogError(uploadErrors.join("</br>"), null, null, true);
        return false;
      }
    });

    this.flow.on('fileSuccess', (file: flowjs.FlowFile, message: string) => {
      let fileUploaded = this.innerUploadingFiles.find(f => f.uid === file.uniqueIdentifier);
      if (message != null && message.length > 0) {
        let jsonMessage = JSON.parse(message);

        if (jsonMessage.ServerId)
          fileUploaded.serverId = jsonMessage.ServerId;

        if (jsonMessage.Thumbnail)
          fileUploaded.thumbnail = jsonMessage.Thumbnail;

        if (jsonMessage.Log)
          fileUploaded.log = jsonMessage.Log;

        if (jsonMessage.Url)
          fileUploaded.url = jsonMessage.Url;
      }
      fileUploaded.displayName = (file as any).displayName;
      this.fileUploaded(fileUploaded);
      fileUploaded.isComplete = true;
      this.showLoader(false);
    });

    this.flow.on('fileError', (file: flowjs.FlowFile, message: string) => {
      this.showLoader(false);
      let errorFile = this.innerUploadingFiles.find(f => f.uid === file.uniqueIdentifier);
      if (errorFile != null) {
        errorFile.isError = true;
        errorFile.progress = 0;

        if (message?.length > 0) {
          let parseErrorMessage = JSON.parse(message);
          errorFile.errorMessage = parseErrorMessage.Message;
        }
      }
      this.isUploading = false;
    });

    this.flow.on('complete', () => {
      this.showLoader(false);
      if (this.uploadComplete != null)
        this.uploadComplete(this.innerUploadingFiles);
    });

    this.flow.on('fileProgress', (file, chunk) => {
      let uploadingFile = this.innerUploadingFiles.find(f => f.uid === file.uniqueIdentifier);
      let progress = file.progress() * 100;
      if (uploadingFile)
        uploadingFile.progress = progress;
    });
  }

  private async processArray(array: Array<flowjs.FlowFile>) {
    // map array to promises
    const promises = array.map(this.resizeImage, this);
    // wait until all promises are resolved
    let results = await Promise.all(promises);
    results.forEach((result) => {
      if (result != null) {
        this.createAndReplaceNewFileFlow(result[0], result[1], array, true);
      }
    });
  }

  private async convertHeicToJpeg(array: Array<flowjs.FlowFile>) {
    let toReplace = new Map<flowjs.FlowFile, File>();
    for (let file of array) {
      let extension = file.name.substr(file.name.lastIndexOf('.')).toLowerCase();
      if (extension == ".heic" || extension == ".heif") {
        let filename = file.name.replace(/\.[^/.]+$/, "");
        file.name = filename + ".jpeg";
        const newFile = (await heic2any({
          blob: file.file,
          toType: "image/jpeg",
          quality: 1.0
        })) as File;
        toReplace.set(file, newFile);
      }
    }

    toReplace.forEach((resizedFile, originalFileFlow) => {
      this.createAndReplaceNewFileFlow(originalFileFlow, resizedFile, array, false);
    });
  }

  private async checkArray(array: Array<flowjs.FlowFile>) {
    const promises = array.map(f => this.askToKeep(f));
    let results = await Promise.all(promises);
    results.forEach((result) => {
      (result.file as flowjs.FlowFile & { isReuploaded: boolean }).isReuploaded = result.toKeep == ExistenceAction.Replace;
      if (result.toKeep == ExistenceAction.Cancel) {
        this.flow.removeFile(result.file);
        array.remove(result.file);
      }
      else if (result.toKeep == ExistenceAction.Duplicate) {
        result.file.name = PictureHelper.getFileNameWithoutExtension(result.file.name) + "_" + new Date().getTime().toString() + PictureHelper.getFileExtension(result.file.name);
      }
    });
  }

  private createAndReplaceNewFileFlow(originalFileFlow: flowjs.FlowFile, resizedFile: File, array: Array<flowjs.FlowFile>, resize: boolean) {
    let uniqueIdentifier = originalFileFlow.uniqueIdentifier;
    let toRemoveIndex = this.flow.files.findIndex(f => f.uniqueIdentifier == uniqueIdentifier);
    this.flow.files.splice(toRemoveIndex, 1);
    let ff: flowjs.FlowFile = new Flow.FlowFile(this.flow, resizedFile, uniqueIdentifier);
    ff.name = originalFileFlow.name;
    this.flow.files.push(ff);
    let originalIndex = array.findIndex(original => original.uniqueIdentifier == ff.uniqueIdentifier);
    array[originalIndex] = ff;
    if (resize) {
      (ff as any).resized = true;
    }
  }

  resizeImage(flowFile) {
    if (flowFile && flowFile.resized) return Promise.resolve(null);
    return new Promise((resolve, reject) => {
      let options = {
        quality: this.pictureQuality,
        maxWidth: 1000,
        success: (result) => {
          resolve([flowFile, result]);
        },
        error: (err) => {
          console.log(err.message);
          reject();
        }
      }
      Object.assign(options, this.compressorOptions);
      new Compressor(flowFile.file, options);
    });
  }

  async askToKeep(file: flowjs.FlowFile) {
    let toKeep = await this.checkExistence(file);
    return { file, toKeep };
  }

  setFileTypes(acceptedExtensions: string) {
    if (acceptedExtensions != null && acceptedExtensions.length > 0) {
      let cleanDots = acceptedExtensions.replace(/\./g, '').replace(/\,/g, "|");
      this.fileTypes = new RegExp('(\.|\/)' + cleanDots + '$', 'i');
    }
    else {
      this.fileTypes = fileTypes;
    }
  }

  public resetFiles() {
    this.innerUploadingFiles = [];
  }

  private get uploadingFiles() {
    return this.innerUploadingFiles.filter(f => !f.isCanceled && !f.isComplete);
  }

  public assignBrowse(element: HTMLElement, attributes) {
    this.flow.assignBrowse([element], false, false, attributes);
  }
  public assignDrop(element: HTMLElement) {
    this.flow.assignDrop([element]);
  }
  public pauseAll() {
    this.uploadingFiles.forEach((f) => f.pause());
  }
  public cancelAll() {
    this.uploadingFiles.forEach((f) => f.cancel());
  }
  public resumeAll() {
    this.uploadingFiles.forEach((f) => f.resume());
  }
}

@autoinject
@inlineView(
  `<template>
        <slot></slot>
        <div show.bind="showOverlay" ref="dropzoneOverlay" class="overlay">
            <div style="display: table-cell; vertical-align: middle; text-align: center"><h5>\${hintText}</h5></div>
        </div>
        <div show.bind="displayLoader" class="flow_uploader__loader">
          <ui-loader></ui-loader>
        </div>
    </template>`
)
export class FlowUploader {
  @bindable({ defaultBindingMode: bindingMode.twoWay }) value;
  @bindable dropzone: HTMLElement;
  @bindable target: string = this.fileStorageService.controller + "upload";
  @bindable hintText: string;

  @bindable uploadButton: HTMLElement;
  @bindable allowMultipleUpload: boolean = true;

  @bindable accept: "all" | "file" | "picture" | "office" | string;

  @bindable fileUploadedFn: (args: { fileUploaded: FileFlow }) => void;
  @bindable confirmUploadFn: Function = null;
  @bindable fileAddedFn: (file: FileFlow) => Promise<any>;
  @bindable uploadCompleteFn: Function = null;
  @bindable resizePicture: boolean = false;
  @bindable folderName: string;
  @bindable({ bindingMode: bindingMode.oneTime }) useUniqueName: boolean = false;

  @bindable compressorOptions: CompressorOptions;
  @bindable autoConvert: boolean = false;
  @bindable allowMultipleUploadBtn: boolean = false;
  @bindable showOverlay: boolean = true;
  @bindable ignoreFileExistence: boolean = false;
  @bindable pictureQuality: number = 0.7;

  displayLoader = false;
  browseButtonAssigned: boolean;
  divContainer: Element;
  input: HTMLInputElement;
  browseButtons: Array<HTMLElement> = [];

  flowHelper: FlowHelper;
  dropzoneOverlay: Element;
  dropZoneTimeout: any;

  constructor(public logger: CustomLogger, private eventAggregator: EventAggregator, private taskQueue: TaskQueue, private box: Box, private userService: AuthService, private i18n: I18N, private fileStorageService: RemoteFileStorage.Service) {
  }

  private async init() {
    if (this.flowHelper == null) {
      let accessToken = await this.userService.accessToken();
      let acceptExpression = this.getAcceptExpression();
      this.flowHelper = new FlowHelper(this.logger, this.target, this.eventAggregator, this.allowMultipleUpload, this.confirmUploadFn, acceptExpression, this.resizePicture, this.folderName, accessToken, this.compressorOptions, this.autoConvert, this.useUniqueName, this.pictureQuality);

      this.flowHelper.fileUploaded = (fileUploaded: any) => {
        if (this.fileUploadedFn != null)
          this.fileUploadedFn({ fileUploaded: fileUploaded });
      };

      this.flowHelper.uploadComplete = (uploadingFiles) => {
        if (this.uploadCompleteFn != null)
          this.uploadCompleteFn(uploadingFiles);
      }

      this.flowHelper.checkExistence = (file) => {
        return this.checkExistence(file);
      }

      this.flowHelper.fileAdded = (file) => {
        return this.internalFileAdded(file);
      }

      this.flowHelper.showLoader = (value) => this.showLoader(value)

      await this.uploadButtonChanged();
    }
  }

  public async attached() {
    this.taskQueue.queueMicroTask(async () => {
      await this.init();

      if (this.dropzone != null) {
        this.dropzone.classList.add("dropzone");
        this.highlightDropzones();
        this.flowHelper.assignDrop(this.dropzone);
      }

      await this.uploadButtonChanged();
    });
  }

  // @computedFrom("config.remoteServiceName", "config.remoteFileController")
  // public get remoteFileExistsUri() {
  //   return `${this.config.globalConfig.remoteServiceName}${this.config.globalConfig.remoteFileController}exists`;
  // }

  private showLoader(value: boolean) {
    this.displayLoader = value;
  }

  public highlightDropzones() {
    let overlay = $(this.dropzoneOverlay);
    $(document).bind('dragover', (e) => {
      let dropZone = $(this.dropzone);
      let foundDropzone;
      let timeout = this.dropZoneTimeout;
      if (!timeout) {
        dropZone.addClass('hover');
      }
      else {
        clearTimeout(timeout);
      }

      let found = false, node: any = e.target;
      e.originalEvent.dataTransfer.dropEffect = 'copy';
      do {
        if ($(node).hasClass('dropzone') && node == this.dropzone) {
          found = true;
          foundDropzone = $(node);
          break;
        }

        node = node.parentNode;
      } while (node != null);

      if (found) {
        dropZone.removeClass('in');
        foundDropzone.addClass('in');
        overlay.addClass('hover');
      } else {
        dropZone.removeClass("in");
        overlay.removeClass('hover');
      }

      this.dropZoneTimeout = setTimeout(() => {
        this.dropZoneTimeout = null;
        dropZone.removeClass('in hover');
        overlay.removeClass('hover');
      }, 100);
    });
    $(window).on('dragenter', function (e) {
      e.preventDefault();
    });
  }

  public async checkExistence(file: flowjs.FlowFile): Promise<ExistenceAction> {
    if (this.ignoreFileExistence) {
      return ExistenceAction.Replace;
    }
    let fileName = (this.folderName != null && this.folderName.trim().length > 0) ? this.folderName + "/" + file.name : file.name;
    let exists = await this.fileStorageService.exists(fileName);
    if (exists) {
      let replaceLabel: string = this.i18n.tr("general.replace", { ns: "common" });
      let duplicateLabel: string = this.i18n.tr("general.duplicate", { ns: "common" });
      let cancelLabel: string = this.i18n.tr("general.cancel", { ns: "common" });
      return await this.box.showQuestion(this.i18n.tr('fileUpload.fileAlreadyExists', { ns: "common", fileName: file.name }), this.i18n.tr('fileUpload.existingFile', { ns: "common" }),
        [
          {
            label: cancelLabel,
            title: cancelLabel,
            theme: 'dark',
            type: 'ghost',
            fn: (thisBox) => {
              thisBox.controller.ok(ExistenceAction.Cancel);
              this.showLoader(false);
            }
          },
          {
            label: duplicateLabel,
            title: duplicateLabel,
            theme: 'primary',
            type: 'solid',
            fn: (thisBox) => {
              thisBox.controller.ok(ExistenceAction.Duplicate);
            }
          },
          {
            label: replaceLabel,
            title: replaceLabel,
            theme: 'primary',
            type: 'solid',
            fn: (thisBox) => {
              thisBox.controller.ok(ExistenceAction.Replace);
            }
          }
        ]
      ).whenClosed((dialogResult) => {
        if (!dialogResult.wasCancelled) {
          return dialogResult.output as ExistenceAction;
        }
        return ExistenceAction.Cancel;
      });
    }
    return ExistenceAction.None;
  }

  public async internalFileAdded(file: FileFlow) {
    if (this.fileAddedFn != null) {
      let createdLinkEntity = await this.fileAddedFn(file);
      if (createdLinkEntity != null) {
        createdLinkEntity.flowObj = file;
        file.linkEntity = createdLinkEntity;
      }
      else {
        console.warn("Flow-Uploader: FileAdded function handler do not return a link entity");
      }
    }
  }

  public getAcceptExpression(): string {
    if (this.accept == null || this.accept.length == 0) return Extensions.all;
    if (Extensions[this.accept] != null) return Extensions[this.accept];
    else return this.accept;
  }

  public async uploadButtonChanged() {
    if (this.uploadButton != null && this.flowHelper != null) {
      if (this.allowMultipleUploadBtn) {
        this.browseButtonAssigned = this.browseButtons.findIndex(x => x == this.uploadButton) > -1;
      }
      if (!this.browseButtonAssigned) {
        let acceptExpression = this.getAcceptExpression();

        // Workaround Android : ajouter android/allowCamera pour forcer le choix caméra/galerie
        const attrs: any = {
          accept: this.buildAcceptWithAndroidWorkaround(acceptExpression)
          // Pas de capture selon le workaround
        };

        this.flowHelper.assignBrowse(this.uploadButton, attrs);
        this.browseButtonAssigned = true;
        this.browseButtons.push(this.uploadButton);
      }

      // Test modif Gianni pour les boutons dans les headers du ui-file (Tentative de suppression de l'event on click)

      // let acceptExpression = this.getAcceptExpression();
      // this.flowHelper.assignBrowse(this.uploadButton, { accept: acceptExpression });
      // const inputs = this.uploadButton.getElementsByTagName('input');
      // if (inputs?.length > 1) {
      //     inputs[0].remove();
      //     this.uploadButton.removeAttribute('onclick');                                
      // }
    }
  }

  public folderNameChanged() {
    if (this.folderName != null && this.folderName.trim().length > 0 && this.flowHelper != null) {
      this.flowHelper.folderName = this.folderName;
    }
  }

  public resizePictureChanged(newValue, oldValue) {
    if (newValue != null && newValue != oldValue && this.flowHelper != null) {
      this.flowHelper.resizePicture = newValue;
    }
  }

  // Ajoute le workaround Android pour forcer l'affichage du choix caméra/galerie

  private buildAcceptWithAndroidWorkaround(originalAccept: string): string {
    const imageExtensions = /\.(jpe?g|png|gif|webp|bmp|heic|heif)/i;

    if (imageExtensions.test(originalAccept)) {
      // Ajouter android/allowCamera pour forcer le choix sur Android
      return `${originalAccept},android/allowCamera`;
    }
    return originalAccept;
  }
}