# Serializable scalar-field expressions; recognizable terrain emerges from parameters.
from dataclasses import dataclass
import math


@dataclass(frozen=True)
class Field:
    op: str
    args: tuple

    def __call__(self, x, z):
        a = self.args
        if self.op == 'constant':
            return a[0]
        if self.op == 'gradient':
            return a[0] * x + a[1] * z + a[2]
        if self.op == 'wave':
            return a[0] * math.sin(x * a[1] + a[3]) * math.cos(z * a[2] + a[3])
        if self.op == 'radial':
            return a[3] * max(0, 1 - math.hypot(x - a[0], z - a[1]) / a[2])
        if self.op == 'add':
            return a[0](x, z) + a[1](x, z)
        if self.op == 'multiply':
            return a[0](x, z) * a[1](x, z)
        if self.op == 'blend':
            mask = min(1, max(0, a[2](x, z)))
            return (1 - mask) * a[0](x, z) + mask * a[1](x, z)
        if self.op == 'transform':
            return a[0]((x - a[1]) / a[3], (z - a[2]) / a[3])
        if self.op == 'clamp':
            return max(a[1], min(a[2], a[0](x, z)))
        raise ValueError(self.op)

    def __add__(self, other):
        return Field('add', (self, as_field(other)))

    def __mul__(self, other):
        return Field('multiply', (self, as_field(other)))

    def blend(self, other, mask):
        return Field('blend', (self, as_field(other), as_field(mask)))

    def transformed(self, dx=0, dz=0, scale=1):
        if scale <= 0:
            raise ValueError('Scale must be positive')
        return Field('transform', (self, dx, dz, scale))

    def clamp(self, low, high):
        return Field('clamp', (self, low, high))

    def expression(self):
        return {'op': self.op, 'args': [a.expression() if isinstance(a, Field) else a for a in self.args]}


def as_field(value):
    return value if isinstance(value, Field) else constant(value)


def constant(value=0):
    return Field('constant', (value,))


def gradient(x=0, z=0, base=0):
    return Field('gradient', (x, z, base))


def wave(amplitude=1, frequency_x=0.1, frequency_z=0.1, phase=0):
    return Field('wave', (amplitude, frequency_x, frequency_z, phase))


def radial(x, z, radius, amplitude):
    if radius <= 0:
        raise ValueError('Radius must be positive')
    return Field('radial', (x, z, radius, amplitude))


def from_expression(expression):
    """Restore only declared field operations, never execute a serialized program."""
    return Field(expression['op'], tuple(from_expression(a) if isinstance(a, dict) and 'op' in a else a for a in expression['args']))
