/*
This is an experimental script. If you find bugs, please consider debugging yourself then submitting a PR on github with the fix, instead of raising an issue. Thank you!

This script modifies the color lightness/hue/saturation/transparency of selected Excalidraw elements and  SVG and nested Excalidraw drawings. Select eligible elements in the scene, then run the script.

- The color of Excalidraw elements (lines, ellipses, rectangles, etc.) will be changed by the script.
- The color of SVG elements and nested Excalidraw drawings will only be mapped. When mapping colors, the original image remains unchanged, only a mapping table is created and the image is recolored during rendering of your Excalidraw screen. In case you want to make manual changes you can also edit the mapping in Markdown View Mode under `## Embedded Files`

If you select only a single SVG or nested Excalidraw element, then the script offers an additional feature. You can map colors one by one in the image. 
```js
*/

const HELP_TEXT = `
- Select SVG images, nested Excalidraw drawings and/or regular Excalidraw elements
- For a single selected image, you can map colors individually in the color mapping section
- For Excalidraw elements: stroke and background colors are modified permanently
- For SVG/nested drawings: original files stay unchanged, color mapping is stored under \`## Embedded Files\`
- Using color maps helps maintain links between drawings while allowing different color themes
- Sliders work on relative scale - the amount of change is applied to current values
- Unlike Excalidraw's opacity setting which affects the whole element:
    - Shade Master can set different opacity for stroke vs background
    - **Note:** SVG/nested drawing colors are mapped at color name level, thus "black" is different from "#000000"
    - Additionally if the same color is used as fill and stroke the color can only be mapped once
- This is an experimental script - contributions welcome on GitHub via PRs

<div class="ex-coffee-div"><a href="https://ko-fi.com/zsolt"><img src="https://storage.ko-fi.com/cdn/kofi6.png?v=6" border="0" alt="Buy Me a Coffee at ko-fi.com"  height=45></a></div>

<a href="https://www.youtube.com/watch?v=ISuORbVKyhQ" target="_blank"><img src ="https://i.ytimg.com/vi/ISuORbVKyhQ/maxresdefault.jpg" style="max-width:560px; width:100%"></a>

`;

if(!ea.verifyMinimumPluginVersion || !ea.verifyMinimumPluginVersion("2.25.0")) {
  new Notice("Please update the Excalidraw Plugin to version 2.25.0 or higher.");
  return;
}

const existingTab = ea.checkForActiveSidepanelTabForScript();
if (existingTab) {
  const hostEA = existingTab.getHostEA();
  if (hostEA && hostEA !== ea) {
    hostEA.setView(ea.targetView);
    existingTab.open();
    return;
  }
}

/*
SVGColorInfo is returned by ea.getSVGColorInfoForImgElement. Color info will all the color strings in the SVG file plus "fill" which represents the default fill color for SVG icons set at the SVG root element level. Fill if not set defaults to black:

type SVGColorInfo = Map<string, {
  mappedTo: string;
  fill: boolean;
  stroke: boolean;
}>;

In the Excalidraw file under `## Embedded Files` the color map is included after the file. That color map implements ColorMap. ea.updateViewSVGImageColorMap takes a ColorMap as input.
interface ColorMap {
  [color: string]: string;
};
*/

// Main script state variables
let allElements = [];
let svgImageElements = [];
let lastSelectionIds = "";

const originalColors = new Map();
const currentColors = new Map();
const colorInputs = new Map();
const sliderResetters = [];
let terminate = false;
const FORMAT = "Color Format";
const STROKE = "Modify Stroke Color";
const BACKGROUND = "Modify Background Color"
const ACTIONS = ["Hue", "Lightness", "Saturation", "Transparency"];
const precision = [1,2,2,3];
const minLigtness = 1/Math.pow(10,precision[2]);
const maxLightness = 100 - minLigtness;
const minSaturation = 1/Math.pow(10,precision[2]);
let dirty = false;

let settings = ea.getScriptSettings();
//set default values on first run
if(!settings[STROKE]) {
  settings = {};
  settings[FORMAT] = {
    value: "HEX",
    valueset: ["HSL", "RGB", "HEX"],
    description: "Output color format."
  };
  settings[STROKE] = { value: true }
  settings[BACKGROUND] = {value: true }
  ea.setScriptSettings(settings);
}

function getRegularElements() {
  if (!ea.targetView) return [];
  ea.clear();
  //loading view elements again as element objects change when colors are updated
  const viewElements = ea.getViewSelectedElements();
  return viewElements.filter(el => 
    ["rectangle", "ellipse", "diamond", "line", "arrow", "freedraw", "text"].includes(el.type)
  );
}

const updatedImageElementColorMaps = new Map();
let isWaitingForSVGUpdate = false;
function updateViewImageColors() {
  if(terminate || isWaitingForSVGUpdate || updatedImageElementColorMaps.size === 0) {
    return;
  }
  isWaitingForSVGUpdate = true;
  elementArray = Array.from(updatedImageElementColorMaps.keys());
  colorMapArray = Array.from(updatedImageElementColorMaps.values());
  updatedImageElementColorMaps.clear();
  ea.updateViewSVGImageColorMap(elementArray, colorMapArray).then(()=>{
    isWaitingForSVGUpdate = false;
    updateViewImageColors();
  });
}

async function storeOriginalColors() {
  // Clear previous state
  originalColors.clear();
  currentColors.clear();

  // Store colors for regular elements  
  for (const el of getRegularElements()) {
    const key = el.id;
    const colorData = {
      type: "regular",
      strokeColor: el.strokeColor,
      backgroundColor: el.backgroundColor
    };
    originalColors.set(key, colorData);
  }

  // Store colors for SVG elements
  for (const el of svgImageElements) {
    const colorInfo = await ea.getSVGColorInfoForImgElement(el);
    const svgColors = new Map();
    for (const [color, info] of colorInfo.entries()) {
      svgColors.set(color, {...info});
    }
    
    originalColors.set(el.id, {type: "svg",colors: svgColors});
  }
  copyOriginalsToCurrent();
}

function copyOriginalsToCurrent() {
  currentColors.clear();
  for (const [key, value] of originalColors.entries()) {
    if(value.type === "regular") {
      currentColors.set(key, {...value});
    } else {
      const newColorMap = new Map();
      for (const [color, info] of value.colors.entries()) {
        newColorMap.set(color, {...info});
      }
      currentColors.set(key, {type: "svg", colors: newColorMap});
    }
  }
}

function clearSVGMapping() {
  for (const resetter of sliderResetters) {
    resetter();
  }
  // Reset SVG elements
  if (svgImageElements.length === 1) {
    const el = svgImageElements[0];
    const original = originalColors.get(el.id);
    const current = currentColors.get(el.id);
    if (original && original.type === "svg") {
      
      for (const color of original.colors.keys()) {
        current.colors.get(color).mappedTo = color;
      }
    }
  } else {
    for (const el of svgImageElements) {
      const original = originalColors.get(el.id);
      const current = currentColors.get(el.id);
      if (original && original.type === "svg") {
        for (const color of original.colors.keys()) {
          current.colors.get(color).mappedTo = color;
        }
      }
    }
  }
  run("clear");
}

// Set colors
async function setColors(colors) {
  debounceColorPicker = true;
  const regularElements = getRegularElements();
  
  if (regularElements.length > 0) {
    ea.copyViewElementsToEAforEditing(regularElements);
    for (const el of ea.getElements()) {
      const original = colors.get(el.id);
      if (original && original.type === "regular") {
        if (original.strokeColor) el.strokeColor = original.strokeColor;
        if (original.backgroundColor) el.backgroundColor = original.backgroundColor;
      }
    }
    await ea.addElementsToView(false, false);
  }

  // Reset SVG elements
  if (svgImageElements.length === 1) {
    const el = svgImageElements[0];
    const original = colors.get(el.id);
    if (original && original.type === "svg") {
      const newColorMap = {};
      
      for (const [color, info] of original.colors.entries()) {
        newColorMap[color] = info.mappedTo;
        // Update UI components
        const inputs = colorInputs.get(color);
        if (inputs) {
          if(info.mappedTo === "fill") {
            info.mappedTo = "black";
            //"fill" is a special value in case the SVG has no fill color defined (i.e black)
            inputs.textInput.setValue("black");
            inputs.colorPicker.setValue("#000000");
          } else {
            const cm = ea.getCM(info.mappedTo);
            inputs.textInput.setValue(info.mappedTo);
            inputs.colorPicker.setValue(cm.stringHEX({alpha: false}).toLowerCase());
          }
        }
      }
      updatedImageElementColorMaps.set(el, newColorMap);
    }
  } else {
    for (const el of svgImageElements) {
      const original = colors.get(el.id);
      if (original && original.type === "svg") {
        const newColorMap = {};
        
        for (const [color, info] of original.colors.entries()) {
          newColorMap[color] = info.mappedTo;
        }  
        updatedImageElementColorMaps.set(el, newColorMap);
      }
    }
  }
  updateViewImageColors();
}

function modifyColor(color, isDecrease, step, action) {
  if (!color) return null;
  
  const cm = ea.getCM(color);
  if (!cm) return color;

  let modified = cm;
  if (modified.lightness === 0) modified = modified.lightnessTo(minLigtness);
  if (modified.lightness === 100) modified = modified.lightnessTo(maxLightness);
  if (modified.saturation === 0) modified = modified.saturationTo(minSaturation);

  switch(action) {
    case "Lightness":
      // handles edge cases where lightness is 0 or 100 would convert saturation and hue to 0
      let lightness = cm.lightness;
      const shouldRoundLight = (lightness === minLigtness || lightness === maxLightness);
      if (shouldRoundLight) lightness = Math.round(lightness);
      lightness += isDecrease ? -step : step;
      if (lightness <= 0) lightness = minLigtness;
      if (lightness >= 100) lightness = maxLightness;
      modified = modified.lightnessTo(lightness);
      break;
    case "Hue":
      modified = isDecrease ? modified.hueBy(-step) : modified.hueBy(step);
      break;
    case "Transparency":
      modified = isDecrease ? modified.alphaBy(-step) : modified.alphaBy(step);
      break;
    default:
      let saturation = cm.saturation;
      const shouldRoundSat = saturation === minSaturation;
      if (shouldRoundSat) saturation = Math.round(saturation);
      saturation += isDecrease ? -step : step;
      if (saturation <= 0) saturation = minSaturation;
      modified = modified.saturationTo(saturation);
  }

  const hasAlpha = modified.alpha < 1;
  const opts = { alpha: hasAlpha, precision };
  
  const format = settings[FORMAT].value;
  switch(format) {
    case "RGB": return modified.stringRGB(opts).toLowerCase();
    case "HEX": return modified.stringHEX(opts).toLowerCase();
    default: return modified.stringHSL(opts).toLowerCase();
  }
}

function slider(contentEl, action, min, max, step, invert) {
  let prevValue = (max-min)/2;
  let debounce = false;
  let sliderControl;
  new ea.obsidian.Setting(contentEl)
  .setName(action)
  .addSlider(slider => {
    sliderControl = slider;
    slider
      .setLimits(min, max, step)
      .setValue(prevValue)
      .onChange(async (value) => {
        if (debounce) return;
        const isDecrease = invert ? value > prevValue : value < prevValue;
        const step = Math.abs(value-prevValue);
        prevValue = value;
        if(step>0) {
          run(action, isDecrease, step);
        }
      });
    }
  );
  return () => {
    debounce = true;
    prevValue = (max-min)/2;
    sliderControl.setValue(prevValue);
    debounce = false;
  }
}

let debounceColorPicker = true;

function renderSidepanel(contentEl) {
  contentEl.empty();
  
  contentEl.createEl('h2', { text: 'Shade Master' });
  
  const helpDiv = contentEl.createEl("details", {
    attr: { style: "margin-bottom: 1em;background: var(--background-secondary); padding: 1em; border-radius: 4px;" }});
  helpDiv.createEl("summary", { text: "Help & Usage Guide", attr: { style: "cursor: pointer; color: var(--text-accent);" } });
  const helpDetailsDiv = helpDiv.createEl("div", {
    attr: { style: "margin-top: 0em; " }
  });
  
  ea.obsidian.MarkdownRenderer.render(ea.plugin.app, HELP_TEXT, helpDetailsDiv, "", ea.plugin);

  if (!ea.targetView) {
    contentEl.createEl("p", { 
      text: "No active Excalidraw view found. Please open a drawing and select elements to use Shade Master.",
      attr: { style: "color: var(--text-muted);" }
    });
    return;
  }

  const { width, height } = ea.getExcalidrawAPI().getAppState();

  if(allElements.length === 0) {
    contentEl.createEl("p", { 
      text: "Select at least one rectangle, ellipse, diamond, line, arrow, freedraw, text or SVG image element",
      attr: { style: "color: var(--text-warning);" }
    });
    // return; // Removed early return to allow rendering of the Close button
  } else {
    // Only render controls if elements are selected
    const component = new ea.obsidian.Setting(contentEl)
      .setName(FORMAT)
      .setDesc("Output color format")
      .addDropdown(dropdown => dropdown
        .addOptions({
          "HSL": "HSL",
          "RGB": "RGB",
          "HEX": "HEX"
        })
        .setValue(settings[FORMAT].value)
        .onChange(value => {
          settings[FORMAT].value = value;
          run();
          dirty = true;
        })
      );

    new ea.obsidian.Setting(contentEl)
      .setName(STROKE)
      .addToggle(toggle => toggle
        .setValue(settings[STROKE].value)
        .onChange(value => {
          settings[STROKE].value = value;
          dirty = true;
        })
      );

    new ea.obsidian.Setting(contentEl)
      .setName(BACKGROUND)
      .addToggle(toggle => toggle
        .setValue(settings[BACKGROUND].value)
        .onChange(value => {
          settings[BACKGROUND].value = value;
          dirty = true;
        })
      );

    // lightness and saturation are on a scale of 0%-100%
    // Hue is in degrees, 360 for the full circle
    // transparency is on a range between 0 and 1 (equivalent to 0%-100%)
    // The range for lightness, saturation and transparency are double since
    // the input could be at either end of the scale
    // The range for Hue is 360 since regarless of the position on the circle moving
    // the slider to the two extremes will travel the entire circle
    // To modify blacks and whites, lightness first needs to be changed to value between 1% and 99%
    sliderResetters.length = 0; // Clear existing resetters
    sliderResetters.push(slider(contentEl, "Hue", 0, 360, 1, false));
    sliderResetters.push(slider(contentEl, "Saturation", 0, 200, 1, false));
    sliderResetters.push(slider(contentEl, "Lightness", 0, 200, 1, false));
    sliderResetters.push(slider(contentEl, "Transparency", 0, 2, 0.05, true));

    // Add color pickers if a single SVG image is selected
    if (svgImageElements.length === 1) {
      const svgElement = svgImageElements[0];
      //note that the objects in currentColors might get replaced when
      //colors are reset, thus in the onChange functions I will always
      //read currentColorInfo from currentColors based on svgElement.id
      const initialColorInfo = currentColors.get(svgElement.id).colors;
      const colorSection = contentEl.createDiv();
      colorSection.createEl('h3', { text: 'SVG Colors' });
      
      colorInputs.clear(); // Clear old inputs map

      for (const [color, info] of initialColorInfo.entries()) {
        const row = new ea.obsidian.Setting(colorSection)
          .setName(color === "fill" ? "SVG default" : color)
          .setDesc(`${info.fill ? "Fill" : ""}${info.fill && info.stroke ? " & " : ""}${info.stroke ? "Stroke" : ""}`);
        row.descEl.style.width = "100px";
        row.nameEl.style.width = "100px";

        // Create color preview div
        const previewDiv = row.controlEl.createDiv();
        previewDiv.style.width = "50px";
        previewDiv.style.height = "20px";
        previewDiv.style.border = "1px solid var(--background-modifier-border)";
        if (color === "transparent") {
          previewDiv.style.backgroundImage = "linear-gradient(45deg, #808080 25%, transparent 25%), linear-gradient(-45deg, #808080 25%, transparent 25%), linear-gradient(45deg, transparent 75%, #808080 75%), linear-gradient(-45deg, transparent 75%, #808080 75%)";
          previewDiv.style.backgroundSize = "10px 10px";
          previewDiv.style.backgroundPosition = "0 0, 0 5px, 5px -5px, -5px 0px";
        } else {
          previewDiv.style.backgroundColor = ea.getCM(color).stringHEX({alpha: false}).toLowerCase();
        }
        
        const resetButton = new ea.obsidian.Setting(row.controlEl)
          .addButton(button => button
            .setButtonText(">>")
            .setClass("reset-color-button")
            .onClick(async () => {
              const original = originalColors.get(svgElement.id);
              const current = currentColors.get(svgElement.id);
              if (original?.type === "svg") {
                const originalInfo = original.colors.get(color);
                const currentInfo = current.colors.get(color);
                if (originalInfo) {
                  currentInfo.mappedTo = color;
                  run("reset single color");
                }
              }
            }))
          resetButton.settingEl.style.padding = "0";
          resetButton.settingEl.style.border = "0";

        // Add text input for color value
        const textInput = new ea.obsidian.TextComponent(row.controlEl)
          .setValue(info.mappedTo)
          .setPlaceholder("Color value");
        textInput.inputEl.style.width = "100%";
        textInput.onChange(value => {
          const lower = value.toLowerCase();
          if (lower === color) return;
          textInput.setValue(lower);
        })

        const applyButtonComponent = new ea.obsidian.Setting(row.controlEl)
          .addButton(button => button
            .setIcon("check")
            .setTooltip("Apply")
            .onClick(async () => {
              const value = textInput.getValue();
              try {
                if(!CSS.supports("color",value)) {
                  new Notice (`${value} is not a valid color string`);
                  return;
                }
                const cm = ea.getCM(value);
                if (cm) {
                  const format = settings[FORMAT].value;
                  const alpha = cm.alpha < 1 ? true : false;
                  const newColor = format === "RGB" 
                    ? cm.stringRGB({alpha , precision }).toLowerCase()
                    : format === "HEX" 
                      ? cm.stringHEX({alpha}).toLowerCase()
                      : cm.stringHSL({alpha, precision }).toLowerCase();

                  textInput.setValue(newColor);
                  const currentInfo = currentColors.get(svgElement.id).colors;
                  currentInfo.get(color).mappedTo = newColor;
                  run("Update SVG color");
                  debounceColorPicker = true;
                  colorPicker.setValue(cm.stringHEX({alpha: false}).toLowerCase());
                }
              } catch (e) {
                console.error("Invalid color value:", e);
              }
            }));
          applyButtonComponent.settingEl.style.padding = "0";
          applyButtonComponent.settingEl.style.border = "0";
        
        // Add color picker
        const colorPicker = new ea.obsidian.ColorComponent(row.controlEl)
          .setValue(ea.getCM(info.mappedTo).stringHEX({alpha: false}).toLowerCase());

        colorPicker.colorPickerEl.style.maxWidth = "2.5rem";

        // Add palette picker button
        const paletteButton = new ea.obsidian.Setting(row.controlEl)
          .addButton(button => button
            .setIcon("swatch-book")
            .setTooltip("Pick from Palette")
            .onClick(async () => {
              const selected = await ea.showColorPicker(button.buttonEl, "elementStroke");
              if (selected) {
                try {
                  const cm = ea.getCM(selected);
                  if (cm) {
                    const format = settings[FORMAT].value;
                    
                    // Preserve alpha from original color
                    const currentInfo = currentColors.get(svgElement.id).colors.get(color);
                    const originalAlpha = ea.getCM(currentInfo.mappedTo).alpha;
                    cm.alphaTo(originalAlpha);
                    const alpha = originalAlpha < 1 ? true : false;

                    const newColor = format === "RGB" 
                      ? cm.stringRGB({alpha , precision }).toLowerCase()
                      : format === "HEX" 
                        ? cm.stringHEX({alpha}).toLowerCase()
                        : cm.stringHSL({alpha, precision }).toLowerCase();
                    
                    // Update text input
                    textInput.setValue(newColor);
                    
                    // Update Color Picker visual
                    colorPicker.setValue(cm.stringHEX({alpha: false}).toLowerCase());

                    // Update SVG mapping
                    currentInfo.mappedTo = newColor;
                    run("Update SVG color");
                  }
                } catch (e) {
                  console.error("Invalid color value:", e);
                }
              }
            }));
        paletteButton.settingEl.style.padding = "0";
        paletteButton.settingEl.style.border = "0";
        paletteButton.infoEl.style.display = "none";

        // Store references to the components
        colorInputs.set(color, {
          textInput,
          colorPicker,
          previewDiv,
          resetButton
        });

        colorPicker.colorPickerEl.addEventListener('click', () => {
          debounceColorPicker = false;
        });

        colorPicker.onChange(async (value) => {
          try {
            if(!debounceColorPicker) {
              const currentInfo = currentColors.get(svgElement.id).colors.get(color);
              // Preserve alpha from original color
              const originalAlpha = ea.getCM(currentInfo.mappedTo).alpha;
              const cm = ea.getCM(value);
              cm.alphaTo(originalAlpha);
              const alpha = originalAlpha < 1 ? true : false;
              const format = settings[FORMAT].value;
              const newColor = format === "RGB" 
                ? cm.stringRGB({alpha, precision }).toLowerCase()
                : format === "HEX" 
                  ? cm.stringHEX({alpha}).toLowerCase()
                  : cm.stringHSL({alpha, precision }).toLowerCase();
              
              // Update text input
              textInput.setValue(newColor);
              
              // Update SVG
              currentInfo.mappedTo = newColor;
              run("Update SVG color");
            }
          } catch (e) {
            console.error("Invalid color value:", e);
          } finally {
            debounceColorPicker = true;
          }
        });
      }
    }
  }

  const buttons = new ea.obsidian.Setting(contentEl);
  if(svgImageElements.length > 0) {
    buttons.addButton(button => button
      .setButtonText("Initialize SVG Colors")
      .onClick(() => {
        debounceColorPicker = true;
        clearSVGMapping();
      })
    );
  }

  if (allElements.length > 0) {
    buttons.addButton(button => button
      .setButtonText("Reset")
      .onClick(() => {
        for (const resetter of sliderResetters) {
          resetter();
        }
        copyOriginalsToCurrent();
        setColors(originalColors);
      }));
  }

  buttons.addButton(button => button
    .setButtonText("Close")
    .onClick(() => {
      if(ea.sidepanelTab) {
        ea.sidepanelTab.close();
      }
      ea.toggleSidepanelView();
    }));
}

function executeChange(isDecrease, step, action) {
  const modifyStroke = settings[STROKE].value;
  const modifyBackground = settings[BACKGROUND].value;
  const regularElements = getRegularElements();

  // Process regular elements
  if (regularElements.length > 0) {
    for (const el of regularElements) {
      const currentColor = currentColors.get(el.id);

      if (modifyStroke && currentColor.strokeColor) {
        currentColor.strokeColor = modifyColor(el.strokeColor, isDecrease, step, action);
      }
      
      if (modifyBackground && currentColor.backgroundColor) {
        currentColor.backgroundColor = modifyColor(el.backgroundColor, isDecrease, step, action);
      }
    }
  }
  
  // Process SVG image elements
  if (svgImageElements.length === 1) { // Only update UI for single SVG
    const el = svgImageElements[0];
    colorInfo = currentColors.get(el.id).colors;

    // Process each color in the SVG
    for (const [color, info] of colorInfo.entries()) {
      let shouldModify = (modifyBackground && info.fill) || (modifyStroke && info.stroke);
      
      if (shouldModify) {
        const modifiedColor = modifyColor(info.mappedTo, isDecrease, step, action);
        colorInfo.get(color).mappedTo = modifiedColor;
        // Update UI components if they exist
        const inputs = colorInputs.get(color);
        if (inputs) {
          const cm = ea.getCM(modifiedColor);
          inputs.textInput.setValue(modifiedColor);
          inputs.colorPicker.setValue(cm.stringHEX({alpha: false}).toLowerCase());
        }
      }
    }
  } else {
    if (svgImageElements.length > 0) {
      for (const el of svgImageElements) {
        const colorInfo = currentColors.get(el.id).colors;
    
        // Process each color in the SVG
        for (const [color, info] of colorInfo.entries()) {
          let shouldModify = (modifyBackground && info.fill) || (modifyStroke && info.stroke);
          
          if (shouldModify) {
            const modifiedColor = modifyColor(info.mappedTo, isDecrease, step, action);
            colorInfo.get(color).mappedTo = modifiedColor;
          }
        }
      }
    }
  }
}

let isRunning = false;
let queue = false;
function processQueue() {
  if (!terminate && !isRunning && queue) {
    queue = false;
    isRunning = true;
    setColors(currentColors).then(() => {
      isRunning = false;
      if (queue) processQueue();
    });
  }
}

function run(action="Hue", isDecrease=true, step=0) {
  // passing invalid action (such as "clear") will bypass rewriting of colors using CM
  // this is useful when resetting colors to original values
  if(ACTIONS.includes(action)) { 
    executeChange(isDecrease, step, action);
  }
  queue = true;
  if (!isRunning) processQueue();
}

// Function to refresh internal state based on current selection
function refreshSelectionState() {
  if (!ea.targetView) {
    allElements = [];
    svgImageElements = [];
    lastSelectionIds = "";
    return;
  }
  allElements = ea.getViewSelectedElements();
  svgImageElements = allElements.filter(el => {
    if(el.type !== "image") return false;
    const file = ea.getViewFileForImageElement(el);
    if(!file) return false;
    return el.type === "image" && (
      file.extension === "svg" ||
      ea.isExcalidrawFile(file)
    );
  });
  lastSelectionIds = allElements.map(e => e.id).sort().join(",");
}

// Sidepanel initialization and logic
ea.createSidepanelTab("Shade Master", false, true).then(tab => {
  if (!tab) return;

  const initializeAndRender = async () => {
    refreshSelectionState();
    await storeOriginalColors();
    renderSidepanel(tab.contentEl);
    processQueue();
  };

  // Register the scene change hook to listen for selection changes
  ea.onSceneChangeHook = {
    appStateKeys: ["selectedElementIds"],
    trackElements: false,
    triggerWhenInvisible: false,
    callback: async (elements, appState, files, view, hookEA) => {
      if (view && view !== ea.targetView) {
        ea.setView(view);
        ea.clear();
      }
      
      // Check if selection changed
      const currentSelectionStr = ea.getViewSelectedElements().map(e => e.id).sort().join(",");
      if (currentSelectionStr !== lastSelectionIds) {
        await initializeAndRender();
      }
    }
  };

  tab.onOpen = async () => {
    terminate = false;
    // Initial load
    await initializeAndRender();
  };

  tab.onFocus = async (view) => {
    if (view && view !== ea.targetView) {
      ea.setView(view);
      ea.clear();
    }
    
    // Check if selection changed
    const currentSelectionStr = ea.getViewSelectedElements().map(e => e.id).sort().join(",");
    if (currentSelectionStr !== lastSelectionIds) {
      await initializeAndRender();
    }
  };

  tab.onClose = async () => {
    // Clean up the hook when closing the tab
    ea.onSceneChangeHook = null;
    
    terminate = true;
    if (dirty) {
      ea.setScriptSettings(settings);
    }
    if(ea.targetView && ea.targetView.isDirty()) {
      ea.targetView.save(false);
    }
  };

  tab.open();
});