/* TODO:
  - history
  - find example and correctly handle "merge" messages
  - messages referencing selected msg
  - sort and filter table
  - loading icons
  - expand referenced object in place
  - paste bytes -> analyze
  - edit fields / files?
  - different menu for message / enum / extend / literal
*/
import { useState, useEffect, useRef } from 'react';
import type { MouseEvent as ReactMouseEvent, ChangeEventHandler, ChangeEvent } from 'react';
import './App.css';
import { ObjectInspector, ObjectLabel, ObjectName, ObjectValue } from 'react-inspector';
import { PanelGroup, Panel, PanelResizeHandle } from 'react-resizable-panels';
import { process, read, ParsedFile } from './iwa';
import { parse_protos, ProtoMap } from './messages';
import type { $_TSP_MessageInfo, $_TSP_Reference } from './messages';
import { Menu, Item, Separator, useContextMenu } from 'react-contexify';
import 'react-contexify/dist/ReactContexify.css';
import { ToastContainer, toast } from 'react-toastify';
import 'react-toastify/dist/ReactToastify.css';

//#region Xxd

//import { vsprintf } from 'printj';
/*const X = "%02hhx", Y = X + X + " ";
const FMT = [...Array.from({length:16}).map((_,i) =>
  Y.repeat(i>>1) + (i%2 ? X:"  ") + "   " + "     ".repeat(7 - (i >> 1)) + "|" + "%c".repeat(i) + " ".repeat(16-i) + "|\n"
), Y.repeat(8) + "|" + "%c".repeat(16) + "|\n"];

const xxd = (u8: Uint8Array): string => {
  let out: string[] = [];
  for(let i = 0; i < u8.length; i+=16) {
    let d = [...u8.slice(i, i+16)];
    out.push(vsprintf(`%04x: ${FMT[d.length]}`, [i, ...d, ...d.map(x => String.fromCharCode(x).replace(/[^\x20-\x7E]/g,"."))]))
  }
  return out.join("");
}

type XxdProps = {
  data?: Uint8Array;
};

function Xxd({data}: XxdProps) {
  return ( <pre>{data && xxd(data)}</pre> );
}*/
//<div className="proto"><Xxd data={id && file.space[+id]?.[0]?.data || void 0} /></div>

//#endregion

//#region TableView

type TableViewProps = {
  id?: string;
  data: any[];
  cols: string[];
  rowclick?: (row: any, R: number, e: ReactMouseEvent<HTMLTableRowElement, MouseEvent>) => void;
  cellclick?: (value: any, R: number, C: number, e: ReactMouseEvent<HTMLTableCellElement, MouseEvent>) => void;
};

function TableView({id, data, cols, rowclick, cellclick}: TableViewProps) {
  return (
    <table>
      <thead>
        <tr>{cols.map((c,idx) => (
          <th key={idx}>{c}</th>
        ))}</tr>
      </thead>
      <tbody>{data.map((row, R) => (
        <tr id={`tr-${R}`} key={R}
          {...(rowclick ? {onClick: (e) => {e.preventDefault(); e.stopPropagation(); rowclick(row, R, e)}} : {})}
          {...(row["id"] == id ? {style: {backgroundColor: "#646cff", color: "#FFFFFF" }} : {})}
        >{/* TODO: forward-ref? */}
          {cols.map((c,C) => (<td key={`${R}-${C}`}
            {...(typeof row[c] == "string" ? {className: "left"} : {})}
            {...(typeof row[c] == "number" ? {className: "right"} : {})}
            {...(cellclick ? {onClick: (e) => {e.preventDefault(); e.stopPropagation(); cellclick(row, R, C, e)}} : {})}
          >{row[c]??""}</td>))}
        </tr>
      ))}</tbody>
    </table>
  );
}

//#endregion

//#region ContextMenu

interface ContextMenuProps {
  ID: string;
  menuType: string;
  menuField: string;
  menuId: string;
  onClickId?: ({props}: any)=>void;
  onClickCopyByteArray?: ({props}: any)=>void;
  onClickCopyJSON: ({props}: any)=>void;
  showProtoDef: ({props}: any)=>void;
}
const ContextMenu = ({ID, menuType, menuField, menuId, onClickId, onClickCopyByteArray, onClickCopyJSON, showProtoDef}: ContextMenuProps) => (
  <Menu id={ID}>
    {menuField && (<Item disabled><b>{menuField}</b></Item>)}
    <Item disabled><b>{menuType}</b></Item>
    <Item hidden={()=>menuType != ".TSP.Reference"} onClick={onClickId}>Go to {menuId}</Item>
    <Separator />
    <Item onClick={onClickCopyByteArray}>Copy byte array</Item>
    <Item onClick={onClickCopyJSON}>Copy JSON</Item>
    <Item onClick={showProtoDef}>Show Definition</Item>
  </Menu> );

//#endregion

function App() {
  /* selected message ID */
  const [id, setId] = useState<string>("0");
  /* parsed file */
  const [file, setFile] = useState<ParsedFile>({ space: {}, tbl: [], type: "N" });
  /* current object */
  const [obj, setObj] = useState<any>({});
  /* current meta */
  const [meta, setMeta] = useState<$_TSP_MessageInfo>({} as any);
  /* protobuf definitions */
  const [protos, setProtos] = useState<ProtoMap>({});
  /* selected message type */
  const [sel, setSel] = useState<string>("");
  /* current protobuf definition */
  const [__html, setProto] = useState<string>("Select a Row");
  /* "dirty" if inspecting a subfield */
  const [dirty, setDirty] = useState<boolean>(false);
  /* history stack */
  const [stack, setStack] = useState<string[]>([]);
  /* react-contexify */
  const MENU_ID = "insp-menu";
  const { show } = useContextMenu({ id: MENU_ID });
  const [menuField, setMenuField] = useState<string>("");
  const [menuType, setMenuType] = useState<string>("");
  const [menuId, setMenuId] = useState<string>("");
  const tblRef = useRef<HTMLDivElement>(null);

  /* update selection based on table row */
  const doitRow = (row: any, R: number, reset?: boolean) => {
    let obj: any, meta: $_TSP_MessageInfo;
    try {
      obj = process(file.space[+row.id][0].data, row.message, protos);
      meta = process(file.space[+row.id][0].rawmeta, ".TSP.MessageInfo", protos);
    } catch(e) {
      console.error(row, e); toast.error(`Could not parse ${row.id} (${row.type})`, {position: toast.POSITION.TOP_CENTER}); return;
    }
    if(reset == true) setStack([]); else if(typeof reset != "undefined") setStack([...stack, id]);
    setSel(row.message);
    setProto(protos[row.message] || "");
    setDirty(false);
    setObj(obj);
    /* .TSP.MessageInfo */
    if(meta.object_references) meta.$object_references = meta.object_references.map((n: BigInt) => {
      /* create a fake reference for the inspector */
      var o: $_TSP_Reference = ({ identifier: n });
      Object.defineProperty(o, "PB_TYPE", {value: ".TSP.Reference", enumerable: false});
      return o;
    });
    if(meta.field_infos) meta.field_infos.forEach((fi) => {
      /* .TSP.FieldInfo */
      if(fi.object_references) fi.$object_references = fi.object_references.map((n: BigInt) => {
        /* create a fake reference for the inspector */
        var o: $_TSP_Reference = ({ identifier: n });
        Object.defineProperty(o, "PB_TYPE", {value: ".TSP.Reference", enumerable: false});
        return o;
      });
    })
    setMeta(meta);
    setId(String(row.id));
    var rowelt = document.getElementById(`tr-${R}`);
    var top = rowelt?.offsetTop || 0;
    if(tblRef.current) {
      let tbl = tblRef.current;
      if(top > tbl.scrollTop + tbl.clientHeight - (rowelt?.clientHeight||0) || top < tbl.scrollTop + (rowelt?.clientHeight||0)) tbl.scrollTop = Math.max(0, top - tbl.clientHeight/2 - (rowelt?.clientHeight||0)/2);
    }
  };
  /* click event handler for the messages table */
  const rowclick = (row: any, R: number) => { doitRow(row, R, true); };
  /* helper for .TSP.Reference */
  const gotoRef = (id: string) => {
    var R = file.tbl.findIndex(t => +t.id == +id);
    if (R == -1) throw new Error(`Message ${id} not found`);
    doitRow(file.tbl[R], R, false);
  };
  /* helper for selecting sub-proto */
  const selectProto = (type: string) => { setProto(protos[type] || ""); setDirty(type != sel); };
  /* go back to the previous message */
  const pop = () => {
    const oldId = stack.pop()||"0";
    setStack([...stack]);
    var R = file.tbl.findIndex(t => +t.id == +oldId);
    if (R == -1) throw new Error(`Message ${oldId} not found`);
    doitRow(file.tbl[R], R);
  };

  /* on load, get protobuf definitions and process the test file */
  useEffect(() => {
    let ignore = false;
    (async () => {
      const protos = await (await fetch("protos")).text();
      const testfile = await (await fetch("test.numbers")).arrayBuffer();
      if(ignore) return;
      setProtos(parse_protos(protos));
      setFile(read(testfile));
    })();
    return () => { ignore = true; }
  }, []);
  useEffect(() => { if(file.tbl[0]) doitRow(file.tbl[0], 0, true); }, [file]);

  const onChange: ChangeEventHandler<HTMLInputElement> = async (e: ChangeEvent<HTMLInputElement>) => {
    if(!e.target.files) { toast.error("must select a file!"); throw new Error("No file selected!"); }
    let data: ParsedFile;
    try { data = read(await e.target.files?.[0].arrayBuffer()); } catch(e) {
      if((e as any).message?.includes("Failed to read archive")) toast.error("Please select an iWork file", { position: toast.POSITION.TOP_CENTER });
      else toast.error(e && (e as any).message || e, { position: toast.POSITION.TOP_CENTER });
      console.error(e); return;
    }
    setFile(data);
  }

  /* Menu machinations */
  interface MenuProps {
    type: string;
    field?: string;
    id: string;
    data: any;
  }
  function displayMenu(event: ReactMouseEvent, props?: MenuProps) {
    setMenuField(props?.field||"");
    setMenuType(props?.type||"");
    if(props?.id) setMenuId(String(props.id));
    show({ event, props });
  }
  function onClickId(){ gotoRef(menuId); }
  function onClickCopyByteArray({ props }: {props: MenuProps}){
    var _data = props?.data?.PB_RAW?.data || (+props.id == +id) && file.space[+id][0].data;
    if(!_data) throw new Error("Could not find raw data");
    navigator.clipboard.writeText("[" + [..._data].map(x => "0x" + x.toString(16).toUpperCase().padStart(2,"0")).join(", ") + "]");
  }
  function onClickCopyJSON({ props }: {props: MenuProps}){
    if(!props?.data) throw new Error("Could not find raw data");
    navigator.clipboard.writeText(JSON.stringify(props.data, (_,v) => typeof v == "bigint" ? v.toString() : v instanceof Uint8Array ? [...v]: v));
  }
  function showProtoDef({props}: {props: MenuProps}) { selectProto(props.type); }

  type NodeRendererProps = {
    depth: number;
    name: string;
    data: any;
    isNonenumerable: boolean;
    expanded: boolean;
  }
  const nodeRenderer = ({ depth, name, data, isNonenumerable }: NodeRendererProps) => {
    if(depth === 0) return ( <b>Message {id} <a onClick={()=>{selectProto(sel);}} onContextMenu={e => {displayMenu(e, { type: sel, id, data })}}><b>[{sel}]</b></a></b> );
    if(typeof data == "bigint" && name.includes("identifier")) return ( <>
      <ObjectName name={name} />: <ObjectValue object={data} /> -&gt; <a onClick={() => {gotoRef(String(data))}}><b>{String(data)}</b></a>
    </> );
    if(data.PB_TYPE) {
      const frag = ( <a onClick={() => {selectProto(data.PB_TYPE);}}>
        {data.PB_ENUM && <ObjectValue object={data.value} />}
        <b>{data.PB_ENUM && <> = {data.PB_ENUM}</>} [{data.PB_TYPE}]</b>
      </a> );
      if(data.PB_TYPE == ".TSP.Reference") {
        let id = String(data?.identifier);
        return ( <span onContextMenu={(e) => {displayMenu(e, { type: data.PB_TYPE, id, data, field: data.PB_FIELD })}}>
          <ObjectName name={name} />: <b>{frag} -&gt; <a onClick={() => {gotoRef(id)}}><b>{id}</b></a></b>
        </span> );
      }
      return ( <span onContextMenu={(e) => {displayMenu(e, { type: data.PB_TYPE, id, data, field: data.PB_FIELD })}}>
        <ObjectName name={name} />: {frag}
      </span> );
    }
    return ( <ObjectLabel name={name} data={data} isNonenumerable={isNonenumerable} /> );
  };
  const metaRenderer = ({ depth, name, data, isNonenumerable }: NodeRendererProps) => {
    if(depth === 0) return ( <b>Metadata</b> );
    if(typeof data == "bigint" && name.includes("identifier")) return ( <>
      <ObjectName name={name} />: <ObjectValue object={data} /> -&gt; <a onClick={() => {gotoRef(String(data))}}><b>{String(data)}</b></a>
    </> );
    if(data.PB_TYPE) {
      const frag = ( <a onClick={() => {selectProto(data.PB_TYPE);}}>
        {data.PB_ENUM && <ObjectValue object={data.value} />}
        <b>{data.PB_ENUM && <> = {data.PB_ENUM}</>} [{data.PB_TYPE}]</b>
      </a> );
      if(data.PB_TYPE == ".TSP.Reference") {
        let id = String(data?.identifier);
        return ( <span onContextMenu={(e) => {displayMenu(e, { type: data.PB_TYPE, id, data, field: data.PB_FIELD })}}>
          <ObjectName name={name} />: <b>{frag} -&gt; <a onClick={() => {gotoRef(id)}}><b>{id}</b></a></b>
        </span> );
      }
      return ( <span onContextMenu={(e) => {displayMenu(e, { type: data.PB_TYPE, id, data, field: data.PB_FIELD })}}>
        <ObjectName name={name} />: {frag}
      </span> );
    }
    return ( <ObjectLabel name={name} data={data} isNonenumerable={isNonenumerable} /> );
  };

  return ( <>
    {/* header */}
    <div id="header" className="header"><b><a href="https://sheetjs.com">SheetJS</a> IWA Inspector <input type="file" id="file" onChange={onChange} /></b></div>

    <div className="page">
      <PanelGroup direction='vertical'>
        <Panel defaultSize={25}><div className="overflow" ref={tblRef}>

          {/* message table */}
          <TableView data={file.tbl} cols={["id", "type", "message", "path"]} id={id} rowclick={rowclick} />

        </div></Panel>
        <PanelResizeHandle style={{ height: "3px", backgroundColor: "#EEEEEE" }} />
        <Panel><div className="overflow">

          {/* selected message bar */}
          <div style={{boxShadow: "0 2px 2px -1px rgba(0, 0, 0, 0.4)", height: "24px"}}>{sel ? (<>
            <b>Selected message {(stack.length > 4 ? [...stack.slice(0,2), "...", ...stack.slice(-1)] : stack).map(i => i + " > ").join("")} {id} ({sel}) {file.space[+id]?.[0]?.data?.length || 0} bytes {stack.length && <a onClick={pop}>Return to {stack[stack.length - 1]}</a> || ""}</b>
          </>) : "Select a message to see the contents"}</div>

          <div style={{width: "100%", height: "calc(100% - 24px)", overflow: "auto"}}><PanelGroup direction='horizontal'>
            <Panel><div className='overflow'>

              {/* proto definition */}
              <pre style={{textAlign: "left"}}>
                {dirty && (<><a onClick={() => { selectProto(sel);}}>Return to {sel}</a><br /><br /></>)}
                {__html.split("\n").map((r, idx) => ( <>{!r.match(/(optional|repeated|required) \./) ? (
                  <span key={idx}>{r}</span>
                ) : (
                  <a key={idx} onClick={() => {selectProto(r.trim().split(" ")[1]);}}>{r}</a>
                )}<br/></> ))}
              </pre>

            </div></Panel>
            <PanelResizeHandle style={{ width: "2px", backgroundColor: "#EEEEEE" }} />
            <Panel><div className="overflow" style={{textAlign: "left", marginTop: "13px", marginLeft: "10px", marginBottom: "13px"}}>

              {/* inspector */}
              <b>Message</b>
              <ObjectInspector data={obj} expandLevel={1} nodeRenderer={nodeRenderer} />
              <b>Meta</b>
              <ObjectInspector data={meta} expandLevel={1} nodeRenderer={metaRenderer} />
              <div style={{height:"13px"}}></div>

            </div></Panel>
          </PanelGroup></div>
        </div></Panel>
      </PanelGroup>

      {/* Menu */}
      <ContextMenu ID={MENU_ID} menuField={menuField} menuType={menuType} menuId={menuId} onClickId={onClickId} onClickCopyByteArray={onClickCopyByteArray} onClickCopyJSON={onClickCopyJSON} showProtoDef={showProtoDef} />

      {/* Toast */}
      <ToastContainer />
    </div>
  </> );
}

export default App
