/**
 * @license
 * Copyright 2022-2026 Matter.js Authors
 * SPDX-License-Identifier: Apache-2.0
 */
import {
  addValueWithOverflow,
  cropValueRange,
  Diagnostic,
  ImplementationError,
  Logger,
  MaybePromise,
  Millis,
  ObserverGroup,
  Time,
  Timespan,
  Timestamp
} from "#general";
const logger = Logger.get("Transition");
class Transitions {
  #endpoint;
  #timer;
  #config;
  #outstandingTick;
  #staticRemainingTime = 0;
  #prevPublishedRemainingTime = 0;
  #propertyStates = {};
  #observers;
  #instrumentedProperties;
  constructor(endpoint, config) {
    this.#config = config;
    this.#endpoint = endpoint;
  }
  /**
   * Iterate over all active transitions.
   */
  [Symbol.iterator]() {
    return Object.values(this.#propertyStates)[Symbol.iterator]();
  }
  /**
   * Get iteration state for a specific property.
   */
  stateOf(name) {
    return this.#propertyStates[name];
  }
  /**
   * Calculate the distance between two values in a cyclic range. This is the default implementation only caring
   * about normal upwards and downwards transitions.
   */
  calculateCyclicDistance(currentValue, targetValue, changePerS, min, max) {
    if (changePerS > 0) {
      if (currentValue > targetValue) {
        return Math.abs(currentValue - max) + Math.abs(max - targetValue);
      }
    } else if (currentValue < targetValue) {
      return Math.abs(currentValue - min) + Math.abs(min - targetValue);
    }
    return Math.abs(currentValue - targetValue);
  }
  /**
   * Initiate transition of an attribute.
   */
  start(transition) {
    const { name, owner, changePerS } = transition;
    let { targetValue } = transition;
    this.stop(name);
    const currentValue = owner.state[name];
    if (currentValue === targetValue) {
      return;
    }
    if (!this.#config.manageTransitions || !changePerS || !Number.isFinite(changePerS)) {
      if (targetValue === void 0 || targetValue === null) {
        return;
      }
      const { min: min2, max: max2 } = this.#config.properties[name] ?? {};
      if (min2 !== void 0 && targetValue < min2) {
        targetValue = min2;
      } else if (max2 !== void 0 && targetValue > max2) {
        targetValue = max2;
      }
      logger.info(this.#logPrefix, "Set", Diagnostic.strong(name), "to", Diagnostic.strong(targetValue));
      return this.transitionImmediately(owner, name, targetValue);
    }
    const { min, max, cyclic } = this.#config.properties[name] ?? {};
    let distanceLeft = void 0;
    if (cyclic && targetValue !== void 0) {
      if (min === void 0 || max === void 0) {
        throw new ImplementationError("Cyclic transition requires min and max values");
      }
      if (!transition.calculateCyclicDistance) {
        transition.calculateCyclicDistance = (currentValue2, targetValue2) => this.calculateCyclicDistance(currentValue2, targetValue2, changePerS, min, max);
      }
      distanceLeft = Math.abs(transition.calculateCyclicDistance(currentValue, targetValue));
      if (distanceLeft === 0) {
        return;
      }
    } else if (min !== void 0 && max !== void 0) {
      if (changePerS < 0 && currentValue === min || changePerS > 0 && currentValue === max) {
        return;
      }
    }
    this.#instrumentProperty(name);
    const attr = {
      name,
      configuration: transition,
      currentValue,
      changePerMs: changePerS / 1e3,
      prevStepAt: Timestamp(Time.nowMs - (this.#config.stepInterval ?? Transitions.DEFAULT_STEP_INTERVAL)),
      distanceLeft
    };
    this.#propertyStates[name] = attr;
    logger.info(
      this.#logPrefix,
      "Transitioning",
      Diagnostic.strong(name),
      Diagnostic.dict({
        from: currentValue,
        to: targetValue,
        rate: `${changePerS.toFixed(3)}/s`,
        distance: cyclic ? distanceLeft : void 0
      })
    );
    this.transitionGradually(attr);
    const { remainingTimeEvent } = this.#config;
    if (!remainingTimeEvent) {
      return;
    }
    const remainingTime = this.remainingTime;
    if (remainingTime < 10) {
      attr.suppressReportingRemainingTimeOnFinish = true;
      return;
    }
    this.#updateRemainingTime(remainingTime, true);
  }
  #updateRemainingTime(newRemainingTime, fromCommand = false) {
    if (newRemainingTime === void 0) {
      newRemainingTime = this.remainingTime;
    }
    if (this.#prevPublishedRemainingTime === 0 && newRemainingTime <= 10) {
      return;
    }
    if (fromCommand && Math.abs(this.#prevPublishedRemainingTime - newRemainingTime) <= 10) {
      return;
    }
    const previousRemainingTime = this.#prevPublishedRemainingTime;
    this.#prevPublishedRemainingTime = newRemainingTime;
    this.#config.remainingTimeEvent?.emit(newRemainingTime, previousRemainingTime, void 0);
  }
  /**
   * Immediately change to a target value.
   *
   * The default implementation updates the local state value and calls `applyUpdates()`.
   */
  transitionImmediately(owner, name, targetValue) {
    const result = this.applyUpdates(owner, { [name]: targetValue });
    return MaybePromise.then(result, () => {
    });
  }
  /**
   * Initiate gradual transition to a target value.
   *
   * The default implementation starts a timer to drive transitions over time.
   */
  transitionGradually(_attr) {
    if (this.#timer === void 0) {
      this.#timer = Time.getPeriodicTimer(
        `transition-${this.#endpoint}-${this.#config.type.name}`,
        this.#config.stepInterval ?? Transitions.DEFAULT_STEP_INTERVAL,
        this.#step.bind(this)
      );
      this.#timer.start();
    }
  }
  /**
   * Stop transition of one or all attributes.
   */
  stop(name) {
    if (name === void 0) {
      this.#propertyStates = {};
    } else {
      if (!(name in this.#propertyStates)) {
        return;
      }
      delete this.#propertyStates[name];
      if (Object.keys(this.#propertyStates).length) {
        return;
      }
    }
    if (this.#timer) {
      this.#timer.stop();
      this.#timer = void 0;
    }
  }
  /**
   * Stop a transition of one or more attributes that has completed successfully.
   */
  finish(name) {
    const finishOne = (state) => {
      const { name: name2 } = state;
      logger.debug(this.#logPrefix, "Transition of", Diagnostic.strong(name2), "finished");
      const { suppressReportingRemainingTimeOnFinish } = this.#propertyStates[name2] ?? {};
      this.stop(state.name);
      const event = this.#endpoint.events[this.#config.type.id]?.[`${name2}$Changed`];
      if (event?.isQuieter) {
        event.quiet.emitNow();
      }
      return !suppressReportingRemainingTimeOnFinish;
    };
    let reportRemainingTime = false;
    if (typeof name === "string") {
      const state = this.#propertyStates[name];
      if (state === void 0) {
        return;
      }
      reportRemainingTime = finishOne(state);
    } else {
      for (const state of this) {
        reportRemainingTime = reportRemainingTime || finishOne(state);
      }
    }
    if (!Object.keys(this.#propertyStates).length && reportRemainingTime) {
      this.#updateRemainingTime(0);
    }
  }
  /**
   * Free resources.
   */
  async close() {
    this.stop();
    if (this.#outstandingTick) {
      await this.#outstandingTick;
    }
    if (this.#observers) {
      this.#observers.close();
      this.#observers = void 0;
    }
    this.#instrumentedProperties = void 0;
  }
  /**
   * Set the static version of remaining time used when transition management is disabled.
   */
  set remainingTime(value) {
    this.#staticRemainingTime = this.#internalTimeOf(value);
  }
  /**
   * Determine time remaining in transition in (possibly fractional) seconds.
   *
   * This is computed dynamically based on the longest running individual attribute transition.
   */
  get remainingTime() {
    if (this.#config.manageTransitions === false) {
      if (this.#config.transitionEndTimeMs !== void 0) {
        const remaining = Timespan(Time.nowMs, this.#config.transitionEndTimeMs).duration;
        if (remaining < 0) {
          return 0;
        }
        return this.#externalTimeOf(remaining);
      }
      return this.#externalTimeOf(this.#staticRemainingTime);
    }
    let remainingTime = 0;
    for (const name in this.#propertyStates) {
      const attrState = this.#propertyStates[name];
      const distanceLeft = this.#determineDistanceLeft(attrState);
      if (distanceLeft === void 0) {
        continue;
      }
      const attrRemainingTime = Math.abs(distanceLeft / attrState.changePerMs);
      if (attrRemainingTime > remainingTime) {
        remainingTime = attrRemainingTime;
      }
    }
    return this.#externalTimeOf(remainingTime);
  }
  #determineDistanceLeft(state) {
    const { distanceLeft, currentValue } = state;
    if (distanceLeft !== void 0) {
      return distanceLeft;
    }
    const { targetValue } = this.#determineTargetValue(state);
    if (targetValue === void 0) {
      return void 0;
    }
    return Math.abs(currentValue - targetValue);
  }
  /**
   * Update transitioning attributes for a behavior.
   *
   * You may override this method if you want matter.js to run a timer, but you want to handle value updates yourself.
   */
  async step(behavior) {
    const now = Time.nowMs;
    for (const prop of this) {
      const { currentValue, name, changePerMs } = prop;
      if (typeof currentValue !== "number") {
        this.#stepError(prop.name, "value is not numeric");
        continue;
      }
      const { cyclic, min, max } = this.#config.properties[name] ?? {};
      if (cyclic && (min === void 0 || max === void 0)) {
        this.#stepError(prop.name, "min/max defined for cyclic property must be set");
        continue;
      }
      const msSinceLastStep = now - prop.prevStepAt;
      const changeSinceLastStep = changePerMs * msSinceLastStep;
      let nextValue = cyclic ? addValueWithOverflow(currentValue, changeSinceLastStep, min, max) : min !== void 0 && max !== void 0 ? cropValueRange(currentValue + changeSinceLastStep, min, max) : currentValue + changeSinceLastStep;
      const { targetValue, targetDescription } = this.#determineTargetValue(prop);
      if (cyclic) {
        let distanceLeft = this.#determineDistanceLeft(prop);
        if (distanceLeft !== void 0 && targetValue !== void 0) {
          distanceLeft -= Math.abs(changeSinceLastStep);
          if (distanceLeft <= 0) {
            nextValue = targetValue;
            prop.distanceLeft = 0;
          } else {
            prop.distanceLeft = distanceLeft;
          }
        }
      } else {
        if (prop.changePerMs < 0) {
          if (targetValue !== void 0 && nextValue < targetValue) {
            nextValue = targetValue;
          }
        } else if (prop.changePerMs > 0) {
          if (targetValue !== void 0 && nextValue > targetValue) {
            nextValue = targetValue;
          }
        } else {
          this.#stepError(prop.name, "rate is zero");
          continue;
        }
        if (targetValue === void 0) {
          this.#stepError(prop.name, `there is no target value or ${targetDescription}`);
          continue;
        }
      }
      prop.currentValue = nextValue;
      const callbackPromise = prop.configuration.onStep?.call(behavior, nextValue);
      if (callbackPromise !== void 0) {
        await callbackPromise;
      }
      prop.prevStepAt = now;
    }
    const applyPromise = this.applyUpdates(behavior, this.determineUpdates(behavior));
    if (MaybePromise.is(applyPromise)) {
      await applyPromise;
    }
  }
  /** Determine the changes to be applied to the behavior and return as key-value object. */
  determineUpdates(behavior) {
    const state = behavior.state;
    const changes = {};
    for (const prop of this) {
      const { name, currentValue } = prop;
      const nextValue = Math.round(currentValue);
      if (state[name] !== nextValue && (nextValue === null || typeof nextValue === "number")) {
        changes[name] = nextValue;
      }
    }
    return changes;
  }
  /**
   * This method is called to apply the changes to the behavior.
   * The default implementation detects the changed attributes using the method `determineChanges()`and updates the
   * state of the behavior.  It also returns them as an key-value object, which can be useful when just requiring the
   * changes values in an extended class.
   */
  applyUpdates(behavior, changes) {
    const state = behavior.state;
    for (const name in changes) {
      const value = changes[name];
      if (value !== void 0 && (value === null || typeof value === "number")) {
        state[name] = value;
      }
    }
    return changes;
  }
  #step() {
    if (this.#outstandingTick) {
      return;
    }
    const executeStep = async (agent) => {
      const behavior = agent.get(this.#config.type);
      agent.context.transaction.addResourcesSync(behavior);
      await agent.context.transaction.begin();
      await this.step(behavior);
    };
    const promise = this.#outstandingTick = Promise.resolve(this.#endpoint.act("transition", executeStep)).catch((error) => {
      logger.error(
        this.#logPrefix,
        "Aborting transition due to unhandled error during step management:",
        error
      );
      this.stop();
    }).finally(() => {
      if (this.#outstandingTick === promise) {
        this.#outstandingTick = void 0;
      }
    });
  }
  #stepError(name, message) {
    logger.warn(this.#logPrefix, "Not transitioning", Diagnostic.strong(name), "because", message);
    this.stop(name);
  }
  #determineTargetValue(state) {
    const { name } = state.configuration;
    let { targetValue } = state.configuration;
    const { cyclic, min: minValue, max: maxValue } = this.#config.properties[name] ?? {};
    let targetDescription = "target value";
    if (!cyclic) {
      if (state.changePerMs < 0) {
        if (targetValue === void 0 || minValue !== void 0 && targetValue < minValue) {
          targetDescription = "min value";
          targetValue = minValue;
        }
      } else if (state.changePerMs > 0) {
        if (targetValue === void 0 || maxValue !== void 0 && targetValue > maxValue) {
          targetDescription = "max value";
          targetValue = maxValue;
        }
      }
    }
    return {
      targetDescription,
      targetValue
    };
  }
  get #logPrefix() {
    return Diagnostic.squash(Diagnostic.strong(`${this.#endpoint}#${this.#config.type.name}`), `:`);
  }
  #externalTimeOf(ms) {
    return Math.round(ms / (this.#config.externalTimeUnit ?? Transitions.DEFAULT_EXTERNAL_TIME_UNIT));
  }
  #internalTimeOf(externalUnits) {
    return Millis(externalUnits * (this.#config.externalTimeUnit ?? Transitions.DEFAULT_EXTERNAL_TIME_UNIT));
  }
  /**
   * We add event handlers for any property we transition.  If the property reaches the target value we end the
   * transition.
   *
   * This allows us to detect "end of transition" independent of the means of transition.
   */
  #instrumentProperty(name) {
    if (this.#instrumentedProperties === void 0) {
      this.#instrumentedProperties = /* @__PURE__ */ new Set();
    }
    if (this.#instrumentedProperties.has(name)) {
      return;
    }
    this.#instrumentedProperties.add(name);
    const event = this.#endpoint.events[this.#config.type.id]?.[`${name}$Changed`];
    if (!event) {
      return;
    }
    if (!this.#observers) {
      this.#observers = new ObserverGroup();
    }
    this.#observers.on(event, (newValue) => {
      const state = this.#propertyStates[name];
      if (state === void 0) {
        return;
      }
      const { targetValue } = this.#determineTargetValue(state);
      if (newValue === targetValue) {
        return this.finish(name);
      }
    });
  }
}
((Transitions2) => {
  Transitions2.DEFAULT_STEP_INTERVAL = Millis(100);
  Transitions2.DEFAULT_EXTERNAL_TIME_UNIT = Millis(100);
})(Transitions || (Transitions = {}));
export {
  Transitions
};
//# sourceMappingURL=Transitions.js.map
