interface Attribute {
  expires?: string | Date | number
}

interface Cookie {
  get: (key: string, json?: boolean) => string | object | undefined;
  set: (key: string, value: any, attributes?: Attribute) => string | undefined;
  getJSON: (key: string) => object | undefined;
  remove: (key: string) => void;
}


const extend = (...sources: object[]) => {
  const result = Object.create({});

  sources.forEach(source => {
    for (let [key, value] of Object.entries(source)) {
      result[key] = value;
    }
  });
  return result;
};

const decode = (s: string) => s.replace(/(%[0-9A-Z]{2})+/g, decodeURIComponent);

const set = (key: string, value: any, attributes: Attribute = {}) => {
  if (typeof document === 'undefined') {
    return;
  }

  attributes = extend({ path: '/', SameSite: 'None' }, attributes);

  if (typeof attributes.expires === 'number') {
    attributes.expires = new Date(Date.now() + attributes.expires * 864e+5);
  }

  if (typeof attributes.expires !== 'string') {
    // We're using "expires" because "max-age" is not supported by IE
    attributes.expires = attributes.expires ? attributes.expires.toUTCString() : '';
  }

  try {
    let result = JSON.stringify(value);
    if (/^[\{\[]/.test(result)) {
      value = result;
    }
  } catch (e) {
  }

  value = encodeURIComponent(String(value))
    .replace(/%(23|24|26|2B|3A|3C|3E|3D|2F|3F|40|5B|5D|5E|60|7B|7D|7C)/g, decodeURIComponent);

  key = encodeURIComponent(String(key))
    .replace(/%(23|24|26|2B|5E|60|7C)/g, decodeURIComponent)
    .replace(/[\(\)]/g, escape);

  let stringifiedAttributes = '';
  for (let [name, value] of Object.entries(attributes)) {
    if (!value) continue;
    stringifiedAttributes += '; ' + name;
    if (value === true) continue;

    // Considers RFC 6265 section 5.2:
    // ...
    // 3.  If the remaining unparsed-attributes contains a %x3B (";")
    //     character:
    // Consume the characters of the unparsed-attributes up to,
    // not including, the first %x3B (";") character.
    // ...
    stringifiedAttributes += '=' + value.split(';')[0];
  }
  stringifiedAttributes += "; Secure"

  return (document.cookie = key + '=' + value + stringifiedAttributes);
};

const get = (key: string, json: boolean) => {
  if (typeof document === 'undefined') {
    return;
  }

  let jar = Object.create({});
  // To prevent the for loop in the first place assign an empty array
  // in case there are no cookies at all.
  let cookies = document.cookie ? document.cookie.split('; ') : [];
  let i = 0;

  for (; i < cookies.length; i++) {
    let parts = cookies[i].split('=');
    let cookie = parts.slice(1).join('=');

    if (!json && cookie.charAt(0) === '"') {
      cookie = cookie.slice(1, -1);
    }

    try {
      let name = decode(parts[0]);

      cookie = decode(cookie);

      if (json) {
        try {
          cookie = JSON.parse(cookie);
        } catch (e) {
        }
      }

      jar[name] = cookie;

      if (key === name) {
        break;
      }
    } catch (e) {
    }
  }

  return key ? jar[key] : jar;
};

const getJSON = (key: string) => get(key, true);
const remove = (key: string, attributes: Attribute = {}) => {
  set(key, '', extend(attributes, {
    expires: -1,
  }));
};

const cookie: Cookie = {
  set,
  get: (key: string) => get(key, false),
  getJSON,
  remove,
};

export default cookie;

