import {
  networkInterfaces as os_networkInterfaces,
  platform as os_platform,
  release as os_release,
} from "os";

import { v4 as uuidv4 } from "uuid";
import { EventContext } from "../core/typings";

import isBrowser from "is-in-browser";

const VISIBILITY_HIDDEN = "hidden";
const VISIBILITY_VISIBLE = "visible";
const UserIdStorageKey = "CFUserIdStorageKey";
let visibilityInitialized = false;

export interface NetworkInformation {
  downlink: number;
  uplink: number;
}

export function getDeviceId(): string {
  const deviceIdStorageKey = "deviceIdStorageKey";

  let deviceId: string;
  if (isBrowser) {
    deviceId = localStorage.getItem(deviceIdStorageKey) || "";
    if (!deviceId) {
      deviceId = uuidv4();
      localStorage.setItem(deviceIdStorageKey, deviceId);
    }
  } else {
    // In Node.js, generate a new device ID each time
    deviceId = uuidv4();
  }
  return deviceId;
}

export function setLocalStorageUserId(userId: string) {
  if (userId && isBrowser) {
    localStorage.setItem(UserIdStorageKey, userId);
  }
}

export function getLocalStorageUserId(): string {
  if (isBrowser) {
    const userId = localStorage.getItem(UserIdStorageKey);
    return userId ? userId : "";
  }
  return "";
}

export function clearLocalStorageUserId() {
  if (isBrowser) {
    localStorage.setItem(UserIdStorageKey, "");
  }
}

export function getDeviceOSDetails(): string {
  if (!isBrowser) {
    return `${os_platform()} -|- ${os_release()}`;
  }

  // Current web specification defines `window.navigator.userAgentData.platform
  // as the official way to retrieve the operating system, but it is not
  // widely adopted by browsers. To avoid that problem this function
  // also takes into account the deprecated mechanism `window.navigator.platform`
  if (!window?.navigator) {
    return "";
  }

  const platform = window.navigator["userAgentData"]?.platform;
  const platformLegacy = window.navigator.platform;
  const userAgentValue = window.navigator?.userAgent || ";";

  return `${platform || platformLegacy} -|- ${userAgentValue
    ?.match(/(([^)]+))/)?.[1]
    .split(";")[1]
    .trim()}`;
}

export function getAppVersionDetails(): string {
  // Current web specification defines `window.navigator.userAgentData.platform
  // as the official way to retrieve the operating system, but it is not
  // widely adopted by browsers. To avoid that problem this function
  // also takes into account the deprecated mechanism `window.navigator.platform`
  // Also for non browser env
  if (!isBrowser || !window?.navigator || window.navigator.userAgent === "") {
    return "NO_BROWSER";
  }

  return getBrowserVersion() || "0";
}

function getBrowserVersion(): string {
  const ua = navigator.userAgent;
  let tem;
  let M =
    ua.match(/(opera|chrome|safari|firefox|msie|trident(?=\/))\/?\s*(\d+)/i) ||
    [];
  if (/trident/i.test(M[1])) {
    tem = /\brv[ :]+(\d+)/g.exec(ua) || [];
    return "IE " + (tem[1] || "");
  }
  if (M[1] === "Chrome") {
    tem = ua.match(/\b(OPR|Edge)\/(\d+)/);
    if (tem != null) return tem.slice(1).join(" ").replace("OPR", "Opera");
  }
  M = M[2] ? [M[1], M[2]] : [navigator.appName, navigator.appVersion, "-?"];
  if ((tem = ua.match(/version\/(\d+)/i)) != null) M.splice(1, 1, tem[1]);
  return M.join(" ");
}

export function getOperatingSystem(): string {
  if (!isBrowser) {
    return `${os_platform()}-${os_release()}`;
  }

  // Current web specification defines `window.navigator.userAgentData.platform
  // as the official way to retrieve the operating system, but it is not
  // widely adopted by browsers. To avoid that problem this function
  // also takes into account the deprecated mechanism `window.navigator.platform`
  if (!window?.navigator) {
    return "";
  }

  const platform = window.navigator["userAgentData"]?.platform;
  const platformLegacy = window.navigator.platform;

  return platform || platformLegacy;
}

export function getNetworkInformation(): NetworkInformation {
  if (!isBrowser) {
    return {
      downlink: 0,
      uplink: 0,
    };
  }

  // in case the speed is not available, we have agreed to send 0
  // otherwise, return the network speed in kbps
  const connection =
    (navigator as any).connection ||
    (navigator as any).mozConnection ||
    (navigator as any).webkitConnection;

  if (!connection) {
    return {
      downlink: 0,
      uplink: 0,
    };
  }

  return {
    downlink: parseInt(((connection.downlink || 0) * 1000).toString()),
    uplink: 0,
  };
}

export function listenLocationChangedEvent(clb) {
  const originalPushState = window.history.pushState;
  const originalReplaceState = window.history.replaceState;

  window.history.pushState = function (data, title, url) {
    let currentPath = url || ""; // first time is undefined
    if (currentPath[0] != "/") {
      currentPath = `/${currentPath}`;
    }
    clb(`${window.location.origin}${currentPath}`);
    return originalPushState.call(window.history, data, title, url);
  };

  window.history.replaceState = function (data, title, url) {
    let currentPath = url || ""; // first time is undefined
    if (currentPath[0] != "/") {
      currentPath = `/${currentPath}`;
    }
    clb(`${window.location.origin}${currentPath}`);

    return originalReplaceState.call(window.history, data, title, url);
  };

  window.addEventListener("popstate", function (evt) {
    clb((evt.target as Window).location.href);
  });
}

export function listenApplicationClose(clb) {
  window.addEventListener("unload", () => {
    clb();
  });
}

export function listenApplicationVisibilityChange(clb_visible, clb_hide) {
  if (document.visibilityState === VISIBILITY_VISIBLE) {
    visibilityInitialized = true;
  }

  document.addEventListener("visibilitychange", (e) => {
    console.warn("[event] [visibilitychange] ", document.visibilityState);
    if (document.visibilityState === VISIBILITY_VISIBLE) {
      visibilityInitialized = true;

      clb_visible();
    } else if (document.visibilityState === VISIBILITY_HIDDEN) {
      if (!visibilityInitialized) {
        return;
      }

      clb_hide();
    }
  });
}

export function listenPageChanged(clb) {
  listenLocationChangedEvent((newUrl) => {
    // the reason of this delay is that the new title
    // is not available yet when the location-changed
    // event is fired
    setTimeout(() => {
      clb(newUrl, document.title);
    }, 10);
  });
}

export function listenPageScrolled(threshold, clb) {
  // const currentScrollTop = (): number => (window.pageYOffset || document.documentElement.scrollTop)  - (document.documentElement.clientTop || 0)

  const getScrollPercent = () => {
    const SCROLL_TOP = "scrollTop";
    const SCROLL_HEIGHT = "scrollHeight";

    const currentScrollTop =
      document.documentElement[SCROLL_TOP] || document.body[SCROLL_TOP];
    const totalScrollHeight =
      document.documentElement[SCROLL_HEIGHT] || document.body[SCROLL_HEIGHT];
    const visibleHeight = document.documentElement.clientHeight;

    return (currentScrollTop / (totalScrollHeight - visibleHeight)) * 100;
  };

  window.addEventListener("scroll", (e: Event) => {
    const scroll = getScrollPercent();

    if (scroll < threshold) {
      return;
    }

    clb(scroll);
  });
}

export function isDevEnvironment() {
  return document.location.search.includes("cfenv=dev");
}
