/**
 * Tensor utilities for Synor Compute SDK
 */

import type { TensorData, TensorShape, Precision } from './types';

/**
 * Tensor wrapper for compute operations.
 */
export class Tensor {
  readonly data: Float32Array | Float64Array | Int32Array | Int8Array | Uint8Array;
  readonly shape: number[];
  readonly dtype: Precision;

  private constructor(
    data: Float32Array | Float64Array | Int32Array | Int8Array | Uint8Array,
    shape: number[],
    dtype: Precision
  ) {
    this.data = data;
    this.shape = shape;
    this.dtype = dtype;
  }

  /**
   * Create a tensor from various input formats.
   */
  static from(data: TensorData, dtype: Precision = 'fp32'): Tensor {
    if (data instanceof Float32Array) {
      return new Tensor(data, [data.length], 'fp32');
    }
    if (data instanceof Float64Array) {
      return new Tensor(data, [data.length], 'fp64');
    }
    if (data instanceof Int32Array) {
      return new Tensor(data, [data.length], 'int8');
    }
    if (data instanceof Int8Array) {
      return new Tensor(data, [data.length], 'int8');
    }
    if (data instanceof Uint8Array) {
      return new Tensor(data, [data.length], 'int8');
    }

    // Handle nested arrays
    const shape = Tensor.inferShape(data);
    const flat = Tensor.flatten(data);

    const typedArray = dtype === 'fp64'
      ? new Float64Array(flat)
      : dtype === 'fp32'
        ? new Float32Array(flat)
        : dtype === 'int8' || dtype === 'int4'
          ? new Int8Array(flat)
          : new Float32Array(flat);

    return new Tensor(typedArray, shape, dtype);
  }

  /**
   * Create a tensor of zeros.
   */
  static zeros(shape: number[], dtype: Precision = 'fp32'): Tensor {
    const size = shape.reduce((a, b) => a * b, 1);
    const data = dtype === 'fp64'
      ? new Float64Array(size)
      : dtype === 'fp32'
        ? new Float32Array(size)
        : new Int8Array(size);
    return new Tensor(data, shape, dtype);
  }

  /**
   * Create a tensor of ones.
   */
  static ones(shape: number[], dtype: Precision = 'fp32'): Tensor {
    const size = shape.reduce((a, b) => a * b, 1);
    const data = dtype === 'fp64'
      ? new Float64Array(size).fill(1)
      : dtype === 'fp32'
        ? new Float32Array(size).fill(1)
        : new Int8Array(size).fill(1);
    return new Tensor(data, shape, dtype);
  }

  /**
   * Create a tensor with random values.
   */
  static random(shape: number[], dtype: Precision = 'fp32'): Tensor {
    const size = shape.reduce((a, b) => a * b, 1);
    const data = dtype === 'fp64'
      ? Float64Array.from({ length: size }, () => Math.random())
      : dtype === 'fp32'
        ? Float32Array.from({ length: size }, () => Math.random())
        : Int8Array.from({ length: size }, () => Math.floor(Math.random() * 256) - 128);
    return new Tensor(data, shape, dtype);
  }

  /**
   * Create a tensor with normal distribution.
   */
  static randn(shape: number[], dtype: Precision = 'fp32'): Tensor {
    const size = shape.reduce((a, b) => a * b, 1);
    const values: number[] = [];

    for (let i = 0; i < size; i++) {
      // Box-Muller transform
      const u1 = Math.random();
      const u2 = Math.random();
      values.push(Math.sqrt(-2 * Math.log(u1)) * Math.cos(2 * Math.PI * u2));
    }

    const data = dtype === 'fp64'
      ? new Float64Array(values)
      : dtype === 'fp32'
        ? new Float32Array(values)
        : new Int8Array(values.map(v => Math.max(-128, Math.min(127, Math.round(v * 50)))));

    return new Tensor(data, shape, dtype);
  }

  /**
   * Get tensor size (total elements).
   */
  get size(): number {
    return this.shape.reduce((a, b) => a * b, 1);
  }

  /**
   * Get number of dimensions.
   */
  get ndim(): number {
    return this.shape.length;
  }

  /**
   * Get byte size.
   */
  get byteSize(): number {
    const bytesPerElement = this.dtype === 'fp64' ? 8
      : this.dtype === 'fp32' ? 4
        : this.dtype === 'fp16' || this.dtype === 'bf16' ? 2
          : 1;
    return this.size * bytesPerElement;
  }

  /**
   * Reshape tensor.
   */
  reshape(newShape: number[]): Tensor {
    const newSize = newShape.reduce((a, b) => a * b, 1);
    if (newSize !== this.size) {
      throw new Error(`Cannot reshape tensor of size ${this.size} to shape [${newShape}]`);
    }
    return new Tensor(this.data, newShape, this.dtype);
  }

  /**
   * Convert to different precision.
   */
  to(dtype: Precision): Tensor {
    if (dtype === this.dtype) return this;

    const newData = dtype === 'fp64'
      ? Float64Array.from(this.data)
      : dtype === 'fp32'
        ? Float32Array.from(this.data)
        : Int8Array.from(this.data);

    return new Tensor(newData, this.shape, dtype);
  }

  /**
   * Get value at index.
   */
  get(...indices: number[]): number {
    const idx = this.flatIndex(indices);
    return this.data[idx];
  }

  /**
   * Set value at index.
   */
  set(value: number, ...indices: number[]): void {
    const idx = this.flatIndex(indices);
    this.data[idx] = value;
  }

  /**
   * Convert to nested array.
   */
  toArray(): number[] | number[][] | number[][][] {
    if (this.ndim === 1) {
      return Array.from(this.data);
    }

    const result: any[] = [];
    const innerSize = this.shape.slice(1).reduce((a, b) => a * b, 1);

    for (let i = 0; i < this.shape[0]; i++) {
      const slice = new Tensor(
        this.data.slice(i * innerSize, (i + 1) * innerSize) as Float32Array,
        this.shape.slice(1),
        this.dtype
      );
      result.push(slice.toArray());
    }

    return result;
  }

  /**
   * Serialize tensor for transmission.
   */
  serialize(): { data: string; shape: number[]; dtype: Precision } {
    // Convert to base64 for efficient transmission
    const bytes = new Uint8Array(this.data.buffer);
    const base64 = btoa(String.fromCharCode(...bytes));

    return {
      data: base64,
      shape: this.shape,
      dtype: this.dtype,
    };
  }

  /**
   * Deserialize tensor from transmission format.
   */
  static deserialize(serialized: { data: string; shape: number[]; dtype: Precision }): Tensor {
    const binary = atob(serialized.data);
    const bytes = new Uint8Array(binary.length);
    for (let i = 0; i < binary.length; i++) {
      bytes[i] = binary.charCodeAt(i);
    }

    const data = serialized.dtype === 'fp64'
      ? new Float64Array(bytes.buffer)
      : serialized.dtype === 'fp32'
        ? new Float32Array(bytes.buffer)
        : new Int8Array(bytes.buffer);

    return new Tensor(data, serialized.shape, serialized.dtype);
  }

  /**
   * Calculate flat index from multi-dimensional indices.
   */
  private flatIndex(indices: number[]): number {
    if (indices.length !== this.ndim) {
      throw new Error(`Expected ${this.ndim} indices, got ${indices.length}`);
    }

    let idx = 0;
    let stride = 1;
    for (let i = this.ndim - 1; i >= 0; i--) {
      if (indices[i] < 0 || indices[i] >= this.shape[i]) {
        throw new Error(`Index ${indices[i]} out of bounds for axis ${i} with size ${this.shape[i]}`);
      }
      idx += indices[i] * stride;
      stride *= this.shape[i];
    }
    return idx;
  }

  /**
   * Infer shape from nested array.
   */
  private static inferShape(data: number[] | number[][]): number[] {
    if (!Array.isArray(data)) return [];
    if (!Array.isArray(data[0])) return [data.length];
    return [data.length, ...Tensor.inferShape(data[0] as number[] | number[][])];
  }

  /**
   * Flatten nested array.
   */
  private static flatten(data: number[] | number[][]): number[] {
    if (!Array.isArray(data)) return [data as unknown as number];
    return (data as any[]).flatMap(item =>
      Array.isArray(item) ? Tensor.flatten(item) : item
    );
  }
}
