import { consume } from "@lit/context";
import {
  STATE_NOT_RUNNING,
  STATE_RUNNING,
  STATE_STARTING,
} from "home-assistant-js-websocket";
import type { PropertyValues } from "lit";
import { customElement, property, state } from "lit/decorators";
import { deepActiveElement } from "../common/dom/deep-active-element";
import { deepEqual } from "../common/util/deep-equal";
import { promiseTimeout } from "../common/util/promise-timeout";
import { narrowViewportContext } from "../data/context";
import { getDefaultPanel } from "../data/panel";
import type { CustomPanelInfo } from "../data/panel_custom";
import type { HomeAssistant, Panels } from "../types";
import { removeLaunchScreen } from "../util/launch-screen";
import type { RouteOptions, RouterOptions } from "./hass-router-page";
import { HassRouterPage } from "./hass-router-page";

const CACHE_URL_PATHS = ["lovelace", "home", "config"];
const PANEL_READY_TIMEOUT = 2000;
const DASHBOARD_READY_TIMEOUT = 5000;
const COMPONENTS = {
  app: { load: () => import("../panels/app/ha-panel-app") },
  energy: {
    load: () => import("../panels/energy/ha-panel-energy"),
    waitForReady: true,
    readyTimeout: DASHBOARD_READY_TIMEOUT,
  },
  calendar: {
    load: () => import("../panels/calendar/ha-panel-calendar"),
    waitForReady: true,
  },
  config: { load: () => import("../panels/config/ha-panel-config") },
  custom: { load: () => import("../panels/custom/ha-panel-custom") },
  lovelace: {
    load: () => import("../panels/lovelace/ha-panel-lovelace"),
    waitForReady: true,
    readyTimeout: DASHBOARD_READY_TIMEOUT,
  },
  history: { load: () => import("../panels/history/ha-panel-history") },
  iframe: { load: () => import("../panels/iframe/ha-panel-iframe") },
  logbook: { load: () => import("../panels/logbook/ha-panel-logbook") },
  map: { load: () => import("../panels/map/ha-panel-map") },
  my: { load: () => import("../panels/my/ha-panel-my") },
  profile: { load: () => import("../panels/profile/ha-panel-profile") },
  todo: { load: () => import("../panels/todo/ha-panel-todo") },
  "media-browser": {
    load: () => import("../panels/media-browser/ha-panel-media-browser"),
    waitForReady: true,
  },
  light: {
    load: () => import("../panels/light/ha-panel-light"),
    waitForReady: true,
    readyTimeout: DASHBOARD_READY_TIMEOUT,
  },
  security: {
    load: () => import("../panels/security/ha-panel-security"),
    waitForReady: true,
    readyTimeout: DASHBOARD_READY_TIMEOUT,
  },
  climate: {
    load: () => import("../panels/climate/ha-panel-climate"),
    waitForReady: true,
    readyTimeout: DASHBOARD_READY_TIMEOUT,
  },
  maintenance: {
    load: () => import("../panels/maintenance/ha-panel-maintenance"),
    waitForReady: true,
    readyTimeout: DASHBOARD_READY_TIMEOUT,
  },
  home: {
    load: () => import("../panels/home/ha-panel-home"),
    waitForReady: true,
    readyTimeout: DASHBOARD_READY_TIMEOUT,
  },
  notfound: { load: () => import("../panels/notfound/ha-panel-notfound") },
} satisfies Record<
  string,
  Pick<RouteOptions, "load" | "waitForReady"> & { readyTimeout?: number }
>;

@customElement("partial-panel-resolver")
class PartialPanelResolver extends HassRouterPage {
  @property({ attribute: false }) public hass!: HomeAssistant;

  @state()
  @consume({ context: narrowViewportContext, subscribe: true })
  private _narrow = false;

  private _waitForStart = false;

  private _disconnectedPanel?: HTMLElement;

  private _disconnectedActiveElement?: HTMLElement;

  private _hiddenTimeout?: number;

  protected firstUpdated(changedProps: PropertyValues<this>) {
    super.firstUpdated(changedProps);

    // Attach listeners for visibility
    document.addEventListener(
      "visibilitychange",
      () => this._checkVisibility(),
      false
    );
    document.addEventListener("resume", () => this._checkVisibility());
  }

  public willUpdate(changedProps: PropertyValues<this>) {
    super.willUpdate(changedProps);

    if (!changedProps.has("hass")) {
      return;
    }

    const oldHass = changedProps.get("hass") as this["hass"];

    if (
      this._waitForStart &&
      (this.hass.config.state === STATE_STARTING ||
        this.hass.config.state === STATE_RUNNING)
    ) {
      this._waitForStart = false;
      this.rebuild();
    }

    if (this.hass.panels && (!oldHass || oldHass.panels !== this.hass.panels)) {
      this._updateRoutes(oldHass?.panels);
    }
  }

  protected createLoadingScreen() {
    const el = super.createLoadingScreen();
    el.rootnav = true;
    el.hass = this.hass;
    el.narrow = this._narrow;
    return el;
  }

  protected updatePageEl(el) {
    const hass = this.hass;

    el.hass = hass;
    el.narrow = this._narrow;
    el.route = this.routeTail;
    el.panel = hass.panels[this._currentPage];
  }

  private _checkVisibility() {
    if (this.hass.suspendWhenHidden === false) {
      return;
    }

    if (document.hidden) {
      this._onHidden();
    } else {
      this._onVisible();
    }
  }

  private _getRoutes(panels: Panels): RouterOptions {
    const routes: RouterOptions["routes"] = {};
    Object.values(panels).forEach((panel) => {
      const component = COMPONENTS[panel.component_name];
      const data: RouteOptions = {
        tag: `ha-panel-${panel.component_name}`,
        cache: CACHE_URL_PATHS.includes(panel.url_path),
        ...(component ?? {}),
      };
      routes[panel.url_path] = data;
    });

    return {
      beforeRender: (page) => {
        if (!page || !routes[page]) {
          return getDefaultPanel(this.hass).url_path;
        }
        return undefined;
      },
      showLoading: true,
      routes,
    };
  }

  private _onHidden() {
    this._hiddenTimeout = window.setTimeout(() => {
      this._hiddenTimeout = undefined;
      // setTimeout can be delayed in the background and only fire
      // when we switch to the tab or app again (Hey Android!)
      if (!document.hidden) {
        return;
      }
      const curPanel = this.hass.panels[this._currentPage];
      if (
        this.lastChild &&
        // iFrames will lose their state when disconnected
        // Do not disconnect any iframe panel
        curPanel.component_name !== "iframe" &&
        curPanel.component_name !== "app" &&
        // Do not disconnect any custom panel that embeds into iframe (ie hassio)
        (curPanel.component_name !== "custom" ||
          !(curPanel as CustomPanelInfo).config._panel_custom.embed_iframe)
      ) {
        this._disconnectedPanel = this.lastChild as HTMLElement;
        const activeEl = deepActiveElement(
          this._disconnectedPanel.shadowRoot || undefined
        );
        if (activeEl instanceof HTMLElement) {
          this._disconnectedActiveElement = activeEl;
        }
        this.removeChild(this.lastChild);
      }
    }, 300000);
    window.addEventListener("focus", () => this._onVisible(), { once: true });
  }

  private _onVisible() {
    if (this._hiddenTimeout) {
      clearTimeout(this._hiddenTimeout);
      this._hiddenTimeout = undefined;
    }
    if (this._disconnectedPanel) {
      this.appendChild(this._disconnectedPanel);
      this._disconnectedPanel = undefined;
    }
    if (this._disconnectedActiveElement) {
      this._disconnectedActiveElement.focus();
      this._disconnectedActiveElement = undefined;
    }
  }

  private async _updateRoutes(oldPanels?: HomeAssistant["panels"]) {
    this.routerOptions = this._getRoutes(this.hass.panels);

    if (
      !this._waitForStart &&
      this._currentPage &&
      !this.hass.panels[this._currentPage]
    ) {
      if (this.hass.config.state === STATE_NOT_RUNNING) {
        this._waitForStart = true;
        if (this.lastChild) {
          this.removeChild(this.lastChild);
        }
        this.appendChild(this.createLoadingScreen());
        return;
      }
    }

    if (
      !oldPanels ||
      !deepEqual(
        oldPanels[this._currentPage],
        this.hass.panels[this._currentPage]
      )
    ) {
      await this.rebuild();
      const component =
        COMPONENTS[this.hass.panels[this._currentPage].component_name];
      await promiseTimeout(
        component?.readyTimeout ?? PANEL_READY_TIMEOUT,
        this.pageRendered
      ).catch(() => undefined);
      // Only fire frontend/loaded when this call actually removed the launch
      // screen, so later panel updates do not fire it again. Native apps remove
      // it instantly because their own splash screen is still visible.
      if (
        removeLaunchScreen(!!this.hass.auth.external?.config.hasSplashscreen)
      ) {
        this.hass.auth.external?.fireMessage({ type: "frontend/loaded" });
      }
    }
  }
}

declare global {
  interface HTMLElementTagNameMap {
    "partial-panel-resolver": PartialPanelResolver;
  }
}
