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

import './components/components.js';

import * as SDK from '../../core/sdk/sdk.js';
import type * as Protocol from '../../generated/protocol.js';
import type * as TreeOutline from '../../ui/components/tree_outline/tree_outline.js';
import * as Lit from '../../ui/lit/lit.js';

const {html} = Lit;

export type AXTreeNodeData = SDK.AccessibilityModel.AccessibilityNode;
export type AXTreeNode = TreeOutline.TreeOutlineUtils.TreeNode<AXTreeNodeData>;

function isLeafNode(node: SDK.AccessibilityModel.AccessibilityNode): boolean {
  return node.numChildren() === 0 && node.role()?.value !== 'Iframe';
}

function getModel(frameId: Protocol.Page.FrameId): SDK.AccessibilityModel.AccessibilityModel {
  const frame = SDK.FrameManager.FrameManager.instance().getFrame(frameId);
  const model = frame?.resourceTreeModel().target().model(SDK.AccessibilityModel.AccessibilityModel);
  if (!model) {
    throw new Error('Could not instantiate model for frameId');
  }
  return model;
}

export async function getRootNode(frameId: Protocol.Page.FrameId): Promise<SDK.AccessibilityModel.AccessibilityNode> {
  const model = getModel(frameId);
  const root = await model.requestRootNode(frameId);
  if (!root) {
    throw new Error('No accessibility root for frame');
  }
  return root;
}

function getFrameIdForNodeOrDocument(node: SDK.DOMModel.DOMNode): Protocol.Page.FrameId {
  let frameId;
  if (node instanceof SDK.DOMModel.DOMDocument) {
    frameId = node.body?.frameId();
  } else {
    frameId = node.frameId();
  }
  if (!frameId) {
    throw new Error('No frameId for DOM node');
  }
  return frameId;
}

export async function getNodeAndAncestorsFromDOMNode(domNode: SDK.DOMModel.DOMNode):
    Promise<SDK.AccessibilityModel.AccessibilityNode[]> {
  let frameId = getFrameIdForNodeOrDocument(domNode);
  const model = getModel(frameId);
  const result = await model.requestAndLoadSubTreeToNode(domNode);
  if (!result) {
    throw new Error('Could not retrieve accessibility node for inspected DOM node');
  }

  const outermostFrameId = SDK.FrameManager.FrameManager.instance().getOutermostFrame()?.id;
  if (!outermostFrameId) {
    return result;
  }
  while (frameId !== outermostFrameId) {
    const node = await SDK.FrameManager.FrameManager.instance().getFrame(frameId)?.getOwnerDOMNodeOrDocument();
    if (!node) {
      break;
    }
    frameId = getFrameIdForNodeOrDocument(node);
    const model = getModel(frameId);
    const ancestors = await model.requestAndLoadSubTreeToNode(node);
    result.push(...ancestors || []);
  }
  return result;
}

async function getChildren(node: SDK.AccessibilityModel.AccessibilityNode):
    Promise<SDK.AccessibilityModel.AccessibilityNode[]> {
  if (node.role()?.value === 'Iframe') {
    const domNode = await node.deferredDOMNode()?.resolvePromise();
    if (!domNode) {
      throw new Error('Could not find corresponding DOMNode');
    }
    const frameId = domNode.frameOwnerFrameId();
    if (!frameId) {
      throw new Error('No owner frameId on iframe node');
    }
    const localRoot = await getRootNode(frameId);
    return [localRoot];
  }
  return await node.accessibilityModel().requestAXChildren(node.id(), node.getFrameId() || undefined);
}

export async function sdkNodeToAXTreeNodes(sdkNode: SDK.AccessibilityModel.AccessibilityNode): Promise<AXTreeNode[]> {
  const treeNodeData = sdkNode;
  if (isLeafNode(sdkNode)) {
    return [{
      treeNodeData,
      id: getNodeId(sdkNode),
    }];
  }

  return [{
    treeNodeData,
    children: async () => {
      const childNodes = await getChildren(sdkNode);
      const childTreeNodes = await Promise.all(childNodes.map(childNode => sdkNodeToAXTreeNodes(childNode)));
      return childTreeNodes.flat(1);
    },
    id: getNodeId(sdkNode),
  }];
}

export function accessibilityNodeRenderer(node: AXTreeNode): Lit.TemplateResult {
  const sdkNode = node.treeNodeData;
  const name = sdkNode.name()?.value || '';
  const role = sdkNode.role()?.value || '';
  const properties = sdkNode.properties() || [];
  const ignored = sdkNode.ignored();
  const id = getNodeId(sdkNode);
  return html`<devtools-accessibility-tree-node .data=${{
    name, role, ignored, properties, id,
  }
  }></devtools-accessibility-tree-node>`;
}

export function getNodeId(node: SDK.AccessibilityModel.AccessibilityNode): string {
  return node.getFrameId() + '#' + node.id();
}
