/**
 * DevExtreme (esm/viz/series/points/symbol_point.js)
 * Version: 24.1.7-build-24272-1937
 * Build date: Sat Sep 28 2024
 *
 * Copyright (c) 2012 - 2024 Developer Express Inc. ALL RIGHTS RESERVED
 * Read about DevExtreme licensing here: https://js.devexpress.com/Licensing/
 */
import {
    extend
} from "../../../core/utils/extend";
import {
    each
} from "../../../core/utils/iterator";
import {
    noop
} from "../../../core/utils/common";
import {
    getWindow,
    hasProperty
} from "../../../core/utils/window";
const window = getWindow();
import {
    Label
} from "./label";
const _extend = extend;
import {
    isDefined as _isDefined
} from "../../../core/utils/type";
import {
    normalizeEnum as _normalizeEnum
} from "../../core/utils";
const _math = Math;
const _round = _math.round;
const _floor = _math.floor;
const _ceil = _math.ceil;
const DEFAULT_IMAGE_WIDTH = 20;
const DEFAULT_IMAGE_HEIGHT = 20;
const LABEL_OFFSET = 10;
const CANVAS_POSITION_DEFAULT = "canvas_position_default";

function getSquareMarkerCoords(radius) {
    return [-radius, -radius, radius, -radius, radius, radius, -radius, radius, -radius, -radius]
}

function getPolygonMarkerCoords(radius) {
    const r = _ceil(radius);
    return [-r, 0, 0, -r, r, 0, 0, r, -r, 0]
}

function getCrossMarkerCoords(radius) {
    const r = _ceil(radius);
    const floorHalfRadius = _floor(r / 2);
    const ceilHalfRadius = _ceil(r / 2);
    return [-r, -floorHalfRadius, -floorHalfRadius, -r, 0, -ceilHalfRadius, floorHalfRadius, -r, r, -floorHalfRadius, ceilHalfRadius, 0, r, floorHalfRadius, floorHalfRadius, r, 0, ceilHalfRadius, -floorHalfRadius, r, -r, floorHalfRadius, -ceilHalfRadius, 0]
}

function getTriangleDownMarkerCoords(radius) {
    return [-radius, -radius, radius, -radius, 0, radius, -radius, -radius]
}

function getTriangleUpMarkerCoords(radius) {
    return [-radius, radius, radius, radius, 0, -radius, -radius, radius]
}
export default {
    deleteLabel: function() {
        this._label.dispose();
        this._label = null
    },
    _hasGraphic: function() {
        return this.graphic
    },
    clearVisibility: function() {
        const graphic = this.graphic;
        if (graphic && graphic.attr("visibility")) {
            graphic.attr({
                visibility: null
            })
        }
    },
    isVisible: function() {
        return this.inVisibleArea && this.series.isVisible()
    },
    setInvisibility: function() {
        const graphic = this.graphic;
        if (graphic && "hidden" !== graphic.attr("visibility")) {
            graphic.attr({
                visibility: "hidden"
            })
        }
        this._errorBar && this._errorBar.attr({
            visibility: "hidden"
        });
        this._label.draw(false)
    },
    clearMarker: function() {
        const graphic = this.graphic;
        graphic && graphic.attr(this._emptySettings)
    },
    _createLabel: function() {
        this._label = new Label({
            renderer: this.series._renderer,
            labelsGroup: this.series._labelsGroup,
            point: this
        })
    },
    _calculateVisibility: function(x, y) {
        const {
            minX: minX,
            maxX: maxX,
            minY: minY,
            maxY: maxY
        } = this._getVisibleArea();
        this.inVisibleArea = minX <= x && maxX >= x && minY <= y && maxY >= y
    },
    _updateLabelData: function() {
        this._label.setData(this._getLabelFormatObject())
    },
    _updateLabelOptions: function() {
        !this._label && this._createLabel();
        this._label.setOptions(this._options.label)
    },
    _checkImage: function(image) {
        return _isDefined(image) && ("string" === typeof image || _isDefined(image.url))
    },
    _fillStyle: function() {
        this._styles = this._options.styles
    },
    _checkSymbol: function(oldOptions, newOptions) {
        const oldSymbol = oldOptions.symbol;
        const newSymbol = newOptions.symbol;
        const symbolChanged = "circle" === oldSymbol && "circle" !== newSymbol || "circle" !== oldSymbol && "circle" === newSymbol;
        const imageChanged = this._checkImage(oldOptions.image) !== this._checkImage(newOptions.image);
        return !!(symbolChanged || imageChanged)
    },
    _populatePointShape: function(symbol, radius) {
        switch (symbol) {
            case "square":
                return getSquareMarkerCoords(radius);
            case "polygon":
                return getPolygonMarkerCoords(radius);
            case "triangle":
            case "triangleDown":
                return getTriangleDownMarkerCoords(radius);
            case "triangleUp":
                return getTriangleUpMarkerCoords(radius);
            case "cross":
                return getCrossMarkerCoords(radius)
        }
    },
    hasCoords: function() {
        return null !== this.x && null !== this.y
    },
    correctValue: function(correction) {
        const that = this;
        const axis = that.series.getValueAxis();
        if (that.hasValue()) {
            that.value = that.properValue = axis.validateUnit(that.initialValue.valueOf() + correction.valueOf());
            that.minValue = axis.validateUnit(correction)
        }
    },
    resetCorrection: function() {
        this.value = this.properValue = this.initialValue;
        this.minValue = CANVAS_POSITION_DEFAULT
    },
    resetValue: function() {
        const that = this;
        if (that.hasValue()) {
            that.value = that.properValue = that.initialValue = 0;
            that.minValue = 0;
            that._label.setDataField("value", that.value)
        }
    },
    _getTranslates: function(animationEnabled) {
        let translateX = this.x;
        let translateY = this.y;
        if (animationEnabled) {
            if (this._options.rotated) {
                translateX = this.defaultX
            } else {
                translateY = this.defaultY
            }
        }
        return {
            x: translateX,
            y: translateY
        }
    },
    _createImageMarker: function(renderer, settings, options) {
        const width = options.width || 20;
        const height = options.height || 20;
        return renderer.image(-_round(.5 * width), -_round(.5 * height), width, height, options.url ? options.url.toString() : options.toString(), "center").attr({
            translateX: settings.translateX,
            translateY: settings.translateY,
            visibility: settings.visibility
        })
    },
    _createSymbolMarker: function(renderer, pointSettings) {
        let marker;
        const symbol = this._options.symbol;
        if ("circle" === symbol) {
            delete pointSettings.points;
            marker = renderer.circle().attr(pointSettings)
        } else if ("square" === symbol || "polygon" === symbol || "triangle" === symbol || "triangleDown" === symbol || "triangleUp" === symbol || "cross" === symbol) {
            marker = renderer.path([], "area").attr(pointSettings).sharp()
        }
        return marker
    },
    _createMarker: function(renderer, group, image, settings) {
        const that = this;
        const marker = that._checkImage(image) ? that._createImageMarker(renderer, settings, image) : that._createSymbolMarker(renderer, settings);
        if (marker) {
            marker.data({
                "chart-data-point": that
            }).append(group)
        }
        return marker
    },
    _getSymbolBBox: function(x, y, r) {
        return {
            x: x - r,
            y: y - r,
            width: 2 * r,
            height: 2 * r
        }
    },
    _getImageBBox: function(x, y) {
        const image = this._options.image;
        const width = image.width || 20;
        const height = image.height || 20;
        return {
            x: x - _round(width / 2),
            y: y - _round(height / 2),
            width: width,
            height: height
        }
    },
    _getGraphicBBox: function() {
        const that = this;
        const options = that._options;
        const x = that.x;
        const y = that.y;
        let bBox;
        if (options.visible) {
            bBox = that._checkImage(options.image) ? that._getImageBBox(x, y) : that._getSymbolBBox(x, y, options.styles.normal.r)
        } else {
            bBox = {
                x: x,
                y: y,
                width: 0,
                height: 0
            }
        }
        return bBox
    },
    hideInsideLabel: noop,
    _getShiftLabelCoords: function(label) {
        const coord = this._addLabelAlignmentAndOffset(label, this._getLabelCoords(label));
        return this._checkLabelPosition(label, coord)
    },
    _drawLabel: function() {
        const customVisibility = this._getCustomLabelVisibility();
        const label = this._label;
        const isVisible = this._showForZeroValues() && this.hasValue() && false !== customVisibility && (this.series.getLabelVisibility() || customVisibility);
        label.draw(!!isVisible)
    },
    correctLabelPosition: function(label) {
        const that = this;
        const coord = that._getShiftLabelCoords(label);
        if (!that.hideInsideLabel(label, coord)) {
            label.setFigureToDrawConnector(that._getLabelConnector(label.pointPosition));
            label.shift(_round(coord.x), _round(coord.y))
        }
    },
    _showForZeroValues: function() {
        return true
    },
    _getLabelConnector: function(pointPosition) {
        const bBox = this._getGraphicBBox(pointPosition);
        const w2 = bBox.width / 2;
        const h2 = bBox.height / 2;
        return {
            x: bBox.x + w2,
            y: bBox.y + h2,
            r: this._options.visible ? Math.max(w2, h2) : 0
        }
    },
    _getPositionFromLocation: function() {
        return {
            x: this.x,
            y: this.y
        }
    },
    _isPointInVisibleArea: function(visibleArea, graphicBBox) {
        return visibleArea.minX <= graphicBBox.x + graphicBBox.width && visibleArea.maxX >= graphicBBox.x && visibleArea.minY <= graphicBBox.y + graphicBBox.height && visibleArea.maxY >= graphicBBox.y
    },
    _checkLabelPosition: function(label, coord) {
        const that = this;
        const visibleArea = that._getVisibleArea();
        const labelBBox = label.getBoundingRect();
        const graphicBBox = that._getGraphicBBox(label.pointPosition);
        const fullGraphicBBox = that._getGraphicBBox();
        const isInside = "inside" === label.getLayoutOptions().position;
        if (that._isPointInVisibleArea(visibleArea, fullGraphicBBox)) {
            if (!that._options.rotated) {
                if (visibleArea.minX > coord.x) {
                    coord.x = visibleArea.minX
                }
                if (visibleArea.maxX < coord.x + labelBBox.width) {
                    coord.x = visibleArea.maxX - labelBBox.width
                }
                if (visibleArea.minY > coord.y) {
                    coord.y = isInside ? visibleArea.minY : graphicBBox.y + graphicBBox.height + 10
                }
                if (visibleArea.maxY < coord.y + labelBBox.height) {
                    coord.y = isInside ? visibleArea.maxY - labelBBox.height : graphicBBox.y - labelBBox.height - 10
                }
            } else {
                if (visibleArea.minX > coord.x) {
                    coord.x = isInside ? visibleArea.minX : graphicBBox.x + graphicBBox.width + 10
                }
                if (visibleArea.maxX < coord.x + labelBBox.width) {
                    coord.x = isInside ? visibleArea.maxX - labelBBox.width : graphicBBox.x - 10 - labelBBox.width
                }
                if (visibleArea.minY > coord.y) {
                    coord.y = visibleArea.minY
                }
                if (visibleArea.maxY < coord.y + labelBBox.height) {
                    coord.y = visibleArea.maxY - labelBBox.height
                }
            }
        }
        return coord
    },
    _addLabelAlignmentAndOffset: function(label, coord) {
        const labelBBox = label.getBoundingRect();
        const labelOptions = label.getLayoutOptions();
        if (!this._options.rotated) {
            if ("left" === labelOptions.alignment) {
                coord.x += labelBBox.width / 2
            } else if ("right" === labelOptions.alignment) {
                coord.x -= labelBBox.width / 2
            }
        }
        coord.x += labelOptions.horizontalOffset;
        coord.y += labelOptions.verticalOffset;
        return coord
    },
    _getLabelCoords: function(label) {
        return this._getLabelCoordOfPosition(label, this._getLabelPosition(label.pointPosition))
    },
    _getLabelCoordOfPosition: function(label, position) {
        const labelBBox = label.getBoundingRect();
        const graphicBBox = this._getGraphicBBox(label.pointPosition);
        const centerY = graphicBBox.height / 2 - labelBBox.height / 2;
        const centerX = graphicBBox.width / 2 - labelBBox.width / 2;
        let x = graphicBBox.x;
        let y = graphicBBox.y;
        switch (position) {
            case "left":
                x -= labelBBox.width + 10;
                y += centerY;
                break;
            case "right":
                x += graphicBBox.width + 10;
                y += centerY;
                break;
            case "top":
                x += centerX;
                y -= labelBBox.height + 10;
                break;
            case "bottom":
                x += centerX;
                y += graphicBBox.height + 10;
                break;
            case "inside":
                x += centerX;
                y += centerY
        }
        return {
            x: x,
            y: y
        }
    },
    _drawMarker: function(renderer, group, animationEnabled) {
        const options = this._options;
        const translates = this._getTranslates(animationEnabled);
        const style = this._getStyle();
        this.graphic = this._createMarker(renderer, group, options.image, _extend({
            translateX: translates.x,
            translateY: translates.y,
            points: this._populatePointShape(options.symbol, style.r)
        }, style))
    },
    _getErrorBarSettings: function() {
        return {
            visibility: "visible"
        }
    },
    _getErrorBarBaseEdgeLength() {
        return 2 * this.getPointRadius()
    },
    _drawErrorBar: function(renderer, group) {
        if (!this._options.errorBars) {
            return
        }
        const that = this;
        const options = that._options;
        const errorBarOptions = options.errorBars;
        const points = [];
        let settings;
        const pos = that._errorBarPos;
        let high = that._highErrorCoord;
        let low = that._lowErrorCoord;
        const displayMode = _normalizeEnum(errorBarOptions.displayMode);
        const isHighDisplayMode = "high" === displayMode;
        const isLowDisplayMode = "low" === displayMode;
        const highErrorOnly = (isHighDisplayMode || !_isDefined(low)) && _isDefined(high) && !isLowDisplayMode;
        const lowErrorOnly = (isLowDisplayMode || !_isDefined(high)) && _isDefined(low) && !isHighDisplayMode;
        let edgeLength = errorBarOptions.edgeLength;
        if (edgeLength <= 1 && edgeLength > 0) {
            edgeLength = this._getErrorBarBaseEdgeLength() * errorBarOptions.edgeLength
        }
        edgeLength = _floor(parseInt(edgeLength) / 2);
        highErrorOnly && (low = that._baseErrorBarPos);
        lowErrorOnly && (high = that._baseErrorBarPos);
        if ("none" !== displayMode && _isDefined(high) && _isDefined(low) && _isDefined(pos)) {
            !lowErrorOnly && points.push([pos - edgeLength, high, pos + edgeLength, high]);
            points.push([pos, high, pos, low]);
            !highErrorOnly && points.push([pos + edgeLength, low, pos - edgeLength, low]);
            options.rotated && each(points, (function(_, p) {
                p.reverse()
            }));
            settings = that._getErrorBarSettings(errorBarOptions);
            if (!that._errorBar) {
                that._errorBar = renderer.path(points, "line").attr(settings).append(group)
            } else {
                settings.points = points;
                that._errorBar.attr(settings)
            }
        } else {
            that._errorBar && that._errorBar.attr({
                visibility: "hidden"
            })
        }
    },
    getTooltipParams: function() {
        const graphic = this.graphic;
        return {
            x: this.x,
            y: this.y,
            offset: graphic ? graphic.getBBox().height / 2 : 0
        }
    },
    setPercentValue: function(absTotal, total, leftHoleTotal, rightHoleTotal) {
        const that = this;
        const valuePercent = that.value / absTotal || 0;
        const minValuePercent = that.minValue / absTotal || 0;
        const percent = valuePercent - minValuePercent;
        that._label.setDataField("percent", percent);
        that._label.setDataField("total", total);
        if (that.series.isFullStackedSeries() && that.hasValue()) {
            if (that.leftHole) {
                that.leftHole /= absTotal - leftHoleTotal;
                that.minLeftHole /= absTotal - leftHoleTotal
            }
            if (that.rightHole) {
                that.rightHole /= absTotal - rightHoleTotal;
                that.minRightHole /= absTotal - rightHoleTotal
            }
            that.value = that.properValue = valuePercent;
            that.minValue = !minValuePercent ? that.minValue : minValuePercent
        }
    },
    _storeTrackerR: function() {
        let navigator = window.navigator;
        const r = this._options.styles.normal.r;
        const minTrackerSize = hasProperty("ontouchstart") || navigator.msPointerEnabled && navigator.msMaxTouchPoints || navigator.pointerEnabled && navigator.maxTouchPoints ? 20 : 6;
        this._options.trackerR = r < minTrackerSize ? minTrackerSize : r;
        return this._options.trackerR
    },
    _translateErrorBars: function() {
        const options = this._options;
        const rotated = options.rotated;
        const errorBars = options.errorBars;
        const translator = this._getValTranslator();
        if (!errorBars) {
            return
        }
        _isDefined(this.lowError) && (this._lowErrorCoord = translator.translate(this.lowError));
        _isDefined(this.highError) && (this._highErrorCoord = translator.translate(this.highError));
        this._errorBarPos = _floor(rotated ? this.vy : this.vx);
        this._baseErrorBarPos = "stdDeviation" === errorBars.type ? this._lowErrorCoord + (this._highErrorCoord - this._lowErrorCoord) / 2 : rotated ? this.vx : this.vy
    },
    _translate: function() {
        const that = this;
        const valTranslator = that._getValTranslator();
        const argTranslator = that._getArgTranslator();
        if (that._options.rotated) {
            that.vx = that.x = valTranslator.translate(that.value, void 0, true);
            that.vy = that.y = argTranslator.translate(that.argument, void 0, true);
            that.minX = valTranslator.translate(that.minValue, void 0, true);
            that.defaultX = valTranslator.translate(CANVAS_POSITION_DEFAULT)
        } else {
            that.vy = that.y = valTranslator.translate(that.value, void 0, true);
            that.vx = that.x = argTranslator.translate(that.argument, void 0, true);
            that.minY = valTranslator.translate(that.minValue, void 0, true);
            that.defaultY = valTranslator.translate(CANVAS_POSITION_DEFAULT)
        }
        that._translateErrorBars();
        that._calculateVisibility(that.x, that.y)
    },
    _updateData: function(data) {
        this.value = this.properValue = this.initialValue = this.originalValue = data.value;
        this.minValue = this.initialMinValue = this.originalMinValue = _isDefined(data.minValue) ? data.minValue : CANVAS_POSITION_DEFAULT
    },
    _getImageSettings: function(image) {
        return {
            href: image.url || image.toString(),
            width: image.width || 20,
            height: image.height || 20
        }
    },
    getCrosshairData: function() {
        const r = this._options.rotated;
        const value = this.properValue;
        const argument = this.argument;
        return {
            x: this.vx,
            y: this.vy,
            xValue: r ? value : argument,
            yValue: r ? argument : value,
            axis: this.series.axis
        }
    },
    getPointRadius: function() {
        const style = this._getStyle();
        const options = this._options;
        const r = style.r;
        let extraSpace;
        const symbol = options.symbol;
        const isSquare = "square" === symbol;
        const isTriangle = "triangle" === symbol || "triangleDown" === symbol || "triangleUp" === symbol;
        if (options.visible && !options.image && r) {
            extraSpace = style["stroke-width"] / 2;
            return (isSquare || isTriangle ? 1.4 * r : r) + extraSpace
        }
        return 0
    },
    _updateMarker: function(animationEnabled, style) {
        const that = this;
        const options = that._options;
        let settings;
        const image = options.image;
        const visibility = !that.isVisible() ? {
            visibility: "hidden"
        } : {};
        if (that._checkImage(image)) {
            settings = _extend({}, {
                visibility: style.visibility
            }, visibility, that._getImageSettings(image))
        } else {
            settings = _extend({}, style, visibility, {
                points: that._populatePointShape(options.symbol, style.r)
            })
        }
        if (!animationEnabled) {
            settings.translateX = that.x;
            settings.translateY = that.y
        }
        that.graphic.attr(settings).sharp()
    },
    _getLabelFormatObject: function() {
        return {
            argument: this.initialArgument,
            value: this.initialValue,
            originalArgument: this.originalArgument,
            originalValue: this.originalValue,
            seriesName: this.series.name,
            lowErrorValue: this.lowError,
            highErrorValue: this.highError,
            point: this
        }
    },
    _getLabelPosition: function() {
        const rotated = this._options.rotated;
        if (this.initialValue > 0) {
            return rotated ? "right" : "top"
        } else {
            return rotated ? "left" : "bottom"
        }
    },
    _getFormatObject: function(tooltip) {
        const labelFormatObject = this._label.getData();
        return _extend({}, labelFormatObject, {
            argumentText: tooltip.formatValue(this.initialArgument, "argument"),
            valueText: tooltip.formatValue(this.initialValue)
        }, _isDefined(labelFormatObject.percent) ? {
            percentText: tooltip.formatValue(labelFormatObject.percent, "percent")
        } : {}, _isDefined(labelFormatObject.total) ? {
            totalText: tooltip.formatValue(labelFormatObject.total)
        } : {})
    },
    getMarkerVisibility: function() {
        return this._options.visible
    },
    coordsIn: function(x, y) {
        const trackerRadius = this._storeTrackerR();
        return x >= this.x - trackerRadius && x <= this.x + trackerRadius && y >= this.y - trackerRadius && y <= this.y + trackerRadius
    },
    getMinValue: function(noErrorBar) {
        const errorBarOptions = this._options.errorBars;
        if (errorBarOptions && !noErrorBar) {
            const displayMode = errorBarOptions.displayMode;
            const lowValue = "high" !== displayMode && _isDefined(this.lowError) ? this.lowError : this.value;
            const highValue = "low" !== displayMode && _isDefined(this.highError) ? this.highError : this.value;
            return lowValue < highValue ? lowValue : highValue
        } else {
            return this.value
        }
    },
    getMaxValue: function(noErrorBar) {
        const errorBarOptions = this._options.errorBars;
        if (errorBarOptions && !noErrorBar) {
            const displayMode = errorBarOptions.displayMode;
            const lowValue = "high" !== displayMode && _isDefined(this.lowError) ? this.lowError : this.value;
            const highValue = "low" !== displayMode && _isDefined(this.highError) ? this.highError : this.value;
            return lowValue > highValue ? lowValue : highValue
        } else {
            return this.value
        }
    }
};
