import { CORE_SHAPE, DEFAULT_OVERLAY_CONFIG, OVERLAY_SHAPES } from '../scripts/models.js';
import { VALID_EXPRESSION, getAllEffectMappings } from '../scripts/hooks/effectMappingHooks.js';
import { evaluateOverlayExpressions, genTexture } from '../scripts/token/overlay.js';
import { SEARCH_TYPE } from '../scripts/utils.js';
import { showArtSelect } from '../token-variants.mjs';
import { sortMappingsToGroups } from './effectMappingForm.js';
import { getFlagMappings } from '../scripts/settings.js';
import { Reticle } from '../scripts/reticle.js';

export class OverlayConfig extends FormApplication {
  constructor(config, callback, token, { mapping, global = false, actor } = {}) {
    super({}, {});
    this.config = config ?? {};

    if ((this.config.img || this.config.imgLinked) && !(this.config.img instanceof Array)) {
      this.config.img = [{ src: this.config.img, linked: this.config.imgLinked }];
    }

    this.config.id = mapping.id;
    this.callback = callback;
    this.token = canvas.tokens.get(token._id);
    this.previewConfig = deepClone(this.config);
    this.mapping = mapping;
    this.global = global;
    this.actor = actor;
  }

  static get defaultOptions() {
    return mergeObject(super.defaultOptions, {
      id: 'token-variants-overlay-config',
      classes: ['sheet'],
      template: 'modules/token-variants/templates/overlayConfig.html',
      resizable: false,
      minimizable: false,
      title: 'Overlay Settings',
      width: 500,
      height: 'auto',
      //tabs: [{ navSelector: '.sheet-tabs', contentSelector: '.content', initial: 'misc' }, ],

      tabs: [
        { navSelector: '.tabs[data-group="main"]', contentSelector: 'form', initial: 'misc' },
        { navSelector: '.tabs[data-group="html"]', contentSelector: '.tab[data-tab="html"]', initial: 'template' },
      ],
    });
  }

  get title() {
    let scope = 'GLOBAL';
    if (!this.global && this.actor) {
      scope = this.actor.name;
    }
    return `${this.mapping.label} — [${scope}]`;
  }

  /**
   * @param {JQuery} html
   */
  activateListeners(html) {
    super.activateListeners(html);

    html.find('.reticle').on('click', (event) => {
      const icons = this.getPreviewIcons();
      if (icons.length) {
        Reticle.activate({ tvaOverlay: icons[0].icon, app: this, config: this.previewConfig });
      }
    });

    html.find('.repeat').on('change', (event) => {
      const fieldset = $(event.target).closest('fieldset');
      const content = fieldset.find('.content');
      if (event.target.checked) {
        content.show();
        fieldset.addClass('active');
      } else {
        content.hide();
        fieldset.removeClass('active');
      }
      this.setPosition();
    });

    // Insert Controls to the Shape Legend
    const shapeLegends = html.find('.shape-legend');
    let config = this.config;
    shapeLegends.each(function (i) {
      const legend = $(this);
      legend.append(
        `&nbsp;<a class="cloneShape" data-index="${i}" title="Clone"><i class="fas fa-clone"></i></a>
         &nbsp;<a class="deleteShape" data-index="${i}" title="Remove"><i class="fas fa-trash-alt"></i></a>`
      );
      if (i != 0) {
        legend.append(
          `&nbsp;<a class="moveShapeUp" data-index="${i}" title="Move Up"><i class="fas fa-arrow-up"></i></a>`
        );
      }
      if (i != shapeLegends.length - 1) {
        legend.append(
          `&nbsp;<a class="moveShapeDown" data-index="${i}" title="Move Down"><i class="fas fa-arrow-down"></i></a>`
        );
      }
      legend.append(
        `<input class="shape-legend-input" type="text" name="shapes.${i}.label" value="${
          config.shapes?.[i]?.label ?? ''
        }">`
      );
    });

    // Shape listeners
    html.find('.addShape').on('click', this._onAddShape.bind(this));
    html.find('.addImage').on('click', this._onAddImage.bind(this));
    html.find('.addEvent').on('click', this._onAddEvent.bind(this));
    html.find('.deleteShape').on('click', this._onDeleteShape.bind(this));
    html.find('.deleteImage').on('click', this._onDeleteImage.bind(this));
    html.find('.deleteEvent').on('click', this._onDeleteEvent.bind(this));
    html.find('.moveShapeUp').on('click', this._onMoveShapeUp.bind(this));
    html.find('.moveShapeDown').on('click', this._onMoveShapeDown.bind(this));
    html.find('.cloneShape').on('click', this._onCloneShape.bind(this));

    html.find('input,select').on('change', this._onInputChange.bind(this));
    html.find('textarea').on('change', this._onInputChange.bind(this));
    const parentId = html.find('[name="parentID"]');
    parentId.on('change', (event) => {
      if (event.target.value === 'TOKEN') {
        html.find('.token-specific-fields').show();
      } else {
        html.find('.token-specific-fields').hide();
      }
      this.setPosition();
    });
    parentId.trigger('change');
    html
      .find('[name="ui"]')
      .on('change', (event) => {
        if (parentId.val() === 'TOKEN') {
          if (event.target.checked) {
            html.find('.ui-hide').hide();
          } else {
            html.find('.ui-hide').show();
          }
          this.setPosition();
        }
      })
      .trigger('change');

    html.find('[name="filter"]').on('change', (event) => {
      html.find('.filterOptions').empty();
      const filterOptions = $(genFilterOptionControls(event.target.value));
      html.find('.filterOptions').append(filterOptions);
      this.setPosition({ height: 'auto' });
      this.activateListeners(filterOptions);
    });

    html.find('.token-variants-image-select-button').click((event) => {
      showArtSelect(this.token?.name ?? 'overlay', {
        searchType: SEARCH_TYPE.TOKEN,
        callback: (imgSrc, imgName) => {
          if (imgSrc) $(event.target).closest('.form-group').find('input').val(imgSrc).trigger('change');
        },
      });
    });

    html.find('.presetImport').on('click', (event) => {
      const presetName = $(event.target).closest('.form-group').find('.tmfxPreset').val();
      if (presetName) {
        const preset = TokenMagic.getPreset(presetName);
        if (preset) {
          $(event.target)
            .closest('.form-group')
            .find('textarea')
            .val(JSON.stringify(preset, null, 2))
            .trigger('change');
        }
      }
    });

    // Controls for locking scale sliders together
    let scaleState = { locked: true };

    // Range inputs need to be triggered when slider moves to initiate preview
    html
      .find('.range-value')
      .siblings('[type="range"]')
      .on('change', (event) => {
        $(event.target).siblings('.range-value').val(event.target.value).trigger('change');
      });

    const lockButtons = $(html).find('.scaleLock > a');
    const sliderScaleWidth = $(html).find('[name="scaleX"]');
    const sliderScaleHeight = $(html).find('[name="scaleY"]');
    const sliderWidth = html.find('.scaleX');
    const sliderHeight = html.find('.scaleY');

    lockButtons.on('click', function () {
      scaleState.locked = !scaleState.locked;
      lockButtons.html(scaleState.locked ? '<i class="fas fa-link"></i>' : '<i class="fas fa-unlink"></i>');
    });

    sliderScaleWidth.on('change', function () {
      if (scaleState.locked && sliderScaleWidth.val() !== sliderScaleHeight.val()) {
        sliderScaleHeight.val(sliderScaleWidth.val()).trigger('change');
        sliderHeight.val(sliderScaleWidth.val());
      }
    });
    sliderScaleHeight.on('change', function () {
      if (scaleState.locked && sliderScaleWidth.val() !== sliderScaleHeight.val()) {
        sliderScaleWidth.val(sliderScaleHeight.val()).trigger('change');
        sliderWidth.val(sliderScaleHeight.val());
      }
    });
    html.on('change', '.scaleX', () => {
      sliderScaleWidth.trigger('change');
    });
    html.on('change', '.scaleY', () => {
      sliderScaleHeight.trigger('change');
    });

    html.find('.me-edit-json').on('click', async (event) => {
      const textarea = $(event.target).closest('.form-group').find('textarea');
      let params;
      try {
        params = eval(textarea.val());
      } catch (e) {
        console.warn('TVA |', e);
      }

      if (params) {
        let param;
        if (Array.isArray(params)) {
          if (params.length === 1) param = params[0];
          else {
            let i = await promptParamChoice(params);
            if (i < 0) return;
            param = params[i];
          }
        } else {
          param = params;
        }

        if (param)
          game.modules.get('multi-token-edit').api.showGenericForm(param, param.filterType ?? 'TMFX', {
            inputChangeCallback: (selected) => {
              mergeObject(param, selected, { inplace: true });
              textarea.val(JSON.stringify(params, null, 2)).trigger('change');
            },
          });
      }
    });

    const underlay = html.find('[name="underlay"]');
    const top = html.find('[name="top"]');
    const bottom = html.find('[name="bottom"]');
    underlay.change(function () {
      if (this.checked) top.prop('checked', false);
      else bottom.prop('checked', false);
    });
    top.change(function () {
      if (this.checked) {
        underlay.prop('checked', false);
        bottom.prop('checked', false);
      }
    });
    bottom.change(function () {
      if (this.checked) {
        underlay.prop('checked', true);
        top.prop('checked', false);
      }
    });

    const linkScale = html.find('[name="linkScale"]');
    const linkDimensions = html.find('[name="linkDimensionsX"], [name="linkDimensionsY"]');
    const linkStageScale = html.find('[name="linkStageScale"]');
    linkScale.change(function () {
      if (this.checked) {
        linkDimensions.prop('checked', false);
        linkStageScale.prop('checked', false);
      }
    });
    linkDimensions.change(function () {
      if (this.checked) {
        linkScale.prop('checked', false);
        linkStageScale.prop('checked', false);
      }
    });
    linkStageScale.change(function () {
      if (this.checked) {
        linkScale.prop('checked', false);
        linkDimensions.prop('checked', false);
      }
    });

    // Setting border color for property expression
    const limitOnProperty = html.find('[name="limitOnProperty"]');
    limitOnProperty.on('input', (event) => {
      const input = $(event.target);
      if (input.val() === '') {
        input.removeClass('tvaValid');
        input.removeClass('tvaInvalid');
      } else if (input.val().match(VALID_EXPRESSION)) {
        input.addClass('tvaValid');
        input.removeClass('tvaInvalid');
      } else {
        input.addClass('tvaInvalid');
        input.removeClass('tvaValid');
      }
    });
    limitOnProperty.trigger('input');

    html.find('.create-variable').on('click', this._onCreateVariable.bind(this));
    html.find('.delete-variable').on('click', this._onDeleteVariable.bind(this));
  }

  _onDeleteVariable(event) {
    let index = $(event.target).closest('tr').data('index');
    if (index != null) {
      this.config = this._getSubmitData();
      if (!this.config.variables) this.config.variables = [];
      this.config.variables.splice(index, 1);
      this.render(true);
    }
  }

  _onCreateVariable(event) {
    this.config = this._getSubmitData();
    if (!this.config.variables) this.config.variables = [];
    this.config.variables.push({ name: '', value: '' });
    this.render(true);
  }

  _onAddShape(event) {
    let shape = $(event.target).siblings('select').val();
    shape = deepClone(OVERLAY_SHAPES[shape]);
    shape = mergeObject(deepClone(CORE_SHAPE), { shape });

    this.config = this._getSubmitData();

    if (!this.config.shapes) this.config.shapes = [];
    this.config.shapes.push(shape);

    this.render(true);
  }

  _onAddImage(event) {
    this.config = this._getSubmitData();
    if (!this.config.img) this.config.img = [];
    else if (!(this.config.img instanceof Array)) {
      this.config.img = [{ src: this.config.img, linked: this.config.imgLinked }];
    }
    this.config.img.push({ src: '', linked: false });
    this.render(true);
  }

  _onAddEvent(event) {
    let listener = $(event.target).siblings('select').val();

    this.config = this._getSubmitData();
    if (!this.config.interactivity) this.config.interactivity = [];
    this.config.interactivity.push({ listener, macro: '', script: '' });

    this.render(true);
  }

  _onDeleteShape(event) {
    const index = $(event.target).closest('.deleteShape').data('index');
    if (index == null) return;

    this.config = this._getSubmitData();
    if (!this.config.shapes) this.config.shapes = [];
    this.config.shapes.splice(index, 1);

    this.render(true);
  }

  _onDeleteImage(event) {
    const index = $(event.target).closest('.deleteImage').data('index');
    if (index == null) return;

    this.config = this._getSubmitData();
    if (!(this.config.img instanceof Array)) this.config.img = [];
    else this.config.img.splice(index, 1);

    if (!this.config.img.length) this.config.img = '';

    this.render(true);
  }

  _onDeleteEvent(event) {
    const index = $(event.target).closest('.deleteEvent').data('index');
    if (index == null) return;

    this.config = this._getSubmitData();
    if (!this.config.interactivity) this.config.interactivity = [];
    this.config.interactivity.splice(index, 1);

    this.render(true);
  }

  _onCloneShape(event) {
    const index = $(event.target).closest('.cloneShape').data('index');
    if (!index && index != 0) return;

    this.config = this._getSubmitData();
    if (!this.config.shapes) return;
    const nShape = deepClone(this.config.shapes[index]);
    if (nShape.label) {
      nShape.label = nShape.label + ' - Copy';
    }
    this.config.shapes.push(nShape);

    this.render(true);
  }

  _onMoveShapeUp(event) {
    const index = $(event.target).closest('.moveShapeUp').data('index');
    if (!index) return;

    this.config = this._getSubmitData();
    if (!this.config.shapes) this.config.shapes = [];
    if (this.config.shapes.length >= 2) this._swapShapes(index, index - 1);

    this.render(true);
  }

  _onMoveShapeDown(event) {
    const index = $(event.target).closest('.moveShapeDown').data('index');
    if (!index && index != 0) return;

    this.config = this._getSubmitData();
    if (!this.config.shapes) this.config.shapes = [];
    if (this.config.shapes.length >= 2) this._swapShapes(index, index + 1);

    this.render(true);
  }

  _swapShapes(i1, i2) {
    let temp = this.config.shapes[i1];
    this.config.shapes[i1] = this.config.shapes[i2];
    this.config.shapes[i2] = temp;
  }

  _convertColor(colString) {
    try {
      const c = Color.fromString(colString);
      const rgba = c.rgb;
      rgba.push(1);
      return rgba;
    } catch (e) {
      return [1, 1, 1, 1];
    }
  }

  async _onInputChange(event) {
    this.previewConfig = this._getSubmitData();
    if (event.target.type === 'color') {
      const color = $(event.target).siblings('.color');
      color.val(event.target.value).trigger('change');
      return;
    }
    this._applyPreviews();
  }

  getPreviewIcons() {
    if (!this.config.id) return [];
    const tokens = this.token ? [this.token] : canvas.tokens.placeables;
    const previewIcons = [];
    for (const tkn of tokens) {
      if (tkn.tvaOverlays) {
        for (const c of tkn.tvaOverlays) {
          if (c.overlayConfig && c.overlayConfig.id === this.config.id) {
            // Effect icon found, however if we're in global preview then we need to take into account
            // a token/actor specific mapping which may override the global one
            if (this.token) {
              previewIcons.push({ token: tkn, icon: c });
            } else if (!getFlagMappings(tkn).find((m) => m.id === this.config.id)) {
              previewIcons.push({ token: tkn, icon: c });
            }
          }
        }
      }
    }
    return previewIcons;
  }

  async _applyPreviews() {
    const targets = this.getPreviewIcons();
    for (const target of targets) {
      const preview = evaluateOverlayExpressions(deepClone(this.previewConfig), target.token, {
        overlayConfig: this.previewConfig,
      });
      target.icon.refresh(preview, {
        preview: true,
        previewTexture: await genTexture(target.token, preview),
        refreshFilters: true,
      });
    }
  }

  async _removePreviews() {
    const targets = this.getPreviewIcons();
    for (const target of targets) {
      target.icon.refresh(null, { refreshFilters: true });
    }
  }

  async getData(options) {
    const data = super.getData(options);
    data.filters = Object.keys(PIXI.filters);
    data.filters.push('OutlineOverlayFilter');
    data.filters.sort();
    data.tmfxActive = game.modules.get('tokenmagic')?.active;
    if (data.tmfxActive) {
      data.tmfxPresets = TokenMagic.getPresets().map((p) => p.name);
      data.filters.unshift('Token Magic FX');
    }
    data.filters.unshift('NONE');
    const settings = mergeObject(DEFAULT_OVERLAY_CONFIG, this.config, {
      inplace: false,
    });

    data.ceActive = game.modules.get('dfreds-convenient-effects')?.active;
    if (data.ceActive) {
      data.ceEffects = game.dfreds.effects.all.map((ef) => ef.name);
    }
    data.macros = game.macros.map((m) => m.name);

    if (settings.filter !== 'NONE') {
      const filterOptions = genFilterOptionControls(settings.filter, settings.filterOptions);
      if (filterOptions) {
        settings.filterOptions = filterOptions;
      } else {
        settings.filterOptions = null;
      }
    } else {
      settings.filterOptions = null;
    }

    data.users = game.users.map((u) => {
      return { id: u.id, name: u.name, selected: settings.limitedUsers.includes(u.id) };
    });

    data.fonts = Object.keys(CONFIG.fontDefinitions);

    const allMappings = getAllEffectMappings(this.token, true).filter((m) => m.id !== this.config.id);
    const [_, groupedMappings] = sortMappingsToGroups(allMappings);

    data.parents = groupedMappings;
    if (!data.parentID) data.parentID = 'TOKEN';
    if (!data.anchor) data.anchor = { x: 0.5, y: 0.5 };

    // Cache Partials
    for (const shapeName of Object.keys(OVERLAY_SHAPES)) {
      await getTemplate(`modules/token-variants/templates/partials/shape${shapeName}.html`);
    }
    await getTemplate('modules/token-variants/templates/partials/repeating.html');
    await getTemplate('modules/token-variants/templates/partials/interpolateColor.html');

    data.allShapes = Object.keys(OVERLAY_SHAPES);
    data.textAlignmentOptions = [
      { value: 'left', label: 'Left' },
      { value: 'center', label: 'Center' },
      { value: 'right', label: 'Right' },
      { value: 'justify', label: 'Justify' },
    ];

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

    return mergeObject(data, settings);
  }

  _getHeaderButtons() {
    const buttons = super._getHeaderButtons();
    buttons.unshift({
      label: 'Core Variables',
      class: '.core-variables',
      icon: 'fas fa-file-import fa-fw',
      onclick: () => {
        let content = `
        <table>
          <tr><th>Variable</th><th>Description</th></tr>
          <tr><td>@hp</td><td>Actor Health</td></tr>
          <tr><td>@hpMax</td><td>Actor Health (Max)</td></tr>
          <tr><td>@gridSize</td><td>Grid Size (Pixels)</td></tr>
          <tr><td>@label</td><td>Mapping's Label Field</td></tr>
        </table>
        `;

        new Dialog({
          title: `Core Variables`,
          content,
          buttons: {},
        }).render(true);
      },
    });
    return buttons;
  }

  async close(options = {}) {
    super.close(options);
    this._removePreviews();
  }

  _getSubmitData() {
    let formData = super._getSubmitData();
    formData = expandObject(formData);
    if (formData.img) {
      const images = Object.values(formData.img);
      if (images.length === 1) {
        formData.img = images[0].src;
        formData.imgLinked = images[0].linked;
      } else {
        formData.img = images;
      }
    }

    if (!formData.repeating) delete formData.repeat;
    if (!formData.text.repeating) delete formData.text.repeat;

    if (formData.shapes) {
      formData.shapes = Object.values(formData.shapes);
      formData.shapes.forEach((shape) => {
        if (!shape.repeating) delete shape.repeat;
      });
    }

    if (formData.interactivity) {
      formData.interactivity = Object.values(formData.interactivity)
        .map((e) => {
          e.macro = e.macro.trim();
          e.script = e.script.trim();
          if (e.tmfxPreset) e.tmfxPreset = e.tmfxPreset.trim();
          if (e.ceEffect) e.ceEffect = e.ceEffect.trim();
          return e;
        })
        .filter((e) => e.macro || e.script || e.ceEffect || e.tmfxPreset);
    } else {
      formData.interactivity = [];
    }

    if (formData.variables) {
      formData.variables = Object.values(formData.variables);
      formData.variables = formData.variables.filter((v) => v.name.trim() && v.value.trim());
    }
    if (formData.limitedUsers) {
      if (getType(formData.limitedUsers) === 'string') formData.limitedUsers = [formData.limitedUsers];
      formData.limitedUsers = formData.limitedUsers.filter((uid) => uid);
    } else {
      formData.limitedUsers = [];
    }

    formData.limitOnEffect = formData.limitOnEffect.trim();
    formData.limitOnProperty = formData.limitOnProperty.trim();
    if (formData.parentID === 'TOKEN') formData.parentID = '';

    if (formData.filter === 'OutlineOverlayFilter' && 'filterOptions.outlineColor' in formData) {
      formData['filterOptions.outlineColor'] = this._convertColor(formData['filterOptions.outlineColor']);
    } else if (formData.filter === 'BevelFilter') {
      if ('filterOptions.lightColor' in formData)
        formData['filterOptions.lightColor'] = Number(Color.fromString(formData['filterOptions.lightColor']));
      if ('filterOptions.shadowColor' in formData)
        formData['filterOptions.shadowColor'] = Number(Color.fromString(formData['filterOptions.shadowColor']));
    } else if (['DropShadowFilter', 'GlowFilter', 'OutlineFilter', 'FilterFire'].includes(formData.filter)) {
      if ('filterOptions.color' in formData)
        formData['filterOptions.color'] = Number(Color.fromString(formData['filterOptions.color']));
    }

    return formData;
  }

  /**
   * @param {Event} event
   * @param {Object} formData
   */
  async _updateObject(event, formData) {
    if (this.callback) this.callback(formData);
  }
}

export const FILTERS = {
  OutlineOverlayFilter: {
    defaultValues: {
      outlineColor: [0, 0, 0, 1],
      trueThickness: 1,
      animate: false,
    },
    controls: [
      {
        type: 'color',
        name: 'outlineColor',
      },
      {
        type: 'range',
        label: 'Thickness',
        name: 'trueThickness',
        min: 0,
        max: 5,
        step: 0.01,
      },
      {
        type: 'boolean',
        label: 'Oscillate',
        name: 'animate',
      },
    ],
    argType: 'args',
  },
  AlphaFilter: {
    defaultValues: {
      alpha: 1,
    },
    controls: [
      {
        type: 'range',
        name: 'alpha',
        min: 0,
        max: 1,
        step: 0.01,
      },
    ],
    argType: 'args',
  },
  BlurFilter: {
    defaultValues: {
      strength: 8,
      quality: 4,
    },
    controls: [
      { type: 'range', name: 'strength', min: 0, max: 20, step: 1 },
      { type: 'range', name: 'quality', min: 0, max: 20, step: 1 },
    ],
    argType: 'args',
  },
  BlurFilterPass: {
    defaultValues: {
      horizontal: false,
      strength: 8,
      quality: 4,
    },
    controls: [
      {
        type: 'boolean',
        name: 'horizontal',
      },
      { type: 'range', name: 'strength', min: 0, max: 20, step: 1 },
      { type: 'range', name: 'quality', min: 0, max: 20, step: 1 },
    ],
    argType: 'args',
  },
  NoiseFilter: {
    defaultValues: {
      noise: 0.5,
      seed: 4475160954091,
    },
    controls: [
      { type: 'range', name: 'noise', min: 0, max: 1, step: 0.01 },
      { type: 'range', name: 'seed', min: 0, max: 100000, step: 1 },
    ],
    argType: 'args',
  },
  AdjustmentFilter: {
    defaultValues: {
      gamma: 1,
      saturation: 1,
      contrast: 1,
      brightness: 1,
      red: 1,
      green: 1,
      blue: 1,
      alpha: 1,
    },
    controls: [
      { type: 'range', name: 'gamma', min: 0, max: 1, step: 0.01 },
      { type: 'range', name: 'saturation', min: 0, max: 1, step: 0.01 },
      { type: 'range', name: 'contrast', min: 0, max: 1, step: 0.01 },
      { type: 'range', name: 'brightness', min: 0, max: 1, step: 0.01 },
      { type: 'range', name: 'red', min: 0, max: 1, step: 0.01 },
      { type: 'range', name: 'green', min: 0, max: 1, step: 0.01 },
      { type: 'range', name: 'blue', min: 0, max: 1, step: 0.01 },
      { type: 'range', name: 'alpha', min: 0, max: 1, step: 0.01 },
    ],
    argType: 'options',
  },
  AdvancedBloomFilter: {
    defaultValues: {
      threshold: 0.5,
      bloomScale: 1,
      brightness: 1,
      blur: 8,
      quality: 4,
    },
    controls: [
      { type: 'range', name: 'threshold', min: 0, max: 1, step: 0.01 },
      { type: 'range', name: 'bloomScale', min: 0, max: 5, step: 0.01 },
      { type: 'range', name: 'brightness', min: 0, max: 1, step: 0.01 },
      { type: 'range', name: 'blur', min: 0, max: 20, step: 1 },
      { type: 'range', name: 'quality', min: 0, max: 20, step: 1 },
    ],
    argType: 'options',
  },
  AsciiFilter: {
    defaultValues: {
      size: 8,
    },
    controls: [{ type: 'range', name: 'size', min: 0, max: 20, step: 0.01 }],
    argType: 'args',
  },
  BevelFilter: {
    defaultValues: {
      rotation: 45,
      thickness: 2,
      lightColor: 0xffffff,
      lightAlpha: 0.7,
      shadowColor: 0x000000,
      shadowAlpha: 0.7,
    },
    controls: [
      { type: 'range', name: 'rotation', min: 0, max: 360, step: 1 },
      { type: 'range', name: 'thickness', min: 0, max: 20, step: 0.01 },
      { type: 'color', name: 'lightColor' },
      { type: 'range', name: 'lightAlpha', min: 0, max: 1, step: 0.01 },
      { type: 'color', name: 'shadowColor' },
      { type: 'range', name: 'shadowAlpha', min: 0, max: 1, step: 0.01 },
    ],
    argType: 'options',
  },
  BloomFilter: {
    defaultValues: {
      blur: 2,
      quality: 4,
    },
    controls: [
      { type: 'range', name: 'blur', min: 0, max: 20, step: 1 },
      { type: 'range', name: 'quality', min: 0, max: 20, step: 1 },
    ],
    argType: 'args',
  },
  BulgePinchFilter: {
    defaultValues: {
      radius: 100,
      strength: 1,
    },
    controls: [
      { type: 'range', name: 'radius', min: 0, max: 500, step: 1 },
      { type: 'range', name: 'strength', min: -1, max: 1, step: 0.01 },
    ],
    argType: 'options',
  },
  CRTFilter: {
    defaultValues: {
      curvature: 1,
      lineWidth: 1,
      lineContrast: 0.25,
      verticalLine: false,
      noise: 0.3,
      noiseSize: 1,
      seed: 0,
      vignetting: 0.3,
      vignettingAlpha: 1,
      vignettingBlur: 0.3,
      time: 0,
    },
    controls: [
      { type: 'range', name: 'curvature', min: 0, max: 20, step: 0.01 },
      { type: 'range', name: 'lineWidth', min: 0, max: 20, step: 0.01 },
      { type: 'range', name: 'lineContrast', min: 0, max: 5, step: 0.01 },
      { type: 'boolean', name: 'verticalLine' },
      { type: 'range', name: 'noise', min: 0, max: 2, step: 0.01 },
      { type: 'range', name: 'noiseSize', min: 0, max: 20, step: 0.01 },
      { type: 'range', name: 'seed', min: 0, max: 100000, step: 1 },
      { type: 'range', name: 'vignetting', min: 0, max: 20, step: 0.01 },
      { type: 'range', name: 'vignettingAlpha', min: 0, max: 1, step: 0.01 },
      { type: 'range', name: 'vignettingBlur', min: 0, max: 5, step: 0.01 },
      { type: 'range', name: 'time', min: 0, max: 10000, step: 1 },
    ],
    argType: 'options',
  },
  DotFilter: {
    defaultValues: {
      scale: 1,
      angle: 5,
    },
    controls: [
      { type: 'range', name: 'scale', min: 0, max: 50, step: 1 },
      { type: 'range', name: 'angle', min: 0, max: 360, step: 0.1 },
    ],
    argType: 'args',
  },
  DropShadowFilter: {
    defaultValues: {
      rotation: 45,
      distance: 5,
      color: 0x000000,
      alpha: 0.5,
      shadowOnly: false,
      blur: 2,
      quality: 3,
    },
    controls: [
      { type: 'range', name: 'rotation', min: 0, max: 360, step: 0.1 },
      { type: 'range', name: 'distance', min: 0, max: 100, step: 0.1 },
      { type: 'color', name: 'color' },
      { type: 'range', name: 'alpha', min: 0, max: 1, step: 0.01 },
      { type: 'boolean', name: 'shadowOnly' },
      { type: 'range', name: 'blur', min: 0, max: 20, step: 0.1 },
      { type: 'range', name: 'quality', min: 0, max: 20, step: 1 },
    ],
    argType: 'options',
  },
  EmbossFilter: {
    defaultValues: {
      strength: 5,
    },
    controls: [{ type: 'range', name: 'strength', min: 0, max: 20, step: 1 }],
    argType: 'args',
  },
  GlitchFilter: {
    defaultValues: {
      slices: 5,
      offset: 100,
      direction: 0,
      fillMode: 0,
      seed: 0,
      average: false,
      minSize: 8,
      sampleSize: 512,
    },
    controls: [
      { type: 'range', name: 'slices', min: 0, max: 50, step: 1 },
      { type: 'range', name: 'distance', min: 0, max: 1000, step: 1 },
      { type: 'range', name: 'direction', min: 0, max: 360, step: 0.1 },
      {
        type: 'select',
        name: 'fillMode',
        options: [
          { value: 0, label: 'TRANSPARENT' },
          { value: 1, label: 'ORIGINAL' },
          { value: 2, label: 'LOOP' },
          { value: 3, label: 'CLAMP' },
          { value: 4, label: 'MIRROR' },
        ],
      },
      { type: 'range', name: 'seed', min: 0, max: 10000, step: 1 },
      { type: 'boolean', name: 'average' },
      { type: 'range', name: 'minSize', min: 0, max: 500, step: 1 },
      { type: 'range', name: 'sampleSize', min: 0, max: 1024, step: 1 },
    ],
    argType: 'options',
  },
  GlowFilter: {
    defaultValues: {
      distance: 10,
      outerStrength: 4,
      innerStrength: 0,
      color: 0xffffff,
      quality: 0.1,
      knockout: false,
    },
    controls: [
      { type: 'range', name: 'distance', min: 1, max: 50, step: 1 },
      { type: 'range', name: 'outerStrength', min: 0, max: 20, step: 1 },
      { type: 'range', name: 'innerStrength', min: 0, max: 20, step: 1 },
      { type: 'color', name: 'color' },
      { type: 'range', name: 'quality', min: 0, max: 5, step: 0.1 },
      { type: 'boolean', name: 'knockout' },
    ],
    argType: 'options',
  },
  GodrayFilter: {
    defaultValues: {
      angle: 30,
      gain: 0.5,
      lacunarity: 2.5,
      parallel: true,
      time: 0,
      alpha: 1.0,
    },
    controls: [
      { type: 'range', name: 'angle', min: 0, max: 360, step: 0.1 },
      { type: 'range', name: 'gain', min: 0, max: 5, step: 0.01 },
      { type: 'range', name: 'lacunarity', min: 0, max: 5, step: 0.01 },
      { type: 'boolean', name: 'parallel' },
      { type: 'range', name: 'time', min: 0, max: 10000, step: 1 },
      { type: 'range', name: 'alpha', min: 0, max: 1, step: 0.01 },
    ],
    argType: 'options',
  },
  KawaseBlurFilter: {
    defaultValues: {
      blur: 4,
      quality: 3,
      clamp: false,
    },
    controls: [
      { type: 'range', name: 'blur', min: 0, max: 20, step: 0.1 },
      { type: 'range', name: 'quality', min: 0, max: 20, step: 1 },
      { type: 'boolean', name: 'clamp' },
    ],
    argType: 'args',
  },
  OldFilmFilter: {
    defaultValues: {
      sepia: 0.3,
      noise: 0.3,
      noiseSize: 1.0,
      scratch: 0.5,
      scratchDensity: 0.3,
      scratchWidth: 1.0,
      vignetting: 0.3,
      vignettingAlpha: 1.0,
      vignettingBlur: 0.3,
    },
    controls: [
      { type: 'range', name: 'sepia', min: 0, max: 1, step: 0.01 },
      { type: 'range', name: 'noise', min: 0, max: 1, step: 0.01 },
      { type: 'range', name: 'noiseSize', min: 0, max: 5, step: 0.01 },
      { type: 'range', name: 'scratch', min: 0, max: 5, step: 0.01 },
      { type: 'range', name: 'scratchDensity', min: 0, max: 5, step: 0.01 },
      { type: 'range', name: 'scratchWidth', min: 0, max: 20, step: 0.01 },
      { type: 'range', name: 'vignetting', min: 0, max: 1, step: 0.01 },
      { type: 'range', name: 'vignettingAlpha', min: 0, max: 1, step: 0.01 },
      { type: 'range', name: 'vignettingBlur', min: 0, max: 5, step: 0.01 },
    ],
    argType: 'options',
  },
  OutlineFilter: {
    defaultValues: {
      thickness: 1,
      color: 0x000000,
      quality: 0.1,
    },
    controls: [
      { type: 'range', name: 'thickness', min: 0, max: 20, step: 0.1 },
      { type: 'color', name: 'color' },
      { type: 'range', name: 'quality', min: 0, max: 1, step: 0.01 },
    ],
    argType: 'args',
  },
  PixelateFilter: {
    defaultValues: {
      size: 1,
    },
    controls: [{ type: 'range', name: 'size', min: 1, max: 100, step: 1 }],
    argType: 'args',
  },
  RGBSplitFilter: {
    defaultValues: {
      red: [-10, 0],
      green: [0, 10],
      blue: [0, 0],
    },
    controls: [
      { type: 'point', name: 'red', min: 0, max: 50, step: 1 },
      { type: 'point', name: 'green', min: 0, max: 50, step: 1 },
      { type: 'point', name: 'blue', min: 0, max: 50, step: 1 },
    ],
    argType: 'args',
  },
  RadialBlurFilter: {
    defaultValues: {
      angle: 0,
      center: [0, 0],
      radius: -1,
    },
    controls: [
      { type: 'range', name: 'angle', min: 0, max: 360, step: 1 },
      { type: 'point', name: 'center', min: 0, max: 1000, step: 1 },
      { type: 'range', name: 'radius', min: -1, max: 1000, step: 1 },
    ],
    argType: 'args',
  },
  ReflectionFilter: {
    defaultValues: {
      mirror: true,
      boundary: 0.5,
      amplitude: [0, 20],
      waveLength: [30, 100],
      alpha: [1, 1],
      time: 0,
    },
    controls: [
      { type: 'boolean', name: 'mirror' },
      { type: 'range', name: 'boundary', min: 0, max: 1, step: 0.01 },
      { type: 'point', name: 'amplitude', min: 0, max: 100, step: 1 },
      { type: 'point', name: 'waveLength', min: 0, max: 500, step: 1 },
      { type: 'point', name: 'alpha', min: 0, max: 1, step: 0.01 },
      { type: 'range', name: 'time', min: 0, max: 10000, step: 1 },
    ],
    argType: 'options',
  },
  DisplacementFilter: {
    defaultValues: {
      sprite: '',
      textureScale: 1,
      displacementScale: 1,
    },
    controls: [
      { type: 'text', name: 'sprite' },
      { type: 'range', name: 'textureScale', min: 0, max: 100, step: 0.1 },
      { type: 'range', name: 'displacementScale', min: 0, max: 100, step: 0.1 },
    ],
    argType: 'options',
  },
  'Token Magic FX': {
    defaultValues: {
      params: [],
    },
    controls: [
      { type: 'tmfxPreset', name: 'tmfxPreset' },
      { type: 'json', name: 'params' },
    ],
  },
};

function genFilterOptionControls(filterName, filterOptions = {}) {
  if (!(filterName in FILTERS)) return;

  const options = mergeObject(FILTERS[filterName].defaultValues, filterOptions);
  const values = getControlValues(filterName, options);

  const controls = FILTERS[filterName].controls;
  let controlsHTML = '<fieldset><legend>Options</legend>';
  for (const control of controls) {
    controlsHTML += genControl(control, values);
  }
  controlsHTML += '</fieldset>';

  return controlsHTML;
}

function getControlValues(filterName, options) {
  if (filterName === 'OutlineOverlayFilter') {
    options.outlineColor = Color.fromRGB(options.outlineColor).toString();
  } else if (filterName === 'BevelFilter') {
    options.lightColor = Color.from(options.lightColor).toString();
    options.shadowColor = Color.from(options.shadowColor).toString();
  } else if (['DropShadowFilter', 'GlowFilter', 'OutlineFilter'].includes(filterName)) {
    options.color = Color.from(options.color).toString();
  }
  return options;
}

function genControl(control, values) {
  const val = values[control.name];
  const name = control.name;
  const label = control.label ?? name.charAt(0).toUpperCase() + name.slice(1);
  const type = control.type;
  if (type === 'color') {
    return `
<div class="form-group">
  <label>${label}</label>
  <div class="form-fields">
      <input class="color" type="text" name="filterOptions.${name}" value="${val}">
      <input type="color" value="${val}" data-edit="filterOptions.${name}">
  </div>
</div>
`;
  } else if (type === 'range') {
    return `
<div class="form-group">
  <label>${label}</label>
  <div class="form-fields">
      <input type="range" name="filterOptions.${name}" value="${val}" min="${control.min}" max="${control.max}" step="${control.step}">
      <span class="range-value">${val}</span>
  </div>
</div>
`;
  } else if (type === 'boolean') {
    return `
<div class="form-group">
  <label>${label}</label>
  <div class="form-fields">
      <input type="checkbox" name="filterOptions.${name}" data-dtype="Boolean" value="${val}" ${val ? 'checked' : ''}>
  </div>
</div>
    `;
  } else if (type === 'select') {
    let select = `
    <div class="form-group">
    <label>${label}</label>
    <div class="form-fields">
      <select name="${name}">
`;

    for (const opt of control.options) {
      select += `<option value="${opt.value}" ${val === opt.value ? 'selected="selected"' : ''}>${opt.label}</option>`;
    }

    select += `</select></div></div>`;

    return select;
  } else if (type === 'point') {
    return `
<div class="form-group">
  <label>${label}</label>
  <div class="form-fields">
      <input type="range" name="filterOptions.${name}" value="${val[0]}" min="${control.min}" max="${control.max}" step="${control.step}">
      <span class="range-value">${val[0]}</span>
  </div>
  <div class="form-fields">
    <input type="range" name="filterOptions.${name}" value="${val[1]}" min="${control.min}" max="${control.max}" step="${control.step}">
    <span class="range-value">${val[1]}</span>
  </div>
</div>
`;
  } else if (type === 'json') {
    let control = `
<div class="form-group">
  <label>${label}</label>
  <div class="form-fields">
      <textarea style="width: 450px; height: 200px;" name="filterOptions.${name}">${val}</textarea>
  </div>`;
    const massEdit = game.modules.get('multi-token-edit');
    if (massEdit?.active && massEdit.api.showGenericForm) {
      control += `
  <div style="text-align: right; color: orangered;">
      <a> <i class="me-edit-json fas fa-edit" title="Show Generic Form"></i></a>
  </div>`;
    }
    control += `</div>`;
    return control;
  } else if (type === 'text') {
    return `
<div class="form-group">
  <label>${label}</label>
  <div class="form-fields">
      <input type="text" name="filterOptions.${name}" value="${val}">
  </div>
</div>
`;
  } else if (type === 'tmfxPreset' && game.modules.get('tokenmagic')?.active) {
    return `
      <div class="form-group">
        <label>Preset <span class="units">(TMFX)</span></label>
        <div class="form-fields">
          <input list="tmfxPresets" class="tmfxPreset">
          <button type="button" class="presetImport"><i class="fas fa-download"></i></button>
        </div>
      `;
  }
  return '';
}

async function promptParamChoice(params) {
  return new Promise((resolve, reject) => {
    const buttons = {};
    for (let i = 0; i < params.length; i++) {
      const label = params[i].filterType ?? params[i].filterId;
      buttons[label] = {
        label,
        callback: () => {
          resolve(i);
        },
      };
    }

    const dialog = new Dialog({
      title: 'Select Filter To Edit',
      content: '',
      buttons,
      close: () => resolve(-1),
    });
    dialog.render(true);
  });
}
