import React, { ReactNode, useState } from "react";
import styles from "./OrgAst.module.css";
import { Fragment } from "react";

const OrgAst = (props: {
  setHighlight: Function;
  clearHighlights: Function;
  astTree: any;
  value: string;
}) => {
  const [selectedNode, setSelectedNode] = useState<OrgNodeReference | null>(
    null,
  );
  const [hoveredNode, setHoveredNode] = useState<OrgNodeReference | null>(null);

  function selectNode(uid: string, start: number, end: number) {
    const new_node: OrgNodeReference = { uid: uid, start: start, end: end };
    props.setHighlight([new_node, hoveredNode].filter((node) => node !== null));
    setSelectedNode({ uid: uid, start: start, end: end });
  }

  function startHoverNode(uid: string, start: number, end: number) {
    const new_node: OrgNodeReference = { uid: uid, start: start, end: end };
    props.setHighlight(
      [selectedNode, new_node].filter((node) => node !== null),
    );
    setHoveredNode({ uid: uid, start: start, end: end });
  }

  function endHoverNode(uid: string) {
    props.setHighlight([selectedNode].filter((node) => node !== null));
    setHoveredNode(null);
  }

  if (props.astTree.status !== "success") {
    return <div className={styles.OrgAst}>Error! {props.astTree.content}</div>;
  } else {
    return (
      <div className={styles.OrgAst}>
        <OrgAstNode
          key="^"
          uid="^"
          selectNode={selectNode}
          startHoverNode={startHoverNode}
          endHoverNode={endHoverNode}
          node={props.astTree.content}
          selectedNode={selectedNode}
          fullSource={props.value}
        />
      </div>
    );
  }
};

interface OrgNodeReference {
  uid: string;
  start: number;
  end: number;
}

const OrgAstNode = (props: {
  selectNode: Function;
  startHoverNode: Function;
  endHoverNode: Function;
  node: any;
  uid: string;
  selectedNode: OrgNodeReference | null;
  fullSource: string;
}) => {
  const [isHovered, setIsHovered] = useState(false);

  function selectNode() {
    props.selectNode(
      props.uid,
      props.node["standard-properties"]["begin"],
      props.node["standard-properties"]["end"],
    );
  }

  function hoverNode() {
    props.startHoverNode(
      props.uid,
      props.node["standard-properties"]["begin"],
      props.node["standard-properties"]["end"],
    );
    setIsHovered(true);
  }

  function endHoverNode() {
    props.endHoverNode(props.uid);
    setIsHovered(false);
  }

  function unicodeAwareSlice(text: string, start: number, end: number) {
    // Boooo javascript
    let i = 0;
    let output = "";
    for (const chr of text) {
      if (i >= end) {
        break;
      }
      if (i >= start) {
        output += chr;
      }
      ++i;
    }
    return output;
  }

  let nodeClassName = styles.OrgAstNode;
  if (props.selectedNode?.uid === props.uid) {
    nodeClassName = nodeClassName + " " + styles.selected;
  }
  if (isHovered) {
    nodeClassName = nodeClassName + " " + styles.hovered;
  }

  const selfSource = JSON.stringify(
    unicodeAwareSlice(
      props.fullSource,
      props.node["standard-properties"].begin - 1,
      props.node["standard-properties"].end - 1,
    ),
  );

  return (
    <div className={nodeClassName}>
      <div
        className={styles.OrgAstNodeType}
        onClick={selectNode}
        onMouseEnter={hoverNode}
        onMouseLeave={endHoverNode}
        title={selfSource}
      >
        <span>{props.node["ast-node"]}</span>
        <span>{selfSource}</span>
      </div>
      <details>
        <summary>Standard Properties</summary>
        <OrgPropertiesList
          selectNode={props.selectNode}
          startHoverNode={props.startHoverNode}
          endHoverNode={props.endHoverNode}
          parentUniqueId={props.uid}
          selectedNode={props.selectedNode}
          properties={props.node["standard-properties"]}
          fullSource={props.fullSource}
        />
      </details>
      {!!Object.keys(props.node.properties).length ? (
        <>
          <details>
            <summary>Properties</summary>
            <OrgPropertiesList
              selectNode={props.selectNode}
              startHoverNode={props.startHoverNode}
              endHoverNode={props.endHoverNode}
              parentUniqueId={props.uid}
              selectedNode={props.selectedNode}
              properties={props.node.properties}
              fullSource={props.fullSource}
            />
          </details>
        </>
      ) : null}
      {Array.isArray(props.node.children) && props.node.children.length > 0 ? (
        <details open={true}>
          <summary>Children</summary>
          <div className={styles.OrgAstChildren}>
            <OrgAstNodeList
              selectNode={props.selectNode}
              startHoverNode={props.startHoverNode}
              endHoverNode={props.endHoverNode}
              parentUniqueId={props.uid}
              selectedNode={props.selectedNode}
              node_list={props.node.children}
              fullSource={props.fullSource}
            />
          </div>
        </details>
      ) : null}
    </div>
  );
};

const OrgAstNodeList = (props: {
  selectNode: Function;
  startHoverNode: Function;
  endHoverNode: Function;
  parentUniqueId: string;
  selectedNode: OrgNodeReference | null;
  node_list: any[];
  fullSource: string;
}): React.JSX.Element[] => {
  return props.node_list.map((node) => {
    const uid =
      props.parentUniqueId +
      "_" +
      node["ast-node"] +
      "/" +
      node["standard-properties"]["begin"] +
      "/" +
      node["standard-properties"]["end"] +
      "#";
    return (
      <OrgAstNode
        key={uid}
        uid={uid}
        selectNode={props.selectNode}
        startHoverNode={props.startHoverNode}
        endHoverNode={props.endHoverNode}
        selectedNode={props.selectedNode}
        node={node}
        fullSource={props.fullSource}
      />
    );
  });
};

const OrgPropertiesList = (props: {
  selectNode: Function;
  startHoverNode: Function;
  endHoverNode: Function;
  parentUniqueId: string;
  selectedNode: OrgNodeReference | null;
  properties: Object;
  fullSource: string;
}): React.JSX.Element => {
  const entries = Object.entries(props.properties)
    .sort((a, b) => {
      if (a[0] < b[0]) {
        return -1;
      } else if (a[0] > b[0]) {
        return 1;
      } else {
        return 0;
      }
    })
    .filter((entry) => {
      return !(is_object(entry[1]) && entry[1]["noop"] == "Noop");
    })
    .map(([key, value]) => {
      return (
        <Fragment key={key}>
          <tr>
            <th scope="row">{key}:</th>
            <td>
              <OrgPropertyValue
                selectNode={props.selectNode}
                startHoverNode={props.startHoverNode}
                endHoverNode={props.endHoverNode}
                parentUniqueId={props.parentUniqueId}
                selectedNode={props.selectedNode}
                value={value}
                fullSource={props.fullSource}
              />
            </td>
          </tr>
        </Fragment>
      );
    });
  return (
    <table className={styles.OrgAstProperties}>
      <tbody>{entries}</tbody>
    </table>
  );
};

const OrgPropertyValue = (props: {
  selectNode: Function;
  startHoverNode: Function;
  endHoverNode: Function;
  parentUniqueId: string;
  selectedNode: OrgNodeReference | null;
  value: any;
  fullSource: string;
}): React.ReactNode => {
  if (
    props.value === null ||
    is_primitive(props.value) ||
    (is_array(props.value) && props.value.length === 0)
  ) {
    return JSON.stringify(props.value);
  } else if (is_list_of_ast_nodes(props.value)) {
    return (
      <div className={styles.OrgAstChildren}>
        <OrgAstNodeList
          selectNode={props.selectNode}
          startHoverNode={props.startHoverNode}
          endHoverNode={props.endHoverNode}
          parentUniqueId={props.parentUniqueId}
          selectedNode={props.selectedNode}
          node_list={props.value}
          fullSource={props.fullSource}
        />
      </div>
    );
  } else if (is_optional_pair(props.value)) {
    return (
      <table className={styles.OrgAstOptionalPair}>
        <tbody>
          <tr>
            <th scope="row">Optional value:</th>
            <td>{JSON.stringify(props.value.optval)}</td>
          </tr>
          <tr>
            <th scope="row">Value:</th>
            <td>{JSON.stringify(props.value.val)}</td>
          </tr>
        </tbody>
      </table>
    );
  } else if (is_object_tree(props.value)) {
    return (
      <OrgObjectTree
        selectNode={props.selectNode}
        startHoverNode={props.startHoverNode}
        endHoverNode={props.endHoverNode}
        parentUniqueId={props.parentUniqueId}
        selectedNode={props.selectedNode}
        value={props.value}
        fullSource={props.fullSource}
      />
    );
  } else {
    alert("Unhandled property value! " + JSON.stringify(props.value));
  }
};

interface OrgObjectTreeProps {
  selectNode: Function;
  startHoverNode: Function;
  endHoverNode: Function;
  parentUniqueId: string;
  selectedNode: OrgNodeReference | null;
  value: any;
  fullSource: string;
}

function OrgObjectTree({
  selectNode,
  startHoverNode,
  endHoverNode,
  parentUniqueId,
  selectedNode,
  value,
  fullSource,
}: OrgObjectTreeProps): React.ReactNode {
  const entries = value["object-tree"].map((entry: any) => {
    return (
      <tbody>
        <tr>
          <th scope="row">Optional value:</th>
          <td>
            <OrgAstNodeList
              selectNode={selectNode}
              startHoverNode={startHoverNode}
              endHoverNode={endHoverNode}
              parentUniqueId={parentUniqueId}
              selectedNode={selectedNode}
              node_list={entry[0]}
              fullSource={fullSource}
            />
          </td>
        </tr>
        <tr>
          <th scope="row">Value:</th>
          <td>
            <OrgAstNodeList
              selectNode={selectNode}
              startHoverNode={startHoverNode}
              endHoverNode={endHoverNode}
              parentUniqueId={parentUniqueId}
              selectedNode={selectedNode}
              node_list={entry[1]}
              fullSource={fullSource}
            />
          </td>
        </tr>
      </tbody>
    );
  });
  return <table className={styles.OrgAstObjectTree}>{entries}</table>;
}

function is_object(val: any): boolean {
  return val instanceof Object && !(val instanceof Array);
}

function is_array(val: any): boolean {
  return val instanceof Array;
}

function is_primitive(val: any): boolean | null {
  if (val === null) {
    return null;
  }
  return !(val instanceof Object || val instanceof Array);
}

function is_list_of_ast_nodes(val: any): boolean {
  if (!is_array(val)) {
    return false;
  }
  return val.every((entry: any) => {
    return is_object(entry) && entry.hasOwnProperty("ast-node");
  });
}

function is_optional_pair(val: any): boolean {
  return (
    is_object(val) && val.hasOwnProperty("optval") && val.hasOwnProperty("val")
  );
}

function is_object_tree(val: any): boolean {
  return is_object(val) && val.hasOwnProperty("object-tree");
}

export { OrgAst as default, OrgNodeReference };
