import Math;

namespace Nori {

    // TODO: convert the property set/get/clear and related juggling into Pastel code.

    const Element_Id = 'ID';

    const Element_Width = 'el.width';
    const Element_Height = 'el.height';
    const Element_Dock = 'el.dock';
    const Element_HAlign = 'el.halign';
    const Element_VAlign = 'el.valign';
    const Element_MarginLeft = 'el.leftmargin';
    const Element_MarginRight = 'el.rightmargin';
    const Element_MarginTop = 'el.topmargin';
    const Element_MarginBottom = 'el.bottommargin';
    const Element_BackgroundColor = 'el.bgcolor';
    const Element_OnLoad = 'el.onload';

    enum HorizontalAlignment {
        STRETCH = 0,
        LEFT = 1,
        CENTER = 2,
        RIGHT = 3,
    }

    enum VerticalAlignment {
        STRETCH = 0,
        TOP = 1,
        CENTER = 2,
        BOTTOM = 3,
    }

    class Element {

        static field ID_ALLOC = 0;

        static field STRING_PROPERTIES = {
            Button_Text: true,
            TextBlock_Text: true,
            Border_LeftColor: true,
            Border_TopColor: true,
            Border_RightColor: true,
            Border_BottomColor: true,
            Element_BackgroundColor: true,
            Image_Source: true,
            InputElement_Value: true,
        };

        field _id;
        field _type;

        field _userDefinedId = null;
        field _parent = null;
        field _frame = null;
        field _isPanel = false;
        field _isClean = false;

        field _properties = {};
        field _newProperties = {};

        field _handlers = {};
        field _colors = {};

        constructor(type) {
            this._id = ++Element.ID_ALLOC;
            this._type = type;
        }

        function setId(id) {
            this._userDefinedId = id;
            return this;
        }

        function getId() {
            return this._userDefinedId;
        }

        function _setEnumProperty(id, value, eMin, eMax) {
            value = Math.floor(value);
            if (value < eMin || value > eMax) throw new InvalidArgumentException();
            this._newProperties[id] = value;
            if (this._frame != null && this._isClean) this._markPropertiesDirty();
            return this;
        }
        function _setStringProperty(id, value) {
            this._newProperties[id] = value == null ? '' : ('' + value);
            if (this._frame != null && this._isClean) this._markPropertiesDirty();
            return this;
        }
        function _setIntProperty(id, value) {
            this._newProperties[id] = Math.floor(value);
            if (this._frame != null && this._isClean) this._markPropertiesDirty();
            return this;
        }
        function _setBoolProperty(id, value) {
            return this._setIntProperty(id, value ? 1 : 0);
        }
        function _setColorProperty(id, r, g, b, a) {
            this._colors[id] = [r, g, b, a];
            return this._setStringProperty(id, NoriUtil.rgbToHex(r, g, b));
        }

        function _markPropertiesDirty() {
            if (!this._frame._fullFlushElements.contains(this._id)) {
                this._frame._partialFlushElements[this._id] = this;
            }
            this._isClean = false;
        }

        // properties are either ints or strings. The new properties lookup may contain
        // null values to indicate the property was removed, therefore null cannot be
        // used as a dictionary miss sentinel value. For that purpose, the boolean false
        // is used instead.
        function _getIntProperty(id) {
            np = this._newProperties.get(id, false);
            if (np == false) return this._properties.get(id, 0);
            return np ?? 0;
        }
        function _getNullableIntProperty(id) {
            np = this._newProperties.get(id, false);
            if (np == false) return this._properties.get(id);
            return np;
        }
        function _getBoolProperty(id) {
            np = this._newProperties.get(id, false);
            if (np == false) return this._properties.get(id) == 1;
            return np == 1;
        }
        function _getStringProperty(id) {
            np = this._newProperties.get(id, false);
            if (np == false) return this._properties.get(id, '');
            return np ?? '';
        }
        function _getColorProperty(id) {
            return this._colors.get(id);
        }

        function _clearProperty(id) {
            this._newProperties[id] = null;
            this._colors[id] = null; // TODO: something more clever than this.
            if (this._frame != null) this._frame._partialFlushElements[this._id] = this;
            return this;
        }

        function _setEnumWithString(fieldName, value) {
            num = NoriUtil.enumLookup[fieldName].get(value.lower());
            if (num == null) throw new InvalidArgumentException("Invalid enum value: " + value);
            return this._setIntProperty(fieldName, num);
        }

        function _setDirectionalIntWithString(left, top, right, bottom, strValue) {
            if (strValue.contains(',')) {
                dirs = strValue.split(',');
                for (i = 0; i < 4; ++i) dirs[i] = parseInt(dirs[i]);
            } else {
                dirs = [parseInt(strValue)] * 4;
            }
            this._setIntProperty(left, dirs[0]);
            this._setIntProperty(top, dirs[1]);
            this._setIntProperty(right, dirs[2]);
            return this._setIntProperty(bottom, dirs[3]);
        }

        function _setColorWithString(fieldName, strValue) {
            c = NoriUtil._parseColor(strValue);
            return this._setColorProperty(fieldName, c[0], c[1], c[2], c[3]);
        }

        function _setBooleanAsString(fieldName, strValue) {
            return this._setBoolProperty(fieldName, NoriUtil.parseBoolean(strValue));
        }

        function _setPixelsOrPercentAsString(fieldName, strValue) {
            isPercent = false;
            multiplier = 1;
            if (strValue[-1:] == '%') {
                isPercent = true;
                num = parseInt(strValue[:-1]);
                multiplier = -1000;
            } else {
                num = parseInt(strValue);
            }
            if (num == null || num < 0) throw new InvalidArgumentException("Invalid pixel or percent value: " + strValue);
            return this._setIntProperty(fieldName, num * multiplier);
        }

        function setAttributeWithString(name, strValue) {
            switch (name) {
                case 'id': this._userDefinedId = strValue; return this;
                case 'width': return this._setPixelsOrPercentAsString(Element_Width, strValue);
                case 'height': return this._setPixelsOrPercentAsString(Element_Height, strValue);
                case 'horizontalAlignment': return this._setEnumWithString(Element_HAlign, strValue);
                case 'verticalAlignment': return this._setEnumWithString(Element_VAlign, strValue);
                case 'dock': return this._setEnumWithString(Element_Dock, strValue);
                case 'margin': return this._setDirectionalIntWithString(Element_MarginLeft, Element_MarginTop, Element_MarginRight, Element_MarginBottom, strValue);
                case 'marginLeft': return this.setMarginLeft(parseInt(strValue));
                case 'marginTop': return this.setMarginTop(parseInt(strValue));
                case 'marginRight': return this.setMarginRight(parseInt(strValue));
                case 'marginBottom': return this.setMarginBottom(parseInt(strValue));
                case 'backgroundColor': return this._setColorWithString(Element_BackgroundColor, strValue);
                default: throw new InvalidArgumentException("Unknown Element attribute: '" + name + "'");
            }
        }

        function getWidth() { return this._getNullableIntProperty(Element_Width); }
        function getHeight() { return this._getNullableIntProperty(Element_Height); }

        function setWidth(value) {
            if (value < 0) throw new InvalidArgumentException("Width cannot be negative");
            return this._setIntProperty(Element_Width, value);
        }
        function setHeight(value) {
            if (value < 0) throw new InvalidArgumentException("Height cannot be negative");
            return this._setIntProperty(Element_Height, value);
        }

        // Because width and height must always be positive and 0 pixels and 0 percent are the same,
        // store the percent values as negative numbers. Also multiply by 1000 to allow for floats to
        // be transmitted as integers.
        function setWidthPercent(value) {
            if (value < 0) this.setWidth(-1); // for error
            return this._setIntProperty(Element_Width, -1000 * value);
        }
        function setHeightPercent(value) {
            if (value < 0) this.setHeight(-1); // for error
            return this._setIntProperty(Element_Height, -1000 * value);
        }

        function clearWidth() { return this._clearProperty(Element_Width); }
        function clearHeight() { return this._clearProperty(Element_Height); }

        function getHorizontalAlignment() { return this._getIntProperty(Element_HAlign); }
        function getVerticalAlignment() { return this._getIntProperty(Element_VAlign); }
        function setHorizontalAlignment(value) { return this._setEnumProperty(Element_HAlign, value, 0, HorizontalAlignment.length - 1); }
        function setVerticalAlignment(value) { return this._setEnumProperty(Element_VAlign, value, 0, VerticalAlignment.length - 1); }

        function getDock() { return this._getIntProperty(Element_Dock); }
        function setDock(value) { return this._setEnumProperty(Element_Dock, value, 0, 3); }

        function setMargin(value) {
            value = Math.floor(value);
            this._newProperties[Element_MarginLeft] = value;
            this._newProperties[Element_MarginTop] = value;
            this._newProperties[Element_MarginRight] = value;
            this._setIntProperty(Element_MarginBottom, value); // to set flushing. others are inline for speed.
            return this;
        }

        function getMarginLeft() { return this._getIntProperty(Element_MarginLeft); }
        function getMarginTop() { return this._getIntProperty(Element_MarginTop); }
        function getMarginRight() { return this._getIntProperty(Element_MarginRight); }
        function getMarginBottom() { return this._getIntProperty(Element_MarginBottom); }
        function setMarginLeft(value) { return this._setIntProperty(Element_MarginLeft, value); }
        function setMarginTop(value) { return this._setIntProperty(Element_MarginTop, value); }
        function setMarginRight(value) { return this._setIntProperty(Element_MarginRight, value); }
        function setMarginBottom(value) { return this._setIntProperty(Element_MarginBottom, value); }

        function getBackgroundColor() { return this._getColorProperty(Element_BackgroundColor); }
        function setBackgroundColor(r, g, b, a = 255) { return this._setColorProperty(Element_BackgroundColor, r, g, b, a); }

        function _setEventHandler(name, fn, parser = null, isVmOnly = false) {
            this._handlers[name] = [fn, parser];
            if (isVmOnly) return this;
            return this._setBoolProperty(name, true);
        }

        function _clearEventHandler(name, isVmOnly = false) {
            this._handlers[name] = null;
            if (isVmOnly) return this;
            return this._setBoolProperty(name, false);
        }

        function _flushVisualProperties(q, full) {
            hasImageSource = false;
            if (full) {
                if (this._newProperties.length > 0) this._flattenProperties();
                q.add('PF', this._id, this._type, this._properties.length);
                // the iteration order for keys and values are the same, guaranteed by the VM regardless of platform.
                keys = this._properties.keys();
                q.concat(keys);
                for (key : keys) {
                    value = this._properties[key];
                    if (Element.STRING_PROPERTIES.get(key, false)) {
                        value = Frame.escapeStringToHex(value);
                        if (key == Image_Source) {
                            hasImageSource = true;
                        }
                    }
                    q.add(value);
                }
            } else {
                keys = this._newProperties.keys();
                deletions = [];
                setValues = [];
                setKeys = [];
                for (k : keys) {
                    v = this._newProperties[k];
                    if (v == null) {
                        if (this._properties.get(k) != null) {
                            this._properties.remove(k);
                            deletions.add(k);
                        }
                    } else {
                        this._properties[k] = v;
                        setKeys.add(k);
                        if (Element.STRING_PROPERTIES.get(k, false)) {
                            v = Frame.escapeStringToHex(v);
                            if (k == Image_Source) {
                                hasImageSource = true;
                            }
                        }
                        setValues.add(v);
                    }
                }
                q.add('PI', this._id, deletions.length);
                q.concat(deletions);
                q.add(setKeys.length);
                q.concat(setKeys);
                q.concat(setValues);
            }
            this._isClean = true;

            if (hasImageSource) {
                this._frame._newImageSources.add(this);
            }
        }

        function _flattenProperties() {
            for (k : this._newProperties.keys()) {
                v = this._newProperties[k];
                if (v == null) {
                    if (this._properties.get(k) != null) {
                        this._properties.remove(k);
                    }
                } else {
                    this._properties[k] = v;
                }
            }
            this._newProperties.clear();
        }

        // load handler takes no arguments. Unlike other handlers, it is invoked conditionally when
        // there's a render pass notification from Nori from Frame.cry in _maybeFireOnLoadHandlers().
        function setLoadHandler(fn) { return this._setEventHandler(Element_OnLoad, fn, null, true); }
        function clearLoadHandler() { return this._clearEventHandler(Element_OnLoad, true); }

        function findElement(id) { return this._userDefinedId == id ? this : null; }
    }
}
