/** ###########################################################################
 * Frontend Queries
 * ##########################################################################*/

const getPauseId = window.getPauseId = () => {
  const state = app.store.getState();
  const pauseId = state?.pause?.id;
  if (!pauseId) {
      throw new Error(`Pause required (but not found) for snippet`);
  }
  return pauseId;
};

const getSessionId = window.getSessionId = () => {
  const state = app.store.getState();
  const sessionId = state?.app?.sessionId;
  if (!sessionId) {
      throw new Error(`sessionId required (but not found) for snippet`);
  }
  return sessionId;
};

// (() => {
//   const state = app.store.getState();
//   const sessionId = state?.app?.sessionId;
//   if (!sessionId) {
//     throw new Error(`sessionId required (but not found) for snippet`);
//   }
//   return sessionId;
// })()

const getAllFramesForPause = window.getAllFramesForPause = async (pauseId) => {
  return await app.client.Pause.getAllFrames(
      {},
      window.getSessionId(),
      pauseId || window.getPauseId()
  );
};


/** ###########################################################################
* point stuff
* ##########################################################################*/

const InvalidCheckpointId = 0;
const FirstCheckpointId = 1;

/**
* Copied from backend/src/shared/point.ts
*/
function pointToBigInt(point) {
  let rv = BigInt(0);
  let shift = 0;

  if (point.position) {
      addValue(point.position.offset || 0, 32);
      switch (point.position.kind) {
          case "EnterFrame":
              addValue(0, 3);
              break;
          case "OnStep":
              addValue(1, 3);
              break;
              // NOTE: In the past, "2" here indicated an "OnThrow" step type.
          case "OnPop":
              addValue(3, 3);
              break;
          case "OnUnwind":
              addValue(4, 3);
              break;
          default:
              throw new Error("UnexpectedPointPositionKind " + point.position.kind);
      }
      // Deeper frames predate shallower frames with the same progress counter.
      console.assert(
          point.position.frameIndex !== undefined,
          "Point should have a frameIndex",
          {
              point,
          }
      );
      addValue((1 << 24) - 1 - point.position.frameIndex, 24);
      // Points with positions are later than points with no position.
      addValue(1, 1);
  } else {
      addValue(point.bookmark || 0, 32);
      addValue(0, 3 + 24 + 1);
  }

  addValue(point.progress, 48);

  // Subtract here so that the first point in the recording is 0 as reflected
  // in the protocol definition.
  addValue(point.checkpoint - FirstCheckpointId, 32);

  return rv;

  function addValue(v, nbits) {
      rv |= BigInt(v) << BigInt(shift);
      shift += nbits;
  }
}

function BigIntToPoint(n) {
  const offset = readValue(32);
  const kindValue = readValue(3);
  const indexValue = readValue(24);
  const hasPosition = readValue(1);
  const progress = readValue(48);
  const checkpoint = readValue(32) + FirstCheckpointId;

  if (!hasPosition) {
    if (offset) {
      return { checkpoint, progress, bookmark: offset };
    }
    return { checkpoint, progress };
  }

  let kind;
  switch (kindValue) {
    case 0:
      kind = "EnterFrame";
      break;
    case 1:
      kind = "OnStep";
      break;
    case 2:
      ThrowError("UnexpectedOnThrowPoint", { point: n.toString() + "n" });
      break;
    case 3:
      kind = "OnPop";
      break;
    case 4:
      kind = "OnUnwind";
      break;
  }

  const frameIndex = (1 << 24) - 1 - indexValue;
  return {
    checkpoint,
    progress,
    position: { kind, offset, frameIndex },
  };

  function readValue(nbits) {
    const mask = (BigInt(1) << BigInt(nbits)) - BigInt(1);
    const rv = Number(n & mask);
    n = n >> BigInt(nbits);
    return rv;
  }
}

/** ###########################################################################
* Code serialization utilities
* ##########################################################################*/

function serializeFunctionCall(f) {
  var code = `(eval(eval(${JSON.stringify(f.toString())})))`;
  code = `(${code})()`;

  return JSON.stringify(`dev:${code}`);
}

function testRunSerializedExpressionLocal(expression) {
  // NOTE: Extra parentheses are added in frontend sometimes
  expression = `(${expression})`;

  var cmd = expression;
  if (cmd.startsWith('(')) {
      // strip "()"
      cmd = cmd.substring(1, expression.length - 1);
  }

  // parse JSON (used for serialization)
  cmd = JSON.parse(cmd);

  // strip "dev:" and run
  cmd = `(${cmd.substring(4)})`;
  eval(cmd);
}


/** ###########################################################################
* {@link chromiumEval} executes arbitrary code inside `chromium`
* ##########################################################################*/

window.chromiumEval = async (expression) => {
  if (expression instanceof Function) {
      // serialize function
      expression = serializeFunctionCall(expression);
  }

  const x = await app.client.Pause.evaluateInGlobal(
      {
          expression,
          pure: false,
      },
      getSessionId(),
      getPauseId()
  );

  try {
  }
  catch (err) {
      console.error(`unable to parse returned value:`, x, '\n\n');
      throw err;
  }
  const {
      result: {
          data,
          returned: {
              value
          } = {},
          exception: {
              value: errValue
          } = {}
      }
  } = x;

  if (errValue) {
      throw new Error(errValue);
  }

  return value;
};

/** ###########################################################################
* util
* ##########################################################################*/

window.flushCommandErrors = async () => {
  let err
  // NOTE: can cause infinite loop if `chromiumEval` itself induces errors
  while ((err = await chromiumEval(() => DevOnly.popCommandError()))) {
      console.error(err);
  }
};



/** ###########################################################################
* DOM protocol queries
* ##########################################################################*/

async function getAllBoundingClientRects() {
  try {
      const { elements } = await app.client.DOM.getAllBoundingClientRects(
          {},
          getSessionId(),
          getPauseId()
      );

      return elements;
  }
  finally {
      await flushCommandErrors();
  }
};

async function getBoxModel(node) {
  try {
      const result = await app.client.DOM.getBoxModel(
          { node },
          getSessionId(),
          getPauseId()
      );

      return result;
  }
  finally {
      await flushCommandErrors();
  }
}

async function DOM_getDocument() {
  try {
      const result = await app.client.DOM.getDocument(
          {},
          getSessionId(),
          getPauseId()
      );

      return result;
  }
  finally {
      await flushCommandErrors();
  }
}

/** ###########################################################################
* High-level tools.
* ##########################################################################*/

let lastCreatedPause;
function getCreatedPause() {
  return lastCreatedPause;
}

async function pauseAt(pointStruct) {
  const point = pointToBigInt(pointStruct).toString();
  lastCreatedPause = await app.client.Session.createPause({ point }, getSessionId());
  console.log(`Paused at ${lastCreatedPause?.pauseId}:`, lastCreatedPause);
}

async function getAllFrames() {
  const pause = getCreatedPause();
  if (!pause?.pauseId) {
      throw new Error(`Not paused at a good point.`);
  }
  if (pause?.pauseId) {
      const res = await getAllFramesForPause(pause.pauseId);
      console.log(`getAllFrames:`, res);
  }
}

async function getTopFrame() {
  const { data: { frames } } = await app.client.Pause.getAllFrames({}, sessionId, getPauseId());
  const topFrame = frames[0];
  return topFrame;
}

function getSelectedLocation() {
  const loc = app.store.getState().sources?.selectedLocation;
  if (!loc) {
      throw new Error(`No source selected`);
  }
  return loc;
}

function getSelectedSourceId() {
  return getSelectedLocation().sourceId;
}

async function selectLocation(sourceId, loc = undefined) {
  return app.actions.selectLocation(loc, {
      sourceId: sourceId + ""
  });
}

function getSourceText(line) {
  return document.querySelector(`[data-test-id="SourceLine-${line}"] [data-test-formatted-source="true"]`).textContent;
}
// getSourceText(24713);

/** ###########################################################################
* DOM Manipulation
* ##########################################################################*/

function getElTestString(el, name) {
  return el.getAttribute(`${name}`);
}
function getElTestNumber(el, name) {
  return parseInt(el.getAttribute(`${name}`));
}

function isElVisible(e) {
  return !!( e.offsetWidth || e.offsetHeight || e.getClientRects().length );
}

function getVisibleLineEls() {
  const lineEls = Array.from(document.querySelectorAll("[data-test-line-number]")).filter(isElVisible);
  const lineNums = lineEls.map(el => getElTestNumber(el, "data-test-line-number"));
  return {
      lineEls,
      lineNums
  };
}

/**
* @example getSourceLineChildElements(24713).columnEls[1]
*/
function getSourceLineChildElements(line) {
  const lineEl = document.querySelector(`[data-test-line-number="${line}"`);
  if (!lineEl) {
      return null;
  }
  const columnEls = Array.from(lineEl.querySelectorAll(`[data-column-index]`))
  if (!columnEls.length) {
      return null;
  }
  const columnIndexes = columnEls.map(el => getElTestNumber(el, "data-column-index"));
  return {
      columnEls,
      columnIndexes
  }
}

function insertIntoString(str, idx, toInsert) {
  return str.slice(0, idx) + toInsert + str.slice(idx);
}

function reset() {
  removeCustomEls();
}

function removeCustomEls() {
  const customEls = Array.from(document.querySelectorAll("[data-custom]"));
  for (const el of customEls) {
      el.remove();
  }
}

async function insertSourceBreakpoints() {
  removeCustomEls();

  const loc = app.store.getState().sources?.selectedLocation;

  if (!loc) {
      throw new Error(`insertSourceBreakpoints requires selected location`);
  }

  const { lineLocations } = await app.client.Debugger.getPossibleBreakpoints(
      { sourceId: loc.sourceId },
      sessionId
  );

  // const sourceIdNum = loc.sourceId.match(/\d+/)[0];
  // const sources = app.store.getState().sources.sourceDetails.entities[sourceIdNum];
  // console.log(sources);

  // const lineMin = nLineFrom;
  // const lineMax = lineMin + nLineDelta;
  // const locs = lineLocations.filter(l => l.line >= lineMin && l.line <= lineMax);
  const breakpointLocsByLine = Object.fromEntries(lineLocations.map(loc => [loc.line, loc]));

  // const breakpointLocations = locs.flatMap(l => {
  //   return l.columns?.map(c => `${l.line}:${c}`) || "";
  // });

  const { lineEls, lineNums } = getVisibleLineEls();
  for (let j = 0; j < lineEls.length; ++j) {
      const line = lineNums[j];
      const breaks = breakpointLocsByLine[line];
      if (breaks) {
          // Modify line
          const sourceEls = getSourceLineChildElements(line);
          if (!sourceEls) {
              continue;
          }

          // if (line === 24713)
          //   debugger;
          const { columnEls, columnIndexes } = sourceEls;
          for (let col of breaks.columns) {
              // Iterate column back to front, so modification does not mess with follow-up indexes.
              const iCol = columnIndexes.findLastIndex((idx, i) => idx < col && (i === columnIndexes.length || columnIndexes[i+1] >= col));
              const targetEl = columnEls[iCol];
              const iOffset = col - columnIndexes[iCol];
              if (!targetEl) {
                  debugger;
                  continue;
              }
              targetEl.innerHTML = insertIntoString(targetEl.innerHTML, iOffset, `<span data-custom="1" style="color: red">|</span>`);
          }
      }
  }
  // locs.forEach(l => {
  //   const source = getSourceText(l.line);
  //   const highlightCss = "color: red";
  //   const clearCss = "color: default";
  //   let lastIdx = 0;
  //   const sourceParts = l.columns.map((col, i) => {
  //     return source.slice(lastIdx, lastIdx = l.columns[i]);
  //   });
  //   sourceParts.push(source.slice(lastIdx));
  //   const format = sourceParts.join("<BREAK>");
  //   console.log(format);
  // });
  // console.log(...printArgs);
  // return {
  //   breakpointLocations
  // };
}


async function getHitCounts() {
  const loc = getSelectedLocation();
  console.log("loc", loc);

  const minCol = 0;
  const maxCol = 100;

  const { lineLocations } = await app.client.Debugger.getHitCounts(
      {
          sourceId: loc.sourceId,
          locations: [{
              line: loc.line,
              columns: range(minCol, maxCol)
          }]
      },
      sessionId
  );
  return lineLocations;
}

async function getCorrespondingSources() {
  function getCorrespondingSources(name) {
      const sources = Object.values(app.store.getState().sources.sourceDetails.entities);
      // TODO: Also look up source-mapped sources?
      return sources.filter(s => s.url?.endsWith(name));
  }

  console.log(getCorrespondingSources("_app-96d565375e549a2c.js"));
}

/** ###########################################################################
* some things we want to play around with
* ##########################################################################*/

async function main() {
  // const pointStruct = JSON.parse("{\"checkpoint\":12,\"progress\":35935,\"position\":{\"kind\":\"OnPop\",\"offset\":0,\"frameIndex\":4,\"functionId\":\"28:1758\"}}");

  // RUN-1576
  // http://admin/crash/controller/dff404d0-e114-4622-8daa-1c3340ba7833
  // const pointStruct = {
  //   "checkpoint": 86,
  //   "progress": 44196840,
  //   "bookmark": 2149
  // };
  // await pauseAt(pointStruct);
  // await getAllFrames();

  console.log(await getSelectedSourceId());
  // await selectLocation(sourceId);


  insertSourceBreakpoints();
  // copy(BigIntToPoint(27584077111921759048236340451739749n));

}

main();

// const initTimer = setInterval(() => {
//   console.log("initTimer checking...");
//   if (window.app) {
//       clearInterval(initTimer);
//       main();
//   }
// }, 100);