/* This Source Code Form is subject to the terms of the Mozilla Public
 * License, v. 2.0. If a copy of the MPL was not distributed with this
 * file, You can obtain one at https://mozilla.org/MPL/2.0/. */

import { describe, it, expect } from 'vitest';
import { decodePly, parsePlyHeader } from './ply.js';

const enc = new TextEncoder();

function buildAsciiPly(rows: Array<[number, number, number]>, withRgb: boolean = false): Uint8Array {
  const props = withRgb
    ? 'property float x\nproperty float y\nproperty float z\nproperty uchar red\nproperty uchar green\nproperty uchar blue\n'
    : 'property float x\nproperty float y\nproperty float z\n';
  const header =
    'ply\n' +
    'format ascii 1.0\n' +
    'comment generated by test\n' +
    `element vertex ${rows.length}\n` +
    props +
    'end_header\n';
  const body = rows.map((r) => withRgb ? `${r[0]} ${r[1]} ${r[2]} 255 128 64` : `${r[0]} ${r[1]} ${r[2]}`).join('\n') + '\n';
  return enc.encode(header + body);
}

function buildBinaryPlyLe(rows: Array<[number, number, number]>, withRgb: boolean = false): Uint8Array {
  const props = withRgb
    ? 'property float x\nproperty float y\nproperty float z\nproperty uchar red\nproperty uchar green\nproperty uchar blue\n'
    : 'property float x\nproperty float y\nproperty float z\n';
  const header =
    'ply\n' +
    'format binary_little_endian 1.0\n' +
    `element vertex ${rows.length}\n` +
    props +
    'end_header\n';
  const headerBytes = enc.encode(header);
  const stride = withRgb ? 15 : 12;
  const body = new ArrayBuffer(rows.length * stride);
  const view = new DataView(body);
  for (let i = 0; i < rows.length; i++) {
    view.setFloat32(i * stride, rows[i][0], true);
    view.setFloat32(i * stride + 4, rows[i][1], true);
    view.setFloat32(i * stride + 8, rows[i][2], true);
    if (withRgb) {
      view.setUint8(i * stride + 12, 255);
      view.setUint8(i * stride + 13, 128);
      view.setUint8(i * stride + 14, 64);
    }
  }
  const merged = new Uint8Array(headerBytes.length + body.byteLength);
  merged.set(headerBytes, 0);
  merged.set(new Uint8Array(body), headerBytes.length);
  return merged;
}

describe('parsePlyHeader', () => {
  it('reads ascii xyz header', () => {
    const buf = buildAsciiPly([[1, 2, 3]]);
    const h = parsePlyHeader(buf);
    expect(h.format).toBe('ascii');
    const v = h.elements.find((e) => e.name === 'vertex')!;
    expect(v.count).toBe(1);
    expect(v.properties.map((p) => p.name)).toEqual(['x', 'y', 'z']);
  });

  it('reads binary_little_endian header with RGB', () => {
    const buf = buildBinaryPlyLe([[0, 0, 0]], true);
    const h = parsePlyHeader(buf);
    expect(h.format).toBe('binary_little_endian');
    const v = h.elements.find((e) => e.name === 'vertex')!;
    expect(v.recordSize).toBe(15);
    expect(v.properties.map((p) => p.name)).toContain('red');
  });

  it('rejects missing magic', () => {
    expect(() => parsePlyHeader(new TextEncoder().encode('not a ply'))).toThrow();
  });
});

describe('decodePly', () => {
  it('decodes ascii xyz', () => {
    const buf = buildAsciiPly([
      [1, 2, 3],
      [-4, -5, -6],
      [10, 20, 30],
    ]);
    const chunk = decodePly(buf);
    expect(chunk.pointCount).toBe(3);
    expect(Array.from(chunk.positions)).toEqual([1, 2, 3, -4, -5, -6, 10, 20, 30]);
    expect(chunk.colors).toBeUndefined();
    expect(chunk.bbox).toEqual({ min: [-4, -5, -6], max: [10, 20, 30] });
  });

  it('decodes ascii xyz + rgb (uchar 0..255)', () => {
    const buf = buildAsciiPly([[1, 1, 1], [2, 2, 2]], true);
    const chunk = decodePly(buf);
    expect(chunk.colors).toBeDefined();
    // Every row uses (255, 128, 64)
    expect(chunk.colors![0]).toBeCloseTo(1.0, 2);
    expect(chunk.colors![1]).toBeCloseTo(128 / 255, 2);
    expect(chunk.colors![2]).toBeCloseTo(64 / 255, 2);
  });

  it('decodes binary little-endian xyz + rgb', () => {
    const buf = buildBinaryPlyLe([[0, 0, 0], [1.5, -2.5, 3.5]], true);
    const chunk = decodePly(buf);
    expect(chunk.pointCount).toBe(2);
    expect(Array.from(chunk.positions.subarray(0, 3))).toEqual([0, 0, 0]);
    expect(Array.from(chunk.positions.subarray(3, 6))).toEqual([1.5, -2.5, 3.5]);
    expect(chunk.colors![0]).toBeCloseTo(1.0, 2);
  });

  it('throws when binary body is short', () => {
    const truncated = buildBinaryPlyLe([[0, 0, 0], [1, 1, 1]], false);
    // chop the last 4 bytes — now we're missing one float of point 2
    const short = truncated.slice(0, truncated.length - 4);
    expect(() => decodePly(short)).toThrow();
  });
});
