/* A list of things. Do automatic iteration of unary and binary operators on
 * us.
 *
 *  List [1, 2] + [2, 3] -> List [3, 5]
 *  hd (List [2, 3]) -> 2
 *  List [] == [] -> true
 *
 * nip4 doesn't allow overloading of ':', annoyingly.
 */
List value = class
    _Object {
    _check_args = [
        [value, "value", check_list]
    ];

    // methods
    oo_binary_table op x = [
        [apply2 op value x',
            op.op_name == "subscript" || op.op_name == "subscript'" ||
            op.op_name == "equal" || op.op_name == "equal'"],
        [this.List (apply2 op value x'),
            op.op_name == "join" || op.op_name == "join'"],
        [this.List (map2 (apply2 op) value x'),
            is_list x'],
        [this.List (map (apply2 op' x) value),
            true]
    ] ++ super.oo_binary_table op x
    {
        op' = oo_converse op;

        // strip the List wrapper, if any
        x'
            = x.value, is_List x
            = x;

        apply2 op x1 x2
            = oo_binary_function op x1 x2, is_class x1
            = oo_binary'_function op x1 x2, is_class x2
            = op.fn x1 x2;
    };

    oo_unary_table op = [
        [apply value,
            op.op_name == "hd" || op.op_name == "tl"],
        [this.List (map apply value),
            true]
    ] ++ super.oo_unary_table op
    {
        apply x
            = oo_unary_function op x, is_class x
            = op.fn x;
    }
}

/* A group of things. Loop the operation over the group.
 */
Group value = class
    _Object {
    _check_args = [
        [value, "value", check_list]
    ];

    // methods
    oo_binary_table op x = [
        // if_then_else is really a trinary operator
        [map_trinary ite this x?0 x?1,
            op.op_name == "if_then_else"],
        [map_binary op.fn this x,
            is_Group x],
        [map_unary (\a op.fn a x) this,
            true]
    ] ++ super.oo_binary_table op x;

    oo_unary_table op = [
        [map_unary op.fn this,
            true]
    ] ++ super.oo_unary_table op;

    // we can't call map_trinary directly, since it uses Group and we
    // don't support mutually recursive top-level functions :-(
    // copy-paste it here, keep in sync with the map_nary in _stdenv
    map_nary fn args
        = fn args, groups == []
        = Group (map process [0, 1 .. shortest - 1])
    {
        groups = filter is_Group args;

        shortest = foldr1 minpair (map (len @ get_value) groups);

        process n
            = NULL, any (map (is_noval n) args)
            = map_nary fn (map (extract n) args)
        {
            extract n arg
                = arg.value?n, is_Group arg
                = arg;

            is_noval n arg = is_Group arg && arg.value?n == NULL;
        }
    }

    // need ite as a true trinary
    ite a b c = if a then b else c;

    map_unary fn a = map_nary (list_1ary fn) [a];
    map_binary fn a b = map_nary (list_2ary fn) [a, b];
    map_trinary fn a b c = map_nary (list_3ary fn) [a, b, c];
}

/* Single real number ... eg slider.
 */
Real value = class
    _Object {
    _check_args = [
        [value, "value", check_real]
    ];

    // methods
    oo_binary_table op x = [
        [this.Real (op.fn this.value x.value),
            is_Real x &&
            op.type == Operator_type.ARITHMETIC],
        [this.Real (op.fn this.value x),
            is_real x &&
            op.type == Operator_type.ARITHMETIC],
        [op.fn this.value x.value,
            is_Real x &&
            op.type == Operator_type.RELATIONAL],
        [op.fn this.value x,
            is_image x &&
            op.name == "join'"],
        [op.fn this.value x,
            !is_class x]
    ] ++ super.oo_binary_table op x;

    oo_unary_table op = [
        [this.Real (op.fn this.value),
            op.type == Operator_type.ARITHMETIC],
        [op.fn this.value,
            true]
    ] ++ super.oo_unary_table op;
}

/* Single bool ... eg Toggle.
 */
Bool value = class
    _Object {
    _check_args = [
        [value, "value", check_bool]
    ];

    // methods
    oo_binary_table op x = [
        [op.fn this.value x,
            op.op_name == "if_then_else"],
        [this.Bool (op.fn this.value x.value),
            is_Bool x],
        [this.Bool (op.fn this.value x),
            is_bool x]
    ] ++ super.oo_binary_table op x;

    oo_unary_table op = [
        [this.Bool (op.fn this.value),
            op.type == Operator_type.ARITHMETIC ||
            op.type == Operator_type.RELATIONAL],
        [op.fn this.value,
            true]
    ] ++ super.oo_unary_table op;
}

/* An editable string.
 */
String caption value = class
    _Object {
    _check_args = [
        [caption, "caption", check_string],
        [value, "value", check_string]
    ];
}

/* An editable real number.
 */
Number caption value = class
    scope.Real value {
    _check_args = [
        [caption, "caption", check_string]
    ];

    Real x = this.Number caption x;
}

/* An editable expression.
 */
Expression caption expr = class
    (if is_class expr then expr else _Object) {
    _check_args = [
        [caption, "caption", check_string],
        [expr, "expr", check_any]
    ];
}

/* An editable filename.
 */
Pathname caption value = class
    _Object {
    _check_args = [
        [caption, "caption", check_string],
        [value, "value", check_string]
    ];
}

/* An editable fontname.
 */
Fontname caption value = class
    _Object {
    _check_args = [
        [caption, "caption", check_string],
        [value, "value", check_string]
    ];
}

/* Vector type ... just a finite list of real. Handy for wrapping an
 * argument to eg. linear. Make it behave like a single pixel image.
 */
Vector value = class
    _Object {
    _check_args = [
        [value, "value", check_real_list]
    ];

    bands = len value;

    // methods
    oo_binary_table op x = [
        // for equality, must be (op Vector Vector)
        [op.fn this.value x.value,
            is_Vector x &&
            (op.op_name == "not_equal" || op.op_name == "not_equal'" ||
             op.op_name == "equal" || op.op_name == "equal'")],

        // Vector ++ Vector means bandwise join
        [this.Vector (op.fn this.value x.value),
            is_Vector x &&
            (op.op_name == "join" || op.op_name == "join'")],
        [this.Vector (op.fn this.value [get_number x]),
            has_number x &&
            (op.op_name == "join" || op.op_name == "join'")],
        // Vector ? number means extract element
        [op.fn this.value (get_real x),
            has_real x &&
            (op.op_name == "subscript" ||
                op.op_name == "subscript'")],
        // extra check for lengths equal
        [this.Vector (map_binaryl op.fn this.value x.value),
            is_Vector x &&
            len value == len x.value &&
            op.type == Operator_type.ARITHMETIC],
        [this.Vector (map_binaryl op.fn this.value (get_real x)),
            has_real x &&
            op.type == Operator_type.ARITHMETIC],

        // need extra length check
        [this.Vector (map bool_to_real
            (map_binaryl op.fn this.value x.value)),
            is_Vector x &&
            len value == len x.value &&
            op.type == Operator_type.RELATIONAL],
        [this.Vector (map bool_to_real
            (map_binaryl op.fn this.value (get_real x))),
            has_real x &&
            op.type == Operator_type.RELATIONAL],
        [this.Vector (op.fn this.value x.value),
            is_Vector x &&
            len value == len x.value &&
            op.type == Operator_type.COMPOUND_REWRAP],
        // (is_Image x) is left for the Image class to handle
        [vec op'.op_name x value,
            is_image x],
        [op.fn this.value x,
            is_real x]
    ] ++ super.oo_binary_table op x
    {
        op' = oo_converse op;
    };

    oo_unary_table op = [
        [this.Vector (map_unaryl op.fn this.value),
            op.type == Operator_type.ARITHMETIC],
        [this.Vector (map bool_to_real
            (map_unaryl op.fn this.value)),
            op.type == Operator_type.RELATIONAL],
        [this.Vector (op.fn this.value),
            op.type == Operator_type.COMPOUND_REWRAP],
        [op.fn this.value,
            true]
    ] ++ super.oo_unary_table op;

    // turn an ip bool (or a number, for Vector) into VIPSs 255/0
    bool_to_real x
        = 255, is_bool x && x
        = 255, is_number x && x != 0
        = 0;
}

/* Base class for enums -- use reflection to get a list of subclass members
 * and make functions to map keys to and from values.
 *
 * Enums map keys <-> values. keys are always $strings, values can be
 * anything. Call them "keys", not "names", since class.name is taken
 * already.
 */
Enum = class {
    // return a list of results for superclasses and this class
    results_for_class fn x
        = inner ++ fn x
    {
        inner
            = results_for_class fn x.super, x.super.name != "Enum"
            = [];
    }

    // get the names of concrete members in a class
    keys_for_class x
        = (remove_std @ remove_fn @ remove_under @ dir) x
    {
        remove_std = filter (not @ member [$this, $name, $super, x.name]);
        remove_fn = filter (not_equal "<function>" @ print @ converse get_member x);
        remove_under = filter (not @ is_prefix "_");
    }

    keys = results_for_class keys_for_class this;
    values = map (converse get_member this) keys;

    has_key key = member keys key;
    get_key value = keys ? (index (equal value) values);

    has_value value = member values value;
    get_value key = get_member key this;

    has_index index = index >= 0 && index < len keys;
    get_index_from_key key = index (equal key) keys;
    get_index_from_value value = index (equal value) values;

    // get a set of key-value pairs for all class descriptions
    descriptions_for_class x
        = x._description, has_member "_description" x
        = [];

    descriptions = results_for_class descriptions_for_class this;

    get_description value
        = get_key value, result == []
        = result?0?1
    {
        result = filter (\x x?0 == value) descriptions;
    }

    // caption defaults to class name, can be overridden in subclasses
    _caption = _ this.name;

    picker default = Option_enum this._caption this default;
}

/* A lookup table.
 */
Table value = class
    _Object {
    _check_args = [
        [value, "value", check_rectangular]
    ];

    /* Extract a column.
     */
    column n = map (extract n) value;

    /* present col x: is there an x in column col
     */
    present col x = member (column col) x;

    /* Look on column from, return matching item in column to.
     */
    lookup from to x
        = value?n?to, n >= 0
        = error (_ "item" ++ " " ++ print x ++ " " ++ _ "not in table")
    {
        n = index (equal x) (column from);
    }
}

/* How to display a matrix: text, sliders, toggles, or text plus scale/offset.
 */
Matrix_display = class Enum {
    TEXT = 0;
    SLIDER = 1;
    TOGGLE = 2;
    TEXT_SCALE_OFFSET = 3;

    _description = [
        [TEXT, _ "Text"],
        [SLIDER, _ "Sliders"],
        [TOGGLE, _ "Toggles"],
        [TEXT_SCALE_OFFSET, _ "Text, scale and offset"]
    ];
}

/* A rectangular array of real.
 */
Matrix_base value = class
    _Object {
    _check_args = [
        [value, "value", check_matrix]
    ];

    // calculate these from value
    width = len value?0;
    height = len value;

    // extract a rectangular area
    extract left top width height
        = this.Matrix_base
            ((map (take width) @ map (drop left) @
                take height @ drop top) value);

    // methods
    oo_binary_table op x = [
        // mat multiply is special
        [this.Matrix_base mul.value,
            is_Matrix x &&
            op.op_name == "multiply"],
        [this.Matrix_base mul'.value,
            is_Matrix x &&
            op.op_name == "multiply'"],

        // mat divide is also special
        [this.Matrix_base div.value,
            is_Matrix x &&
            op.op_name == "divide"],
        [this.Matrix_base div'.value,
            is_Matrix x &&
            op.op_name == "divide'"],

        // power -1 means invert
        [this.Matrix_base inv.value,
            is_real x && x == -1 &&
            op.op_name == "power"],
        [this.Matrix_base sq.value,
            is_real x && x == 2 &&
            op.op_name == "power"],
        [error "matrix **-1 and **2 only",
            op.op_name == "power" ||
            op.op_name == "power'"],

        // matrix op vector ... treat a vector as a 1 row matrix
        [this.Matrix_base (map (map_binaryl op'.fn x.value) this.value),
            is_Vector x &&
            op.type == Operator_type.ARITHMETIC],
        [this.Matrix_base (map_binaryl op.fn this.value x.value),
            (is_Matrix x || is_Real x) &&
            op.type == Operator_type.ARITHMETIC],

        [this.Matrix_base (map_binaryl op.fn this.value x),
            is_real x &&
            op.type == Operator_type.ARITHMETIC],

        // compound ... don't do iteration
        [this.Matrix_base (op.fn this.value x.value),
            (is_Matrix x || is_Real x || is_Vector x) &&
            op.type == Operator_type.COMPOUND_REWRAP],

        // for equality, must be Matrix op Matrix, and only compare value
        [op.fn this.value x.value,
            is_Matrix x &&
            (op.op_name == "not_equal" || op.op_name == "not_equal'" ||
             op.op_name == "equal" || op.op_name == "equal'")],

        [op.fn this.value x,
            op.type == Operator_type.COMPOUND]

    ] ++ super.oo_binary_table op x
    {
        mul = matmul this x;
        mul' = matmul x this;
        div = matmul this (matinv x);
        div' = matmul x (matinv this);
        inv = matinv this;
        sq = matmul this this;
        op' = oo_converse op;

        // we can't use the versions in _stdenv.def since that would be a
        // circular dep
        mat1 fn = vips_matrix_from_image @ fn @ vips_image_from_matrix;
        mat2 fn a b
            = vips_matrix_from_image
                (fn (vips_image_from_matrix a) (vips_image_from_matrix b));
        matmul = mat2 (vips_matrixmultiply []);
        matinv = mat1 (vips_matrixinvert []);
    }

    oo_unary_table op = [
        [this.Matrix_base (map_unaryl op.fn this.value),
            op.type == Operator_type.ARITHMETIC],
        [this.Matrix_base (op.fn this.value),
            op.type == Operator_type.COMPOUND_REWRAP],
        [op.fn this.value,
            true]
    ] ++ super.oo_unary_table op;
}

/* A matrix as VIPS sees them ... add scale, offset and filename. For nip, add
 * a display type as well to control how the widget renders.
 */
Matrix_vips value scale offset filename display = class
    scope.Matrix_base value {
    _check_args = [
        [scale, "scale", check_real],
        [offset, "offset", check_real],
        [filename, "filename", check_string],
        [display, "display", check_matrix_display]
    ];

    Matrix_base x = this.Matrix_vips x scale offset filename display;
}

/* A plain 'ol matrix which can be passed to VIPS.
 */
Matrix value = class
    Matrix_vips value 1 0 "" Matrix_display.TEXT {}

/* Specialised constructors ... for convolutions, recombinations and
 * morphologies.
 */
Matrix_con scale offset value = class
    Matrix_vips value scale offset "" Matrix_display.TEXT_SCALE_OFFSET {};

Matrix_rec value = class
    Matrix_vips value 1 0 "" Matrix_display.SLIDER {};

Matrix_mor value = class
    Matrix_vips value 1 0 "" Matrix_display.TOGGLE {};

Matrix_file filename
    = (vips_matrix_from_image @ vips_image @ expand @ search) filename;

/* A CIE colour ... a triple, plus a format (eg XYZ, Lab etc)
 */
Colour colour_space value = class
    scope.Vector value {
    _check_args = [
        [colour_space, "colour_space", check_colour_space]
    ];
    _check_all = [
        [is_list_len 3 value, "len value == 3"]
    ];

    Vector x = this.Colour colour_space x;

    // make a colour-ish thing from an image
    // back to Colour if we have another 3 band image
    // to a vector if bands > 1
    // to a number otherwise
    itoc im
        = this.Colour colour_space (to_matrix im).value?0,
            bands == 3
        = scope.Vector (map mean (bandsplit im)),
            bands > 1
        = mean im
    {
        interp = get_interpretation im;
        bands = get_bands im;
        colour_space = Interpretation.get_key interp;
    }

    // methods
    oo_binary_table op x = [
        // for (op Colour Image), transform this to a large image
        [op.fn this_image x,
            is_Image x || is_image x],

        // everything else (op Colour thing) becomes a 1x1 pixel image and
        // gets rewrapped
        [itoc (op.fn
            ((float) (to_image this).value)
            ((float) (to_image x).value)),
            // here REWRAP means go via image
            // for eg. ARITHMETIC, such as Colour?real, fall back to Vector
            op.type == Operator_type.COMPOUND_REWRAP]

    ] ++ super.oo_binary_table op x
    {
        // make this into a const image that matches image x in size
        // don't transform the colourspace to match x for consistency with
        // Image
        this_image = image_new_from_image [
            $interpretation => Interpretation.get_value colour_space,
            $format => Format.FLOAT,
            $bands => 3,
            $coding => Coding.NOCODING,
            $pixel => scope.Vector this.value
        ] (get_image x) (get_width x) (get_height x);
    }

    oo_unary_table op = [
        [itoc (op.fn ((float) (to_image this).value)),
            op.type == Operator_type.COMPOUND_REWRAP]
    ] ++ super.oo_unary_table op;
}

// a subclass with widgets for picking a space and value
Colour_picker default_colour default_value = class
    Colour interp.key colour.expr {
    _vislevel = 3;

    interp = Interpretation_3ary.picker default_colour;
    colour = Expression "Value" default_value;

    Colour_edit colour_space value =
        Colour_picker colour_space value;
}

/* Base scale type.
 */
Scale caption from to value = class
    scope.Real value {
    _check_args = [
        [caption, "caption", check_string],
        [from, "from", check_real],
        [to, "to", check_real]
    ];
    _check_all = [
        [from <= to, "from <= to"]
    ];

    Real x = this.Scale caption from to x;

    // methods
    oo_binary_table op x = [
        [this.Scale caption (op.fn this.from x.from) (op.fn this.to x.to)
            (op.fn this.value x.value),
            is_Scale x &&
            op.type == Operator_type.ARITHMETIC],
        [this.Scale caption (op.fn this.from x) (op.fn this.to x)
            (op.fn this.value x),
            is_real x &&
            op.type == Operator_type.ARITHMETIC]
    ] ++ super.oo_binary_table op x;
}

/* Base toggle type.
 */
Toggle caption value = class
    scope.Bool value {
    _check_args = [
        [caption, "caption", check_string],
        [value, "value", check_bool]
    ];

    Bool x = this.Toggle caption x;
}

/* Base option type. value is the index in the labels array
 */
Option caption labels value = class
    scope.Real value {
    _check_args = [
        [caption, "caption", check_string],
        [labels, "labels", check_string_list],
        [value, "value", check_uint]
    ];
}

/* An option whose value is a string rather than a number.
 */
Option_string caption labels item = class
    Option caption labels (index (equal item) labels) {
    Option_edit caption labels value
        = this.Option_string caption labels (labels?value);
}

/* An option from an enum. The default value is a key.
 */
Option_enum caption enum key = class
    Option caption
        (map enum.get_description enum.values)
        (enum.get_index_from_key key) {
    // the value for the current key (Option.value is the index in the string
    // list)
    value_key = enum.get_value key;

    Option_edit caption labels index
        = this.Option_enum caption enum (enum.keys?index);
}

/* A rectangle. width and height can be -ve.
 */
Rect left top width height = class
    _Object {
    _check_args = [
        [left, "left", check_real],
        [top, "top", check_real],
        [width, "width", check_real],
        [height, "height", check_real]
    ];

    // derived
    right = left + width;
    bottom = top + height;

    oo_binary_table op x = [
        [equal x,
            is_Rect x &&
            (op.op_name == "equal" || op.op_name == "equal'")],
        [!equal x,
            is_Rect x &&
            (op.op_name == "not_equal" || op.op_name == "not_equal'")],

        // relational ops are includes_rect
        [includes_rect x,
            is_Rect x &&
            op.type == Operator_type.RELATIONAL &&
            op.op_name == "less_equal'"],
        [includes_rect x && !equal x,
            is_Rect x &&
            op.type == Operator_type.RELATIONAL &&
            op.op_name == "less'"],

        // mul and add are intersect and union
        [union x,
            is_Rect x &&
            op.type == Operator_type.ARITHMETIC &&
            (op.op_name == "add" || op.op_name == "add'")],
        [intersect x,
            is_Rect x &&
            op.type == Operator_type.ARITHMETIC &&
            (op.op_name == "multiply" || op.op_name == "multiply'")],

        // binops with a complex are the same as (comp op comp)
        [oo_binary_function op this (Rect (re x) (im x) 0 0),
            is_complex x],

        // all others are just pairwise
        [this.Rect left' top' width' height',
            is_Rect x &&
            op.type == Operator_type.ARITHMETIC],
        [this.Rect left'' top'' width'' height'',
            has_number x &&
            op.type == Operator_type.ARITHMETIC]
    ] ++ super.oo_binary_table op x
    {
        left' = op.fn left x.left;
        top' = op.fn top x.top;
        width' = op.fn width x.width;
        height' = op.fn height x.height;

        left'' = op.fn left x';
        top'' = op.fn top x';
        width'' = op.fn width x';
        height'' = op.fn height x';
        x' = get_number x;

        equal x = left == x.left && top == x.top &&
              width == x.width && height == x.height;

        // contains a rect? just test top left and bottom right points
        includes_rect r
            = includes_point r.left r.top &&
                includes_point r.right r.bottom;

        // bounding box of two rects
        // if either is empty, can just return the other
        union r
            = r, is_empty
            = this, r.is_empty
            = Rect left' top' width' height'
        {
            left' = minpair left r.left;
            top' = minpair top r.top;
            width' = maxpair right r.right - left';
            height' = maxpair bottom r.bottom - top';
        }

        // intersection of two rects ... empty rect if no intersection
        intersect r
            = Rect left' top' width'' height''
        {
            left' = maxpair left r.left;
            top' = maxpair top r.top;
            width' = minpair right r.right - left';
            height' = minpair bottom r.bottom - top';
            width''
                = width', width > 0
                = 0;
            height''
                = height', height > 0
                = 0;
        }
    }

    oo_unary_table op = [
        // arithmetic uops just map
        [this.Rect left' top' width' height',
            op.type == Operator_type.ARITHMETIC],

        // compound uops are just like ops on complex
        // do (width, height) so thing like abs(Arrow) work as you'd expect
        [op.fn (width, height),
            op.type == Operator_type.COMPOUND]
    ] ++ super.oo_unary_table op
    {
        left' = op.fn left;
        top' = op.fn top;
        width' = op.fn width;
        height' = op.fn height;
    }

    // empty? ie. contains no pixels
    is_empty = width == 0 || height == 0;

    // contains a point?
    includes_point x y
        = left <= x && x <= right && top <= y && y <= bottom;

    // expand/collapse by n pixels
    margin_adjust n
        = this.Rect (left - n) (top - n) (width + 2 * n) (height + 2 * n);
}

/* Values for Coding field in image.
 */
Coding = class Enum {
    NOCODING = 0;
    LABPACK = 2;
    RAD = 6;

    _description = [
        [NOCODING, _ "Uncoded"],
        [LABPACK, _ "Packed Lab"],
        [RAD, _ "Radiance"]
    ];
}

/* Values for format field in image.
 */
Format = class Enum {
    UCHAR = 0;
    CHAR = 1;
    USHORT = 2;
    SHORT = 3;
    UINT = 4;
    INT = 5;
    FLOAT = 6;
    COMPLEX = 7;
    DOUBLE = 8;
    DPCOMPLEX = 9;

    _description = [
        [UCHAR, _ "8-bit unsigned integer"],
        [CHAR, _ "8-bit signed integer"],
        [USHORT, _ "16-bit unsigned integer"],
        [SHORT, _ "16-bit signed integer"],
        [UINT, _ "32-bit unsigned integer"],
        [INT, _ "32-bit signed integer"],
        [FLOAT, _ "Single-precision float"],
        [COMPLEX, _ "Single-precision complex"],
        [DOUBLE, _ "Double-precision float"],
        [DPCOMPLEX, _ "Double-precision complex"]
    ];

    _maxval = (Table [
        [UCHAR, 255],
        [CHAR, 127],
        [USHORT, 65535],
        [SHORT, 32767],
        [UINT, 4294967295],
        [INT, 2147483647],
        [FLOAT, 255],
        [COMPLEX, 255],
        [DOUBLE, 255],
        [DPCOMPLEX, 255]
    ]).lookup 0 1;
}

// interpretations which are 3-ary colorimetric spaces
Interpretation_3ary = class Enum {
    // aim for a logical order
    sRGB = 22;
    scRGB = 28;
    HSV = 29;
    XYZ = 12;
    YXY = 23;
    LAB = 13;
    OKLAB = 30;
    LCH = 19;
    OKLCH = 31;
    CMC = 18;

    _description = [
        [XYZ, _ "XYZ"],
        [LAB, _ "Lab"],
        [CMC, _ "CMC"],
        [LCH, _ "LCh"],
        [sRGB, _ "sRGB"],
        [YXY, _ "Yxy"],
        [scRGB, _ "scRGB"],
        [HSV, _ "HSV"],
        [OKLAB, _ "OKLab"],
        [OKLCH, _ "OKLCh"]
    ];

    // name for pickers
    _caption = _ "Interpretation";
}

// also colourmetric, but some dupes, format hints, 1-aries
Interpretation_nary = class Interpretation_3ary {
    B_W = 1;
    LABQ = 16;
    LABS = 21;
    RGB16 = 25;
    GREY16 = 26;

    _description = [
        [B_W, _ "b-w"],
        [LABQ, _ "LabQ"],
        [LABS, _ "LabS"],
        [RGB16, _ "16-bit RGB"],
        [GREY16, _ "16-bit mono"]
    ];
}

// And all all the non-colourimetric interpretations
Interpretation = class Interpretation_nary {
    MULTIBAND = 0;
    HISTOGRAM = 10;
    CMYK = 15;
    FOURIER = 24;
    MATRIX = 27;

    _description = [
        [MULTIBAND, _ "Multiband"],
        [HISTOGRAM, _ "Histogram"],
        [CMYK, _ "CMYK"],
        [FOURIER, _ "Fourier"],
        [MATRIX, _ "Matrix"]
    ];
}

/* Base image class. Simple layer over vips_image.
 */
Image value = class
    _Object {
    _check_args = [
        [value, "value", check_image]
    ];

    // fields from VIPS header
    width = get_width value;
    height = get_height value;
    bands = get_bands value;
    format = get_format value;
    coding = get_coding value;
    interpretation = get_interpretation value;
    xres = get_xres value;
    yres = get_yres value;
    xoffset = get_xoffset value;
    yoffset = get_yoffset value;
    filename = get_header "filename" value;

    // operator overloading
    // (op Image Vector) done in Vector class
    oo_binary_table op x = [
        // handle (this.op constant), where op only works for
        // (this.op image)
        [wrap twoary_result_image,
            (has_real x ||
             is_real_list x ||
             is_Vector x) &&
            (is_prefix "join" op.op_name ||
             is_prefix "maxpair" op.op_name ||
             is_prefix "minpair" op.op_name)],
        [wrap ite_result_image,
            op.op_name == "if_then_else"],
        [wrap (op.fn this.value (get_image x)),
            has_image x],
        // (op Image Colour) is handled in the colour class, so be careful not
        // to catch colour as a vector subtype
        [wrap (vec op.op_name this.value x.value),
            is_Vector x && !is_Colour x],
        [wrap (op.fn this.value (get_number x)),
            has_number x],
        // if it's not a class on the RHS, handle here ... just apply and
        // rewrap
        [wrap (op.fn this.value x),
            !is_class x]
        // all other cases handled by other classes
    ] ++ super.oo_binary_table op x
    {
        // wrap the result with this
        // x can be a non-image, eg. compare "Image v == []" vs.
        // "Image v == 12"
        wrap x
            = x, op.type == Operator_type.COMPOUND ||
                !is_image x
            = this.Image x;

        // things like (Image ++ constant) or (maxpair Image constant)
        // where constants are real or realvec
        twoary_result_image
            = op.fn this.value new_stuff
        {
            new_stuff = image_new [
                $bands => new_bands,
                $format => format,
                $coding => coding,
                $pixel => x,
                $xoffset => xoffset,
                $yoffset => yoffset
            ] width height;

            new_bands
                = get_bands x, has_bands x
                = 1;
        }

        [then_part, else_part] = x;

        // get things about our output from inputs in this order
        objects = [then_part, else_part, this];

        // properties of our output image
        target_bands = get_member_list has_bands get_bands objects;
        target_interpretation = get_member_list
            has_interpretation get_interpretation objects;

        // if one of then/else is an image, get the target format from that
        // otherwise, let the non-image objects set the target
        target_format
            = get_member_list has_format get_format x,
                has_member_list has_format x
            = NULL;

        to_image_target x
            = to_image_size width height target_bands target_format x;

        [then', else'] = map to_image_target x;

        ite_result_image = copy [$interpretation => target_interpretation]
            (if value then then' else else');
    }

    // FIXME ... yuk ... don't use operator hints, just always rewrap if
    // we have an image result
    // forced on us by things like abs:
    //  abs Vector -> real
    //  abs Image -> Image
    // does not fit well with COMPOUND/whatever scheme
    oo_unary_table op = [
        [this.Image result,
            is_image result],
        [result,
            true]
    ] ++ super.oo_unary_table op
    {
        result = op.fn this.value;
    }
}

/* Construct an image from a file.
 */
Image_file filename = class
    Image value {
    _check_args = [
        [filename, "filename", check_string]
    ];

    value = vips_image filename;
}

Region image left top width height = class
    Image value {
    _check_args = [
        [image, "Image", check_Image],
        [left, "left", check_real],
        [top, "top", check_real],
        [width, "width", check_preal],
        [height, "height", check_preal]
    ];

    // we need to always succeed ... value is our enclosing image if we're
    // out of bounds
    value
        = crop left top width height image.value,
            region_rect <= image_rect
        = image.value
    {
        image_rect = Rect 0 0 image.width image.height;
        region_rect = Rect left top width height;
    }
}

Area image left top width height = class
    scope.Region image left top width height {
    Region image left top width height
        = this.Area image left top width height;
}

Arrow image left top width height = class
    scope.Rect left top width height {
    _check_args = [
        [image, "Image", check_Image],
        [left, "left", check_real],
        [top, "top", check_real],
        [width, "width", check_real],
        [height, "height", check_real]
    ];

    Rect l t w h = this.Arrow image l t w h;
}

HGuide image top = class
    scope.Arrow image 0 top image.width 0 {
    Arrow image left top width height = this.HGuide image top;
}

VGuide image left = class
    scope.Arrow image left 0 0 image.height {
    Arrow image left top width height = this.VGuide image left;
}

Mark image left top = class
    scope.Arrow image left top 0 0 {
    Arrow image left top width height = this.Mark image left top;
}

// convenience functions: ... specify position as [0 .. 1)

Region_relative image u v w h
    = Region image
        (image.width * u)
        (image.height * v)
        (image.width * w)
        (image.height * h);

Area_relative image u v w h
    = Area image
        (image.width * u)
        (image.height * v)
        (image.width * w)
        (image.height * h);

Arrow_relative image u v w h
    = Arrow image
        (image.width * u)
        (image.height * v)
        (image.width * w)
        (image.height * h);

VGuide_relative image v
    = VGuide image (image.height * v);

HGuide_relative image u
    = HGuide image (image.width * u);

Mark_relative image u v
    = Mark image
        (image.width * u)
        (image.height * v);

Kernel = class Enum {
    NEAREST_NEIGHBOUR = 0;
    LINEAR = 1;
    CUBIC = 2;
    MITCHELL = 3;
    LANCZOS2 = 4;
    LANCZOS3 = 5;
    MKS2013 = 6;
    MKS2021 = 7;

    _description = [
        [NEAREST_NEIGHBOUR, _ "Nearest neighbour"],
        [LINEAR, _ "Linear"],
        [CUBIC, _ "Cubic"],
        [MITCHELL, _ "Mitchell"],
        [LANCZOS2, _ "Lanczos, two lobes"],
        [LANCZOS3, _ "Lanczos, three lobes"],
        [MKS2013, _ "Magic, 2013"],
        [MKS2021, _ "Magic, 2021"]
    ];
}

Interpolate = class Enum {
    NEAREST_NEIGHBOUR = 0;
    BILINEAR = 1;
    BICUBIC = 2;
    LBB = 3;
    NOHALO = 4;
    VSQBS = 5;

    _description = [
        [NEAREST_NEIGHBOUR, _ "Nearest neighbour"],
        [BILINEAR, _ "Bilinear"],
        [BICUBIC, _ "Bicubic"],
        [LBB, _ "Upsize: reduced halo bicubic (LBB)"],
        [NOHALO,
            _ "Upsharp: reduced halo bicubic with edge sharpening (Nohalo)"],
        [VSQBS, _ "Upsmooth: quadratic B-splines with jaggy reduction (VSQBS)"]
    ];

    /* Map to vips type names.
     */
    _to_vips = [
        [NEAREST_NEIGHBOUR, $VipsInterpolateNearest],
        [BILINEAR, $VipsInterpolateBilinear],
        [BICUBIC, $VipsInterpolateBicubic],
        [LBB, $VipsInterpolateLbb],
        [NOHALO, $VipsInterpolateNohalo],
        [VSQBS, $VipsInterpolateVsqbs]
    ];

    _new options type = vips_object_new Interpolate._to_vips?type?1 [] options;
}

Intent = class Enum {
    PERCEPTUAL = 0;
    RELATIVE = 1;
    SATURATION = 2;
    ABSOLUTE = 3;
    AUTO = 4;

    _description = [
        [PERCEPTUAL, _ "Perceptual"],
        [RELATIVE, _ "Relative"],
        [SATURATION, _ "Saturation"],
        [ABSOLUTE, _ "Absolute"],
        [AUTO, _ "Auto"]
    ];
}

Precision = class Enum {
    INTEGER = 0;
    FLOAT = 1;
    APPROX = 2;

    _description = [
        [INTEGER, _ "Integer"],
        [FLOAT, _ "Float"],
        [APPROX, _ "Approximate"]
    ];
}

Align = class Enum {
    LOW = 0;
    CENTRE = 1;
    HIGH = 2;

    _description = [
        [LOW, _ "Low"],
        [CENTRE, _ "Centre"],
        [HIGH, _ "High"]
    ];
}

Blend = class Enum {
    CLEAR = 0;
    SOURCE = 1;
    OVER = 2;
    IN = 3;
    OUT = 4;
    ATOP = 5;
    DEST = 6;
    DEST_OVER = 7;
    DEST_IN = 8;
    DEST_OUT = 9;
    DEST_ATOP = 10;
    XOR = 11;
    ADD = 12;
    SATURATE = 13;
    MULTIPLY = 14;
    SCREEN = 15;
    OVERLAY = 16;
    DARKEN = 17;
    LIGHTEN = 18;
    COLOUR_DODGE = 19;
    COLOUR_BURN = 20;
    HARD_LIGHT = 21;
    SOFT_LIGHT = 22;
    DIFFERENCE = 23;
    EXCLUSION = 24;
    HUE = 25;
    SATURATION = 26;
    COLOUR = 27;
    LUMINOSITY = 28;

    _description = [
        [CLEAR,  "Clear"],
        [SOURCE,  "Source"],
        [OVER,  "Over"],
        [IN,  "In"],
        [OUT,  "Out"],
        [ATOP,  "Atop"],
        [DEST,  "Dest"],
        [DEST_OVER,  "Dest over"],
        [DEST_IN,  "Dest in"],
        [DEST_OUT,  "Dest out"],
        [DEST_ATOP,  "Dest atop"],
        [XOR,  "XOR"],
        [ADD,  "Add"],
        [SATURATE,  "Saturate"],
        [MULTIPLY,  "Multiply"],
        [SCREEN,  "Screen"],
        [OVERLAY,  "Overlay"],
        [DARKEN,  "Darken"],
        [LIGHTEN,  "Lighten"],
        [COLOUR_DODGE,  "Colour dodge"],
        [COLOUR_BURN,  "Colour burn"],
        [HARD_LIGHT,  "Hard light"],
        [SOFT_LIGHT,  "Soft light"],
        [DIFFERENCE,  "Difference"],
        [EXCLUSION,  "Exclusion"],
        [HUE,  "Hue"],
        [SATURATION,  "Saturation"],
        [COLOUR,  "Colour"],
        [LUMINOSITY,  "Luminosity"]
    ];
}

// abstract base class for toolkit menus
Menu = class {}

// a "----" line in a menu
Menuseparator = class Menu {}

// abstract base class for items in menus
Menuitem label tooltip = class Menu {}

Menupullright label tooltip = class Menuitem label tooltip {}

Menuaction label tooltip = class Menuitem label tooltip {}

/* Plots.
 */

Plot_style = class Enum {
    POINT = 0;
    LINE = 1;
    SPLINE = 2;
    BAR = 3;

    _description = [
        [POINT, _ "Point"],
        [LINE, _ "Line"],
        [SPLINE, _ "Spline"],
        [BAR, _ "Bar"]
    ];
}

Plot_format = class Enum {
    YYYY = 0;
    XYYY = 1;
    XYXY = 2;

    _description = [
        [YYYY, _ "YYYY"],
        [XYYY, _ "XYYY"],
        [XYXY, _ "XYXY"]
    ];
}

Plot options value = class
    scope.Image value {
    Image value = this.Plot options value;
    to_image dpi = extract_bands 0 3 (graph_export_image (to_real dpi) this);
}

Plot_matrix options value = class
    Plot options (to_image value).value {
}

Plot_histogram value = class
    scope.Plot [] value {
}

Plot_xy value = class
    scope.Plot [$format => Plot_format.XYYY] value {
}

/* A no-value type. Call it NULL for C-alike fun. Used by Group to indicate
 * empty slots, for example.
 */
NULL = class
    _Object {
    oo_binary_table op x = [
        // the only operation we allow is equality .. use pointer equality,
        // this lets us test a == NULL and a != NULL
        [this === x,
            op.type == Operator_type.RELATIONAL &&
            op.op_name == "equal"],
        [this !== x,
            op.type == Operator_type.RELATIONAL &&
            op.op_name == "not_equal"]
    ] ++ super.oo_binary_table op x;
}

Combine = class Enum {
    MAX = 0;
    SUM = 1;
    MIN = 2;

    _description = [
        [MAX, _ "Maximum"],
        [SUM, _ "Sum"],
        [MIN, _ "Minimum"]
    ];
}

Direction = class Enum {
    HORIZONTAL = 0;
    VERTICAL = 1;

    _description = [
        [HORIZONTAL, _ "Horizontal"],
        [VERTICAL, _ "Vertical"]
    ];
}

Extend = class Enum {
    BLACK = 0;
    COPY = 1;
    REPEAT = 2;
    MIRROR = 3;
    WHITE = 4;
    BACKGROUND = 5;

    _description = [
        [BLACK, _ "Black"],
        [COPY, _ "Copy"],
        [REPEAT, _ "Repeat"],
        [MIRROR, _ "Mirror"],
        [WHITE, _ "White"],
        [BACKGROUND, _ "Background"]
    ];
}

PCS = class Enum {
    LAB = 0;
    XYZ = 1;

    _description = [
        [LAB, _ "Lab"],
        [XYZ, _ "XYZ"]
    ];
}
