/**
 * @file Manages logic to validate file uploads client-side. In general, the
 * validations work by looping over input form fields with a type of file and
 * checking the size and format of the files upload by the user.
 */

/**
 * An object containing the top level MIME type as the key and the max file
 * size in MB for the value. To use a different value than these defaults,
 * simply add a data-max-file-mb attribute to the input form field with the
 * max file size in MB. If that attribute is found, it takes priority over these
 * defaults.
 *
 * @constant {Object.<string, number>}
 */
const MAX_FILE_SIZE_MB = Object.freeze({
  image: 25,
  video: 50,
});

/**
 * Permitted file types using the top level MIME type (i.e. image for
 * image/png). To specify permitted file types, simply add a
 * data-permitted-file-types attribute to the input form field as an Array of
 * strings specifying the top level MIME types that are permitted.
 *
 * @constant {string[]}
 */
const PERMITTED_FILE_TYPES = ['image'];

/**
 * The maximum length of the file name to prevent errors on the backend when a
 * file name is too long.
 *
 * @constant {number}
 */
const MAX_FILE_NAME_LENGTH = 250;

/**
 * Removes any pre-existing error messages from the DOM related to file
 * validation.
 *
 * @param {HTMLElement} fileInput - An input form field with type of file
 */
function removeErrorMessage(fileInput) {
  const errorMessage = fileInput.parentNode.querySelector(
    'div.file-upload-error',
  );

  if (errorMessage) {
    errorMessage.remove();
  }
}

/**
 * Adds error messages in the form of a div with red text.
 *
 * @param {HTMLElement} fileInput - An input form field with type of file
 * @param {string} msg - The error message to be displayed to the user
 *
 * @returns {HTMLElement} The error element that was added to the DOM
 */
function addErrorMessage(fileInput, msg) {
  const fileInputField = fileInput;
  const error = document.createElement('div');
  error.style.color = 'red';
  error.innerHTML = msg;
  error.classList.add('file-upload-error');

  fileInputField.parentNode.append(error);
}

/**
 * Handles errors for files that are too large.
 *
 * @param {object} fileSizeErrorHandler - A custom function to be ran after the default error handling
 * @param {HTMLElement} fileInput - An input form field with type of file
 * @param {number} fileSizeMb - The size of the file in MB
 * @param {?number} maxFileSizeMb - The max file size limit in MB
 */
function handleFileSizeError(
  fileSizeErrorHandler,
  fileInput,
  fileSizeMb,
  maxFileSizeMb,
) {
  const fileInputField = fileInput;
  fileInputField.value = null;

  if (fileSizeErrorHandler) {
    fileSizeErrorHandler();
  } else {
    let errorMessage = `File size too large (${fileSizeMb} MB).`;

    // If a user uploads a file type that we haven't defined a max size limit for then maxFileSizeMb
    // could be NaN
    if (maxFileSizeMb >= 0) {
      errorMessage += ` The limit is ${maxFileSizeMb} MB.`;
    }

    addErrorMessage(fileInput, errorMessage);
  }
}

/**
 * Handles errors for files that are not a valid format.
 *
 * @param {object} fileSizeErrorHandler - A custom function to be ran after the default error handling
 * @param {HTMLElement} fileInput - An input form field with type of file
 * @param {string} fileType - The top level file type (i.e. image for image/png)
 * @param {string[]} permittedFileTypes - The top level file types (i.e. image for image/png) that are permitted
 */
function handleFileTypeError(
  fileTypeErrorHandler,
  fileInput,
  fileType,
  permittedFileTypes,
) {
  const fileInputField = fileInput;
  fileInputField.value = null;

  if (fileTypeErrorHandler) {
    fileTypeErrorHandler();
  } else {
    const errorMessage = `Invalid file format (${fileType}). Only ${permittedFileTypes.join(
      ', ',
    )} files are permitted.`;
    addErrorMessage(fileInput, errorMessage);
  }
}

/**
 * Handles errors for files with names that are too long.
 *
 * @param {object} fileNameLengthErrorHandler - A custom function to be ran after the default error handling
 * @param {HTMLElement} fileInput - An input form field with type of file
 * @param {number} maxFileNameLength - The max number of characters permitted for a file name
 */
function handleFileNameLengthError(
  fileNameLengthErrorHandler,
  fileInput,
  maxFileNameLength,
) {
  const fileInputField = fileInput;
  fileInputField.value = null;

  if (fileNameLengthErrorHandler) {
    fileNameLengthErrorHandler();
  } else {
    const errorMessage = `File name is too long. It can't be longer than ${maxFileNameLength} characters.`;
    addErrorMessage(fileInput, errorMessage);
  }
}

/**
 * Validates the file size and handles the error if it's invalid.
 *
 * @external File
 * @see {@link https://developer.mozilla.org/en-US/docs/Web/API/File File}
 *
 * @param {File} file - The file attached by the user
 * @param {string} fileType - The top level file type (i.e. image for image/png)
 * @param {HTMLElement} fileInput - An input form field with type of file
 *
 * @returns {Boolean} Returns false if the file is too big. Otherwise, returns true.
 */
function validateFileSize(file, fileType, fileInput) {
  let { maxFileSizeMb } = fileInput.dataset;

  const { fileSizeErrorHandler } = fileInput.dataset;

  const fileSizeMb = (file.size / (1024 * 1024)).toFixed(2);
  maxFileSizeMb = Number(maxFileSizeMb || MAX_FILE_SIZE_MB[fileType]);

  const isValidFileSize = fileSizeMb <= maxFileSizeMb;

  if (!isValidFileSize) {
    handleFileSizeError(
      fileSizeErrorHandler,
      fileInput,
      fileSizeMb,
      maxFileSizeMb,
    );
  }

  return isValidFileSize;
}

/**
 * Validates the file type and handles the error if it's invalid.
 *
 * @external File
 * @see {@link https://developer.mozilla.org/en-US/docs/Web/API/File File}
 *
 * @param {File} file - The file attached by the user
 * @param {string} fileType - The top level file type (i.e. image for image/png)
 * @param {HTMLElement} fileInput - An input form field with type of file
 *
 * @returns {Boolean} Returns false if the files is an invalid format. Otherwise, returns true.
 */
function validateFileType(file, fileType, fileInput) {
  let { permittedFileTypes } = fileInput.dataset;

  if (permittedFileTypes) {
    permittedFileTypes = JSON.parse(permittedFileTypes);
  }

  permittedFileTypes = permittedFileTypes || PERMITTED_FILE_TYPES;

  const { fileTypeErrorHandler } = fileInput.dataset;

  const isValidFileType = permittedFileTypes.includes(fileType);

  if (!isValidFileType) {
    handleFileTypeError(
      fileTypeErrorHandler,
      fileInput,
      fileType,
      permittedFileTypes,
    );
  }

  return isValidFileType;
}

/**
 * Validates the length of the file name and handles the error if it's invalid.
 *
 * @external File
 * @see {@link https://developer.mozilla.org/en-US/docs/Web/API/File File}
 *
 * @param {File} file - The file attached by the user
 * @param {HTMLElement} fileInput - An input form field with type of file
 *
 * @returns {Boolean} Returns false if the file name is too long. Otherwise, returns true.
 */
function validateFileNameLength(file, fileInput) {
  let { maxFileNameLength } = fileInput.dataset;

  maxFileNameLength = Number(maxFileNameLength || MAX_FILE_NAME_LENGTH);

  const { fileNameLengthErrorHandler } = fileInput.dataset;

  const isValidFileNameLength = file.name.length <= maxFileNameLength;

  if (!isValidFileNameLength) {
    handleFileNameLengthError(
      fileNameLengthErrorHandler,
      fileInput,
      maxFileNameLength,
    );
  }

  return isValidFileNameLength;
}

/**
 * This is the core function to handle validations of uploaded files. It loops
 * through all the uploaded files for the given fileInput and checks the file
 * size, file format, and file name length. If a file fails a validation, the
 * error is handled.
 *
 * @param {HTMLElement} fileInput - An input form field with type of file
 *
 * @returns {Boolean} Returns false if any files failed validations. Otherwise, returns true.
 */
function validateFileInput(fileInput) {
  let isValidFileInput = true;

  removeErrorMessage(fileInput);
  const files = Array.from(fileInput.files);

  for (let i = 0; i < files.length; i += 1) {
    const file = files[i];
    const fileType = file.type.split('/')[0];

    const isValidFileSize = validateFileSize(file, fileType, fileInput);

    if (!isValidFileSize) {
      isValidFileInput = false;
      break;
    }

    const isValidFileType = validateFileType(file, fileType, fileInput);

    if (!isValidFileType) {
      isValidFileInput = false;
      break;
    }

    const isValidFileNameLength = validateFileNameLength(file, fileInput);

    if (!isValidFileNameLength) {
      isValidFileInput = false;
      break;
    }
  }

  return isValidFileInput;
}

/**
 * This function is designed to be exported in areas where we are doing more
 * custom implementations of file uploading using Preact. It can then be used
 * in Preact event handlers. It loops through all file input fields on the DOM
 * and validates any attached files.
 *
 * @returns {Boolean} Returns false if any files failed validations. Otherwise, returns true.
 */
export function validateFileInputs() {
  let validFileInputs = true;
  const fileInputs = document.querySelectorAll('input[type="file"]');

  for (let i = 0; i < fileInputs.length; i += 1) {
    const fileInput = fileInputs[i];
    const validFileInput = validateFileInput(fileInput);

    if (!validFileInput) {
      validFileInputs = false;
      break;
    }
  }

  return validFileInputs;
}

// This is written so that it works automagically by just including this pack
// in a view.
const fileInputs = document.querySelectorAll('input[type="file"]');

fileInputs.forEach((fileInput) => {
  fileInput.addEventListener('change', () => {
    validateFileInput(fileInput);
  });
});
