/* eslint-disable max-len */
import { CircularGauge } from '../circular-gauge';
import { Axis, Label, Range, Tick } from './axis';
import { stringToNumber, toPixel, textElement, appendPath, getAngleFromValue, getLocationFromAngle, getPathArc, getRoundedPathArc, VisibleLabels, getDegree, isCompleteAngle, PathOption, GaugeLocation, TextOption } from '../utils/helper-common';
import { getRangeColor } from '../utils/helper-axis-renderer';
import { TickModel } from './axis-model';
import { getRangePalette } from '../model/theme';
import { isNullOrUndefined} from '@syncfusion/ej2-base';
import { FontModel } from '../model/base-model';

/**
 * Specifies the Axis rendering for circular gauge
 */

export class AxisRenderer {

    private majorValues: number[];
    private gauge: CircularGauge;
    /**
     * Constructor for axis renderer.
     *
     * @param {CircularGauge} gauge - Specifies the instance of the gauge
     * @private.
     */

    constructor(gauge: CircularGauge) {
        this.gauge = gauge;
    }

    /**
     * Method to render the axis element of the circular gauge.
     *
     * @param {Axis} axis - Specifies the axis.
     * @param {number} index - Specifies the index.
     * @param {Element} element - Specifies the element.
     * @param {CircularGauge} gauge - Specifies the gauge.
     * @returns {void}
     * @private
     */
    public drawAxisOuterLine(axis: Axis, index: number, element: Element, gauge: CircularGauge): void {
        const background: string = axis.background;
        this.setRangeColor(axis);
        if (background !== null) {
            appendPath(
                new PathOption(
                    gauge.element.id + '_AxisOuterLine_' + index,
                    background, 0, 'transparent', null, '0',
                    getPathArc(gauge.midPoint, 0, 360, (Math.min(axis.rect.width, axis.rect.height) / 2)), '',
                    'pointer-events:none;'),
                element, gauge
            );
        }
    }

    /**
     * Method to check the angles.
     *
     * @param {Axis} axis - Specifies the axis.
     * @returns {void}
     * @private
     */
    public checkAngles(axis: Axis): void {
        axis.startAngle = axis.startAngle >= 360 ? 360 : axis.startAngle <= -360 ? -360 : axis.startAngle;
        axis.endAngle = axis.endAngle >= 360 ? 360 : axis.endAngle <= -360 ? -360 : axis.endAngle;
    }

    /**
     * Method to render the axis line of the circular gauge.
     *
     * @param {Axis} axis - Specifies the axis.
     * @param {number} index - Specifies the index.
     * @param {Element} element - Specifies the element.
     * @param {CircularGauge} gauge - Specifies the gauge.
     * @returns {void}
     * @private
     */
    public drawAxisLine(axis: Axis, index: number, element: Element, gauge: CircularGauge): void {
        let startAngle: number = axis.startAngle;
        let endAngle: number = axis.endAngle;
        const color: string = axis.lineStyle.color || this.gauge.themeStyle.lineColor;
        if (axis.lineStyle.width > 0 && this.gauge.allowComponentRender) {
            startAngle = !isCompleteAngle(startAngle, endAngle) ? startAngle : [0, endAngle = 360][0];
            appendPath(
                new PathOption(
                    gauge.element.id + '_AxisLine_' + index, 'transparent', axis.lineStyle.width, color,
                    null, axis.lineStyle.dashArray,
                    getPathArc(gauge.midPoint, startAngle - 90, endAngle - 90, axis.currentRadius),
                    '', gauge.allowLoadingAnimation ? 'visibility: hidden; pointer-events:none;' : 'pointer-events:none;'),
                element, gauge
            );
        }
    }

    /**
     * Method to render the axis labels of the circular gauge.
     *
     * @param {Axis} axis - Specifies the axis.
     * @param {number} index - Specifies the index.
     * @param {Element} element - Specifies the element.
     * @param {CircularGauge} gauge - Specifies the gauge.
     * @returns {void}
     * @private
     */
    public drawAxisLabels(axis: Axis, index: number, element: Element, gauge: CircularGauge): void {
        const labelElement: Element = gauge.renderer.createGroup({
            id: gauge.element.id + '_Axis_Labels_' + index, style: gauge.allowLoadingAnimation ? 'visibility: hidden;' : 'pointer-events:auto;'
        });
        const min: number = axis.visibleRange.min;
        const max: number = axis.visibleRange.max;
        const labelCollection: VisibleLabels[] = axis.visibleLabels;
        let location: GaugeLocation;
        let textWidth: number; let textHeight: number; let labelsVisible: boolean = true;
        let currentTextWidth: number; let currentTextHeight: number;
        let previousLocation: GaugeLocation; let currentLocation: GaugeLocation;
        let lastLabelLocation: GaugeLocation;
        let lastLabelAngle: number; let lastLabelAnchor: string;
        let lastTextWidth: number; let lastTextHeight: number;
        const style: Label = <Label>axis.labelStyle; let anchor: string; let angle: number;
        let label: VisibleLabels; let radius: number = axis.currentRadius;
        let checkLabelOpposed: number = 0;
        checkLabelOpposed = (style.position === 'Inside' && axis.majorTicks.position === 'Outside' &&
            axis.minorTicks.position === 'Outside') || (style.position === 'Outside' &&
                axis.minorTicks.position === 'Inside' && axis.majorTicks.position === 'Inside') ?
            axis.lineStyle.width + axis.currentRadius / 20 :
            (style.position === axis.majorTicks.position ? axis.currentRadius / 20 : axis.currentRadius / 40);
        const labelPadding: number = axis.labelStyle.shouldMaintainPadding ? 10 : checkLabelOpposed;
        const color: string = style.font.color || this.gauge.themeStyle.labelColor;
        if (style.position === 'Outside') {
            radius += (axis.nearSize - (axis.maxLabelSize.height + axis.lineStyle.width / 2)) + (labelPadding / 2);
        } else if (style.position === 'Cross') {
            radius = radius - (axis.maxLabelSize.height / 4) - axis.labelStyle.offset;
        } else {
            radius -= (axis.farSize - (axis.maxLabelSize.height + axis.lineStyle.width / 2) + (style.autoAngle ? labelPadding : 0));
        }
        //To get and store lastlabelposition
        if (axis.hideIntersectingLabel) {
            lastLabelAngle = Math.round(getAngleFromValue(labelCollection[labelCollection.length - 1].value, max,
                                                          min, axis.startAngle, axis.endAngle, axis.direction === 'ClockWise'));
            lastLabelLocation = getLocationFromAngle(lastLabelAngle, radius, gauge.midPoint);
            lastLabelAnchor = this.findAnchor(lastLabelLocation, style, lastLabelAngle, labelCollection[labelCollection.length - 1]);
            lastTextWidth = (!axis.showLastLabel && (isCompleteAngle(axis.startAngle, axis.endAngle)) && (style.hiddenLabel !== 'First')) ?
                labelCollection[0].size.width : labelCollection[labelCollection.length - 1].size.width;
            lastTextHeight = (!axis.showLastLabel && (isCompleteAngle(axis.startAngle, axis.endAngle)) && (style.hiddenLabel !== 'First')) ?
                (!style.autoAngle ? labelCollection[0].size.height : labelCollection[0].size.width) :
                (!style.autoAngle ? labelCollection[labelCollection.length - 1].size.height :
                    labelCollection[labelCollection.length - 1].size.width);
            lastTextHeight = lastTextHeight - this.offsetAxisLabelsize(lastLabelAngle, lastTextHeight);
            lastLabelLocation = this.getAxisLabelStartPosition(lastLabelLocation, lastTextWidth, lastLabelAnchor);
        }
        for (let i: number = 0, length: number = labelCollection.length; i < length; i++) {
            label = labelCollection[i as number];
            angle = Math.round(getAngleFromValue(label.value, max, min, axis.startAngle, axis.endAngle, axis.direction === 'ClockWise'));
            location = getLocationFromAngle(angle, radius, gauge.midPoint);
            anchor = this.findAnchor(location, style, angle, label);
            //To get the current label and previous label position for initial stage
            if (axis.hideIntersectingLabel) {
                currentLocation = getLocationFromAngle(angle, radius, gauge.midPoint);
                currentTextWidth = label.size.width;
                currentTextHeight = !style.autoAngle ? label.size.height : currentTextWidth;
                currentTextHeight = currentTextHeight - this.offsetAxisLabelsize(angle, currentTextHeight);
                currentLocation = this.getAxisLabelStartPosition(currentLocation, currentTextWidth, anchor);
                if (i === 0) {
                    previousLocation = getLocationFromAngle(angle, radius, gauge.midPoint);
                    textWidth = label.size.width;
                    textHeight = !style.autoAngle ? label.size.height : textWidth;
                    textHeight = textHeight - this.offsetAxisLabelsize(angle, textHeight);
                    previousLocation = this.getAxisLabelStartPosition(previousLocation, textWidth, anchor);
                }
            }
            if ((i === 0 && style.hiddenLabel === 'First') || (i === (length - 1) && style.hiddenLabel === 'Last')) {
                continue;
            }
            const textFont: FontModel = {
                size: style.font.size || this.gauge.themeStyle.fontSize,
                color: style.font.color,
                fontFamily: style.font.fontFamily || this.gauge.themeStyle.labelFontFamily,
                fontWeight: style.font.fontWeight || this.gauge.themeStyle.fontWeight,
                fontStyle: style.font.fontStyle,
                opacity: style.font.opacity
            };
            if (axis.hideIntersectingLabel && (i !== 0)) {
                //To remove the labels which is intersecting with last label.
                const lastlabel: boolean = ((i !== (labelCollection.length - 1)) && ((isCompleteAngle(axis.startAngle, axis.endAngle) ||
                    axis.showLastLabel))) ? this.FindAxisLabelCollision(lastLabelLocation, lastTextWidth, lastTextHeight, currentLocation,
                                                                        currentTextWidth, currentTextHeight) : true;
                //Checking wether the axis label is intersecting with previous label or not.
                labelsVisible = (this.FindAxisLabelCollision(previousLocation, textWidth, textHeight, currentLocation, currentTextWidth,
                                                             currentTextHeight) && lastlabel);
            } else {
                labelsVisible = true;
            }
            if (labelsVisible || (i === labelCollection.length - 1)) {
                //To hide first and last label based on requirement
                label.text = (!axis.showLastLabel && ((isCompleteAngle(axis.startAngle, axis.endAngle) && style.hiddenLabel !== 'First') ||
                    !labelsVisible)
                    && axis.hideIntersectingLabel && (i === (length - 1))) ? '' : label.text;
                label.text = (axis.showLastLabel && axis.hideIntersectingLabel && isCompleteAngle(axis.startAngle, axis.endAngle)
                    && (i === 0)) ? '' : label.text;
                const labelTextElement: Element = textElement(
                    new TextOption(
                        gauge.element.id + '_Axis_' + index + '_Label_' + i,
                        location.x, location.y, anchor, label.text,
                        style.autoAngle ? 'rotate(' + (angle + 90) + ',' + (location.x) + ',' + location.y + ')' : '', 'auto'
                    ),
                    textFont, style.useRangeColor ? getRangeColor(label.value, <Range[]>axis.ranges, color) : color,
                    labelElement, 'pointer-events:auto;'
                );
                labelTextElement.setAttribute('aria-label', label.text);
                labelTextElement.setAttribute('role', 'region');
                if (axis.hideIntersectingLabel) {
                    textWidth = label.size.width;
                    textHeight = !style.autoAngle ? label.size.height : textWidth;
                    textHeight = textHeight - this.offsetAxisLabelsize(angle, textHeight);
                    previousLocation.x = currentLocation.x;
                    previousLocation.y = currentLocation.y;
                }
            }
        }
        element.appendChild(labelElement);
    }

    /**
     * Method to find the anchor of the axis label.
     *
     * @param {GaugeLocation} location - Specifies the location.
     * @param {Label} style - Specifies the label style.
     * @param {number} angle - Specifies the angle.
     * @param {VisibleLabels} label - Specifies the labels.
     * @returns {string} - Returns the anchor.
     * @private
     */
    private findAnchor(location: GaugeLocation, style: Label, angle: number, label: VisibleLabels): string {
        if (style.autoAngle) {
            return 'middle';
        }
        const anchor: string = style.position === 'Inside' ?
            ((angle > 120 && angle < 240) ? 'start' : ((300 < angle || angle < 60) ? 'end' : 'middle')) :
            ((angle > 120 && angle < 240) ? 'end' : ((300 < angle || angle < 60) ? 'start' : 'middle'));
        location.y += style.position === 'Inside' ?
            ((angle >= 240 && angle <= 300) ? (label.size.height / 2) :
                (angle >= 60 && angle <= 120) ? 0 : label.size.height / 4) :
            ((angle >= 240 && angle <= 300) ? 0 :
                (angle >= 60 && angle <= 120) ? label.size.height / 2 : label.size.height / 4);
        return anchor;
    }

    /**
     * Methode to check whether the labels are intersecting or not.
     *
     * @param {GaugeLocation} previousLocation - Specifies the previous location.
     * @param {number} previousWidth - Specifies the previous width.
     * @param {number} previousHeight - Specifies the previous height.
     * @param {GaugeLocation} currentLocation - Specifies the current location.
     * @param {number} currentWidth - Specifies the current width.
     * @param {number} currentHeight - Specifies the current height.
     * @returns {boolean} - Returns the boolean value.
     * @private
     */

    private FindAxisLabelCollision(previousLocation: GaugeLocation, previousWidth: number, previousHeight: number,
                                   currentLocation: GaugeLocation, currentWidth: number, currentHeight: number): boolean {
        const labelVisisble: boolean = ((previousLocation.x > (currentLocation.x + (currentWidth))) ||
            ((previousLocation.x + (previousWidth)) < (currentLocation.x)) ||
            ((previousLocation.y + (previousHeight)) < (currentLocation.y)) ||
            ((previousLocation.y) > (currentLocation.y + (currentHeight))));
        return labelVisisble;
    }

    /**
     * Methode to get anchor position of label as start.
     *
     * @param {GaugeLocation} actualLocation - Specifies the actual location.
     * @param {number} textWidth - Specifies the text width.
     * @param {string} anchorPosition - Specifies the anchor position.
     * @returns {GaugeLocation} - Returns the gauge location.
     * @private
     */
    private getAxisLabelStartPosition(actualLocation: GaugeLocation, textWidth: number, anchorPosition: string): GaugeLocation {
        if (anchorPosition === 'end') {
            actualLocation.x = actualLocation.x - textWidth;
        } else if (anchorPosition === 'middle') {
            actualLocation.x = actualLocation.x - (textWidth / 2);
        }
        return actualLocation;
    }

    /**
     * Methode to offset label height and width based on angle.
     *
     * @param {number} angle - Specifies the angle.
     * @param {number} size - Specifies the size.
     * @returns {number} - Returns the fineal size.
     * @private
     */
    private offsetAxisLabelsize(angle: number, size: number): number {
        const finalSize: number = ((angle >= 20 && angle <= 60) || (angle >= 120 && angle <= 160) || (angle >= 200 && angle <= 240) ||
            (angle >= 300 && angle <= 340)) ? size / 5 : 0;
        return finalSize;
    }

    /**
     * Method to render the axis minor tick lines of the circular gauge.
     *
     * @param {Axis} axis - Specifies the axis.
     * @param {number} index - Specifies the index.
     * @param {Element} element - Specifies the element.
     * @param {CircularGauge} gauge - Specifies the gauge.
     * @returns {void}
     * @private
     */
    public drawMinorTickLines(axis: Axis, index: number, element: Element, gauge: CircularGauge): void {
        const minorTickElements: Element = gauge.renderer.createGroup({
            id: gauge.element.id + '_Axis_MinorTickLines_' + index
        });
        const minorLineStyle: TickModel = axis.minorTicks;
        const minorInterval: number = minorLineStyle.interval !== null ?
            minorLineStyle.interval : (axis.visibleRange.interval / 2);
        const isRangeColor: boolean = minorLineStyle.useRangeColor;
        const color: string = minorLineStyle.color || this.gauge.themeStyle.minorTickColor;
        if (minorLineStyle.width && minorLineStyle.height && minorInterval) {
            let j: number = 0;
            for (let i: number = axis.visibleRange.min, max: number = axis.visibleRange.max; i <= max; i += minorInterval) {
                if (this.majorValues.indexOf(+i.toFixed(3)) < 0) {
                    const tickElement: HTMLElement = appendPath(
                        new PathOption(
                            gauge.element.id + '_Axis_Minor_TickLine_' + index + '_' + j++, 'transparent', minorLineStyle.width,
                            isRangeColor ? getRangeColor(i, <Range[]>axis.ranges, color) : color,
                            null, minorLineStyle.dashArray, this.calculateTicks(i, <Tick>minorLineStyle, axis), '',
                            gauge.allowLoadingAnimation ? 'visibility: hidden;pointer-events: auto;' : 'pointer-events:auto;'
                        ),
                        minorTickElements, gauge
                    ) as HTMLElement;
                    tickElement.setAttribute('data-interval', i.toString());
                }
            }
            element.appendChild(minorTickElements);
        }
    }

    /**
     * Method to render the axis major tick lines of the circular gauge.
     *
     * @param {Axis} axis - Specifies the axis.
     * @param {number} index - Specifies the index.
     * @param {Element} element - Specifies the element.
     * @param {CircularGauge} gauge - Specifies the gauge.
     * @returns {void}
     * @private
     */
    public drawMajorTickLines(axis: Axis, index: number, element: Element, gauge: CircularGauge): void {
        const majorTickElements: Element = gauge.renderer.createGroup({
            id: gauge.element.id + '_Axis_MajorTickLines_' + index
        });
        const majorLineStyle: TickModel = axis.majorTicks;
        const isRangeColor: boolean = majorLineStyle.useRangeColor;
        this.majorValues = [];
        const color: string = majorLineStyle.color || this.gauge.themeStyle.majorTickColor;
        if (majorLineStyle.width && majorLineStyle.height && axis.visibleRange.interval) {
            let j: number = 0;
            for (let i: number = axis.visibleRange.min, max: number = axis.visibleRange.max,
                interval: number = axis.visibleRange.interval; i <= max; i += interval) {
                this.majorValues.push(+i.toFixed(3));
                const tickElement: HTMLElement = appendPath(
                    new PathOption(
                        gauge.element.id + '_Axis_Major_TickLine_' + index + '_' + j, 'transparent', majorLineStyle.width,
                        isRangeColor ? getRangeColor(i, <Range[]>axis.ranges, color) : color,
                        null, majorLineStyle.dashArray, this.calculateTicks(i, <Tick>majorLineStyle, axis), '',
                        gauge.allowLoadingAnimation ? 'visibility: hidden;pointer-events:auto;' : 'pointer-events:auto;'
                    ),
                    majorTickElements, gauge
                ) as HTMLElement;
                tickElement.setAttribute('data-interval', i.toString());
                j++;
            }
            element.appendChild(majorTickElements);
        }
    }

    /**
     * Method to calcualte the tick elements for the circular gauge.
     *
     * @param {number} value - Specifies the value.
     * @param {Tick} options - Specifies the options.
     * @param {Axis} axis - Specifies the axis.
     * @returns {string} - Returns the string.
     * @private
     */
    public calculateTicks(value: number, options: Tick, axis: Axis): string {
        const axisLineWidth: number = (axis.lineStyle.width / 2) + options.offset;
        const angle: number = getAngleFromValue(
            value, axis.visibleRange.max, axis.visibleRange.min,
            axis.startAngle, axis.endAngle, axis.direction === 'ClockWise'
        );
        const start: GaugeLocation =
            getLocationFromAngle(angle, axis.currentRadius +
                (options.position === 'Outside' ? axisLineWidth : options.position === 'Cross' ?
                    options.height / 2 - options.offset : -axisLineWidth),
                                 this.gauge.midPoint);
        const end: GaugeLocation =
            getLocationFromAngle(angle, axis.currentRadius +
                (options.position === 'Outside' ? axisLineWidth : options.position === 'Cross' ?
                    options.height / 2 - options.offset : -axisLineWidth) +
                (options.position === 'Outside' ? options.height : -options.height),
                                 this.gauge.midPoint);
        return 'M ' + start.x + ' ' + start.y + ' L ' + end.x + ' ' + end.y + ' ';
    }

    /**
     * Method to render the range path of the circular gauge.
     *
     * @param {Axis} axis - Specifies the axis.
     * @param {Range} range - Specifies the range.
     * @param {number} startWidth - Specifies the startwidth for the range.
     * @param {number} endWidth - Specifies the endwidth for the range.
     * @param {number} rangeIndex - Specifies the index of the range.
     * @param {number} index - Specifies the index of the axis.
     * @param {Element} rangeElement - Specifies the element.
     * @param {number} colorIndex - Specifies the index of the lineargradient colorstop.
     * @returns {void}
     * @private
     */
    public drawRangePath(
        axis: Axis, range: Range, startWidth: number, endWidth: number,
        rangeIndex: number, index: number, rangeElement: Element, colorIndex: number
    ): void {
        let startValue: number; let direction: string;
        let endValue: number;
        const location: GaugeLocation = this.gauge.midPoint;
        let startAngle: number;
        let endAngle: number;
        const isClockWise: boolean = axis.direction === 'ClockWise';
        const min: number = axis.visibleRange.min;
        const max: number = axis.visibleRange.max;
        let roundedStartAngle: number;
        let roundedEndAngle: number;
        let oldStart: number;
        let oldEnd: number;
        let gradientRangeColor: string;
        if (range.isLinearCircularGradient) {
            const rangeSplitValue: number = ((range.end - range.start) / range.linearGradient.colorStop.length);
            const rangeStart: number = range.linearGradient.colorStop.length > 1 ?
                (range.start + (rangeSplitValue * (colorIndex))) : range.start;
            const rangeEnd: number = range.linearGradient.colorStop.length > 1 ? (rangeStart + rangeSplitValue) : range.end;
            startValue = Math.min(Math.max(rangeStart, min), rangeEnd);
            endValue = Math.min(Math.max(rangeStart, rangeEnd), max);
        } else {
            startValue = Math.min(Math.max(range.start, min), range.end);
            endValue = Math.min(Math.max(range.start, range.end), max);
        }
        startAngle = getAngleFromValue(startValue, max, min, axis.startAngle, axis.endAngle, isClockWise);
        endAngle = getAngleFromValue(endValue, max, min, axis.startAngle, axis.endAngle, isClockWise);
        const isAngleCross360: boolean = (startAngle > endAngle);
        if (axis.rangeGap != null && axis.rangeGap > 0 && range.start !== range.end
            || (!isNullOrUndefined(range.linearGradient) && !range.isLinearCircularGradient
                && (colorIndex === (!isNullOrUndefined(range.linearGradient.colorStop) && range.linearGradient.colorStop.length - 1)))) {
            startAngle = (rangeIndex === 0 && !axis.startAndEndRangeGap) ? startAngle :
                colorIndex === 0 && range.isLinearCircularGradient ? axis.direction === 'AntiClockWise' ?
                    startAngle - (axis.rangeGap / Math.PI) :
                    startAngle + (axis.rangeGap / Math.PI) : !range.isLinearCircularGradient
                    ? (axis.direction === 'AntiClockWise' ? startAngle - (axis.rangeGap / Math.PI) :  startAngle + (axis.rangeGap / Math.PI)) : startAngle;
            endAngle = (rangeIndex === axis.ranges.length - 1 && !axis.startAndEndRangeGap) ? endAngle :
                !isNullOrUndefined(range.linearGradient) && colorIndex === range.linearGradient.colorStop.length - 1
				&& range.isLinearCircularGradient ?
                    axis.direction === 'AntiClockWise' ? endAngle + (axis.rangeGap / Math.PI) :
                        endAngle - (axis.rangeGap / Math.PI) : !range.isLinearCircularGradient ?
                        (axis.direction === 'AntiClockWise' ? endAngle + (axis.rangeGap / Math.PI) : endAngle - (axis.rangeGap / Math.PI)) : endAngle;
        }
        if (this.gauge.allowComponentRender) {
            if ((startValue !== endValue) && (isAngleCross360 ? startAngle < (endAngle + 360) : (startAngle < endAngle)) && ((range.start >= min && range.end <= max) || (range.end >= min && range.start <= max))) {
                endAngle = isClockWise ? endAngle : [startAngle, startAngle = endAngle][0];
                endWidth = isClockWise ? endWidth : [startWidth, startWidth = endWidth][0];
                const radius: number = typeof range.roundedCornerRadius === 'string' ? parseFloat(<string>range.roundedCornerRadius) : range.roundedCornerRadius;
                const process: number = (radius * 0.25);
                const degreeValue: number = getDegree(startAngle, endAngle);
                oldStart = ((((range.currentRadius - (startWidth / 2)) * ((startAngle * Math.PI) / 180) -
                    (radius / process)) / (range.currentRadius - (startWidth / 2))) * 180) / Math.PI;
                oldEnd = ((((range.currentRadius - (endWidth / 2)) * ((endAngle * Math.PI) / 180) +
                    (radius / process)) / (range.currentRadius - (endWidth / 2))) * 180) / Math.PI;
                roundedStartAngle = ((((range.currentRadius) * ((startAngle * Math.PI) / 180) +
                    (degreeValue < (range.roundedCornerRadius / 2) && range.isLinearCircularGradient
                        ? degreeValue <= 1 ? 0 : (radius / 4) : radius)) / (range.currentRadius)) * 180) / Math.PI;
                roundedEndAngle = ((((range.currentRadius) * ((endAngle * Math.PI) / 180) -
                    (degreeValue < (range.roundedCornerRadius / 2) && range.isLinearCircularGradient
                        ? degreeValue <= 1 ? 0 : (radius / 4) : radius)) / (range.currentRadius)) * 180) / Math.PI;
                if (roundedStartAngle > roundedEndAngle && (roundedStartAngle - roundedEndAngle) <= radius) {
                    roundedStartAngle = startAngle;
                    roundedEndAngle = endAngle;
                }
                if (this.gauge.gradientModule && ((!isNullOrUndefined(range.linearGradient)
                    && !isNullOrUndefined(range.linearGradient.colorStop)) || (!isNullOrUndefined(range.radialGradient)
                        && !isNullOrUndefined(range.radialGradient.colorStop)))) {
                    if (range.isLinearCircularGradient) {
                        endAngle -= isCompleteAngle(startAngle, endAngle) ? 0.0001 : 0;
                        const degree: number = getDegree(startAngle, endAngle);
                        const rangeColorLength: number = range.linearGradient.colorStop.length;
                        const degreeRange: number = ((axis.startAngle === axis.endAngle ?
                            (axis.startAngle === 0 && axis.endAngle === 0 ? 360 : axis.startAngle) :
                            (axis.endAngle - axis.startAngle)) - degree * (rangeColorLength - 1));
                        let degreeRangeValue: number;
                        if (degreeRange <= 360 && degreeRange >= 270) {
                            degreeRangeValue = 270;
                        } else if (degreeRange <= 270 && degreeRange >= 180) {
                            degreeRangeValue = 180;
                        } else if (degreeRange <= 180 && degreeRange >= 90) {
                            degreeRangeValue = 90;
                        } else if (degreeRange <= 90 && degreeRange >= 0) {
                            degreeRangeValue = 0;
                        }
                        const gradientDegree: number = axis.direction === 'AntiClockWise' ?
                            (axis.startAngle === axis.endAngle ? 0 : axis.startAngle) + degree * ((rangeColorLength - 1) - colorIndex)
                            : axis.startAngle + degree * (colorIndex);
                        let gradientAngle: number = axis.startAngle < axis.endAngle ? axis.direction === 'AntiClockWise'
                            ? axis.ranges.length > 1 ? rangeIndex === 0 ? (360 - (axis.startAngle
                                + (degree * (colorIndex)))) : (axis.startAngle + (degree * (colorIndex + 1))) :
                                axis.startAngle + (degreeRangeValue + degree * ((rangeColorLength - 1) - colorIndex)) : axis.startAngle
                            + (degree * (colorIndex)) : axis.endAngle === 360 || axis.startAngle === axis.endAngle
                            ? axis.direction === 'AntiClockWise' ? axis.startAngle === axis.endAngle ?
                                (axis.startAngle === 0 && axis.endAngle === 0 ? 0 : 360) - axis.startAngle +
                                degreeRangeValue + (degree * ((rangeColorLength - 1) - colorIndex))
                                : degree * ((rangeColorLength - 1) - colorIndex) : degree * (colorIndex) :
                            gradientDegree < 360 ? gradientDegree : gradientDegree - 360;
                        range.gradientAngle = rangeIndex === 0 ? axis.rangeGap ? gradientAngle + axis.rangeGap
                            : gradientAngle : axis.rangeGap > 0 ? axis.ranges[rangeIndex - 1]['gradientAngle'] + axis.rangeGap
                            : axis.ranges[rangeIndex - 1]['gradientAngle'];
                        if (axis.direction === 'AntiClockWise' && (axis.ranges.length > 1
                            ? colorIndex === rangeColorLength - 1 : colorIndex === 0)) {
                            range.gradientAntiAngle = gradientAngle;
                        }
                        if (rangeIndex !== 0) {
                            gradientAngle = axis.direction === 'AntiClockWise' ? axis.ranges.length > 1 ?
                                axis.ranges[rangeIndex - 1]['gradientAntiAngle'] - gradientAngle + axis.startAngle :
                                axis.ranges[rangeIndex - 1]['gradientAntiAngle'] + gradientAngle :
                                range.gradientAngle + gradientAngle - axis.startAngle;
                            range.gradientAngle = axis.rangeGap != null && axis.rangeGap > 0 ? colorIndex === rangeColorLength - 1 ?
                                gradientAngle + axis.ranges[rangeIndex - 1]['gradientAngle'] : gradientAngle : gradientAngle;
                            if (axis.direction === 'AntiClockWise' && (axis.ranges.length > 1
                                ? colorIndex === rangeColorLength - 1 : colorIndex === 0)) {
                                range.gradientAntiAngle = gradientAngle;
                            }
                        }
                        if (gradientAngle > 45 && gradientAngle <= 115
                            || (gradientAngle >= 0 && gradientAngle <= 45 && (rangeColorLength - 1) <= 2)) {
                            direction = axis.direction === 'AntiClockWise' ? 'bottom' : 'top';
                        } else if (gradientAngle > 115 && gradientAngle < 170) {
                            direction = axis.direction === 'AntiClockWise' ? 'left' : 'right';
                        } else if (gradientAngle >= 170 && gradientAngle <= 280) {
                            direction = axis.direction === 'AntiClockWise' ? 'top' : 'bottom';
                        } else if (gradientAngle > 280 && gradientAngle <= 360
                            || (gradientAngle >= 0 && gradientAngle <= 45 && (rangeColorLength - 1) >= 2)) {
                            direction = axis.direction === 'AntiClockWise' ? 'right' : 'left';
                        }
                    }
                    gradientRangeColor = this.gauge.gradientModule.getGradientColorString(
                        range, colorIndex, direction, rangeIndex
                    );
                }
                range.rangeColor = gradientRangeColor ? gradientRangeColor : range.rangeColor;
                if (range.roundedCornerRadius) {
                    if (range.isLinearCircularGradient && range.linearGradient.colorStop.length > 1) {
                        if (colorIndex === 0 || colorIndex === range.linearGradient.colorStop.length - 1) {
                            if (axis.direction === 'ClockWise') {
                                this.roundedRangeAppendPathCalculation(
                                    range, rangeIndex, index, startWidth, endWidth, rangeElement,
                                    (colorIndex === range.linearGradient.colorStop.length - 1
                                        ? Math.floor(startAngle) : Math.floor(roundedStartAngle)),
                                    (colorIndex !== 0 ? Math.ceil(roundedEndAngle) : Math.ceil(endAngle)),
                                    ((colorIndex === range.linearGradient.colorStop.length - 1) ? startAngle : oldStart),
                                    (colorIndex !== 0 ? oldEnd : endAngle), location, colorIndex
                                );
                            } else {
                                this.roundedRangeAppendPathCalculation(
                                    range, rangeIndex, index, startWidth, endWidth, rangeElement,
                                    (colorIndex === 0 ? Math.floor(startAngle) : Math.floor(roundedStartAngle)),
                                    (colorIndex === range.linearGradient.colorStop.length - 1
                                        ? Math.ceil(endAngle) : Math.ceil(roundedEndAngle)),
                                    ((colorIndex === 0) ? startAngle : oldStart),
                                    (colorIndex === range.linearGradient.colorStop.length - 1 ? endAngle : oldEnd),
                                    location, colorIndex
                                );
                            }
                        } else {
                            this.rangeAppendPathCalculation(
                                range, rangeIndex, index, startWidth, endWidth, rangeElement,
                                Math.floor(startAngle), Math.ceil(endAngle), colorIndex
                            );
                        }
                    } else {
                        this.roundedRangeAppendPathCalculation(
                            range, rangeIndex, index, startWidth, endWidth, rangeElement,
                            Math.floor(roundedStartAngle), Math.ceil(roundedEndAngle), oldStart,
                            oldEnd, location, colorIndex
                        );
                    }
                } else {
                    this.rangeAppendPathCalculation(
                        range, rangeIndex, index, startWidth, endWidth, rangeElement,
                        Math.floor(startAngle), Math.ceil(endAngle), colorIndex
                    );
                }
            } else if ((range.start === range.end) && ((range.start >= min && range.end <= max) || (range.end >= min && range.start <= max))) {
                this.rangeAppendPathCalculation(
                    range, rangeIndex, index, startWidth, endWidth, rangeElement,
                    Math.floor(startAngle), Math.ceil(endAngle), colorIndex
                );
            }
        }
    }

    /**
     * Method to render the rounded range path of the circular gauge.
     *
     * @param {Range} range - Specifies the range.
     * @param {number} rangeIndex - Specifies the index of the range.
     * @param {number} index - Specifies the index of the axis.
     * @param {number} startWidth - Specifies the startwidth for the range.
     * @param {number} endWidth - Specifies the endwidth for the range.
     * @param {Element} rangeElement - Specifies the element.
     * @param {number} roundedStartAngle - Specifies the rounded path of the start angle.
     * @param {number} roundedEndAngle - Specifies the rounded path of the end angle.
     * @param {number} oldStart - Specifies the rounded path of the old start value.
     * @param {number} oldEnd - Specifies the rounded path of the old end value..
     * @param {GaugeLocation} location - Specifies the location.
     * @param {number} colorIndex - Specifies the index of the lineargradient colorstop.
     * @returns {void}
     * @private
     */
    public roundedRangeAppendPathCalculation(
        range: Range, rangeIndex: number,
        index: number, startWidth: number, endWidth: number, rangeElement: Element,
        roundedStartAngle: number, roundedEndAngle: number, oldStart: number, oldEnd: number,
        location: GaugeLocation, colorIndex?: number
    ): void {
        range.pathElement.push(appendPath(
            new PathOption(
                (!range.isLinearCircularGradient ? this.gauge.element.id + '_Axis_' + index + '_Range_' + rangeIndex
                    : this.gauge.element.id + '_Axis_' + index + '_Range_' + rangeIndex + '_Circular_' + colorIndex),
                range.rangeColor, 0, range.rangeColor, range.opacity, '0',
                getRoundedPathArc(
                    location,
                    Math.floor(roundedStartAngle), Math.ceil(roundedEndAngle), oldStart, oldEnd,
                    range.currentRadius, startWidth, endWidth, range, this.gauge.axes[index as number] as Axis
                ),
                '', ''
            ),
            rangeElement, this.gauge
        ));
    }

    /**
     * Method to render the rounded range path of the circular gauge.
     *
     * @param {Range} range - Specifies the range.
     * @param {number} rangeIndex - Specifies the index of the range.
     * @param {number} index - Specifies the index of the axis.
     * @param {number} startWidth - Specifies the startwidth for the range.
     * @param {number} endWidth - Specifies the endwidth for the range.
     * @param {Element} rangeElement - Specifies the element.
     * @param {number} startAngle - Specifies the rounded path of the start angle.
     * @param {number} endAngle - Specifies the rounded path of the end angle.
     * @param {number} colorIndex - Specifies the index of the lineargradient colorstop.
     * @returns {void}
     * @private
     */
    public rangeAppendPathCalculation(
        range: Range, rangeIndex: number,
        index: number, startWidth: number, endWidth: number, rangeElement: Element, startAngle: number,
        endAngle: number, colorIndex?: number
    ): void {
        range.pathElement.push(appendPath(
            new PathOption(
                !range.isLinearCircularGradient ? this.gauge.element.id + '_Axis_' + index + '_Range_' +
                    rangeIndex : this.gauge.element.id + '_Axis_' + index + '_Range_' +
                    rangeIndex + '_Circular_' + colorIndex,
                range.rangeColor,
                0, range.rangeColor, range.opacity, '0',
                getPathArc(
                    this.gauge.midPoint,
                    Math.floor(startAngle), Math.ceil(endAngle),
                    range.currentRadius, startWidth,
                    endWidth, range, this.gauge.axes[index as number] as Axis
                ),
                '', ''
            ),
            rangeElement, this.gauge
        ));
    }

    /**
     * Method to render the axis range of the circular gauge.
     *
     * @param {Axis} axis - Specifies the axis.
     * @param {number} index - Specifies the index.
     * @param {Element} element - Specifies the element.
     * @returns {void}
     * @private
     */
    public drawAxisRange(axis: Axis, index: number, element: Element): void {
        const ele: Element = (document.getElementById(this.gauge.element.id + '_Axis_Ranges_ ' + index));
        const rangeElement: Element = (ele) ? document.getElementById(this.gauge.element.id + '_Axis_Ranges_ ' + index) :
            this.gauge.renderer.createGroup({ id: this.gauge.element.id + '_Axis_Ranges_' + index,
                style: this.gauge.allowLoadingAnimation ? 'opacity: 0;' : '' });
        let startWidth: number; let startEndDifference: number;
        let endWidth: number; let previousEndWidth: number; let previousStartWidth: number;
        axis.ranges.map((range: Range, rangeIndex: number) => {
            range.isLinearCircularGradient = !isNullOrUndefined(this.gauge.gradientModule)
            && !isNullOrUndefined(range.linearGradient) && isNullOrUndefined(range.linearGradient.startValue)
            && isNullOrUndefined(range.linearGradient.endValue) && !isNullOrUndefined(range.linearGradient.colorStop);
            range.pathElement = [];
            if (!isNullOrUndefined(range.offset) && (<string>range.offset).length > 0) {
                range.currentDistanceFromScale = stringToNumber(<string>range.offset, axis.currentRadius);
            } else {
                range.currentDistanceFromScale = <number>range.offset;
            }
            this.calculateRangeRadius(axis, range);
            if (!isNullOrUndefined((<string>range.startWidth)) && (<string>range.startWidth).length > 0) {
                startWidth = toPixel(<string>range.startWidth, range.currentRadius);
            } else {
                startWidth = <number>range.startWidth;
            }
            if (!isNullOrUndefined((<string>range.endWidth)) && (<string>range.endWidth).length > 0) {
                endWidth = toPixel(<string>range.endWidth, range.currentRadius);
            } else {
                endWidth = <number>range.endWidth;
            }
            range.currentRadius = this.calculateRangeRadiusWithPosition(axis, range, startWidth);
            if (range.isLinearCircularGradient) {
                for (let i: number = 0; i < range.linearGradient.colorStop.length; i++) {
                    if (i <= (range.linearGradient.colorStop.length - 1)) {
                        previousEndWidth = i === 0 ? endWidth : previousEndWidth;
                        previousStartWidth = i === 0 ? startWidth : previousStartWidth;
                        startEndDifference = (Math.abs(previousStartWidth - previousEndWidth) / (range.linearGradient.colorStop.length));
                        if (i > 0) {
                            startWidth = endWidth;
                            endWidth = previousStartWidth > previousEndWidth ? startWidth - startEndDifference
                                : startWidth + startEndDifference;
                        } else {
                            endWidth = previousStartWidth > previousEndWidth ? startWidth - startEndDifference
                                : startWidth + startEndDifference;
                        }
                    } else {
                        startWidth = previousStartWidth > previousEndWidth ? startWidth - startEndDifference
                            : startWidth + startEndDifference;
                        endWidth = (previousEndWidth);
                    }
                    this.drawRangePath(axis, range, startWidth, endWidth, rangeIndex, index, rangeElement, i);
                }
            } else {
                if (!(range.start === range.end && axis.direction === 'AntiClockWise' && axis.startAngle === axis.endAngle)) {
                    this.drawRangePath(axis, range, startWidth, endWidth, rangeIndex, index, rangeElement, null);
                }
            }
        }
        );
        element.appendChild(rangeElement);
    }

    /**
     * Method to calculate the radius of the axis range.
     *
     * @return {void}
     */

    private calculateRangeRadius(axis: Axis, range: Range): void {
        const radius: string = range.radius !== null ? range.radius : '100%';
        range.currentRadius = stringToNumber(
            radius, axis.currentRadius
        );
    }

    private calculateRangeRadiusWithPosition(axis: Axis, range: Range, startWidth: number): number {
        const actualRadius: number = !isNullOrUndefined(range.position) && range.position !== 'Auto' && isNullOrUndefined(range.radius) ?
            (range.position === 'Outside' ? (range.currentRadius + axis.lineStyle.width / 2 + range.currentDistanceFromScale) :
                range.position === 'Inside' ? (range.currentRadius - axis.lineStyle.width / 2 - range.currentDistanceFromScale) :
                    (range.currentRadius + startWidth / 2 - range.currentDistanceFromScale)) : range.currentRadius;
        return actualRadius;
    }

    /**
     * Method to get the range color of the circular gauge.
     *
     * @param {Axis} axis - Specifies the axis
     * @returns {void}
     * @private
     */
    public setRangeColor(axis: Axis): void {
        const rangeColors: string[] = getRangePalette(this.gauge.theme);
        axis.ranges.map((range: Range, index: number) => {
            range.rangeColor = range.color ? range.color : rangeColors[index % rangeColors.length];
        });
    }

    /**
     *
     * @returns {void}
     * @private
     */
    public destroy(): void {
        this.gauge = null;
        this.majorValues = [];
    }
}
