/**
 * @mergeTarget
 * @module Src/Controllers
 */

import {
  LisioBooleanParameterNames,
  LisioNumericParameterNames,
  LisioParameterNames,
  LisioProfileNames,
  LisioStringParameterNames,
  LisioUserParsed,
} from "@lisio/lisio-profils";
import { ErrorCodes } from "../../misc/error-codes";
import { CoreErrorCodes } from "@lisio/lisio-engine";

/**
 * ## How to add a profile or parameter to be encoded ?
 *
 * If you want to add a :
 *  * Parameter : add his name in {@link CompressorController._parametersToId | _parametersToId} with a unique encoding string
 *  * Profile : add his name in {@link CompressorController._profileToId | _profileToId} with a unique encoding string
 * And it's done
 *
 * ## Documentation
 *
 * Class representing a compressor controller.\
 * It aims to centralize the logic of compressing and encoding datas to send them to book page using url query string.\
 * If something need to be compressed and compressed it has to use this class.\
 * To execute his responsability this class does :
 *  * Compress and encodes stored datas revelant to users
 *  * Decompress and encodes stored datas revelant to users
 */
class CompressorController {
  /**
   * Private attribute to store separator character
   * @source
   */
  private _separator: string = "-";

  /**
   * Private attribute to store corresponding lisio parameter names to 2 digit number.\
   * @source
   */
  private _parametersToId: Map<LisioParameterNames, string> = new Map<
    LisioParameterNames,
    string
  >([
    [LisioBooleanParameterNames.IS_ACTIVE, "01"],
    [LisioNumericParameterNames.BIGGER_CLICK, "02"],
    [LisioNumericParameterNames.CONTRAST, "03"],
    [LisioNumericParameterNames.FONT_SIZE, "04"],
    [LisioNumericParameterNames.LETTER_SPACING, "05"],
    [LisioNumericParameterNames.LINE_HEIGHT, "06"],
    [LisioNumericParameterNames.WORD_SPACING, "07"],
    [LisioNumericParameterNames.ZOOM, "08"],
    [LisioStringParameterNames.THEME, "09"],
    [LisioStringParameterNames.CURSOR_SIZE, "10"],
    [LisioStringParameterNames.DALTONISM, "11"],
    [LisioStringParameterNames.FONT_FAMILY, "12"],
    [LisioStringParameterNames.TEXT_ALIGN, "13"],
    [LisioBooleanParameterNames.HIGHLIGHT, "14"],
    [LisioBooleanParameterNames.KEYBOARD_NAVIGATION, "15"],
    [LisioBooleanParameterNames.LIST, "16"],
    [LisioBooleanParameterNames.READING_MASK, "17"],
    [LisioBooleanParameterNames.SHOW_ALT, "18"],
    [LisioBooleanParameterNames.DISABLE_ANIMATION, "19"],
    [LisioBooleanParameterNames.VOCA, "20"],
    [LisioBooleanParameterNames.BOOK_PAGE, "21"],
    [LisioStringParameterNames.DEEPL_TRANSLATION, "22"],
    [LisioStringParameterNames.GOOGLE_TRANSLATION, "23"],
    [LisioBooleanParameterNames.SPEECH_SYNTHESIS_STATE, "24"],
    [LisioNumericParameterNames.SPEECH_SYNTHESIS_PITCH, "25"],
    [LisioNumericParameterNames.SPEECH_SYNTHESIS_RATE, "26"],
    [LisioStringParameterNames.SPEECH_SYNTHESIS_VOICE, "27"],
    [LisioBooleanParameterNames.ACTIVE_UNDERLINE, "28"],
    [LisioStringParameterNames.UNDERLINE_RED, "29"],
    [LisioStringParameterNames.UNDERLINE_GREEN, "30"],
    [LisioStringParameterNames.UNDERLINE_BLUE, "31"],
    [LisioNumericParameterNames.DALTONISM_R, "32"],
    [LisioNumericParameterNames.DALTONISM_G, "33"],
    [LisioNumericParameterNames.DALTONISM_B, "34"],
    [LisioNumericParameterNames.DALTONISM_HUE, "35"],
    [LisioNumericParameterNames.DALTONISM_SATURATION, "36"],
    [LisioNumericParameterNames.DALTONISM_BRIGHTNESS, "37"],
  ]);

  /**
   * Private attribute to store corresponding 2 digit number to lisio parameter names
   * @source
   */
  private _idToParameters: Map<string, LisioParameterNames> = new Map<
    string,
    LisioParameterNames
  >(
    Array.from(
      this._parametersToId,
      (a) => a.reverse() as [string, LisioParameterNames],
    ),
  );

  /**
   * Private attribute to store corresponding lisio profile names to 2 characters
   * @source
   */
  private _profileToId: Map<LisioProfileNames, string> = new Map<
    LisioProfileNames,
    string
  >([
    [LisioProfileNames.ATTENTION, "ab"],
    [LisioProfileNames.CATARACT, "ac"],
    [LisioProfileNames.DICTIONARY, "ad"],
    [LisioProfileNames.DYSLEXIA, "ae"],
    [LisioProfileNames.ECOLO, "af"],
    [LisioProfileNames.EPILEPSY, "ag"],
    [LisioProfileNames.FOCUS, "ah"],
    [LisioProfileNames.GESTURES, "ai"],
    [LisioProfileNames.GLASS_X2, "aj"],
    [LisioProfileNames.GLASS_X4, "ak"],
    [LisioProfileNames.HEADACHE, "al"],
    [LisioProfileNames.INEXPERIENCE, "am"],
    [LisioProfileNames.LEARNING, "an"],
    [LisioProfileNames.LOW_EYE_SIGHT, "ao"],
    [LisioProfileNames.LOW_NETWORK, "ap"],
    [LisioProfileNames.MEMORY, "aq"],
    [LisioProfileNames.MOVEMENTS, "ar"],
    [LisioProfileNames.LIGHT_ECOLO, "as"],
    [LisioProfileNames.PRESBYOPIA, "at"],
    [LisioProfileNames.READING, "au"],
    [LisioProfileNames.UNDERLINE, "av"],
    [LisioProfileNames.VERY_LOW_EYE_SIGHT, "aw"],
    [LisioProfileNames.VISION, "ax"],
    [LisioProfileNames.DARK_MODE, "az"],
  ]);

  /**
   * Private attribute to store corresponding 2 characters to lisio profile names
   * @source
   */
  private _idToProfile: Map<string, LisioProfileNames> = new Map<
    string,
    LisioProfileNames
  >(
    Array.from(
      this._profileToId,
      (a) => a.reverse() as [string, LisioProfileNames],
    ),
  );

  /**
   * @static
   * Private attribute to store instance of {@link CompressorController | CompressorController}
   * @source
   */
  private static _current: CompressorController;

  /**
   * @static
   * Getter for attribute {@link _current | _current}
   * @returns Returns _current attribute
   * @source
   */
  public static get current(): CompressorController {
    return CompressorController._current;
  }

  /**
   * Private method to generate desired amount of seperator
   * @param {number} amount - Amount of desired separator
   * @returns Returns desired amount of separator
   * @source
   */

  public generateSeparator(amount: number): string {
    return new Array(amount).fill(this._separator).join("");
  }

  /**
   * Public method to encode and compress users for query string URL.\
   * To encodes and compress users this method does :
   *  * Encodes parameters as follows : parameterNameEncoded_parameterValue (if value is a boolean, it will be converted in 0 = false or 1 = true) using {@link _parametersToId | _parametersToId} and store all in an array. Each parameter will be split by 2 separator.
   *  * Encodes profiles as follows : profilesNameEncoded using {@link _profileToId | _profileToId} and store all in an array. Each profile will be split by 2 separator.
   *  * Encodes username in base64 and if is selected in number (0 = false, 1 = true). Each user will be split by 4 separator.
   *  * Each "components" of a user will be split by 3 separator.
   *  * At the end, the string will looks as follows with "_" as separator :
   *    * base64Username___stringIsSelected___encodedParameterName1_encodedParameterValue1__encodedParameterName2_encodedParameterValue2___encodedProfileName1__encodedProfileName2____encodedUser2 ...
   * @param {LisioUserParsed[]} users
   * @returns Returns a string representing all encoded compressed users
   * @throws {Error} If parameter name or profile name doesn't have corresponding encoding.\
   * See {@link Src/Utils.ErrorCodes.FORGOT_PARAMETER_ENCODING | ErrorCodes.FORGOT_PARAMETER_ENCODING} or {@link Src/Utils.ErrorCodes.FORGOT_PROFILE_ENCODING | ErrorCodes.FORGOT_PROFILE_ENCODING}
   * @source
   */
  public encodeUsers(users: LisioUserParsed[]): string {
    const encodedUsers: string[] = [];
    for (const user of users) {
      const encodedParameters: string[] = [];
      for (const customParameter of user.customParameters) {
        const encodedParameterName: string | undefined =
          this._parametersToId.get(customParameter.name);
        if (encodedParameterName == undefined) {
          throw new Error(
            `Code : ${ErrorCodes.FORGOT_PARAMETER_ENCODING}. Parameter ${customParameter.name} doesn't have corresponding encoding. Reffer to documentation.`,
          );
        }
        encodedParameters.push(
          `${encodedParameterName}${this.generateSeparator(1)}${
            typeof customParameter.value === "boolean"
              ? Number(customParameter.value)
              : customParameter.value
          }`,
        );
      }
      const encodedProfiles: string[] = [];
      for (const profile of user.profiles) {
        const encodedProfileName: string | undefined =
          this._profileToId.get(profile);
        if (encodedProfileName == undefined) {
          throw new Error(
            `Code : ${ErrorCodes.FORGOT_PROFILE_ENCODING}. Parameter ${profile} doesn't have corresponding encoding. Reffer to documentation.`,
          );
        }
        encodedProfiles.push(encodedProfileName);
      }
      if (encodedParameters.length === 0) {
        encodedParameters.push("00");
      }
      if (encodedProfiles.length === 0) {
        encodedProfiles.push("aa");
      }
      encodedUsers.push(
        `${btoa(encodeURIComponent(user.name))}${this.generateSeparator(
          3,
        )}${Number(user.isSelected)}${this.generateSeparator(
          3,
        )}${encodedParameters.join(
          this.generateSeparator(2),
        )}${this.generateSeparator(3)}${encodedProfiles.join(
          this.generateSeparator(2),
        )}`,
      );
    }
    return encodedUsers.join(this.generateSeparator(4));
  }

  /**
   * Public method to decode compressed users string produced by encodeUser.
   * Algorithm works in the same way but in reverse, it will :
   *  * Split all encoded users using 4 separator
   *  * Split all "components" of a encoded user using 3 separator
   *  * Decodes username and isSelected
   *  * Split all encodes parameters using 2 separator
   *  * Decodes parameter by splitting name and value using 1 separator and using {@link _idToParameters | _idToParameters} to decode parameter name.
   *  * Split all encodes profiles using 2 separator
   *  * Decodes profile using {@link _idToProfile | _idToProfile}
   * @param {string} encodedUsers
   * @returns
   * @throws {Error} If encoded parameter name or profile name doesn't have corresponding value.\
   * See {@link Src/Utils.ErrorCodes.FORGOT_PARAMETER_ENCODING | ErrorCodes.FORGOT_PARAMETER_ENCODING} or {@link Src/Utils.ErrorCodes.FORGOT_PROFILE_ENCODING | ErrorCodes.FORGOT_PROFILE_ENCODING}
   * @source
   */
  public decodeUsers(encodedUsers: string): LisioUserParsed[] {
    const parsedUsers: LisioUserParsed[] = encodedUsers
      .split(this.generateSeparator(4))
      .map((encodedUser) => {
        const [username, isSelected, encodedCustomParameters, encodedProfiles] =
          encodedUser.split(this.generateSeparator(3));
        const customParameters =
          encodedCustomParameters == "00"
            ? []
            : encodedCustomParameters
                .split(this.generateSeparator(2))
                .map((encodedCustomParameter) => {
                  const customParameter = encodedCustomParameter.split(
                    this.generateSeparator(1),
                  );
                  const decodedParameterName: LisioParameterNames | undefined =
                    this._idToParameters.get(customParameter[0]);
                  if (decodedParameterName == undefined) {
                    throw new Error(
                      `Code : ${ErrorCodes.FORGOT_PARAMETER_ENCODING}. Encoded arameter ${decodedParameterName} doesn't have corresponding value. Reffer to documentation.`,
                    );
                  }
                  customParameter[1] = decodeURIComponent(customParameter[1]);
                  return {
                    name: decodedParameterName,
                    value:
                      decodedParameterName.toUpperCase() in
                      LisioBooleanParameterNames
                        ? customParameter[1] === "1"
                        : decodedParameterName.toUpperCase() in
                            LisioNumericParameterNames
                          ? Number(customParameter[1])
                          : customParameter[1],
                  };
                });
        const profiles =
          encodedProfiles == "aa"
            ? []
            : encodedProfiles
                .split(this.generateSeparator(2))
                .map((encodedProfile) => {
                  const decodedProfileName: LisioProfileNames | undefined =
                    this._idToProfile.get(encodedProfile);
                  if (decodedProfileName == undefined) {
                    throw new Error(
                      `Code : ${ErrorCodes.FORGOT_PROFILE_ENCODING}. Encoded arameter ${decodedProfileName} doesn't have corresponding value. Reffer to documentation.`,
                    );
                  }
                  return decodedProfileName;
                });

        return {
          name: atob(username),
          isSelected: isSelected === "1",
          customParameters: customParameters,
          profiles: profiles,
        };
      });
    return parsedUsers;
  }

  /**
   * Constructor of class {@link CompressorController | CompressorController}
   * @throws If singleton already initialize.\
   * See {@link https://env-preprod-docs.lisio.fr/lisio-engine/enums/Src_Core.CoreErrorCodes.html#SINGLETON_NOT_UNIQUE | CoreErrorCodes.SINGLETON_NOT_UNIQUE}
   * @source
   */
  constructor() {
    if (CompressorController._current == undefined) {
      CompressorController._current = this;
    } else {
      throw new Error(
        `Code : ${CoreErrorCodes.SINGLETON_NOT_UNIQUE}. Singleton already initialize`,
      );
    }
  }
}

export { CompressorController };
