import { FILTERS } from '../../applications/overlayConfig.js';
import { evaluateComparator, getTokenEffects } from '../hooks/effectMappingHooks.js';
import { registerOverlayRefreshHook, unregisterOverlayRefreshHooks } from '../hooks/overlayHooks.js';
import { DEFAULT_OVERLAY_CONFIG } from '../models.js';
import { interpolateColor, removeMarkedOverlays } from '../token/overlay.js';
import { executeMacro, toggleCEEffect, toggleTMFXPreset, tv_executeScript } from '../utils.js';
import { HTMLOverlay } from './HTMLOverlay.js';

export class TVAOverlay extends TokenMesh {
  constructor(pTexture, token, config) {
    super(token);
    if (pTexture.shapes) pTexture.shapes = this.addChild(pTexture.shapes);

    this.pseudoTexture = pTexture;
    this.texture = pTexture.texture;
    //this.setTexture(pTexture, { refresh: false });

    this.ready = false;
    this.overlaySort = 0;
    this.filtersApplied = false;

    this.overlayConfig = mergeObject(DEFAULT_OVERLAY_CONFIG, config, { inplace: false });
    if (pTexture.html) this.addHTMLOverlay();

    // linkDimensions has been converted to linkDimensionsX and linkDimensionsY
    // Make sure we're using the latest fields
    // 20/07/2023
    if (!('linkDimensionsX' in this.overlayConfig) && this.overlayConfig.linkDimensions) {
      this.overlayConfig.linkDimensionsX = true;
      this.overlayConfig.linkDimensionsY = true;
    }

    this._registerHooks(this.overlayConfig);
    this._tvaPlay().then(() => this.refresh());

    // Workaround needed for v11 visible property
    Object.defineProperty(this, 'visible', {
      get: this._customVisible,
      set: function () {},
      configurable: true,
    });

    this.eventMode = 'none';
  }

  enableInteractivity() {
    if (this.mouseInteractionManager && !this.overlayConfig.interactivity?.length) {
      this.removeAllListeners();
      this.mouseInteractionManager = null;
      this.cursor = null;
      this.eventMode = 'none';
      return;
    } else if (this.mouseInteractionManager || !this.overlayConfig.interactivity?.length) return;

    if (!this.overlayConfig.ui) {
      if (canvas.primary.eventMode === 'passive') {
        canvas.primary.eventMode = 'passive';
      }
    }

    this.eventMode = 'static';

    // If this overlay iss interactable all of its parents need to be set as interactable too
    let parent = this.parent;
    while (parent instanceof TVAOverlay || parent?.parent instanceof TVAOverlay) {
      parent.eventMode = 'passive';
      parent = parent.parent;
    }

    this.cursor = 'pointer';
    const token = this.object;
    const sprite = this;

    const runInteraction = function (event, listener) {
      sprite.overlayConfig.interactivity.forEach((i) => {
        if (i.listener === listener) {
          event.preventDefault();
          event.stopPropagation();
          if (i.script) tv_executeScript(i.script, { token });
          if (i.macro) executeMacro(i.macro, token);
          if (i.ceEffect) toggleCEEffect(token, i.ceEffect);
          if (i.tmfxPreset) toggleTMFXPreset(token, i.tmfxPreset);
        }
      });
    };

    const permissions = {
      hoverIn: () => true,
      hoverOut: () => true,
      clickLeft: () => true,
      clickLeft2: () => true,
      clickRight: () => true,
      clickRight2: () => true,
      dragStart: () => false,
    };

    const callbacks = {
      hoverIn: (event) => runInteraction(event, 'hoverIn'),
      hoverOut: (event) => runInteraction(event, 'hoverOut'),
      clickLeft: (event) => runInteraction(event, 'clickLeft'),
      clickLeft2: (event) => runInteraction(event, 'clickLeft2'),
      clickRight: (event) => runInteraction(event, 'clickRight'),
      clickRight2: (event) => runInteraction(event, 'clickRight2'),
      dragLeftStart: null,
      dragLeftMove: null,
      dragLeftDrop: null,
      dragLeftCancel: null,
      dragRightStart: null,
      dragRightMove: null,
      dragRightDrop: null,
      dragRightCancel: null,
      longPress: null,
    };

    const options = { target: null };

    // Create the interaction manager
    const mgr = new MouseInteractionManager(this, canvas.stage, permissions, callbacks, options);
    this.mouseInteractionManager = mgr.activate();
  }

  _customVisible() {
    const ov = this.overlayConfig;
    if (!this.ready || !(this.object.visible || ov.alwaysVisible)) return false;

    if (ov.limitedToOwner && !this.object.owner) return false;
    if (ov.limitedUsers?.length && !ov.limitedUsers.includes(game.user.id)) return false;

    if (ov.limitOnEffect || ov.limitOnProperty) {
      const speaker = ChatMessage.getSpeaker();
      let token = canvas.ready ? canvas.tokens.get(speaker.token) : null;
      if (!token) return false;
      if (ov.limitOnEffect) {
        if (!getTokenEffects(token).includes(ov.limitOnEffect)) return false;
      }
      if (ov.limitOnProperty) {
        if (!evaluateComparator(token.document, ov.limitOnProperty)) return false;
      }
    }

    if (
      ov.limitOnHover ||
      ov.limitOnControl ||
      ov.limitOnHighlight ||
      ov.limitOnHUD ||
      ov.limitOnTarget ||
      ov.limitOnAnyTarget
    ) {
      if (ov.limitOnHover && canvas.controls.ruler._state === Ruler.STATES.INACTIVE && this.object.hover) return true;
      if (ov.limitOnControl && this.object.controlled) return true;
      if (ov.limitOnHighlight && (canvas.tokens.highlightObjects ?? canvas.tokens._highlight)) return true;
      if (ov.limitOnHUD && this.object.hasActiveHUD) return true;
      if (ov.limitOnAnyTarget && this.object.targeted.size) return true;
      if (ov.limitOnTarget && this.object.targeted.some((u) => u.id === game.userId)) return true;
      return false;
    }
    return true;
  }

  // Overlays have the same sort order as the parent
  get sort() {
    const sort = this.object.mesh.sort;
    if (this.overlayConfig.top) return sort + 9999;
    else if (this.overlayConfig.bottom) return sort - 9999;
    return sort;
  }

  get _lastSortedIndex() {
    return (this.object.mesh._lastSortedIndex || 0) + this.overlaySort;
  }

  get elevation() {
    const elevation = this.object.mesh?.data.elevation;
    if (this.overlayConfig.bottom && elevation > 0) return 0;
    else if (this.overlayConfig.top) return elevation + 9999;
    return elevation;
  }

  set _lastSortedIndex(val) {}

  async _tvaPlay() {
    // Ensure playback state for video
    const source = foundry.utils.getProperty(this.texture, 'baseTexture.resource.source');
    if (source && source.tagName === 'VIDEO') {
      // Detach video from others
      const s = source.cloneNode();

      if (this.overlayConfig.playOnce) {
        s.onended = () => {
          this.alpha = 0;
          this.tvaVideoEnded = true;
        };
      }

      await new Promise((resolve) => (s.oncanplay = resolve));
      this.texture = PIXI.Texture.from(s, { resourceOptions: { autoPlay: false } });

      const options = {
        loop: this.overlayConfig.loop && !this.overlayConfig.playOnce,
        volume: 0,
        offset: 0,
        playing: true,
      };
      game.video.play(s, options);
    }
  }

  addChildAuto(...children) {
    if (this.pseudoTexture?.shapes) {
      return this.pseudoTexture.shapes.addChild(...children);
    } else {
      return this.addChild(...children);
    }
  }

  setTexture(pTexture, { preview = false, refresh = true, configuration = null, refreshFilters = false } = {}) {
    // Text preview handling
    if (preview) {
      this._swapChildren(pTexture);
      if (this.originalTexture) this._destroyTexture();
      else {
        this.originalTexture = this.pseudoTexture;
        if (this.originalTexture.shapes) this.removeChild(this.originalTexture.shapes);
      }
      this.pseudoTexture = pTexture;
      this.texture = pTexture.texture;
      if (pTexture.shapes) pTexture.shapes = this.addChild(pTexture.shapes);
    } else if (this.originalTexture) {
      this._swapChildren(this.originalTexture);
      this._destroyTexture();
      this.pseudoTexture = this.originalTexture;
      this.texture = this.originalTexture.texture;
      if (this.originalTexture.shapes) this.pseudoTexture.shapes = this.addChild(this.originalTexture.shapes);
      delete this.originalTexture;
    } else {
      this._swapChildren(pTexture);
      this._destroyTexture();
      this.pseudoTexture = pTexture;
      this.texture = pTexture.texture;
      if (pTexture.shapes) this.pseudoTexture.shapes = this.addChild(pTexture.shapes);
    }

    if (this.pseudoTexture.html) this.addHTMLOverlay();
    if (refresh) this.refresh(configuration, { fullRefresh: !preview, refreshFilters });
  }

  refresh(configuration, { preview = false, fullRefresh = true, previewTexture = null, refreshFilters = false } = {}) {
    if (!this.overlayConfig || !this.texture) return;

    // Text preview handling
    if (previewTexture || this.originalTexture) {
      this.setTexture(previewTexture, { preview: Boolean(previewTexture), refresh: false });
    }

    // Register/Unregister hooks that should refresh this overlay
    if (configuration) {
      this._registerHooks(configuration);
    }

    const config = mergeObject(this.overlayConfig, configuration ?? {}, { inplace: !preview });

    if (preview && this.htmlOverlay) this.htmlOverlay.render(config, true);

    this.enableInteractivity(config);

    if (fullRefresh) {
      const source = foundry.utils.getProperty(this.texture, 'baseTexture.resource.source');
      if (source && source.tagName === 'VIDEO') {
        if (!source.loop && config.loop) {
          game.video.play(source);
        } else if (source.loop && !config.loop) {
          game.video.stop(source);
        }
        source.loop = config.loop;
      }
    }

    const shapes = this.pseudoTexture.shapes;

    // Scale the image using the same logic as the token
    const dimensions = shapes ?? this.texture;
    if (config.linkScale && !config.parentID) {
      const scale = this.scale;
      const aspect = dimensions.width / dimensions.height;
      if (aspect >= 1) {
        scale.x = (this.object.w * this.object.document.texture.scaleX) / dimensions.width;
        scale.y = Number(scale.x);
      } else {
        scale.y = (this.object.h * this.object.document.texture.scaleY) / dimensions.height;
        scale.x = Number(scale.y);
      }
    } else if (config.linkStageScale) {
      this.scale.x = 1 / canvas.stage.scale.x;
      this.scale.y = 1 / canvas.stage.scale.y;
    } else if (config.linkDimensionsX || config.linkDimensionsY) {
      if (config.linkDimensionsX) {
        this.scale.x = this.object.document.width;
      }
      if (config.linkDimensionsY) {
        this.scale.y = this.object.document.height;
      }
    } else {
      this.scale.x = config.width ? config.width / dimensions.width : 1;
      this.scale.y = config.height ? config.height / dimensions.height : 1;
    }

    // Adjust scale according to config
    this.scale.x = this.scale.x * config.scaleX;
    this.scale.y = this.scale.y * config.scaleY;

    // Check if mirroring should be inherited from the token and if so apply it
    if (config.linkMirror && !config.parentID) {
      this.scale.x = Math.abs(this.scale.x) * (this.object.document.texture.scaleX < 0 ? -1 : 1);
      this.scale.y = Math.abs(this.scale.y) * (this.object.document.texture.scaleY < 0 ? -1 : 1);
    }

    if (this.anchor) {
      if (!config.anchor) this.anchor.set(0.5, 0.5);
      else this.anchor.set(config.anchor.x, config.anchor.y);
    }

    let xOff = 0;
    let yOff = 0;
    if (shapes) {
      shapes.position.x = -this.anchor.x * shapes.width;
      shapes.position.y = -this.anchor.y * shapes.height;
      if (config.animation.relative) {
        this.pivot.set(0, 0);
        shapes.pivot.set((0.5 - this.anchor.x) * shapes.width, (0.5 - this.anchor.y) * shapes.height);
        xOff = shapes.pivot.x * this.scale.x;
        yOff = shapes.pivot.y * this.scale.y;
      }
    } else if (config.animation.relative) {
      xOff = (0.5 - this.anchor.x) * this.width;
      yOff = (0.5 - this.anchor.y) * this.height;
      this.pivot.set((0.5 - this.anchor.x) * this.texture.width, (0.5 - this.anchor.y) * this.texture.height);
    }

    // Position
    const pOffsetX = config.pOffsetX || 0;
    const pOffsetY = config.pOffsetY || 0;
    if (config.parentID) {
      const anchor = this.parent.anchor ?? { x: 0, y: 0 };
      const pWidth = (this.parent.shapesWidth ?? this.parent.width) / this.parent.scale.x;
      const pHeight = (this.parent.shapesHeight ?? this.parent.height) / this.parent.scale.y;
      this.position.set(
        pOffsetX + -config.offsetX * pWidth - anchor.x * pWidth + pWidth / 2,
        pOffsetY + -config.offsetY * pHeight - anchor.y * pHeight + pHeight / 2
      );
    } else {
      if (config.animation.relative) {
        this.position.set(
          this.object.document.x + this.object.w / 2 + pOffsetX + -config.offsetX * this.object.w + xOff,
          this.object.document.y + this.object.h / 2 + pOffsetY + -config.offsetY * this.object.h + yOff
        );
      } else {
        this.position.set(this.object.document.x + this.object.w / 2, this.object.document.y + this.object.h / 2);
        this.pivot.set(
          -pOffsetX / this.scale.x + (config.offsetX * this.object.w) / this.scale.x,
          -pOffsetY / this.scale.y + (config.offsetY * this.object.h) / this.scale.y
        );
      }
    }

    // Set alpha but only if playOnce is disabled and the video hasn't
    // finished playing yet. Otherwise we want to keep alpha as 0 to keep the video hidden
    if (!this.tvaVideoEnded) {
      this.alpha = config.linkOpacity ? this.object.document.alpha : config.alpha;
    }

    // Angle in degrees
    if (fullRefresh) {
      if (config.linkRotation) this.angle = this.object.document.rotation + config.angle;
      else this.angle = config.angle;
    } else if (!config.animation.rotate) {
      if (config.linkRotation) this.angle = this.object.document.rotation + config.angle;
    }

    // Apply color tinting
    const tint = config.inheritTint
      ? this.object.document.texture.tint
      : interpolateColor(config.tint, config.interpolateColor, true);
    this.tint = tint ? Color.from(tint) : 0xffffff;
    if (shapes) {
      shapes.tint = this.tint;
      shapes.alpha = this.alpha;
    }

    if (fullRefresh) {
      if (config.animation.rotate) {
        this.animate(config);
      } else {
        this.stopAnimation();
      }
    }

    // Apply filters
    if (fullRefresh && (refreshFilters || !this.filtersApplied)) this._applyFilters(config);
    //if (fullRefresh) this.filters = this._getFilters(config);

    if (preview && this.children) {
      this.children.forEach((ch) => {
        if (ch instanceof TVAOverlay) ch.refresh(null, { preview: true });
      });
    }

    if (this.htmlOverlay) {
      this.htmlOverlay.setPosition({
        left: this.x - this.pivot.x * this.scale.x - this.width * this.anchor.x,
        top: this.y - this.pivot.y * this.scale.y - this.height * this.anchor.y,
        width: this.width,
        height: this.height,
        angle: this.angle,
      });
    }

    this.ready = true;
  }

  _activateTicker() {
    this._deactivateTicker();
    canvas.app.ticker.add(this.updatePosition, this, PIXI.UPDATE_PRIORITY.HIGH);
  }

  _deactivateTicker() {
    canvas.app.ticker.remove(this.updatePosition, this);
  }

  updatePosition() {
    let coord = canvas.canvasCoordinatesFromClient({
      x: window.innerWidth / 2 + this.overlayConfig.offsetX * window.innerWidth,
      y: window.innerHeight / 2 + this.overlayConfig.offsetY * window.innerHeight,
    });
    this.position.set(coord.x, coord.y);
  }

  async _applyFilters(config) {
    this.filtersApplied = true;
    const filterName = config.filter;
    const FilterClass = PIXI.filters[filterName];
    const options = mergeObject(FILTERS[filterName]?.defaultValues || {}, config.filterOptions);
    let filter;
    if (FilterClass) {
      if (FILTERS[filterName]?.argType === 'args') {
        let args = [];
        const controls = FILTERS[filterName]?.controls;
        if (controls) {
          controls.forEach((c) => args.push(options[c.name]));
        }
        filter = new FilterClass(...args);
      } else if (FILTERS[filterName]?.argType === 'options') {
        filter = new FilterClass(options);
      } else {
        filter = new FilterClass();
      }
    } else if (filterName === 'OutlineOverlayFilter') {
      filter = OutlineFilter.create(options);
      filter.thickness = options.trueThickness ?? 1;
      filter.animate = options.animate ?? false;
    } else if (filterName === 'Token Magic FX') {
      this.applyTVAFilters(await constructTMFXFilters(options.params || [], this));
      return;
    }

    if (filter) {
      this.applyTVAFilters([filter]);
      this.filters = [filter];
    } else {
      this.removeTVAFilters();
    }

    if (this.overlayConfig.ui && this.overlayConfig.bottom) this.applyReverseMask();
    else this.removeReverseMask();
  }

  applyReverseMask() {
    if (!this.filters?.find((f) => f.tvaReverse)) {
      const filters = this.filters || [];
      const reverseMask = ReverseMaskFilter.create({
        uMaskSampler: canvas.primary.tokensRenderTexture,
        channel: 'a',
      });
      reverseMask.tvaReverse = true;
      filters.push(reverseMask);
      this.filters = filters;
    }
    if (!this.filters) filters = [];
  }

  removeReverseMask() {
    if (this.filters?.length) {
      this.filters = this.filters.filter((f) => !f.tvaReverse);
    }
  }

  applyTVAFilters(filters) {
    if (filters?.length) {
      this.removeTVAFilters();
      this.filters = (this.filters || []).concat(filters);
    }
  }

  removeTVAFilters() {
    if (this.filters) this.filters = this.filters.filter((f) => !f.tvaFilter);
  }

  async stopAnimation() {
    if (this.animationName) {
      CanvasAnimation.terminateAnimation(this.animationName);
    }
  }

  async animate(config) {
    if (!this.animationName) this.animationName = this.object.sourceId + '.' + randomID(5);

    let newAngle = this.angle + (config.animation.clockwise ? 360 : -360);
    const rotate = [{ parent: this, attribute: 'angle', to: newAngle }];

    const completed = await CanvasAnimation.animate(rotate, {
      duration: config.animation.duration,
      name: this.animationName,
    });
    if (completed) {
      this.animate(config);
    }
  }

  _registerHooks(configuration) {
    if (configuration.linkStageScale) registerOverlayRefreshHook(this, 'canvasPan');
    else unregisterOverlayRefreshHooks(this, 'canvasPan');
  }

  _swapChildren(to) {
    const from = this.pseudoTexture;
    if (from.shapes) {
      this.removeChild(this.pseudoTexture.shapes);
      const children = from.shapes.removeChildren();
      if (to?.shapes) children.forEach((c) => to.shapes.addChild(c)?.refresh());
      else children.forEach((c) => this.addChild(c)?.refresh());
    } else if (to?.shapes) {
      const children = this.removeChildren();
      children.forEach((c) => to.shapes.addChild(c)?.refresh());
    }
  }

  _destroyTexture() {
    if (this.texture.textLabel || this.texture.destroyable) {
      this.texture.destroy(true);
    }

    if (this.pseudoTexture?.shapes) {
      this.removeChild(this.pseudoTexture.shapes);
      this.pseudoTexture.shapes.destroy();
    } else if (this.pseudoTexture?.html) {
      this.removeHTMLOverlay();
    }
  }

  destroy() {
    this.stopAnimation();
    unregisterOverlayRefreshHooks(this);

    if (this.children) {
      for (const ch of this.children) {
        if (ch instanceof TVAOverlay) ch.tvaRemove = true;
      }
      removeMarkedOverlays(this.object);
      if (this.pseudoTexture.shapes) {
        this.pseudoTexture.shapes.children.forEach((c) => c.destroy());
        this.removeChild(this.pseudoTexture.shapes)?.destroy();
        //  this.pseudoTexture.shapes.destroy();
      }
    }

    if (this.texture.textLabel || this.texture.destroyable) {
      return super.destroy(true);
    } else if (this.texture?.baseTexture.resource?.source?.tagName === 'VIDEO') {
      this.texture.baseTexture.destroy();
    }

    this.removeHTMLOverlay();

    super.destroy();
  }

  // Foundry BUG Fix
  calculateTrimmedVertices() {
    return PIXI.Sprite.prototype.calculateTrimmedVertices.call(this);
  }

  addHTMLOverlay() {
    if (!this.htmlOverlay) this.htmlOverlay = new HTMLOverlay(this.overlayConfig, this.object);
  }

  removeHTMLOverlay() {
    if (this.htmlOverlay) this.htmlOverlay.remove();
    this.htmlOverlay = null;
  }
}

async function constructTMFXFilters(paramsArray, sprite) {
  if (typeof TokenMagic === 'undefined' || !('filterTypes' in TokenMagic)) return [];

  try {
    paramsArray = eval(paramsArray);
  } catch (e) {
    return [];
  }

  if (!Array.isArray(paramsArray)) {
    paramsArray = TokenMagic.getPreset(paramsArray);
  }
  if (!(paramsArray instanceof Array && paramsArray.length > 0)) return [];

  let filters = [];
  for (const params of paramsArray) {
    if (!params.hasOwnProperty('filterType') || !TokenMagic.filterTypes.hasOwnProperty(params.filterType)) {
      // one invalid ? all rejected.
      return [];
    }

    if (!params.hasOwnProperty('rank')) {
      params.rank = 5000;
    }

    if (!params.hasOwnProperty('filterId') || params.filterId == null) {
      params.filterId = randomID();
    }

    if (!params.hasOwnProperty('enabled') || !(typeof params.enabled === 'boolean')) {
      params.enabled = true;
    }

    params.filterInternalId = randomID();

    const gms = game.users.filter((user) => user.isGM);
    params.filterOwner = gms.length ? gms[0].id : game.data.userId;
    // params.placeableType = placeable._TMFXgetPlaceableType();
    params.updateId = randomID();

    const filterClass = TokenMagic.filterTypes[params.filterType];
    if (filterClass) {
      class ModTMFX extends filterClass {
        assignPlaceable() {
          this.targetPlaceable = sprite.object;
          this.placeableImg = sprite;
        }

        async _TMFXsetAnimeFlag() {}
      }

      const filter = new ModTMFX(params);
      if (filter) {
        // Patch fixes
        filter.placeableImg = sprite;
        filter.targetPlaceable = sprite.object;
        // end of fixes
        filter.tvaFilter = true;
        filters.unshift(filter);
      }
    }
  }
  return filters;
}

class OutlineFilter extends OutlineOverlayFilter {
  /** @inheritdoc */
  static createFragmentShader() {
    return `
    varying vec2 vTextureCoord;
    varying vec2 vFilterCoord;
    uniform sampler2D uSampler;
    
    uniform vec2 thickness;
    uniform vec4 outlineColor;
    uniform vec4 filterClamp;
    uniform float alphaThreshold;
    uniform float time;
    uniform bool knockout;
    uniform bool wave;
    
    ${this.CONSTANTS}
    ${this.WAVE()}
    
    void main(void) {
        float dist = distance(vFilterCoord, vec2(0.5)) * 2.0;
        vec4 ownColor = texture2D(uSampler, vTextureCoord);
        vec4 wColor = wave ? outlineColor * 
                             wcos(0.0, 1.0, dist * 75.0, 
                                  -time * 0.01 + 3.0 * dot(vec4(1.0), ownColor)) 
                             * 0.33 * (1.0 - dist) : vec4(0.0);
        float texAlpha = smoothstep(alphaThreshold, 1.0, ownColor.a);
        vec4 curColor;
        float maxAlpha = 0.;
        vec2 displaced;
        for ( float angle = 0.0; angle <= TWOPI; angle += ${this.#quality.toFixed(7)} ) {
            displaced.x = vTextureCoord.x + thickness.x * cos(angle);
            displaced.y = vTextureCoord.y + thickness.y * sin(angle);
            curColor = texture2D(uSampler, clamp(displaced, filterClamp.xy, filterClamp.zw));
            curColor.a = clamp((curColor.a - 0.6) * 2.5, 0.0, 1.0);
            maxAlpha = max(maxAlpha, curColor.a);
        }
        float resultAlpha = max(maxAlpha, texAlpha);
        vec3 result = (ownColor.rgb + outlineColor.rgb * (1.0 - texAlpha)) * resultAlpha;
        gl_FragColor = vec4((ownColor.rgb + outlineColor.rgb * (1. - ownColor.a)) * resultAlpha, resultAlpha);
    }
    `;
  }

  static get #quality() {
    switch (canvas.performance.mode) {
      case CONST.CANVAS_PERFORMANCE_MODES.LOW:
        return (Math.PI * 2) / 10;
      case CONST.CANVAS_PERFORMANCE_MODES.MED:
        return (Math.PI * 2) / 20;
      default:
        return (Math.PI * 2) / 30;
    }
  }
}
