#!/usr/bin/env node

const fs = require('fs');
const os = require('os');
const path = require('path');

function numericField(line, name, fallback = 0) {
  const match = line.match(new RegExp(`(?:^| )${name}=(\\d+(?:\\.\\d+)?)`));
  return match ? Number(match[1]) : fallback;
}

function gapField(line, name) {
  const match = line.match(new RegExp(`${name}Avg/max=(n/a|([\\d.]+)\\/([\\d.]+)ms>\\d+=(\\d+))`));
  if (!match || match[1] === 'n/a') {
    return {
      average: null,
      maximum: null,
      overFrame: 0,
    };
  }
  return {
    average: Number(match[2]),
    maximum: Number(match[3]),
    overFrame: Number(match[4]),
  };
}

function parseMotionTraceLine(line) {
  const header = line.match(/^\[([^\]]+)\] \[MOTION_TRACE\] (.+?) duration=(\d+)ms /);
  if (!header) {
    return null;
  }

  return {
    timestamp: header[1],
    subject: header[2],
    durationMs: Number(header[3]),
    arrivals: numericField(line, 'recv'),
    renders: numericField(line, 'render'),
    interpolated: numericField(line, 'interpolated'),
    coalesced: numericField(line, 'coalesced'),
    stale: numericField(line, 'stale'),
    seqSkipped: numericField(line, 'seqSkipped'),
    source: gapField(line, 'source'),
    arrival: gapField(line, 'arrival'),
    render: gapField(line, 'render'),
  };
}

function percent(part, total) {
  return total > 0 ? (part / total) * 100 : 0;
}

function cadenceSummary(trace) {
  const arrivalSamples = trace.arrivalSamples === undefined
    ? Math.max(0, trace.arrivals - 1)
    : trace.arrivalSamples;
  const renderSamples = trace.renderSamples === undefined
    ? Math.max(0, trace.renders - 1)
    : trace.renderSamples;
  const arrivalOverFramePercent = percent(trace.arrival.overFrame, arrivalSamples);
  const renderOverFramePercent = percent(trace.render.overFrame, renderSamples);
  const overFrameReductionPercent = arrivalOverFramePercent > 0
    ? (1 - (renderOverFramePercent / arrivalOverFramePercent)) * 100
    : 0;

  return {
    receiveHz: trace.durationMs > 0 ? (trace.arrivals * 1000) / trace.durationMs : 0,
    renderHz: trace.durationMs > 0 ? (trace.renders * 1000) / trace.durationMs : 0,
    arrivalOverFramePercent,
    renderOverFramePercent,
    overFrameReductionPercent,
  };
}

function helloFromLine(line) {
  const match = line.match(/^\[([^\]]+)\] \[HELLO\] (.+?) protocol=(\d+) /);
  return match
    ? { timestamp: match[1], peer: match[2], protocol: Number(match[3]) }
    : null;
}

function peerForTrace(trace, protocols) {
  return Array.from(protocols.keys())
    .sort((left, right) => right.length - left.length)
    .find(peer => trace.subject === peer || trace.subject.startsWith(`${peer} `)) || null;
}

function analyzeMotionLog(source) {
  const protocols = new Map();
  const traces = [];

  String(source || '').split(/\r?\n/).forEach(line => {
    const hello = helloFromLine(line);
    if (hello) {
      protocols.set(hello.peer, hello.protocol);
      return;
    }

    const trace = parseMotionTraceLine(line);
    if (!trace) {
      return;
    }
    const peer = peerForTrace(trace, protocols);
    traces.push({
      ...trace,
      peer,
      protocol: peer ? protocols.get(peer) : null,
      summary: cadenceSummary(trace),
    });
  });

  return traces;
}

function gapDelta(current, previous, sampleCount) {
  return {
    average: null,
    maximum: current.maximum,
    overFrame: Math.max(0, current.overFrame - previous.overFrame),
    samples: sampleCount,
  };
}

function motionIntervals(traces, options = {}) {
  const minimumReceiveHz = options.minimumReceiveHz === undefined
    ? 30
    : Number(options.minimumReceiveHz);
  const previousByStream = new Map();
  const intervals = [];

  traces.forEach(trace => {
    const streamKey = `${trace.protocol || '?'}:${trace.peer || trace.subject}`;
    const previous = previousByStream.get(streamKey);
    previousByStream.set(streamKey, trace);
    if (!previous ||
        trace.durationMs <= previous.durationMs ||
        trace.arrivals < previous.arrivals ||
        trace.renders < previous.renders) {
      return;
    }

    const arrivals = trace.arrivals - previous.arrivals;
    const renders = trace.renders - previous.renders;
    const durationMs = trace.durationMs - previous.durationMs;
    const interval = {
      ...trace,
      durationMs,
      arrivals,
      renders,
      interpolated: Math.max(0, trace.interpolated - previous.interpolated),
      coalesced: Math.max(0, trace.coalesced - previous.coalesced),
      stale: Math.max(0, trace.stale - previous.stale),
      seqSkipped: Math.max(0, trace.seqSkipped - previous.seqSkipped),
      arrivalSamples: arrivals,
      renderSamples: renders,
      source: gapDelta(trace.source, previous.source, arrivals),
      arrival: gapDelta(trace.arrival, previous.arrival, arrivals),
      render: gapDelta(trace.render, previous.render, renders),
      intervalStartedAt: previous.timestamp,
    };
    interval.summary = cadenceSummary(interval);
    if (interval.summary.receiveHz >= minimumReceiveHz) {
      intervals.push(interval);
    }
  });

  return intervals;
}

function fixed(value, digits = 1) {
  return Number.isFinite(value) ? value.toFixed(digits) : 'n/a';
}

function printAnalysis(traces) {
  if (!traces.length) {
    console.log('No MOTION_TRACE entries found.');
    return;
  }

  console.log('timestamp                     proto  recvHz  renderHz  arrival>16  render>16  reduction');
  traces.slice(-12).forEach(trace => {
    const summary = trace.summary;
    console.log([
      trace.timestamp.padEnd(29),
      String(trace.protocol || '?').padStart(5),
      fixed(summary.receiveHz).padStart(7),
      fixed(summary.renderHz).padStart(9),
      `${fixed(summary.arrivalOverFramePercent, 2)}%`.padStart(11),
      `${fixed(summary.renderOverFramePercent, 2)}%`.padStart(10),
      `${fixed(summary.overFrameReductionPercent, 1)}%`.padStart(10),
    ].join('  '));
  });
}

if (require.main === module) {
  const args = process.argv.slice(2);
  const includeSlowIntervals = args.includes('--all');
  const logPath = args.find(argument => argument !== '--all') || path.join(
    os.homedir(),
    'Library',
    'Application Support',
    'mouse',
    'openmouse.log'
  );
  const source = fs.readFileSync(logPath, 'utf8');
  const traces = analyzeMotionLog(source);
  printAnalysis(motionIntervals(traces, {
    minimumReceiveHz: includeSlowIntervals ? 0 : 30,
  }));
}

module.exports = {
  analyzeMotionLog,
  cadenceSummary,
  motionIntervals,
  parseMotionTraceLine,
  printAnalysis,
};
