import { assert } from "../../core/assert.js";
import { isTypedArray } from "../../core/collection/array/typed/isTypedArray.js";
import { noop } from "../../core/function/noop.js";
import { passThrough } from "../../core/function/passThrough.js";
import Vector1 from "../../core/geom/Vector1.js";
import BoundedValue from "../../core/model/BoundedValue.js";
import ObservedBoolean from "../../core/model/ObservedBoolean.js";
import ObservedInteger from "../../core/model/ObservedInteger.js";
import ObservedString from "../../core/model/ObservedString.js";
import ObservedValue from "../../core/model/ObservedValue.js";
import Stat from "../../core/model/stat/Stat.js";
import { number_pretty_print } from "../../core/primitives/numbers/number_pretty_print.js";
import { isInstanceOf } from "../../core/process/matcher/isInstanceOf.js";
import { isTypeOf } from "../../core/process/matcher/isTypeOf.js";
import { or } from "../../core/process/matcher/or.js";
import { frameThrottle } from "../../engine/graphics/FrameThrottle.js";
import View from "../View.js";


/**
 *
 * @param {Number|String|Boolean} value
 * @returns {*}
 */
function format(value) {
    if (typeof value === 'number') {
        return number_pretty_print(value);
    } else {
        return value;
    }
}

/**
 *
 * @param {string} v
 * @returns {string|number}
 */
function formatNumber(v) {
    return number_pretty_print(v);
}

function formatArray(arr) {
    return format(arr[0]) + " / " + format(arr[1]);
}

function extractorGetValue(m) {
    return m.getValue();
}

function extractFunction(f) {
    return f();
}

/**
 *
 * @param {BoundedValue} m
 */
function extractBoundedValue(m) {
    return [m.getValue(), m.getUpperLimit()];
}

function arrayUnwrap(elements) {
    return elements.map(function (element) {
        const processor = findProcessor(element);
        const extractor = processor.extractor;
        return extractor(element);
    });
}

/**
 *
 * @param model
 * @returns {ValueProcessor | undefined}
 */
function findProcessor(model) {
    return processors.find(function (p) {
        return p.matcher(model);
    });
}


class ValueProcessor {
    /**
     * @template Container, Value
     * @param {function(Container):boolean} matcher
     * @param {function(Container):Value} extractor
     * @param {function(Value):string} formatter
     * @constructor
     */
    constructor(matcher, extractor, formatter) {
        /**
         *
         * @type {function(*): boolean}
         */
        this.matcher = matcher;
        /**
         *
         * @type {function(*): *}
         */
        this.extractor = extractor;
        /**
         *
         * @type {function(*): string}
         */
        this.formatter = formatter;
    }
}

/**
 *
 * @param {function(*):boolean} m
 * @param {function(*):*} e
 * @param {function(*):string} f
 * @returns {ValueProcessor}
 */
function p(m, e, f) {
    return new ValueProcessor(m, e, f);
}

/**
 *
 * @type {Array.<ValueProcessor>}
 */
const processors = [
    p(isInstanceOf(ObservedBoolean), extractorGetValue, format),
    p(isInstanceOf(ObservedValue), extractorGetValue, format),
    p(isInstanceOf(ObservedString), extractorGetValue, format),
    p(isInstanceOf(BoundedValue), extractBoundedValue, formatArray),
    p(isInstanceOf(Stat), extractorGetValue, formatNumber),
    p(isInstanceOf(Vector1), extractorGetValue, formatNumber),
    p(isInstanceOf(ObservedInteger), extractorGetValue, formatNumber),
    p(or(isTypedArray, Array.isArray), arrayUnwrap, formatArray),
    p(isTypeOf("number"), passThrough, formatNumber),
    p(isTypeOf("string"), passThrough, passThrough),
    p(isTypeOf("boolean"), passThrough, passThrough),
    p(isTypeOf("function"), extractFunction, format),
    p(isTypeOf("undefined"), passThrough, passThrough)
];

class LabelView extends View {
    constructor(model, {
        classList = [],
        transform = passThrough,
        format = noop,
        tag = 'div',
        size,
        css
    } = {}) {
        super();

        this.model = model;

        const processor = findProcessor(model);
        const extractor = processor.extractor;
        const formatter = format !== noop ? format : processor.formatter;

        assert.notEqual(extractor, null, `No extractor was found for ${typeof model}(${model})`);
        assert.notEqual(formatter, null, `No formatter was found for ${typeof model}(${model})`);

        this.__extractor = extractor;
        this.__formatter = formatter;
        this.__transform = transform;

        this.el = document.createElement(tag);
        this.el.classList.add('label');

        for (const c of classList) {
            this.el.classList.add(c);
        }

        this.size.onChanged.add(this.updateSize, this);

        if (typeof this.model === "object" && this.model.onChanged !== undefined) {

            const throttledUpdate = frameThrottle(this.updateTransform, this);

            this.bindSignal(this.model.onChanged, throttledUpdate);

        }

        if (css !== undefined) {
            this.css(css);
        }

        if (size !== undefined) {
            this.size.fromJSON(size);
        }
    }

    /**
     * @private
     * @param {number} x
     * @param {number} y
     */
    updateSize(x, y) {
        this.el.style.lineHeight = y + "px";
    }


    /**
     *
     * @param {string} v
     */
    updateText(v) {
        this.el.textContent = v;
    }

    updateTransform() {

        const model = this.model;

        const data = this.__extractor(model);

        const transformed = this.__transform(data);

        const text = this.__formatter(transformed);

        this.updateText(text);

    }

    link() {
        super.link();

        this.updateTransform();
    }
}


export default LabelView;
