/**
 * @license
 * Copyright 2022-2026 Matter.js Authors
 * SPDX-License-Identifier: Apache-2.0
 */
import { Transitions } from "#behavior/Transitions.js";
import { ColorControlServer } from "#behaviors/color-control";
import { GeneralDiagnosticsBehavior } from "#behaviors/general-diagnostics";
import { OnOffServer } from "#behaviors/on-off";
import { ScenesManagementServer } from "#behaviors/scenes-management";
import { AggregatorEndpoint } from "#endpoints/aggregator";
import { ServerNode } from "#node/ServerNode.js";
import { AsyncObservable, cropValueRange, Logger, MaybePromise, Millis } from "@matter/general";
import { Val } from "@matter/protocol";
import { ClusterType, StatusCode, StatusResponseError } from "@matter/types";
import { GeneralDiagnostics } from "@matter/types/clusters/general-diagnostics";
import { LevelControl } from "@matter/types/clusters/level-control";
import { LevelControlBehavior } from "./LevelControlBehavior.js";
const logger = Logger.get("LevelControlServer");
const LevelControlBase = LevelControlBehavior.with(LevelControl.Feature.OnOff, LevelControl.Feature.Lighting);
class LevelControlBaseServer extends LevelControlBase {
  /** Returns the minimum level, including feature specific fallback value handling. */
  get minLevel() {
    return this.state.minLevel ?? (this.features.lighting ? 1 : 0);
  }
  /** Returns the maximum level, including feature specific fallback value handling. */
  get maxLevel() {
    return this.state.maxLevel ?? 254;
  }
  /**
   * The current level value as number.
   *
   * Throws a StatusResponse Error when null.
   */
  get currentLevel() {
    if (this.state.currentLevel === null) {
      throw new StatusResponseError(
        "The currentLevel value is null, so we cannot operate on it.",
        StatusCode.Failure
      );
    }
    return this.state.currentLevel;
  }
  initialize() {
    if (this.events.remainingTime$Changed !== void 0) {
      this.events.remainingTime$Changed.quiet.config = {
        suppressionEnabled: false
      };
    }
    this.internal.transitions = this.#initializeTransitions();
    if (this.features.lighting) {
      this.initializeLighting();
    }
    if (this.features.onOff && this.agent.has(OnOffServer)) {
      this.initializeOnOff();
    }
    this.agent.maybeGet(ScenesManagementServer)?.implementScenes(this, this.#applySceneValues);
  }
  /**
   * Create a transition management instance.
   *
   * We manage transitions using {@link Transitions} if
   * {@link LevelControlBaseServer.State#managedTransitionTimeHandling} is true.
   *
   * You may override this method to replace the {@link Transitions} implementation customized for your application.
   * The provided configuration object is the default one used for Level Control transitions, but can be adjusted
   * if needed.
   */
  createTransitions(config) {
    return new Transitions(this.endpoint, config);
  }
  #initializeTransitions() {
    const { endpoint } = this;
    const readOnlyState = endpoint.state.levelControl;
    return this.createTransitions({
      type: LevelControlBaseServer,
      remainingTimeEvent: this.events.remainingTime$Changed,
      get manageTransitions() {
        return readOnlyState.managedTransitionTimeHandling;
      },
      get transitionEndTimeMs() {
        return readOnlyState.transitionEndTimeMs;
      },
      get stepInterval() {
        return readOnlyState.transitionStepInterval;
      },
      properties: {
        currentLevel: {
          get min() {
            return readOnlyState.minLevel;
          },
          get max() {
            return readOnlyState.maxLevel;
          }
        }
      }
    });
  }
  /**
   * Initialize lighting features.
   *
   * This only applies if the Level Control cluster has the "LT" feature enabled.
   */
  initializeLighting() {
    if (this.state.currentLevel === 0) {
      logger.warn(
        `The currentLevel value of ${this.state.currentLevel} is invalid according to Matter specification. The value must not be 0.`
      );
    }
    if (this.minLevel !== 1) {
      logger.warn(
        `The minLevel value of ${this.minLevel} is invalid according to Matter specification. The value should be 1.`
      );
    }
    if (this.maxLevel !== 254) {
      logger.warn(
        `The maxLevel value of ${this.maxLevel} is invalid according to Matter specification. The value should be 254.`
      );
    }
    if (this.#getBootReason() !== GeneralDiagnostics.BootReason.SoftwareUpdateCompleted && !this.endpoint.ownerOfType(AggregatorEndpoint)) {
      const startUpLevelValue = this.state.startUpCurrentLevel ?? null;
      const currentLevelValue = this.state.currentLevel;
      let targetLevelValue;
      switch (startUpLevelValue) {
        case 0:
          targetLevelValue = this.minLevel;
          break;
        case null:
          targetLevelValue = currentLevelValue;
          break;
        default:
          targetLevelValue = startUpLevelValue;
          break;
      }
      if (targetLevelValue !== currentLevelValue) {
        this.state.currentLevel = asIntOrNull(targetLevelValue);
      }
    }
  }
  /**
   * Initialize On/Off cluster integration.
   *
   * This only applies if the Level Control cluster has the "OO" feature enabled.
   */
  initializeOnOff() {
    this.reactTo(this.agent.get(OnOffServer).events.onOff$Changed, this.handleOnOffChange);
  }
  /**
   * Default command implementation.
   *
   * After checking input we use {@link moveToLevelLogic} method to set the level.  To replace the default logic,
   * override {@link moveToLevelLogic} which also implements {@link moveToLevelWithOnOff}.
   */
  moveToLevel({
    level,
    transitionTime,
    optionsMask,
    optionsOverride
  }) {
    const effectiveOptions = this.#calculateEffectiveOptions(optionsMask, optionsOverride);
    if (!this.#optionsAllowExecution(effectiveOptions)) {
      return;
    }
    level = cropValueRange(level, this.minLevel, this.maxLevel);
    this.#invalidateScenes();
    return this.moveToLevelLogic(level, transitionTime, false, effectiveOptions);
  }
  /**
   * Default command implementation.
   *
   * To replace this logic, override {@link moveToLevelLogic} whicih also implements {@link moveToLevel}.
   */
  moveToLevelWithOnOff({ level, transitionTime }) {
    level = cropValueRange(level, this.minLevel, this.maxLevel);
    this.#invalidateScenes();
    return this.moveToLevelLogic(level, transitionTime, true);
  }
  /**
   * Assert the rate value, throws error on invalid values and set default value if null
   */
  #assertRateValue(rate) {
    if (rate === 0) {
      throw new StatusResponseError(`Illegal move rate of 0`, StatusCode.InvalidCommand);
    }
    if (rate === null) {
      return this.state.defaultMoveRate ?? null;
    }
    return rate;
  }
  /**
   * Default "MoveToLevel" implementation.
   *
   * When a transition time is not null the implementation uses a step based logic to manage the move. It also checks
   * if the level is within min/max range and sets the level accordingly.
   *
   * @param level Level to set
   * @param transitionTime transition time
   * @param withOnOff true if the method is called by a "WithOnOff" command variant
   * @param options Options for the command
   */
  moveToLevelLogic(level, transitionTime, withOnOff, options = {}) {
    const effectiveTransitionTime = transitionTime ?? this.state.onOffTransitionTime ?? null;
    let effectiveRate;
    if (effectiveTransitionTime) {
      effectiveRate = (currentLevel) => (level - currentLevel) / effectiveTransitionTime * 10;
    }
    this.#invalidateScenes();
    return this.transition(level, effectiveRate, withOnOff, options);
  }
  /**
   * Default command implementation.
   *
   * After checking input we use {@link moveLogic} method to set the level.
   *
   * To replace default behavior, override {@link moveLogic} which also implements {@link moveWithOnOff}.
   */
  move({ moveMode, rate, optionsMask, optionsOverride }) {
    rate = this.#assertRateValue(rate);
    const effectiveOptions = this.#calculateEffectiveOptions(optionsMask, optionsOverride);
    if (!this.#optionsAllowExecution(effectiveOptions)) {
      return;
    }
    this.#invalidateScenes();
    return this.moveLogic(moveMode, rate, false, effectiveOptions);
  }
  /**
   * Default command implementation.
   *
   * We use {@link moveLogic} method to set the level.
   *
   * To replace default behavior, override {@link moveLogic} which also implements {@link move}.
   */
  moveWithOnOff({ moveMode, rate }) {
    rate = this.#assertRateValue(rate);
    this.#invalidateScenes();
    return this.moveLogic(moveMode, rate, true);
  }
  /**
   * Default implementation of the "Move" commands.
   *
   * When move rate is null and there is no default move rate, we move to to the min or max level directly. Otherwise
   * we apply step logic and increase or decrease by step size for every step.
   *
   * @param moveMode Mode (Up/Down) of the move action
   * @param rate Rate of the move action, null if no rate is provided and the default should be used
   * @param withOnOff true if the method is called by a *WithOnOff command
   * @param options Options for the command
   */
  moveLogic(moveMode, rate, withOnOff, options = {}) {
    let targetLevel;
    if (moveMode === LevelControl.MoveMode.Up) {
      targetLevel = Infinity;
    } else {
      targetLevel = -Infinity;
      if (rate) {
        rate *= -1;
      }
    }
    return this.transition(targetLevel, rate, withOnOff, options);
  }
  /**
   * Default command implementation.
   *
   * After checking options we use {@link stepLogic} to set the level.
   *
   * To replace default beahavior, override {@link stepLogic} which also implements {@link stepWithOnOff}.
   */
  step({
    stepMode,
    stepSize,
    transitionTime,
    optionsMask,
    optionsOverride
  }) {
    const effectiveOptions = this.#calculateEffectiveOptions(optionsMask, optionsOverride);
    if (!this.#optionsAllowExecution(effectiveOptions)) {
      return;
    }
    this.#invalidateScenes();
    return this.stepLogic(stepMode, stepSize, transitionTime, false, effectiveOptions);
  }
  /**
   * Default command implementation.
   *
   * To replace default beahavior, override {@link stepLogic} which also implements {@link step}.
   */
  stepWithOnOff({ stepMode, stepSize, transitionTime }) {
    this.#invalidateScenes();
    return this.stepLogic(stepMode, stepSize, transitionTime, true);
  }
  /**
   * Default step implementation.
   *
   * When transition time is null, we move immediately to the min or max level. Otherwise we increase or decrease the
   * level by the step size for each step.
   *
   * @param stepMode Mode (Up/Down) of the step action
   * @param stepSize Size of the step action
   * @param transitionTime Time of the step action in 10th of a second
   * @param withOnOff true if the method is called by a *WithOnOff command
   * @param options Options for the command
   */
  stepLogic(stepMode, stepSize, transitionTime, withOnOff, options = {}) {
    const direction = stepMode === LevelControl.StepMode.Up ? 1 : -1;
    let effectiveRate;
    if (transitionTime !== null) {
      effectiveRate = stepSize / transitionTime * 10 * direction;
    }
    return this.transition(this.currentLevel + stepSize * direction, effectiveRate, withOnOff, options);
  }
  stop({ optionsMask, optionsOverride }) {
    const effectiveOptions = this.#calculateEffectiveOptions(optionsMask, optionsOverride);
    if (!this.#optionsAllowExecution(effectiveOptions)) {
      return;
    }
    this.#invalidateScenes();
    return this.stopLogic(effectiveOptions);
  }
  stopWithOnOff(request) {
    return this.stop(request);
  }
  /**
   * Default stop logic. This aborts any level transition currently underway and sets the remaining time to 0.
   */
  stopLogic(_options = {}) {
    this.internal.transitions?.stop();
  }
  /**
   * Change to a designated level.
   *
   * @param targetLevel the new level once transition completes; if omitted transition will stop at min or max value
   * @param changePerS the move rate; 0 or nullish means transition instantly; use function to calculate rate based on current level after coupling logics
   * @param withOnOff if true follows rules for On/Off command variants
   * @param options additional options supplied by the client
   */
  transition(targetLevel, changePerS, withOnOff = false, options = {}) {
    return MaybePromise.then(this.couple(withOnOff, options, targetLevel), () => {
      if (typeof changePerS === "function") {
        changePerS = changePerS(this.currentLevel);
      }
      this.internal.transitions?.start({
        name: "currentLevel",
        owner: this,
        changePerS,
        targetValue: targetLevel,
        onStep() {
          this.couple(withOnOff, options, targetLevel);
        }
      });
    });
  }
  /**
   * Configure the current transaction to synchronize state with other clusters.
   *
   * This handles of on/off state in the On/Off cluster and color temperature in the Color Control cluster.
   */
  couple(withOnOff, options = {}, targetLevel) {
    let result = void 0;
    if (this.features.onOff && withOnOff && this.agent.has(OnOffServer)) {
      if (targetLevel === void 0) {
        targetLevel = this.currentLevel;
      }
      if (targetLevel === this.minLevel) {
        this.context.transaction.addParticipants({
          preCommit: () => {
            if (this.currentLevel === this.minLevel) {
              const onOff = this.agent.get(OnOffServer);
              if (onOff.state.onOff) {
                onOff.state.onOff = false;
                return true;
              }
            }
            return false;
          }
        });
      } else {
        const onOff = this.agent.get(OnOffServer);
        if (!onOff.state.onOff) {
          onOff.state.onOff = true;
          if (this.state.onLevel !== null) {
            result = this.handleOnOffChange(true);
            this.internal.blockOnOffCouplingOnce = true;
            this.context.transaction.addParticipants({
              postCommit: () => {
                if (this.internal.blockOnOffCouplingOnce) {
                  this.internal.blockOnOffCouplingOnce = false;
                }
              }
            });
          }
        }
      }
    }
    if (this.features.lighting && options.coupleColorTempToLevel && this.agent.has(ColorControlServer)) {
      this.context.transaction.addParticipants({
        preCommit: () => {
          const colorControl = this.agent.get(ColorControlServer);
          const prevTemp = colorControl.mireds;
          const promise = colorControl.syncColorTemperatureWithLevel(this.currentLevel);
          if (promise) {
            return promise.then(() => colorControl.mireds !== prevTemp);
          }
          return colorControl.mireds !== prevTemp;
        }
      });
    }
    return result;
  }
  /**
   * Implement mandatory interaction with the OnOff cluster on the same endpoint when "OnOff" feature is enabled
   *
   * By default we set the current level to the onLevel value when the device is turned on.
   *
   * Other fading up/down logic required by the {@link MatterSpecification.v12.Cluster} §1.6.4.1.1 needs to be
   * implemented in a specialized implementation if needed.
   *
   * @param onOff The new onOff state
   */
  handleOnOffChange(onOff) {
    if (this.internal.blockOnOffCouplingOnce) {
      this.internal.blockOnOffCouplingOnce = false;
      return;
    }
    if (!onOff || this.state.onLevel === null) {
      return;
    }
    logger.debug(`OnOff changed to ON, setting level to onLevel value of ${this.state.onLevel}`);
    this.state.currentLevel = this.state.onLevel;
  }
  #calculateEffectiveOptions(optionsMask, optionsOverride) {
    const options = this.state.options ?? {};
    return {
      executeIfOff: optionsMask.executeIfOff ? optionsOverride.executeIfOff : options.executeIfOff,
      coupleColorTempToLevel: this.features.lighting ? optionsMask.coupleColorTempToLevel ? optionsOverride.coupleColorTempToLevel : options.coupleColorTempToLevel : false
    };
  }
  #optionsAllowExecution(options) {
    return options.executeIfOff || !this.features.onOff || !this.agent.has(OnOffServer) || this.agent.get(OnOffServer).state.onOff;
  }
  #getBootReason() {
    const rootEndpoint = this.env.get(ServerNode);
    if (rootEndpoint.behaviors.has(GeneralDiagnosticsBehavior)) {
      return rootEndpoint.stateOf(GeneralDiagnosticsBehavior).bootReason;
    }
  }
  /** Apply Scene values when requested from ScenesManagement cluster */
  #applySceneValues(values, transitionTime) {
    const level = values.currentLevel;
    if (typeof level !== "number" || this.state.currentLevel === level || cropValueRange(level, this.minLevel, this.maxLevel) !== level) {
      return;
    }
    return this.moveToLevelLogic(level, transitionTime / 100, false, { executeIfOff: true });
  }
  /** Invalidate all stored scenes manually for this endpoint in the Scenesmanagement cluster because SDK behavior. */
  #invalidateScenes() {
    this.agent.maybeGet(ScenesManagementServer)?.makeAllFabricSceneInfoEntriesInvalid();
  }
  async [Symbol.asyncDispose]() {
    if (this.internal.transitions) {
      await this.internal.transitions.close();
      this.internal.transitions = void 0;
    }
    await super[Symbol.asyncDispose]?.();
  }
}
((LevelControlBaseServer2) => {
  class Internal {
    /** Transition management. */
    transitions;
    /** Internal flag to avoid on/off coupling during transitions */
    blockOnOffCouplingOnce = false;
  }
  LevelControlBaseServer2.Internal = Internal;
  class State extends LevelControlBase.State {
    /**
     * The default implementation always set the target level immediately and so ignores all transition times
     * requested or configured.
     *
     * Set this to true to manage transition changes using {@link Transitions}.  You should only use this if your
     * hardware doesn't support transition management on its own.
     */
    managedTransitionTimeHandling = false;
    /**
     * If transition management is disabled you may specify this as the "end time" for transitions.  The remaining
     * time attribute will then report correctly.
     */
    transitionEndTimeMs = void 0;
    /**
     * When managing transitions, this is the interval at which steps occur in ms.
     */
    transitionStepInterval = Millis(100);
    [Val.properties](endpoint) {
      if (endpoint.behaviors.optionsFor(LevelControlBaseServer2)?.remainingTime === void 0 && endpoint.behaviors.defaultsFor(LevelControlBaseServer2)?.remainingTime === void 0) {
        return {};
      }
      return {
        set remainingTime(value) {
          const transition = endpoint.behaviors.internalsOf(LevelControlBaseServer2).transitions;
          if (transition) {
            transition.remainingTime = value;
          }
        },
        get remainingTime() {
          return endpoint.behaviors.internalsOf(LevelControlBaseServer2).transitions?.remainingTime ?? 0;
        }
      };
    }
  }
  LevelControlBaseServer2.State = State;
  class Events extends LevelControlBase.Events {
    transitionEndTime$Changed = AsyncObservable();
  }
  LevelControlBaseServer2.Events = Events;
})(LevelControlBaseServer || (LevelControlBaseServer = {}));
class LevelControlServer extends LevelControlBaseServer.for(ClusterType(LevelControl.Base)) {
}
function asIntOrNull(value) {
  if (value === null) {
    return null;
  }
  return Math.round(value);
}
export {
  LevelControlBaseServer,
  LevelControlServer
};
//# sourceMappingURL=LevelControlServer.js.map
