// Copyright 2018 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

import * as Common from '../../core/common/common.js';
import * as Host from '../../core/host/host.js';
import * as i18n from '../../core/i18n/i18n.js';
import * as Root from '../../core/root/root.js';
import * as SDK from '../../core/sdk/sdk.js';
import type * as Protocol from '../../generated/protocol.js';
import * as MobileThrottling from '../../panels/mobile_throttling/mobile_throttling.js';
import * as Components from '../../ui/legacy/components/utils/utils.js';
import * as UI from '../../ui/legacy/legacy.js';
import * as Lit from '../../ui/lit/lit.js';

import nodeIconStyles from './nodeIcon.css.js';

const {html} = Lit;

const UIStrings = {
  /**
   * @description Text that refers to the main target. The main target is the primary webpage that
   * DevTools is connected to. This text is used in various places in the UI as a label/name to inform
   * the user which target/webpage they are currently connected to, as DevTools may connect to multiple
   * targets at the same time in some scenarios.
   */
  main: 'Main',
  /**
   * @description Text that refers to the tab target. The tab target is the Chrome tab that
   * DevTools is connected to. This text is used in various places in the UI as a label/name to inform
   * the user which target they are currently connected to, as DevTools may connect to multiple
   * targets at the same time in some scenarios.
   * @meaning Tab target that's different than the "Tab" of Chrome. (See b/343009012)
   */
  tab: 'Tab',
  /**
   * @description A warning shown to the user when JavaScript is disabled on the webpage that
   * DevTools is connected to.
   */
  javascriptIsDisabled: 'JavaScript is disabled',
  /**
   * @description A message that prompts the user to open devtools for a specific environment (Node.js)
   */
  openDedicatedTools: 'Open dedicated DevTools for `Node.js`',
} as const;
const str_ = i18n.i18n.registerUIStrings('entrypoints/inspector_main/InspectorMain.ts', UIStrings);
const i18nString = i18n.i18n.getLocalizedString.bind(undefined, str_);

export class InspectorMainImpl implements Common.Runnable.Runnable {
  async run(): Promise<void> {
    let firstCall = true;
    await SDK.Connections.initMainConnection(async () => {
      const type = Root.Runtime.Runtime.queryParam('v8only') ?
          SDK.Target.Type.NODE :
          (Root.Runtime.Runtime.queryParam('targetType') === 'tab' || Root.Runtime.Runtime.isTraceApp() ?
               SDK.Target.Type.TAB :
               SDK.Target.Type.FRAME);
      // TODO(crbug.com/1348385): support waiting for debugger with tab target.
      const waitForDebuggerInPage =
          type === SDK.Target.Type.FRAME && Root.Runtime.Runtime.queryParam('panel') === 'sources';
      const name = type === SDK.Target.Type.FRAME ? i18nString(UIStrings.main) : i18nString(UIStrings.tab);
      const target = SDK.TargetManager.TargetManager.instance().createTarget(
          'main', name, type, null, undefined, waitForDebuggerInPage);

      const waitForPrimaryPageTarget = (): Promise<SDK.Target.Target> => {
        return new Promise(resolve => {
          const targetManager = SDK.TargetManager.TargetManager.instance();
          targetManager.observeTargets({
            targetAdded: (target: SDK.Target.Target): void => {
              if (target === targetManager.primaryPageTarget()) {
                target.setName(i18nString(UIStrings.main));
                resolve(target);
              }
            },
            targetRemoved: (_: unknown): void => {},
          });
        });
      };
      await waitForPrimaryPageTarget();

      // Only resume target during the first connection,
      // subsequent connections are due to connection hand-over,
      // there is no need to pause in debugger.
      if (!firstCall) {
        return;
      }
      firstCall = false;

      if (waitForDebuggerInPage) {
        const debuggerModel = target.model(SDK.DebuggerModel.DebuggerModel);
        if (debuggerModel) {
          if (!debuggerModel.isReadyToPause()) {
            await debuggerModel.once(SDK.DebuggerModel.Events.DebuggerIsReadyToPause);
          }
          debuggerModel.pause();
        }
      }

      if (type !== SDK.Target.Type.TAB) {
        void target.runtimeAgent().invoke_runIfWaitingForDebugger();
      }
    }, Components.TargetDetachedDialog.TargetDetachedDialog.connectionLost);

    new SourcesPanelIndicator();
    new BackendSettingsSync();
    new MobileThrottling.NetworkPanelIndicator.NetworkPanelIndicator();

    Host.InspectorFrontendHost.InspectorFrontendHostInstance.events.addEventListener(
        Host.InspectorFrontendHostAPI.Events.ReloadInspectedPage, ({data: hard}) => {
          SDK.ResourceTreeModel.ResourceTreeModel.reloadAllPages(hard);
        });
  }
}

Common.Runnable.registerEarlyInitializationRunnable(() => new InspectorMainImpl());

export class ReloadActionDelegate implements UI.ActionRegistration.ActionDelegate {
  handleAction(_context: UI.Context.Context, actionId: string): boolean {
    switch (actionId) {
      case 'inspector-main.reload':
        SDK.ResourceTreeModel.ResourceTreeModel.reloadAllPages(false);
        return true;
      case 'inspector-main.hard-reload':
        SDK.ResourceTreeModel.ResourceTreeModel.reloadAllPages(true);
        return true;
    }
    return false;
  }
}

export class FocusDebuggeeActionDelegate implements UI.ActionRegistration.ActionDelegate {
  handleAction(_context: UI.Context.Context, _actionId: string): boolean {
    const mainTarget = SDK.TargetManager.TargetManager.instance().primaryPageTarget();
    if (!mainTarget) {
      return false;
    }
    void mainTarget.pageAgent().invoke_bringToFront();
    return true;
  }
}

interface ViewInput {
  nodeProcessRunning: Boolean;
}

type View = (input: ViewInput, _output: object, target: HTMLElement) => void;

const isNodeProcessRunning = (targetInfos: Protocol.Target.TargetInfo[]): Boolean => {
  return Boolean(targetInfos.find(target => target.type === 'node' && !target.attached));
};

export const DEFAULT_VIEW: View = (input, output, target) => {
  const {
    nodeProcessRunning,
  } = input;

  // clang-format off
  Lit.render(html`
    <style>${nodeIconStyles}</style>
    <div
        class="node-icon ${!nodeProcessRunning ? 'inactive' : ''}"
        title=${i18nString(UIStrings.openDedicatedTools)}
        @click=${
            () => Host.InspectorFrontendHost.InspectorFrontendHostInstance.openNodeFrontend()}>
    </div>
    `, target);
  // clang-format on
};

export class NodeIndicator extends UI.Widget.Widget {
  readonly #view: View;
  #targetInfos: Protocol.Target.TargetInfo[] = [];
  #wasShown = false;

  constructor(element?: HTMLElement, view = DEFAULT_VIEW) {
    super(element, {useShadowDom: true});
    this.#view = view;

    SDK.TargetManager.TargetManager.instance().addEventListener(
        SDK.TargetManager.Events.AVAILABLE_TARGETS_CHANGED, event => {
          this.#targetInfos = event.data;
          this.requestUpdate();
        });
  }

  override performUpdate(): void {
    // Disable when we are testing, as debugging e2e
    // attaches a debug process and this changes some view sizes
    if (Host.InspectorFrontendHost.isUnderTest()) {
      return;
    }

    const nodeProcessRunning: Boolean = isNodeProcessRunning(this.#targetInfos);

    if (!this.#wasShown && !nodeProcessRunning) {
      // This widget is designed to be hidden until the first debuggable Node process is detected. Therefore
      // we don't construct the view if there's no data. After we've shown it once, it remains on-sreen and
      // indicates via its disabled state whether Node debugging is available.
      return;
    }
    this.#wasShown = true;

    const input: ViewInput = {
      nodeProcessRunning,
    };
    this.#view(input, {}, this.contentElement);
  }
}

let nodeIndicatorProviderInstance: NodeIndicatorProvider;
export class NodeIndicatorProvider implements UI.Toolbar.Provider {
  #toolbarItem: UI.Toolbar.ToolbarItem;
  #widgetElement: UI.Widget.WidgetElement<NodeIndicator>;

  private constructor() {
    this.#widgetElement = document.createElement('devtools-widget') as UI.Widget.WidgetElement<NodeIndicator>;
    new NodeIndicator(this.#widgetElement);

    this.#toolbarItem = new UI.Toolbar.ToolbarItem(this.#widgetElement);
    this.#toolbarItem.setVisible(false);
  }

  item(): UI.Toolbar.ToolbarItem|null {
    return this.#toolbarItem;
  }

  static instance(opts: {forceNew: boolean|null} = {forceNew: null}): NodeIndicatorProvider {
    const {forceNew} = opts;
    if (!nodeIndicatorProviderInstance || forceNew) {
      nodeIndicatorProviderInstance = new NodeIndicatorProvider();
    }

    return nodeIndicatorProviderInstance;
  }
}

export class SourcesPanelIndicator {
  constructor() {
    Common.Settings.Settings.instance()
        .moduleSetting('java-script-disabled')
        .addChangeListener(javaScriptDisabledChanged);
    javaScriptDisabledChanged();

    function javaScriptDisabledChanged(): void {
      const warnings = [];
      if (Common.Settings.Settings.instance().moduleSetting('java-script-disabled').get()) {
        warnings.push(i18nString(UIStrings.javascriptIsDisabled));
      }
      UI.InspectorView.InspectorView.instance().setPanelWarnings('sources', warnings);
    }
  }
}

export class BackendSettingsSync implements SDK.TargetManager.Observer {
  readonly #autoAttachSetting: Common.Settings.Setting<boolean>;
  readonly #adBlockEnabledSetting: Common.Settings.Setting<boolean>;
  readonly #emulatePageFocusSetting: Common.Settings.Setting<boolean>;

  constructor() {
    this.#autoAttachSetting = Common.Settings.Settings.instance().moduleSetting('auto-attach-to-created-pages');
    this.#autoAttachSetting.addChangeListener(this.#updateAutoAttach, this);
    this.#updateAutoAttach();

    this.#adBlockEnabledSetting = Common.Settings.Settings.instance().moduleSetting('network.ad-blocking-enabled');
    this.#adBlockEnabledSetting.addChangeListener(this.#update, this);

    this.#emulatePageFocusSetting = Common.Settings.Settings.instance().moduleSetting('emulate-page-focus');
    this.#emulatePageFocusSetting.addChangeListener(this.#update, this);
    SDK.TargetManager.TargetManager.instance().addModelListener(
        SDK.ChildTargetManager.ChildTargetManager, SDK.ChildTargetManager.Events.TARGET_INFO_CHANGED,
        this.#targetInfoChanged, this);

    SDK.TargetManager.TargetManager.instance().observeTargets(this);
  }

  #updateTarget(target: SDK.Target.Target): void {
    if (target.type() !== SDK.Target.Type.FRAME || target.parentTarget()?.type() === SDK.Target.Type.FRAME) {
      return;
    }
    void target.pageAgent().invoke_setAdBlockingEnabled({enabled: this.#adBlockEnabledSetting.get()});
    void target.emulationAgent().invoke_setFocusEmulationEnabled({enabled: this.#emulatePageFocusSetting.get()});
  }

  #updateAutoAttach(): void {
    Host.InspectorFrontendHost.InspectorFrontendHostInstance.setOpenNewWindowForPopups(this.#autoAttachSetting.get());
  }

  #update(): void {
    for (const target of SDK.TargetManager.TargetManager.instance().targets()) {
      this.#updateTarget(target);
    }
  }

  #targetInfoChanged(event: Common.EventTarget.EventTargetEvent<Protocol.Target.TargetInfo>): void {
    const targetManager = SDK.TargetManager.TargetManager.instance();
    const target = targetManager.targetById(event.data.targetId);
    if (!target || target.outermostTarget() !== target) {
      return;
    }
    this.#updateTarget(target);
  }

  targetAdded(target: SDK.Target.Target): void {
    this.#updateTarget(target);
  }

  targetRemoved(_target: SDK.Target.Target): void {
  }
}

SDK.ChildTargetManager.ChildTargetManager.install();
