/*
 * The code below has been adopted from https://www.npmjs.com/package/utf7
 */

// Character classes defined by RFC 2152.
const setD = 'A-Za-z0-9' + escape(`'(),-./:?`);
const setO = escape(`!"#$%&*;<=>@[]^_'{|}`);
const setW = escape(` \r\n\t`);

// Stores compiled regexes for various replacement pattern.
const regexes: Record<string, RegExp> = {};
const regexAll = new RegExp(`[^${setW}${setD}${setO}]+`, 'g');

/**
 * RFC 2152 UTF-7 encoding.
 *
 * @param mask Optional mask characters to exclude from encoding
 */
export const encode = function encode(str: string, mask: string | null = null): string {
  // Generate a RegExp object from the string of mask characters.
  if (!mask) {
    mask = '';
  }
  if (!regexes[mask]) {
    regexes[mask] = new RegExp(`[^${setD}${escape(mask)}]+`, 'g');
  }

  // We replace subsequent disallowed chars with their escape sequence.
  return str.replace(
    regexes[mask],
    (chunk) =>
      // + is represented by an empty sequence +-, otherwise call encode().
      `+${chunk === '+' ? '' : _encode(chunk)}-`,
  );
};

/**
 * RFC 2152 UTF-7 encoding with all optionals.
 * Encodes all non-ASCII characters, including those that would normally
 * be represented directly in standard UTF-7 encoding.
 */
export function encodeAll(str: string): string {
  // We replace subsequent disallowed chars with their escape sequence.
  return str.replace(
    regexAll,
    (chunk) =>
      // + is represented by an empty sequence +-, otherwise call encode().
      `+${chunk === '+' ? '' : _encode(chunk)}-`,
  );
}

/**
 * RFC 2152 UTF-7 decoding.
 */
export const decode = function decode(str: string): string {
  return str.replace(/\+([A-Za-z0-9/]*)-?/gi, (_, chunk) =>
    // &- represents &.
    chunk === '' ? '+' : _decode(chunk),
  );
};

export const imap = {
  /**
   * RFC 3501, section 5.1.3 UTF-7 encoding.
   */
  encode(str: string): string {
    // All printable ASCII chars except for & must be represented by themselves.
    // We replace subsequent non-representable chars with their escape sequence.
    return str.replace(/&/g, '&-').replace(/[^\x20-\x7e]+/g, (chunk) => {
      // & is represented by an empty sequence &-, otherwise call encode().
      chunk = (chunk === '&' ? '' : _encode(chunk)).replace(/\//g, ',');
      return `&${chunk}-`;
    });
  },

  /**
   * RFC 3501, section 5.1.3 UTF-7 decoding.
   */
  decode(str: string): string {
    return str.replace(/&([^-]*)-/g, (_, chunk) =>
      // &- represents &.
      chunk === '' ? '&' : _decode(chunk.replace(/,/g, '/')),
    );
  },
};

/**
 * Allocates an ASCII buffer of the specified length.
 */
function allocateAsciiBuffer(length: number): Buffer {
  return Buffer.alloc(length, 'ascii');
}

/**
 * Encodes a string using modified UTF-7 encoding.
 */
function _encode(str: string): string {
  const b = allocateAsciiBuffer(str.length * 2);
  for (let i = 0, bi = 0; i < str.length; i++) {
    // Note that we can't simply convert a UTF-8 string to Base64 because
    // UTF-8 uses a different encoding. In modified UTF-7, all characters
    // are represented by their two byte Unicode ID.
    const c = str.charCodeAt(i);
    // Upper 8 bits shifted into lower 8 bits so that they fit into 1 byte.
    b[bi++] = c >> 8;
    // Lower 8 bits. Cut off the upper 8 bits so that they fit into 1 byte.
    b[bi++] = c & 0xff;
  }
  // Modified Base64 uses , instead of / and omits trailing =.
  return b.toString('base64').replace(/=+$/, '');
}

/**
 * Allocates a buffer from a base64 string.
 */
function allocateBase64Buffer(str: string): Buffer {
  return Buffer.from(str, 'base64');
}

/**
 * Decodes a string using modified UTF-7 encoding.
 */
function _decode(str: string): string {
  const b = allocateBase64Buffer(str);
  const r: string[] = [];
  // eslint-disable-next-line @stylistic/space-in-parens
  for (let i = 0; i < b.length; ) {
    // Calculate charcode from two adjacent bytes.
    r.push(String.fromCharCode((b[i++] << 8) | b[i++]));
  }
  return r.join('');
}

/**
 * Escapes special regex characters.
 * From http://simonwillison.net/2006/Jan/20/escape/
 */
function escape(chars: string): string {
  return chars.replace(/[-[\]{}()*+?.,\\^$|#\s]/g, '\\$&');
}
