# frozen_string_literal: true

module SynorCompute
  # Multi-dimensional tensor for compute operations.
  #
  # @example
  #   # Create a 2D tensor
  #   matrix = Tensor.new([2, 3], [1, 2, 3, 4, 5, 6])
  #
  #   # Create random tensor
  #   random = Tensor.rand([512, 512])
  #
  #   # Operations
  #   mean = random.mean
  #   transposed = matrix.transpose
  #
  class Tensor
    attr_reader :shape, :data, :dtype

    def initialize(shape, data, dtype: Precision::FP32)
      expected_size = shape.reduce(1, :*)
      unless data.size == expected_size
        raise ArgumentError, "Data size #{data.size} does not match shape #{shape}"
      end

      @shape = shape.dup.freeze
      @data = data.dup.freeze
      @dtype = dtype
    end

    # @return [Integer] total number of elements
    def size
      @data.size
    end

    # @return [Integer] number of dimensions
    def ndim
      @shape.size
    end

    # Get element at indices
    def [](*indices)
      raise ArgumentError, "Index dimensions must match tensor dimensions" unless indices.size == @shape.size

      idx = 0
      stride = 1
      (@shape.size - 1).downto(0) do |i|
        idx += indices[i] * stride
        stride *= @shape[i]
      end
      @data[idx]
    end

    # Factory methods

    def self.of(data)
      if data.first.is_a?(Array)
        # 2D array
        rows = data.size
        cols = data.first.size
        flat = data.flatten
        new([rows, cols], flat)
      else
        # 1D array
        new([data.size], data)
      end
    end

    def self.zeros(*shape)
      shape = shape.first if shape.size == 1 && shape.first.is_a?(Array)
      size = shape.reduce(1, :*)
      new(shape, Array.new(size, 0.0))
    end

    def self.ones(*shape)
      shape = shape.first if shape.size == 1 && shape.first.is_a?(Array)
      size = shape.reduce(1, :*)
      new(shape, Array.new(size, 1.0))
    end

    def self.rand(*shape)
      shape = shape.first if shape.size == 1 && shape.first.is_a?(Array)
      size = shape.reduce(1, :*)
      new(shape, Array.new(size) { Random.rand })
    end

    def self.randn(*shape)
      shape = shape.first if shape.size == 1 && shape.first.is_a?(Array)
      size = shape.reduce(1, :*)
      new(shape, Array.new(size) do
        # Box-Muller transform
        u1 = Random.rand
        u2 = Random.rand
        Math.sqrt(-2 * Math.log(u1)) * Math.cos(2 * Math::PI * u2)
      end)
    end

    def self.eye(n)
      data = Array.new(n * n, 0.0)
      n.times { |i| data[i * n + i] = 1.0 }
      new([n, n], data)
    end

    def self.arange(start, stop, step = 1.0)
      size = ((stop - start) / step).ceil
      data = Array.new(size) { |i| start + i * step }
      new([size], data)
    end

    def self.linspace(start, stop, num)
      step = (stop - start).to_f / (num - 1)
      data = Array.new(num) { |i| start + i * step }
      new([num], data)
    end

    # Operations

    def reshape(*new_shape)
      new_shape = new_shape.first if new_shape.size == 1 && new_shape.first.is_a?(Array)
      new_size = new_shape.reduce(1, :*)
      raise ArgumentError, "Cannot reshape tensor of size #{size} to #{new_shape}" unless new_size == size

      Tensor.new(new_shape, @data.dup, dtype: @dtype)
    end

    def transpose
      raise "Transpose only supported for 2D tensors" unless ndim == 2

      rows, cols = @shape
      transposed = Array.new(size)
      rows.times do |i|
        cols.times do |j|
          transposed[j * rows + i] = @data[i * cols + j]
        end
      end
      Tensor.new([cols, rows], transposed, dtype: @dtype)
    end

    # Reductions

    def mean
      @data.sum / @data.size.to_f
    end

    def sum
      @data.sum
    end

    def std
      m = mean
      variance = @data.map { |x| (x - m)**2 }.sum / @data.size
      Math.sqrt(variance)
    end

    def max
      @data.max
    end

    def min
      @data.min
    end

    # Activations

    def relu
      Tensor.new(@shape, @data.map { |x| [0, x].max }, dtype: @dtype)
    end

    def sigmoid
      Tensor.new(@shape, @data.map { |x| 1.0 / (1.0 + Math.exp(-x)) }, dtype: @dtype)
    end

    def softmax
      max_val = max
      exp_values = @data.map { |x| Math.exp(x - max_val) }
      sum = exp_values.sum
      Tensor.new(@shape, exp_values.map { |x| x / sum }, dtype: @dtype)
    end

    # Conversion

    def to_nested_array
      case ndim
      when 1
        @data.dup
      when 2
        rows, cols = @shape
        Array.new(rows) { |i| @data[i * cols, cols] }
      else
        raise "to_nested_array only supports 1D and 2D tensors"
      end
    end

    def to_h
      {
        shape: @shape,
        data: @data,
        dtype: @dtype.to_s
      }
    end

    def ==(other)
      return false unless other.is_a?(Tensor)

      @shape == other.shape && @data == other.data && @dtype == other.dtype
    end

    def to_s
      "Tensor(shape=#{@shape}, dtype=#{@dtype})"
    end
  end
end
